Стадия жизненного цикла модуля: Experimental
У модуля есть требования для установки

Руководство описывает административные действия: включить модуль, подключить S3-совместимое хранилище, выбрать способ доставки моделей, подготовить локальный кэш на узлах, настроить публичный каталог и проверить рабочее состояние.

Требования

Минимальные версии Deckhouse Kubernetes Platform и Kubernetes см. в разделе Требования на странице «Конфигурация».

  • S3-совместимое объектное хранилище и bucket для данных DMCR и временных данных подготовки моделей. Один bucket обслуживает один кластер (см. Объектное хранилище и DMCR).
  • секрет в d8-system с ключами accessKey и secretKey.
  • RWX StorageClass с volumeBindingMode: Immediate для режима SharedPVC (см. Требования к хранилищу).
  • Модули sds-node-configurator и sds-local-volume для режима NodeCache.

Включение

Создайте секрет с доступом к объектному хранилищу:

apiVersion: v1
kind: Secret
metadata:
  name: ai-models-artifacts
  namespace: d8-system
type: Opaque
stringData:
  accessKey: "<access-key>"
  secretKey: "<secret-key>"

Включите модуль:

apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
  name: ai-models
spec:
  enabled: true
  version: 1
  settings:
    logLevel: Info
    artifacts:
      bucket: ai-models
      endpoint: https://s3.example.com
      region: us-east-1
      credentialsSecretName: ai-models-artifacts
      usePathStyle: true

Если объектное хранилище использует частный центр сертификации, добавьте ca.crt в отдельный секрет в d8-system и укажите artifacts.caSecretName. Также можно положить ca.crt в секрет с учётными данными: модуль будет использовать его как доверенный сертификат.

Служебный секрет в d8-ai-models создаётся Helm’ом из данных, которые подготовил hook синхронизации. Администратор управляет только исходным секретом в d8-system.

Доставка моделей

delivery.type выбирает способ подключения моделей в фазе Ready к рабочей нагрузке. Эта настройка действует внутри одного кластера; она не является источником данных модели и не описывает внешний каталог. Если блок delivery не задан, используется SharedPVC.

Независимо от выбранного режима admission добавляет scheduling gate в шаблон Pod рабочей нагрузки, которая ссылается на модель, а затем контроллер записывает в тот же объект служебные тома, монтирования и переменные окружения. Gate ставится по аннотации со ссылкой на модель, а не по значению delivery.type.

Для Deployment это означает появление второго ReplicaSet: изменённый шаблон Pod заставляет Kubernetes создать новую ревизию, а устаревшую — ту, в которой gate ещё есть, а состояния доставки уже нет — контроллер удаляет целиком вместе с её Pod в состоянии SchedulingGated, поэтому вручную ничего убирать не нужно. Gate ставится заново каждый раз, когда в отправленном шаблоне Pod нет состояния доставки или изменился набор ссылок на модели, поэтому повторное применение исходного манифеста — то есть обычная работа GitOps — проходит через ту же лишнюю ревизию.

SharedPVC

SharedPVC подходит для кластеров с хранилищем, которое поддерживает ReadWriteMany:

spec:
  settings:
    delivery:
      type: SharedPVC
      sharedPVCStorageClassName: rwx-storage-class

Если sharedPVCStorageClassName пустой, класс хранения выбирается в таком порядке:

  1. global.modules.storageClass;
  2. global.defaultClusterStorageClass;
  3. default StorageClass Kubernetes.

Выбранный класс должен существовать. После этого провайдер хранилища должен создать PVC с ReadWriteMany. Невыполненные требования модуль сообщает на уровне модуля сразу после старта — см. раздел Требования к хранилищу ниже — и повторно по каждой рабочей нагрузке, когда она появляется: причина SharedPVCStorageClassMissing, если класс не найден, и SharedPVCStorageClassAmbiguous, если в кластере несколько default StorageClass (задайте класс в настройках модуля или в глобальных настройках Deckhouse, чтобы выбор был однозначным).

Если провайдер хранилища не может создать том, модуль показывает его собственное объяснение в статусе доставки с причиной SharedPVCProvisioningFailed. Берётся только объяснение об этом самом PVC, поэтому событие, созданное вручную в пространстве имён нагрузки, не может подменить отчёт модуля. Том, который не создан больше 10 минут и о котором кластер ничего не сообщил, получает причину SharedPVCProvisioningTimedOut.

Два случая сообщаются сразу, не дожидаясь этого таймера. Класс с volumeBindingMode: WaitForFirstConsumer получает причину SharedPVCStorageClassDefersBinding: кластер сообщает об отложенной привязке событием типа Normal, а не отказом, тогда как для этой доставки такой класс неработоспособен — потребитель, который запустил бы привязку, создаётся только после привязки тома. Том, который был привязан и потерял свой PersistentVolume, получает причину SharedPVCClaimLost, потому что данные потеряны, а не задержаны.

Ни одна из этих причин не залипает: как только том привязан, доставка продолжается сама.

Доставка, остановленная по временной причине — модель потеряла готовность, нагрузка заблокирована по своему контракту, — сохраняет уже привязанный том и ссылку, которая его удерживает, поэтому при возобновлении модель не скачивается заново. Тома, которые так и не были привязаны, по-прежнему освобождаются, а нагрузка, которую удалили, у которой убрали ссылку на модель или которая перешла в другой режим доставки, освобождает свои тома как раньше.

Требования к хранилищу

Модуль заказывает тома лениво — это делает первая рабочая нагрузка, которая ссылается на модель, — поэтому успешная установка модуля ещё не доказывает, что доставка моделей будет работать. Поэтому модуль непрерывно и независимо от наличия моделей и рабочих нагрузок проверяет требования к хранилищу для выбранного режима доставки и сообщает вердикт через метрику d8_ai_models_storage_prerequisites_satisfied, алерт D8AIModelsStoragePrerequisitesNotSatisfied и лог контроллера (kubectl -n d8-ai-models logs deploy/ai-models-controller -c controller | grep storage-readiness).

Невыполненные требования не отключают модуль: каталог, источники и read API продолжают работать. Модуль деградирован, а не сломан, и вердикт снимается сам, как только кластер исправлен — без перезапуска и без изменения настроек.

Что требует каждый режим:

  • SharedPVCStorageClass, который умеет тома ReadWriteMany и использует volumeBindingMode: Immediate. Класс с volumeBindingMode: WaitForFirstConsumer работать не может: такой класс привязывает том только после появления пода-потребителя, а этот режим создаёт Job-материализатор только после привязки тома и до этого держит под рабочей нагрузки на scheduling gate — то есть первый потребитель у тома не появится никогда. Причина — StorageClassDefersBinding.
  • NodeCache — локальный класс, который предоставляют модули sds-node-configurator и sds-local-volume.

Причины и что исправлять:

Причина Значение
NoStorageClassInCluster В кластере нет ни одного StorageClass.
ConfiguredStorageClassMissing Класс, выбранный для доставки моделей, не существует.
DefaultStorageClassMissing Класс не выбран, и в кластере нет default StorageClass.
DefaultStorageClassAmbiguous В кластере несколько default StorageClass, поэтому выбор неоднозначен.
StorageClassDefersBinding Выбранный класс откладывает привязку до появления потребителя, которого доставка моделей предоставить не может.
EvaluationFailed Проверка не смогла прочитать объекты StorageClass в кластере.

Сама поддержка ReadWriteMany в этот вердикт не входит: StorageClass не объявляет режимы доступа, поэтому модуль не угадывает — поддержка RWX подтверждается или опровергается первой попыткой создания тома, а её неудача сообщается так, как описано выше.

Локальный RWO PVC не является отдельным режимом доставки. Если нужно хранить модель рядом с приложениями на выбранных узлах, используйте NodeCache: модуль создаёт кэш на узле и передаёт модель в рабочую нагрузку через CSI-монтирование только для чтения.

Каждый namespace-потребитель материализует модель в свой ReadWriteMany PVC, и под остаётся в состоянии SchedulingGated, пока материализация не завершится.

Параллельные материализации конкурируют за полосу чтения из реестра и записи в хранилище, поэтому delivery.maxConcurrentMaterializations ограничивает число одновременно выполняющихся Job-материализаторов в пределах кластера (по умолчанию 2). Увеличивайте значение на кластерах с бо́льшим числом узлов или более быстрым хранилищем; уменьшайте, если параллельные материализации перегружают бэкенд:

spec:
  settings:
    delivery:
      type: SharedPVC
      maxConcurrentMaterializations: 4

NodeCache

NodeCache подходит для больших моделей и повторного использования модели несколькими рабочими нагрузками на одной ноде.

  1. Включите sds-node-configurator и sds-local-volume.

  2. Пометьте ноды для кэша:

    d8 k label node <node-name> ai.deckhouse.io/model-cache=true
  3. Пометьте свободные BlockDevice:

    d8 k label blockdevice <block-device-name> ai.deckhouse.io/model-cache=true
  4. Включите режим доставки:

    spec:
      settings:
        delivery:
          type: NodeCache
          nodeCacheSize: 200Gi

По умолчанию ноды и диски выбираются по label ai.deckhouse.io/model-cache=true. Если в кластере уже есть своя схема меток, задайте delivery.nodeCacheNodeSelector и delivery.nodeCacheBlockDeviceSelector.

Проверка подготовленных ресурсов:

d8 k get blockdevices.storage.deckhouse.io -o wide
d8 k get lvmvolumegroupsets.storage.deckhouse.io
d8 k get lvmvolumegroups.storage.deckhouse.io
d8 k get localstorageclasses.storage.deckhouse.io
d8 k -n d8-ai-models get pods,pvc -l app=ai-models-node-cache-runtime -o wide

Диск должен быть свободным и иметь consumable=true.

Лимит хранилища

artifacts.capacityLimit задаёт общий лимит для моделей, которыми управляет модуль:

spec:
  settings:
    artifacts:
      capacityLimit: 500Gi

При включённом лимите шлюз загрузки принимает файл только когда известен размер данных. Обычный curl -T передаёт Content-Length, multipart-клиент передаёт размер на /probe.

Объектное хранилище и DMCR

Bucket из artifacts.bucket принадлежит модулю. Не размещайте в нём сторонние данные и не удаляйте объекты вручную: контроллер и DMCR используют собственные связи между объектами, а ручное удаление может сломать локальную копию модели или повторную доставку в рабочую нагрузку.

Один bucket обслуживает один кластер. Модуль пишет свой реестр в фиксированное место внутри bucket и не разделяет его по кластерам, поэтому два кластера, настроенные на один bucket, перезапишут состояние реестра друг друга. Два кластера могут использовать общий S3-совместимый endpoint и общие ключи доступа, но bucket нужен каждому свой. Это относится и к DMZ-кластеру с кластером во внутреннем контуре: раздача копирует модели между кластерами через API каталога, а не через общий bucket.

DMCR (Deckhouse Model Container Registry) — служебный OCI-реестр модуля. Он хранит подготовленные модели как OCI-артефакты поверх настроенного S3-compatible bucket. Администратор настраивает bucket и доступ к нему, но не управляет OCI-путями, тегами, служебными ссылками и объектами DMCR вручную.

Модель в DMCR не хранится как один файл. Контроллер упаковывает исходные файлы модели во внутренний OCI-артефакт ModelPack, не меняя формат весов модели. Поэтому в интерфейсе объектного хранилища одна модель может выглядеть как десятки или сотни объектов. Часть объектов — это манифесты, конфигурации, слои и ссылки реестра; часть — промежуточные данные загрузки или зеркалирования источника; часть — служебные маркеры, которые позволяют безопасно возобновлять подготовку и удалять только неиспользуемые данные.

В интерфейсе S3-совместимого хранилища можно ориентироваться на такие группы. Имена префиксов являются внутренней структурой и не считаются стабильным API.

Группа объектов Для чего нужна Когда удаляется
docker/registry/... Данные и метаданные внутреннего OCI-реестра: манифесты, конфигурации, ссылки реестра и слои моделей. После удаления владельца и успешной сборки мусора. Общие слои остаются, пока нужны другой модели.
raw/... Промежуточные данные подготовки: загруженный файл, снимок источника HuggingFace/Ollama или данные для повторного запуска подготовки. После удаления модели или после завершения связанной процедуры очистки.
_ai_models/direct-upload/... Физические объекты прямой загрузки и multipart-сессии, которые затем привязываются к OCI-артефакту. После успешной финализации или как устаревшие сиротские данные.
Открытые multipart-загрузки Незавершённые части загрузки, которые не всегда видны как обычные объекты в интерфейсе. Сборка мусора прерывает устаревшие multipart-загрузки отдельно от удаления объектов.

Количество объектов не равно количеству моделей. Один маленький объект может быть служебной ссылкой, а один слой большой модели может занимать гигабайты. Общий слой может использоваться несколькими артефактами, поэтому удаление одной модели не всегда сразу уменьшает занятое место на размер этой модели.

Удаление Model или ClusterModel запускает асинхронную очистку:

  1. Контроллер убирает ссылку модели из каталога и ставит запрос на очистку.
  2. Служебный процесс DMCR объединяет запросы, включает режим обслуживания и ждёт подтверждения от реплик.
  3. Затем удаляются устаревшие префиксы промежуточных данных, прерываются старые multipart-загрузки и запускается сборка мусора OCI-реестра.
  4. Результат очистки публикуется в метриках и логах.

Поэтому сразу после удаления модели в bucket ещё могут оставаться объекты. Это нормально, пока нет алертов D8AIModelsPublicationCleanupBacklogStale или D8AIModelsPublicationCleanupFailed, а дашборд показывает завершённые циклы очистки.

Безопасные проверки:

d8 k get models.ai.deckhouse.io -A
d8 k get clustermodels.ai.deckhouse.io
d8 k -n d8-ai-models get secrets -l ai.deckhouse.io/dmcr-gc-request=true
d8 k -n d8-ai-models logs deploy/dmcr -c dmcr-garbage-collection --since=2h

В логах ищите сообщения dmcr garbage collection completed: в них есть количество удалённых объектов, освобождённые байты и число удалённых слоёв DMCR. Если объекты в bucket растут, а запросы очистки зависли или падают, сначала исправьте причину по алерту и логам. Ручное удаление объектов из bucket допустимо только как отдельная аварийная процедура с подтверждённым списком префиксов.

Подготовка моделей и доставка в рабочие нагрузки

У модуля один путь подготовки модели и два способа доставки в рабочие нагрузки.

Подготовка читает источник модели, проверяет данные и упаковывает исходные файлы во внутренний OCI-артефакт ModelPack. Это не конвертация весов модели: GGUF остаётся GGUF, Safetensors остаётся Safetensors. DMCR сохраняет полученный артефакт как локальную проверенную копию. В мониторинге видно исходный размер модели и фактически занятое место, поэтому оператор понимает, дают ли разбиение на части и сжатие экономию в bucket.

SharedPVC переносит модель из DMCR в управляемый контроллером RWX PVC в неймспейсе рабочей нагрузки. Служебная задача не использует Kubernetes API для чтения модели. Прогресс считается в DMCR по подписанному разрешению на чтение, выданному именно этой задаче, поэтому дашборд показывает ожидаемый объём, уже прочитанный объём и скорость по каждой задаче.

NodeCache переносит модель из DMCR в локальный кэш на узле. Долгоживущий служебный компонент node-cache отдаёт ожидаемый и загруженный объём по узлу и модели, а также занятое место кэша и задержку CSI-запросов.

Раздача каталога использует отдельную передачу данных. Потребляющий кластер сначала читает понятный каталог, затем импортирует выбранные OCI-артефакты как локальные копии. DMCR логирует чтение с идентификатором потребителя и отдаёт метрики скорости и объёма, сгруппированные по назначению передачи.

Раздача каталога между контурами

Раздача каталога отвечает за обмен ClusterModel в фазе Ready между сетевыми зонами. Она не меняет способ подключения модели к рабочей нагрузке.

Типовой сценарий:

  1. В DMZ работает раздающий кластер, который отдаёт ClusterModel в фазе Ready.
  2. Во внутреннем периметре потребляющий кластер импортирует выбранные модели как локальные копии.
  3. Доставка в рабочие нагрузки во внутреннем кластере остаётся SharedPVC или NodeCache.

Такой сценарий полезен, когда кластер в DMZ только раздаёт каталог: он подготавливает и отдаёт модели, но в нём нет рабочих нагрузок с аннотациями. Поэтому раздача каталога не должна превращаться в третье значение delivery.type; это отдельная ось конфигурации.

В каждом кластере работает свой экземпляр модуля со своим bucket — см. Объектное хранилище и DMCR. Модель появляется во внутреннем кластере только после импорта в него: раздающий кластер сам ничего не отправляет.

Включите публичный каталог в раздающем кластере:

spec:
  settings:
    distribution:
      mode: PublicCatalog

После применения настройки модуль готовит публичный адрес и маршруты:

https://ai-models.example.com/api/distribution/v1/models
https://ai-models.example.com/v2

/api/distribution/v1/models отдаёт понятный каталог: только ClusterModel в фазе Ready, без внутренних имён реестра и служебных UID. /v2 остаётся OCI-путём для управляемого контроллером копирования и импорта.

Доступ потребителей

Доступ к публичному каталогу проверяется через аутентификацию и авторизацию Kubernetes в раздающем кластере. Модуль не создаёт отдельную CRD для потребителей.

Создайте ServiceAccount для каждого потребляющего кластера, организации или периметра и привяжите его к роли чтения каталога:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: perimeter-a
  namespace: d8-ai-models
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ai-models-distribution-reader-perimeter-a
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: d8:ai-models:distribution:reader
subjects:
  - kind: ServiceAccount
    name: perimeter-a
    namespace: d8-ai-models

Выпустите токен в раздающем кластере и передайте администратору потребляющего кластера только его значение по защищённому внешнему каналу:

d8 k -n d8-ai-models create token perimeter-a --duration=720h

Для долгоживущих эксплуатационных учётных данных можно создать Kubernetes service-account-token секрет в раздающем кластере и прочитать ключ token после заполнения Kubernetes:

apiVersion: v1
kind: Secret
metadata:
  name: perimeter-a-token
  namespace: d8-ai-models
  annotations:
    kubernetes.io/service-account.name: perimeter-a
type: kubernetes.io/service-account-token

Модуль не выпускает и не переносит этот токен автоматически между кластерами: для этого нужен внешний доверенный канал или secret-manager. Ротация выполняется выпуском нового токена в раздающем кластере и обновлением секрета в потребляющем кластере. После обновления секрета контроллер перечитает ModelCatalogSource и продолжит работу с новым токеном.

Отзыв доступа выполняется удалением RoleBinding или ServiceAccount. Это закрывает новые запросы каталога и новые разрешения на чтение; уже выданные короткоживущие разрешения на чтение истекают по TTL.

Подключение потребляющего кластера

Потребляющий кластер описывает внешние каталоги через ModelCatalogSource:

apiVersion: ai.deckhouse.io/v1alpha1
kind: ModelCatalogSource
metadata:
  name: dmz
spec:
  url: https://ai-models.dmz.example.com
  credentialsSecretName: ai-models-dmz-read
  caSecretName: ai-models-dmz-ca

Секреты из credentialsSecretName и caSecretName находятся в d8-system. Контроллер читает их напрямую для обновления каталога и выдачи разрешений на чтение. Секрет из credentialsSecretName содержит токен ServiceAccount, выпущенный раздающим кластером; caSecretName содержит ca.crt и нужен только для частного CA внешнего каталога. Эти секреты не копируются в неймспейсы приложений.

Модели выбираются не в ModuleConfig. Раздающий кластер экспортирует все ClusterModel в фазе Ready, а пользователи потребляющего кластера импортируют нужные модели через spec.source.catalog.name.

Представление внешнего каталога является кластерным. Оно показывает удалённые записи и локальные копии как ссылки на Model или ClusterModel, включая неймспейс для Model. Это представление доступно только ролям управления модулем и не содержит URL источника, имена секретов, токены, OCI-репозитории, теги или списки blob.

Аудит

API публичного каталога проверяет токен через TokenReview и авторизует запросы через SubjectAccessReview: список каталога требует list на clustermodels.ai.deckhouse.io, просмотр модели и выдача разрешения на чтение требуют get на выбранный ClusterModel.

Контроллер пишет структурированные события аудита для catalog_list, catalog_get, pull_grant_issued, catalog_auth_denied, а также для конфликтов и ошибок. В события попадают имя пользователя Kubernetes, UID, группы, IP-адрес клиента, имя модели, контрольная сумма и результат проверки. DMCR пишет события чтения манифеста и слоёв с той же идентичностью. Токен в исходном виде и его хэш в аудит не попадают.

Каталог-провайдер Hugging Face

Помимо каталогов между кластерами (type: Catalog), ModelCatalogSource может индексировать каталог-провайдер Hugging Face. Вместо url задайте type: HuggingFace и ограниченную policy:

apiVersion: ai.deckhouse.io/v1alpha1
kind: ModelCatalogSource
metadata:
  name: hf-openai
spec:
  type: HuggingFace
  policy:
    allowedOrganizations:
      - openai-community
    maxIndexedEntries: 200
  # huggingFace:
  #   tokenSecretName: hf-token   # опционально, для gated/private репозиториев

Требования и поведение:

  • Должны быть включены data-сервисы внешнего каталога (PostgreSQL и Valkey, см. CONFIGURATION). Если они выключены, провайдер-источники остаются в Waiting.
  • policy обязательна и должна быть ограниченной: хотя бы один селектор области (allowedOrganizations, allowedRepositories или allowedCollections) и maxIndexedEntries (1..10000). Неограниченный источник блокируется с причиной SourcePolicyTooBroad до любого обращения к провайдеру.
  • huggingFace.tokenSecretName опционален и указывает на Secret в d8-system с ключом token. Просмотр и импорт публичных моделей работают анонимно; gated и приватные репозитории требуют токен.
  • Источник индексируется в PostgreSQL ограниченным циклом синхронизации сразу после объявления и переиндексируется периодически. Большие каталоги наполняются инкрементально в пределах rate-limit провайдера. Число проиндексированных записей — в status.entryCount, здоровье — в условии Ready.

Записи провайдера остаются в durable-индексе до импорта; индекс — это слой обнаружения, а не второй источник истины. Импорт записи — тот же декларативный поток, что и для любого каталога (см. руководство пользователя): создать Model или ClusterModel с spec.source.catalog.{sourceName, name}.

Просмотр каталога

Поверхность просмотра — субресурс modelcatalogsources/catalog-import, отдаётся только на чтение по HTTP и авторизуется через kube-rbac-proxy. Это API, которое потребляет фронтенд для списка источников и их записей.

Эндпоинты (метод GET, префикс /api/catalog-import/v1):

Путь Возвращает
/sources все источники каталога с готовностью и числом записей
/sources/{name}/entries записи одного источника

Для источников type: HuggingFace /entries принимает ограниченные параметры пагинации и фильтра: limit (по умолчанию 50, максимум 200), offset, query (совпадение по имени), org и tag (повторяемый). В ответе появляется page.nextOffset, когда есть ещё записи. Для источников type: Catalog возвращается полный снапшот, пагинация игнорируется.

Авторизация — Kubernetes SubjectAccessReview на get для modelcatalogsources/catalog-import. Это право входит в поверхность чтения модуля: его даёт уровень доступа User и выше (уровни доступа накопительные), а также роль просмотра rbacv2/manage, поэтому просматривать может токен, привязанный к любому из них. Привязка должна быть кластерной: проверка идёт по cluster-scoped ресурсу, поэтому субъект, у которого уровень доступа ограничен неймспейсами, получит 403 даже на нужном уровне. Просмотр каталога — это то, как выбирается spec.source.catalog.{sourceName, name} для Model, поэтому он следует за поверхностью чтения; объявление провайдер-источника по-прежнему требует ClusterAdmin.

Контракт API

Обе HTTP-поверхности каталога — этот API просмотра и кросс-периметровый API каталога распространения (/api/distribution/v1) — описаны единым контрактом OpenAPI 3, закоммиченным в images/controller/api/catalog/openapi.yaml; он и есть их источник правды. Потребитель может сгенерировать по нему клиент. Байтовый путь OCI (/v2) и dataplane загрузки (/v1/upload/*) — это байтовые/потоковые протоколы, и они намеренно не входят в этот контракт.

Метаданные записи

Для источников type: HuggingFace каждая запись несёт описательные метаданные, собранные при синхронизации из листинга провайдера, чтобы модель можно было оценить до импорта: task (pipeline), libraryName, license, downloads, likes, createdAt, lastModified и закреплённую version (commit-ревизию). Поля, которые провайдер не отдаёт для конкретной модели, опускаются. Эти факты обновляются на каждом успешном цикле синхронизации; поле настолько свежее, насколько свежа последняя успешная синхронизация источника (см. lastSuccessfulRefreshTime). Источники type: Catalog (снапшот) несут только то, что экспортирует вышестоящий каталог, и не заполняют поля популярности провайдера.

Факты для сайзинга инференса

Для записей type: HuggingFace, как только модель профилирована, ответ несёт опциональный объект sizingFacts с архитектурными и артефактными фактами, нужными внешнему модулю расчёта железа — только факты; этот модуль не считает VRAM, подбор GPU или параллелизм. Две части:

  • model: инвариантные для архитектуры факты из config.json — architecture, family, model type, task, число параметров, окно контекста и сырые размерности (число слоёв, hidden size, число attention- и KV-голов, head dimension, intermediate size, размер словаря, sliding window, число MoE-экспертов, флаг encoder-decoder), плюс карта confidence по каждому выведенному факту.
  • variants: по записи на упаковку весов (safetensors, каждый GGUF-квант, pytorch), с format, quantization, precision и размерами весов/шардов — так как каждый вариант это отдельная цель сайзинга.

Факты выводятся без скачивания весов (тянутся только config.json, tokenizer_config.json и листинг размеров файлов) и кешируются с привязкой к ревизии. Их наполняет bounded фоновый прогрев после синхронизации, поэтому sizingFacts опускается для ещё не профилированных моделей и для gated, приватных или иначе недоступных. Неизвестные поля опускаются, а не угадываются.

Список источников

GET /api/catalog-import/v1/sources
Authorization: Bearer <token>
{
  "apiVersion": "catalogimport.ai.deckhouse.io/v1",
  "kind": "CatalogImportSourceList",
  "items": [
    {
      "name": "hf-openai",
      "ready": "True",
      "entryCount": 200,
      "lastSuccessfulRefreshTime": "2026-06-24T21:15:35Z",
      "conditions": [
        { "type": "Ready", "status": "True", "reason": "Ready" }
      ]
    }
  ]
}

Поля сводки источника:

Поле Значение
name имя ModelCatalogSource
ready готовность (True / False)
reachable, fresh доступность и свежесть, когда сообщаются
catalogRevision ревизия снапшота (только type: Catalog)
entryCount число проиндексированных записей
lastSuccessfulRefreshTime время последней успешной синхронизации
conditions[] type, status, reason

Список записей

GET /api/catalog-import/v1/sources/hf-openai/entries?limit=2&offset=0
Authorization: Bearer <token>
{
  "apiVersion": "catalogimport.ai.deckhouse.io/v1",
  "kind": "CatalogImportEntryList",
  "source": { "name": "hf-openai", "ready": "True", "entryCount": 200 },
  "sourceName": "hf-openai",
  "items": [
    {
      "name": "openai-community/gpt2",
      "lifecycle": "Active",
      "artifact": { "digest": "" },
      "updatedAt": "2026-06-24T21:15:09Z",
      "source": { "type": "huggingface", "revision": "" },
      "projection": { "remoteState": "RemoteDiscovered" }
    }
  ],
  "page": { "limit": 2, "offset": 0, "nextOffset": 2 }
}

Поля ответа верхнего уровня:

Поле Значение
source, sourceName сводка просматриваемого источника и его имя
catalogRevision ревизия снапшота (только type: Catalog)
items[] записи каталога (см. ниже)
page метаданные пагинации (только type: HuggingFace): limit, offset, nextOffset при наличии ещё страниц

Поля записи (items[]):

Поле Значение
name идентификатор записи — id репозитория для Hugging Face (например openai-community/gpt2)
version ревизия/тег, когда известны
lifecycle Active или другая стадия жизненного цикла провайдера
artifact digest, mediaType, sizeBytes, когда известны (пусто до резолва)
format, family, architecture, parameterCount, quantization, contextWindowTokens факты о модели, когда известны; иначе опускаются
supportedEndpointTypes, supportedFeatures возможности обслуживания, когда известны
updatedAt когда запись в последний раз виделась у провайдера
source.type, source.revision тип провайдера и закреплённая ревизия
projection.remoteState RemoteDiscovered (в каталоге, не импортирована)
projection.localCopies[] связанные локальные Model/ClusterModel и состояние импорта, если есть: kind, namespace, name, state, phase, reason
projection.downloadVerdict ожидается ли, что скачивание уместится в хранилище модуля: stateFeasible, NotFeasible или EstimateUnavailable, а также reason (InsufficientStorage) и requiredBytes, когда известны. EstimateUnavailable — если у записи нет разрешённого размера артефакта (обычно у только что синхронизированных provider-записей) или у модуля нет бюджета хранилища: скачивание никогда не блокируется из-за оценки, которой у модуля нет

Записи провайдера после синхронизации несут минимум фактов (id, lifecycle, revision); богатые поля вроде format или parameterCount заполняются глубже, при резолве/импорте. Ошибки возвращаются как { "error": "<сообщение>" } с HTTP статусом (400 нет имени источника, 404 источник не найден, 409 источник не готов, 502 ошибка провайдера, 503 просмотр провайдера недоступен, 401/403 авторизация).

Сводка по хранилищу моделей

GET /api/catalog-import/v1/storage-summary
Authorization: Bearer <token>
{
  "apiVersion": "catalogimport.ai.deckhouse.io/v1",
  "kind": "CatalogImportStorageSummary",
  "summary": {
    "capacityKnown": true,
    "usageKnown": true,
    "limitBytes": 2199023255552,
    "usedBytes": 812345678901,
    "reservedBytes": 10737418240,
    "availableBytes": 1375940158411,
    "namespaced": { "usedBytes": 512345678901, "reservedBytes": 10737418240 },
    "cluster": { "usedBytes": 300000000000, "reservedBytes": 0 }
  }
}

Хранилище здесь — принадлежащее модулю хранилище артефактов, где лежат скачанные модели, по учёту самого контроллера; это не тома доставки, которые позже монтирует нагрузка. usedBytes — опубликованные артефакты, reservedBytes — незавершённые скачивания, а секции namespaced и cluster распределяют те же байты по владельцам Model и ClusterModel.

Бюджет берётся из настроенного лимита ёмкости артефактов. Если лимит не задан, capacityKnown равен false, а limitBytes и availableBytes отсутствуют: потребитель обязан показать «неизвестно», а не подставлять собственную константу. Занятость при этом всё равно возвращается: отсутствие бюджета не означает отсутствие учёта.

Достоверность самой занятости — отдельный флаг usageKnown. Он равен false, когда учёт хранилища выключен, и когда его состояние недоступно: ledger удалён или пересоздан пустым. В обоих случаях usedBytes, reservedBytes и разбивка по scope равны нулю потому, что ничего не известно, а не потому, что ничего не хранится, — потребитель обязан показать «неизвестно», а не «0 B».

Восстанавливает недоступное состояние синхронизатор инвентаря: он пересобирает учёт по фактически сохранённым моделям и только после этого помечает его достоверным. Пока полный проход не завершён, занятость остаётся неизвестной, даже если завершившееся скачивание уже записало свои байты: такая запись описывает одну модель, а не всё хранилище. По той же причине новые скачивания в это время отклоняются — не дольше одного интервала синхронизации.

Эта граница держится, только пока проход успешен. Проход, который стабильно падает — модель, учёт которой не удаётся записать, недоступный API-сервер, отозванные права на ConfigMap учёта, — оставляет занятость неизвестной, а новые скачивания отклонёнными на всё время сбоя, потому что снять это состояние может только проход.

Уже выданное подтверждение назад не отзывается, поэтому проход, начавший падать после успешного, скачивания не отклоняет. Он прекращает саму сверку: занятость больше не проверяется по фактически сохранённым моделям, поэтому расхождение — байты завершившегося скачивания, не попавшие в ledger, — не исправляется, и скачивания допускаются против занятости, которая может быть занижена. Отказ и несверенная занятость требуют разных действий, поэтому публикуются оба состояния:

Метрика Что означает
d8_ai_models_publication_store_ledger_inventory_confirmed 1 — занятость достоверна и скачивания допускаются; 0 — они отклоняются
d8_ai_models_publication_store_ledger_present различает потерянный ledger (0) и существующий, но ещё не подтверждённый (1)
d8_ai_models_publication_store_inventory_sync_up завершился ли последний проход синхронизации; 0 означает, что занятость больше не сверяется, а не что скачивания отклоняются
d8_ai_models_publication_store_inventory_last_success_timestamp_seconds когда учёт был подтверждён последний раз

На этих же фактах срабатывают алерты D8AIModelsStorageAccountingLedgerUnconfirmed, D8AIModelsStorageInventorySyncFailing и D8AIModelsStorageInventorySyncStale. Когда учёт хранилища выключен, ни одна из четырёх серий не публикуется: в этом режиме ничего не отклоняется, значит и сообщать не о чем. Проход записывает учёт по всем моделям, по которым это возможно, и только потом отказывает в подтверждении, поэтому в журнале контроллера перечислены все модели, мешающие подтверждению, а не только первая.

Именно этот флаг различает три возможных состояния, которые одним capacityKnown не различались:

Состояние usageKnown capacityKnown
Учёт выключен false false
Лимит не настроен, учёт работает true false
Состояние учёта недоступно false false

При неизвестной занятости бюджет тоже не может быть предъявлен — availableBytes не вычисляется, — поэтому в третьем состоянии бюджет так же отсутствует.

У вердикта по записи каталога есть две границы, которые стоит учесть до того, как UI на нём что-то построит. Он судит одну запись, а не выборку: бюджет читается один раз на ответ, и каждая запись сравнивается с ним независимо — при свободном 1 ТиБ две записи по 600 ГиБ обе придут Feasible, а второе скачивание получит отказ на резервировании. Сценарию «скачать выбранное» нужно самому суммировать размеры и сверять со сводкой. Вердикт также присутствует у уже скачанных записей, где описывает гипотетическое повторное скачивание, а не доступное действие.

Доступ для фронтенда

Когда внешний каталог включён и у модуля есть публичный HTTPS-хост, Ingress публикует API просмотра по адресу https://<хост-модуля>/api/catalog-import/v1, маршрутизируя его на kube-rbac-proxy контроллера. Фронтенд, предъявляющий Dex/OIDC-токен пользователя, авторизуется тем же RBAC, что описан выше; отдельный слой аутентификации не вводится.

Импорт не входит в это API. Фронтенд импортирует модель, создавая Model или ClusterModel через обычное Kubernetes API с тем же токеном, что сохраняет нативные RBAC, аудит и GitOps.

Внутреннее API запроса сведений о модели

Модуль предоставляет внутрикластерный HTTP-запрос для платформенных потребителей — например, контроллера заказа ai-inference — которым нужны сведения о модели по её полной ссылке. Это внутренний сервисный интерфейс, а не публичный каталог: он обслуживается на kube-rbac-proxy контроллера по пути /api/internal/v1/ и не экспонируется через публичный Ingress.

GET /api/internal/v1/models/lookup?kind=ClusterModel&name=<name>
GET /api/internal/v1/models/lookup?kind=Model&name=<name>&namespace=<namespace>

Ответ — единый JSON-объект ModelFacts с готовностью и описательными сведениями модели (phase, ready, modelScope, format, supportedEndpointTypes, parameterCount, family, quantization, contextWindowTokens, sourceURL и необязательный artifactSizeBytes). Он содержит только факты — не принимает политику и не возвращает заключение о допуске; проверка политики класса остаётся на стороне потребителя.

Семантика ошибок отличает отсутствие от неготовности, в отличие от API распространения:

  • 404 возвращается только если объекта не существует.
  • Существующая, но неготовая модель возвращает 200 с ready: false и явным phase (её никогда не скрывают за 404).
  • Запрос kind=Model без namespace отклоняется клиентской ошибкой, отличной от 404.

Контракт входит в документ OpenAPI 3, зафиксированный в images/controller/api/catalog/openapi.yaml; потребитель может сгенерировать по нему клиент.

Доступ потребителя

Каждый запрос авторизуется сайдкаром kube-rbac-proxy через SubjectAccessReview против виртуального субресурса clustermodels/lookup. Привяжите поставляемую роль потребителя к вызывающему ServiceAccount:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ai-models-internal-model-lookup-ai-inference
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: d8:ai-models:internal:model-lookup
subjects:
  - kind: ServiceAccount
    name: <consumer-service-account>
    namespace: <consumer-namespace>

Авторизация крупнозернистая: обладатель роли может запросить сведения любой модели (и Model, и ClusterModel). Вызовы без роли получают 403, а неаутентифицированные — 401.

RBAC

Модуль использует модель уровней доступа Deckhouse. Уровни накопительные: каждый уровень наследует нижние, поэтому поверхность чтения объявлена один раз на уровне User, а каждый следующий уровень добавляет только свою дельту на запись.

Уровень Доступ
User чтение Model, ClusterModel, ModelCatalogSource — включая status в теле объекта, но не subresource */status: его не получает ни одна человеческая роль (он есть только у ServiceAccount контроллера), — и представления импортов каталога;
Editor управление Model в неймспейсе;
ClusterEditor управление ClusterModel;
ClusterAdmin управление ModelCatalogSource и привязками доступа к публичному каталогу;
rbacv2/use чтение Model, ClusterModel; управление Model в неймспейсе;
rbacv2/manage чтение Model, ClusterModel, ModelCatalogSource, представления импортов и ModuleConfig модуля; управление Model, ClusterModel, ModelCatalogSource и ModuleConfig модуля.

rbacv2/use и rbacv2/manage — два независимых дерева ролей, а не вложенные одно в другое: d8:use:capability:* привязываются в неймспейсе, d8:manage:* — кластерно, и ни одно не включает другое. Списки ресурсов у них пересекаются, привязки — нет.

ClusterModel и ModelCatalogSource — cluster-scoped, поэтому уровни выше получают к ним доступ только при кластерной привязке. ClusterAuthorizationRule с limitNamespaces или namespaceSelector раскладывается через RoleBinding, а RoleBinding не может выдать cluster-scoped ресурс: такой субъект продолжает читать Model в своих неймспейсах и получает Forbidden на два кластерных типа. Если пользователю нужен общий каталог кластера, выдавайте уровень доступа без ограничения по неймспейсам.

Доступ к загрузке выдаётся отдельной Role на один секрет из status.upload.secretName. Role создаётся в пространстве имён модели и называется ai-model-upload-reader-<model-name> либо получает стабильный хэш для длинных имён.

Восстановление импорта из внешнего каталога

Импорт хранит зафиксированное происхождение: выбранный внешний каталог, имя модели, ревизию каталога и удалённую контрольную сумму. Это защищает рабочую нагрузку от незаметного переезда на другую версию модели при следующем согласовании.

Следующие ошибки восстанавливаются после исправления причины:

  • CatalogAuthFailed — истёк токен, секрет обновлён или исправлен RBAC на раздающем кластере;
  • CatalogTLSInvalid — исправлен caSecretName или содержимое ca.crt;
  • CatalogSourceNotReady — внешний каталог снова перешёл в фазу Ready.

После восстановления ModelCatalogSource контроллер повторяет импорт той же зафиксированной модели. Ошибки ManifestInvalid, InsufficientStorage и нарушенный контракт каталога не повторяются автоматически: сначала нужно исправить исходный артефакт, лимит хранилища или спецификацию каталога.

Проверка:

d8 k get modelcatalogsources.ai.deckhouse.io
d8 k describe modelcatalogsource <name>
d8 k -n <namespace> describe model <name>

Мониторинг

Проверьте ресурсы мониторинга:

d8 k -n d8-ai-models get podmonitor,prometheusrule

Основные разделы дашбордов:

  • Обзор кластера. Показывает общий инвентарь Model и ClusterModel, количество объектов в фазах Publishing, Ready и Failed, общий размер подготовленных локальных копий, число управляемых рабочих нагрузок и ссылки на модели, которые контроллер не смог разрешить. Начинайте диагностику с этого раздела: ненулевые Failed и неразрешённые ссылки означают, что нужно перейти к конкретной модели или рабочей нагрузке.
  • Состояние каталога. Отдельные дашборды для Model в области неймспейса и кластерных ClusterModel помогают понять, где находится проблема: в конкретном неймспейсе, в общем каталоге кластера или в отдельной модели. Смотрите фазу, готовность, условия, источник, формат, размер локальной копии, потребителей модели и таблицу рабочих нагрузок с неразрешённой доставкой.
  • Подготовка моделей. Раздел показывает текущие объекты в подготовке, прогресс загрузки и упаковки, скорость передачи данных, ошибки завершения или проверки, а также повторные попытки. Если прогресс долго не меняется, сравните скорость передачи с состоянием bucket/DMCR и проверьте события соответствующего Model или ClusterModel.
  • DMCR и bucket. Панели ёмкости показывают настроенный лимит, занятое, зарезервированное и доступное место для подготовленных локальных копий. Отдельно отображается эффективность хранения ModelPack: логический размер модели и фактически занятое место могут отличаться из-за разбиения на слои, архивирования и переиспользования данных. Очередь очистки показывает pending, active и failed cleanup requests после удаления моделей.
  • Доставка в рабочие нагрузки. Раздел показывает, какие workloads управляются модулем, сколько Pod’ов готово, какой способ доставки выбран и почему. Для SharedPVC смотрите состояние PVC, очередь копирования и пропускную способность задач подготовки. Для неизвестного способа доставки или неразрешённой ссылки проверьте имя модели, неймспейс и права на её использование.
  • Кэш на узлах. Раздел нужен для режима NodeCache: он показывает служебные Pod’ы, привязанные PVC, занятое и доступное место на каждом узле, число записей в кэше, скорость копирования, параллелизм подготовки и задержки CSI mount/unmount-запросов. Рост задержек или нехватка эффективного свободного места обычно указывает на проблему локального диска, PVC или node-cache runtime.
  • Раздача каталога. Если включена раздача между контурами, смотрите частоту запросов к публичному каталогу, выдачу разрешений на чтение, задержки API и пропускную способность импортов. Ошибки авторизации или рост задержек нужно сопоставлять с RBAC потребителей, состоянием API-сервера и аудитом раздающего кластера.

Эксплуатационные проверки

Проверить компоненты:

d8 k -n d8-ai-models get pods -o wide
d8 k get models.ai.deckhouse.io -A
d8 k get clustermodels.ai.deckhouse.io

Проверить модель:

d8 k -n <namespace> describe model <name>
d8 k get clustermodel <name> -o yaml

Ключевые поля:

  • status.phase;
  • status.conditions;
  • status.artifact.digest;
  • status.artifact.sizeBytes;
  • status.resolved.format;
  • status.resolved.supportedEndpointTypes;
  • status.resolved.supportedFeatures.

Выключение

При spec.enabled=false удаляются временные служебные ресурсы, которыми владеет модуль: node-cache runtime Pod/PVC, CSIDriver, LocalStorageClass, LVMVolumeGroupSet, managed LVMVolumeGroup и StorageClass ai-models-node-cache.

Model, ClusterModel и уже подготовленные локальные копии моделей остаются. Удаление модели выполняется через удаление соответствующего Model или ClusterModel; контроллер завершает очистку через finalizer и GC-request.