Стадия жизненного цикла модуля: 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 пустой, класс хранения выбирается в таком
порядке:
global.modules.storageClass;global.defaultClusterStorageClass;- default
StorageClassKubernetes.
Выбранный класс должен существовать. После этого провайдер хранилища должен
создать 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 продолжают работать. Модуль деградирован, а не сломан, и вердикт снимается сам, как только кластер исправлен — без перезапуска и без изменения настроек.
Что требует каждый режим:
SharedPVC—StorageClass, который умеет тома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: 4NodeCache
NodeCache подходит для больших моделей и повторного использования модели
несколькими рабочими нагрузками на одной ноде.
-
Включите
sds-node-configuratorиsds-local-volume. -
Пометьте ноды для кэша:
d8 k label node <node-name> ai.deckhouse.io/model-cache=true -
Пометьте свободные
BlockDevice:d8 k label blockdevice <block-device-name> ai.deckhouse.io/model-cache=true -
Включите режим доставки:
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 запускает асинхронную очистку:
- Контроллер убирает ссылку модели из каталога и ставит запрос на очистку.
- Служебный процесс DMCR объединяет запросы, включает режим обслуживания и ждёт подтверждения от реплик.
- Затем удаляются устаревшие префиксы промежуточных данных, прерываются старые multipart-загрузки и запускается сборка мусора OCI-реестра.
- Результат очистки публикуется в метриках и логах.
Поэтому сразу после удаления модели в 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
между сетевыми зонами. Она не меняет способ подключения модели к рабочей
нагрузке.
Типовой сценарий:
- В DMZ работает раздающий кластер, который отдаёт
ClusterModelв фазеReady. - Во внутреннем периметре потребляющий кластер импортирует выбранные модели как локальные копии.
- Доставка в рабочие нагрузки во внутреннем кластере остаётся
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 |
ожидается ли, что скачивание уместится в хранилище модуля: state — Feasible, 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.