Стадия жизненного цикла модуляExperimental

У модуля есть требования для установки

Это руководство для администратора кластера. Вы включаете модуль, решаете, что его заказам позволено, и следите за ним. Сам заказ инференса — дело пользователя namespace, об этом Руководство пользователя.

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

Что нужно

  • Deckhouse Kubernetes Platform >= 1.75.0;
  • Kubernetes >= 1.34. Пол совпадает с модулями gpu и ai-models на том же контуре; основной API Dynamic Resource Allocation resource.k8s.io/v1 на этой версии уже есть;
  • редакции fe и ee;
  • ускорители, отданные через Dynamic Resource Allocation, с классами устройств, по которым платформа умеет отбирать. Размещение планируется по настоящим сведениям о железе, а не запрашивается вручную.

Включение модуля

apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
  name: ai-inference
spec:
  enabled: true
  version: 1

У модуля ровно одна настройка, и она про связь с модулем каталога ai-models:

spec:
  settings:
    catalog:
      mode: Enabled   # или None

Enabled — значение по умолчанию — оставляет обращения к каталогу и права на межмодульный доступ. None выключает клиентов каталога и эти права; заказы, называющие прямой источник Hugging Face, продолжают работать. Выключайте, если ваши заказы каталогом не пользуются, а ai-models не установлен. Полный справочник — Конфигурация.

Модуль работает в namespace d8-ai-inference и поставляет две составляющие: контроллер, который согласует заказы с рабочими нагрузками, и планировщик ресурсов, который решает о размещении.

Написание класса заказа

InferenceServiceClass живёт на уровне кластера. Это единственное место, где живёт политика, и заказ не может переопределить ничего из неё.

apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceServiceClass
metadata:
  name: llm-chat-external
spec:
  exposurePolicy:
    type: External
    authentication: Token
    https:
      mode: CertManager
      certManager:
        clusterIssuerName: letsencrypt
  admissionPolicy:
    allowedNamespaces:
      - support
      - analytics
  modelPolicy:
    allowedEndpointTypes:
      - Chat
  scalingPolicy:
    minReplicas: 1
    maxReplicas: 4
  acceleratorPolicy:
    allowedSharingModes:
      - Shared
      - Dedicated
    allowedPlacementTypes:
      - WholeDevice
      - Partition
    maxAcceleratorCount: 1
  updatePolicy:
    strategy: RollingUpdate

О чём решает каждый блок:

Блок О чём решает Если не задан
exposurePolicy ClusterLocal или External, нужен ли токен, какая ветвь TLS обязателен
admissionPolicy.allowedNamespaces какие namespace могут заказывать этим классом обязателен
modelPolicy.allowedEndpointTypes какие договоры API получат заказы класса обязателен и непуст
modelPolicy.allowedFormats, maxParameterCount какие модели допускаются вообще без ограничений
scalingPolicy пол и потолок числа копий, разрешённые классы важности одна копия, автоматики роста нет
acceleratorPolicy какие классы устройств, размещения, способы разделения, сколько устройств, границы доли заказ берёт ускоритель целиком
modelStorageClassName класс хранения тома модели, для томов, созданных после того, как вы его задали значение платформы по умолчанию
updatePolicy.strategy как переводятся копии заказа, когда платформа вправе их двигать: RollingUpdate обязателен

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

Три из них требуют предупреждения: не задать их — тоже решение, а не отсутствие решения.

modelPolicy.allowedEndpointTypes обязателен и непуст — проверка стоит на самом ресурсе. Договор заказа выбирается из этого перечня; без перечня заказы остались бы вовсе без договора, поэтому сервер отвергает такой класс при записи.

Порядок перечня — часть решения. Платформа берёт первый элемент вашего перечня, суженного тем, что об умениях модели говорит её источник. На источнике, который несёт сведения о модели — в каталоге, — модель представлений получит договор представлений. На источнике без сведений стоит весь перечень, побеждает первый элемент, и модель представлений, заказанная по голой ссылке Hugging Face, будет обслужена как беседа. Если для ваших пользователей это важно, объявите класс, чей перечень называет один договор.

Класс без scalingPolicy даёт своим заказам одну копию и никакой автоматики роста. Это решение о вместимости, и модуль не принимает его за вас.

Класс без acceleratorPolicy отдаёт каждому заказу ускоритель целиком. Два заказа, каждый из которых поместился бы в половине карты, займут тогда две карты, и совместного размещения, ради которого модуль существует, на таком классе не случится само.

Встроенный класс

Модуль поставляет класс уровня кластера с именем default-llm. Он разрешает все три договора, Chat первым, допускает оба способа разделения и оба вида размещения при одном устройстве на копию, не называет границ доли — поэтому план назначает наименьшую долю, вмещающую память модели, — и ограничивает число копий от одной до двух. Публикует он External с authentication: Token.

Это отправная точка, а не политика вашего кластера: выбор admissionPolicy.allowedNamespaces он за вас не делает, а его потолок в две копии — наименьшее число, при котором автоматика роста вообще может существовать.

TLS на внешней точке входа

exposurePolicy.https.mode решает, откуда у точки входа, опубликованной с type: External, берётся сертификат. Значений четыре, и у каждого свой блок рядом:

mode Что получает точка входа Что пишется рядом
CertManager сертификат, выписанный cert-manager certManager.clusterIssuerName; если опустить, платформа подставит selfsigned
CustomCertificate сертификат, который у вас уже есть customCertificate.secretName, обязателен и непуст
Disabled TLS нет, точка входа отдаётся открыто ничего
OnlyInURI TLS на точке входа нет, а опубликованный адрес читается как https ничего

certManager и customCertificate взаимно исключаются: класс, назвавший оба, отвергается, и CustomCertificate без непустого secretName — тоже.

Чтобы принести свой сертификат, положите его в Secret того namespace, который обслуживает класс, и назовите этот секрет:

spec:
  exposurePolicy:
    type: External
    authentication: Token
    https:
      mode: CustomCertificate
      customCertificate:
        secretName: llm-chat-tls

При type: ClusterLocal блок https присутствовать не должен.

Выдача прав

Модуль поставляет роли уровня кластера; связывание за вами.

Уровень или роль Ресурсы Действия
Пользователь (d8:user-authz:ai-inference:user, rbacv2 use/view) inferenceservices get, list, watch
Пользователь / use/view inferenceserviceclasses/placement-preview create
Пользователь / use/view inferenceserviceclasses/cluster-view get
Редактор (:editor, rbacv2 use/edit) inferenceservices create, update, patch, delete, deletecollection
Редактор кластера (:cluster-editor) inferenceserviceclasses get, list, watch, create, update, patch, delete, deletecollection
Редактор кластера / manage подресурсы интерфейса выше create / get
manage/view moduleconfigs/ai-inference, классы get, list, watch и подресурсы интерфейса
manage/edit moduleconfigs/ai-inference, классы правка и подресурсы интерфейса

Людям намеренно не даётся: inferenceserviceclasses/planner, любой подресурс */status, Secret, а также bind, escalate и impersonate. Прибавки уровней PrivilegedUser, Admin и ClusterAdmin пусты — здесь эти уровни не получают ни одного лишнего действия.

Обратите внимание, чего пользователь namespace не может: прочитать класс. Именно поэтому у заказа есть status.model.endpointType — это единственное окно пользователя в решение, которое принял класс.

Присмотр за модулем

С модулем поставляются два предупреждения, оба про переход вместимости между заказами:

  • D8AIInferenceCapacityPreempted — вместимость вытеснена в пользу заказа-получателя на классе устройств. Что-то выросло за счёт другого заказа;
  • D8AIInferenceDonorHeldAtLoweredBound — заказ-даритель удерживается на понижённой границе числа копий. Он отдал вместимость и не получил её назад.

Поставляются и два табло Grafana, оба про среду исполнения запросов: статистика работы и статистика запросов.

Заказ в состоянии Degraded — то, что стоит замечать: его точка доступа отвечает, но вырасти он не может. Причина обычно ScaleUpBlocked — вытеснение исчерпало вместимость, нужную автоматике роста.

Проверки при присмотре

# какие классы есть и прошли ли они проверку
kubectl get inferenceserviceclass

# заказы по всему кластеру и класс, которым пользуется каждый
kubectl get inferenceservice -A

# почему класс не Ready
kubectl get inferenceserviceclass llm-chat-external \
  -o jsonpath='{range .status.conditions[*]}{.type}{"="}{.status}{" "}{.reason}{"\n"}{end}'

Класс публикует два условия: Validated и Ready. Заказ, сообщающий PlacementUndecidable, указывает на ваш класс, а не на вместимость кластера: размещение не удалось рассудить вовсе, значит выражению отбора класса нужен взгляд. Сравните с ResourceClaimExhausted, который значит, что поиск был и все разрешённые классы устройств перебраны, — этот про вместимость.

Обновление модуля

Обновление модуля может принести новый выпуск среды исполнения запросов, и стоит знать заранее, что станет с заказами, которые уже живут в кластере.

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

Заказ сам говорит, где он. Пока выпуски расходятся, заказ несёт условие RuntimeCurrent=False с причиной ReplicasHeldOnPreviousRuntime, и его сообщение называет оба выпуска и число копий, дошедших до поставляемого:

kubectl get inferenceservice support-llm \
  -o jsonpath='{range .status.conditions[?(@.type=="RuntimeCurrent")]}{.reason}: {.message}{"\n"}{end}'

Два способа довести переход до конца. Правка заказа переводит все копии так, как просит класс, — это перекат с тем простоем, который он подразумевает. Удаление копии переводит только её:

kubectl delete pod support-llm-0 -n my-namespace

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

Когда переход не может быть постепенным. Если пересчитанный план сдвинул размещение заказа — другой класс устройства, другая доля устройства, больший том модели, — копии сохранить нельзя: меняться должны как раз те объекты, что держат устройства. Тогда заказ сообщает RuntimeCurrent=False с причиной WorkloadRebuilding, проходит через Pending и возвращается на новом выпуске.

Отключение модуля

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

Сначала уберите, в таком порядке:

  1. удалите каждый InferenceService во всех namespace;
  2. удалите объекты InferenceServiceClass, на которые эти namespace ссылались.

Отключение без этой уборки оставляет объекты заказов в кластере, и согласовывать их будет некому.

Куда смотреть дальше