kubectl и понимание, что GPU в Kubernetes выдаётся как ресурс nvidia.com/gpu - 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 (сверьтесь с документацией по актуальным тегам образов)

kubectl, есть рабочий kubeconfig с правами создавать namespace, deployment, service, pvc, job.allocatable (см. Шаг 1).llama-server, он работает на CPU (сверьтесь с документацией по поддерживаемым квантизациям).Если кластер разворачивается на домашнем железе и вы ещё не решили, где держать сервисы, начните с материала Как подключить ИИ к домашнему серверу: пошаговое руководство по self-hosted сервисам — там разобрана база, а этот гайд продолжает её на уровне Kubernetes.
Пример: модель на 7–8 млрд параметров в 4-битной квантизации занимает единицы гигабайт, а полные веса в fp16 — десятки. Возьмите точный размер из карточки модели и умножьте на 2 для запаса под кэш.
Что сделать: создать 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_ваш_токен>

Что сделать: скачать веса в 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> — это упрощает смену весов без правки клиентского кода.

Что сделать: пробросить порт, запросить список моделей и отправить тестовый чат-запрос.
Зачем: успешный 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"}
Чек-лист приёмки:
kubectl get pods — под vllm-* в статусе Running, READY 1/1kubectl get pvc models-pvc — Bound/v1/models возвращает id: local-model/v1/chat/completions возвращает непустой contentkubectl logs deploy/vllm не содержит ERROR и OOMДальше сервер можно подключить к клиентам как обычный 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, ноды «не подходят».
0/3 nodes are available: 3 Insufficient nvidia.com/gpu.allocatable из Шага 1 и поды плагина; затем освободите GPU или уменьшите limits до 1:kubectl describe node <имя-ноды> | grep -A5 "Allocatable"
kubectl get pods -A -o json | grep nvidia.com/gpu
Проблема. Под падает при загрузке модели.
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate ...--gpu-memory-utilization выставлен слишком высоко и сервер не оставляет места под активации.kubectl logs deploy/vllm --previous | tail -50
Уменьшите --gpu-memory-utilization до 0.80, ограничьте --max-model-len (например, 4096) и перейдите на квантизованную версию весов — точные поддерживаемые флаги сверьте с документацией образа.
Проблема. Под бесконечно рестартует, readiness падает.
Readiness probe failed: Get "http://10.244.0.12:8000/health": dial tcp 10.244.0.12:8000: connect: connection refusedinitialDelaySeconds, или сервер слушает не тот интерфейс/порт.args есть --host=0.0.0.0 (иначе сервер слушает только 127.0.0.1 внутри контейнера и probe до него не достаёт), и увеличьте задержку: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 без простоя.
Нужна ли 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 разобран в этом руководстве.