← Все гайды AI Release
EN Подписаться на @ai_release1

Как развернуть локальную ИИ-модель на Kubernetes: пошаговое руководство

Обновлено: 21.10.2026 · AI Release · @ai_release1
Как развернуть локальную ИИ-модель на Kubernetes: пошаговое руководство

TL;DR

- kubectl, совместимый с версией вашего кластера (сверьтесь с документацией)

- кластер Kubernetes с GPU-нодой либо kind/minikube/k3s для учебного прогона на CPU

- NVIDIA Container Toolkit на нодах и NVIDIA device plugin в кластере — без него GPU не появится в allocatable

- StorageClass, поддерживающий ReadWriteOnce (PVC — сетевое хранилище, которое живёт дольше пода)

- токен Hugging Face, если модель закрыта лицензией

- сервер инференса с OpenAI-совместимым API: vLLM для GPU или llama-server из llama.cpp для CPU (сверьтесь с документацией по актуальным тегам образов)

Требования и подготовка

Иллюстрация к разделу «Требования и подготовка»
Иллюстрация к разделу «Требования и подготовка»

Если кластер разворачивается на домашнем железе и вы ещё не решили, где держать сервисы, начните с материала Как подключить ИИ к домашнему серверу: пошаговое руководство по self-hosted сервисам — там разобрана база, а этот гайд продолжает её на уровне Kubernetes.

Пример: модель на 7–8 млрд параметров в 4-битной квантизации занимает единицы гигабайт, а полные веса в fp16 — десятки. Возьмите точный размер из карточки модели и умножьте на 2 для запаса под кэш.

Шаг 1. Пространство имён, проверка GPU и хранилище под веса

Что сделать: создать namespace, убедиться, что кластер видит GPU, и подготовить PVC.

Зачем: namespace изолирует ресурсы, проверка allocatable отсекает 80% будущих проблем с Pending, а PVC нужен, чтобы веса модели скачались один раз и пережили пересоздание пода.

Как проверить: PVC в статусе Bound, в колонке GPU — число больше нуля.

kubectl create namespace llm
kubectl config set-context --current --namespace=llm
kubectl get nodes -o custom-columns=NAME:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu

Ожидаемый вывод для GPU-ноды:

NAME       GPU
gpu-node1  1

Если в колонке <none> — драйвер или device plugin не установлены. Проверьте поды плагина (имя DaemonSet сверьте с документацией вашей установки):

kubectl -n kube-system get pods | grep -i device-plugin
kubectl -n kube-system logs <pod-плагина>

Почему это работает: kubelet публикует GPU как расширенный ресурс nvidia.com/gpu, и планировщик выдаёт его только тем подам, у которых он указан в limits.

Дальше — хранилище. Список классов:

kubectl get storageclass

Манифест PVC (models-pvc.yaml):

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: models-pvc
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: <ваш-storageclass>
  resources:
    requests:
      storage: 200Gi
kubectl apply -f models-pvc.yaml
kubectl get pvc models-pvc

Ожидаемый вывод: models-pvc Bound ... 200Gi RWO .... Значение 200Gi — пример, замените на размер весов модели плюс запас. <ваш-storageclass> — имя из вывода kubectl get storageclass (например, local-path или standard).

Пример: если модель весит около 15 ГБ в квантизованном виде, 50Gi хватит с запасом на две версии модели одновременно — это удобно при обновлении без простоя.

Токен для закрытых моделей:

kubectl create secret generic hf-token --from-literal=token=<hf_ваш_токен>

Шаг 2. Загрузка весов и запуск сервера инференса

Иллюстрация к разделу «Шаг 2. Загрузка весов и запуск сервера инференса»
Иллюстрация к разделу «Шаг 2. Загрузка весов и запуск сервера инференса»

Что сделать: скачать веса в PVC отдельным Job, затем поднять Deployment с vLLM и Service на порт 8000.

Зачем: Job отделяет долгую загрузку от запуска — под не перезапускается каждые 30 секунд, а модель не скачивается заново при каждом рестарте. Порт 8000 — стандартный для OpenAI-совместимого API vLLM, поэтому к серверу подойдёт любой клиент, умеющий OPENAI_BASE_URL.

Как проверить: Job в статусе Complete, затем rollout status вернёт успех.

Сначала Job (download-job.yaml):

apiVersion: batch/v1
kind: Job
metadata:
  name: model-download
spec:
  backoffLimit: 2
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: downloader
          image: <образ-с-huggingface-cli>   # сверьтесь с документацией
          env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-token
                  key: token
          command:
            - huggingface-cli
            - download
            - <org>/<model>
            - --local-dir
            - /models/<model>
          volumeMounts:
            - name: models
              mountPath: /models
      volumes:
        - name: models
          persistentVolumeClaim:
            claimName: models-pvc
kubectl apply -f download-job.yaml
kubectl get job model-download -w
kubectl logs job/model-download

Ожидаемый вывод: job.batch/model-download condition met и в логах — список скачанных .safetensors. <org>/<model> — идентификатор модели на Hugging Face, например org/model-name.

Теперь Deployment (vllm-deploy.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm
spec:
  replicas: 1
  selector:
    matchLabels: { app: vllm }
  template:
    metadata:
      labels: { app: vllm }
    spec:
      containers:
        - name: vllm
          image: <образ-vllm-openai>   # тег сверьте с документацией
          args:
            - --model=/models/<model>
            - --served-model-name=local-model
            - --host=0.0.0.0
            - --port=8000
            - --gpu-memory-utilization=0.90
          ports:
            - containerPort: 8000
          resources:
            limits:
              nvidia.com/gpu: 1
          readinessProbe:
            httpGet: { path: /health, port: 8000 }
            initialDelaySeconds: 60
            periodSeconds: 10
            failureThreshold: 30
          volumeMounts:
            - name: models
              mountPath: /models
      volumes:
        - name: models
          persistentVolumeClaim:
            claimName: models-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: vllm
spec:
  selector: { app: vllm }
  ports:
    - port: 8000
      targetPort: 8000
kubectl apply -f vllm-deploy.yaml
kubectl rollout status deploy/vllm --timeout=15m

Ожидаемый вывод: deployment "vllm" successfully rolled out.

Почему это работает: nvidia.com/gpu: 1 в limits — единственное место, где запрашивается GPU (в requests его дублировать не нужно, Kubernetes подставит значение сам). initialDelaySeconds: 60 даёт серверу время загрузить веса до первой проверки, иначе под уйдёт в CrashLoopBackOff ещё до готовности. --gpu-memory-utilization ограничивает долю VRAM под веса и KV-кэш (кэш ключей/значений — память под контекст диалога).

Пример: при --served-model-name=local-model все клиенты обращаются к модели по имени local-model, а не по длинному пути /models/<model> — это упрощает смену весов без правки клиентского кода.

Шаг 3. Проверка работы и финальный результат

Иллюстрация к разделу «Шаг 3. Проверка работы и финальный результат»
Иллюстрация к разделу «Шаг 3. Проверка работы и финальный результат»

Что сделать: пробросить порт, запросить список моделей и отправить тестовый чат-запрос.

Зачем: успешный rollout говорит только о том, что контейнер стартовал. Реальная проверка — ответ API с непустым content.

Как проверить: три команды ниже; на третьей в choices[0].message.content должен появиться текст.

kubectl port-forward svc/vllm 8080:8000

Во втором терминале:

curl -s localhost:8080/v1/models

Ожидаемый вывод (сокращённо):

{"object":"list","data":[{"id":"local-model","object":"model","owned_by":"vllm"}]}
curl -s localhost:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"local-model","messages":[{"role":"user","content":"Ответь одним словом: работает"}],"max_tokens":16}'

Ожидаемый вывод:

{"choices":[{"message":{"role":"assistant","content":"Работает"}}],"model":"local-model"}

Чек-лист приёмки:

Дальше сервер можно подключить к клиентам как обычный OpenAI-совместимый endpoint: в коде задайте OPENAI_BASE_URL=http://vllm.llm.svc.cluster.local:8000/v1 и OPENAI_API_KEY=local (значение-заглушка, если сервер запущен без ключа). Порт наружу через port-forward держите только для отладки — для постоянной работы нужен Ingress с TLS и аутентификацией. Как подключить такой сервер к конкретному инструменту, разобрано в гайде Как подключить локальную модель к Claude Code: llama-server, OpenAI-совместимый API и лаунчер.

Возможные проблемы и решения

Проблема. Под висит в Pending, ноды «не подходят».

kubectl describe node <имя-ноды> | grep -A5 "Allocatable"
kubectl get pods -A -o json | grep nvidia.com/gpu

Проблема. Под падает при загрузке модели.

kubectl logs deploy/vllm --previous | tail -50

Уменьшите --gpu-memory-utilization до 0.80, ограничьте --max-model-len (например, 4096) и перейдите на квантизованную версию весов — точные поддерживаемые флаги сверьте с документацией образа.

Проблема. Под бесконечно рестартует, readiness падает.

kubectl get deploy vllm -o jsonpath='{.spec.template.spec.containers[0].readinessProbe}'
kubectl patch deploy vllm --type=json -p '[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/initialDelaySeconds","value":180}]'

Резюме и следующие шаги

Вы получили в кластере свой OpenAI-совместимый endpoint на порту 8000 с весами в PVC и проверкой через /v1/chat/completions. Следующий шаг — вынести сервис на Ingress с TLS и аутентификацией и добавить вторую версию модели, чтобы обновлять веса через новый PVC без простоя.

FAQ

Нужна ли GPU, или модель заработает на CPU?

Заработает: llama-server из llama.cpp умеет работать на CPU, теряя в скорости. vLLM ориентирован на GPU — точный список поддерживаемых бэкендов сверьте с документацией проекта.

Сколько VRAM потребуется?

Ориентируйтесь на размер весов из карточки модели плюс запас на KV-кэш под длину контекста. Точные цифры зависят от квантизации и --max-model-len — сверьтесь с документацией модели, готовую таблицу VRAM не выдумывайте.

Как обновить модель без простоя?

Скачайте веса в новую папку на том же PVC, поднимите второй Deployment с --served-model-name=local-model-v2, проверьте его через отдельный Service, затем переключите трафик и удалите старый Deployment.

Можно ли в этом же кластере запустить голосового агента?

Да, namespace и Service — обычные объекты Kubernetes. Пример развёртывания self-hosted голосового агента с импортом агента из Dialogflow CX разобран в этом руководстве.

🤖 Кратко для ИИ