Deploy modela na Kubernetes
Kubernetes ne ubrzava inferenciju i ne rešava nijedan problem koji si do sada imao. Ono što donosi je raspoređivanje: kada imaš više servera sa karticama i više modela, neko mora da odluči šta ide gde — i da to ponovo uradi kad node otkaže.
Ovaj tekst pokazuje kako se model pokreće u klasteru i gde se GPU postavka razlikuje od svega ostalog što tamo vrtiš.
Kada ovo ima smisla
| Ima smisla | Nema smisla |
|---|---|
| Više servera sa karticama | Jedan server, jedan model |
| Klaster već postoji i drži ostalo | Klaster bi se dizao samo zbog ovoga |
| Više modela deli isti skup kartica | Nema rezervnog hardvera |
| Opterećenje se menja kroz dan | Stalno, predvidivo opterećenje |
Za jedan server systemd jedinica ostaje bolje rešenje. Kubernetes uvodi sloj koji treba održavati, a ako imaš tačno jednu karticu, raspoređivati nema šta.
Preduslov je poznavanje osnovnih pojmova — pod, deployment, servis.
Device plugin
Kubernetes ne zna za grafičke kartice dok mu se ne kaže. Na svakom node-u sa karticom mora da postoji drajver i nvidia-container-toolkit, a u klasteru device plugin koji karticu prijavljuje kao resurs.
$ kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.17.0/deployments/static/nvidia-device-plugin.yml
Provera da je kartica vidljiva:
$ kubectl get nodes -o custom-columns=\ IME:.metadata.name,GPU:.status.allocatable.nvidia\\.com/gpu IME GPU node-01 1 node-02 2 node-03 <none>
Ako svuda piše <none>, plugin ne radi:
$ kubectl logs -n kube-system -l name=nvidia-device-plugin-ds --tail=50
Najčešći uzrok je da NVIDIA izvršno okruženje nije podrazumevano na node-u:
$ sudo nvidia-ctk runtime configure --runtime=containerd --set-as-default $ sudo systemctl restart containerd
Za veće postavke postoji GPU Operator, koji instalira drajvere, toolkit, plugin i DCGM exporter odjednom. Za tri node-a je preteran; za trideset je jedini razuman put.
Prostor za modele
Ovo je prva stvar koja se razlikuje od običnih aplikacija. Model je desetine gigabajta i ne sme se preuzimati pri svakom podizanju poda.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: modeli
namespace: ai
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 200Gi
storageClassName: nfs
Režim ReadWriteMany je ovde bitan — više podova mora da čita isti skup modela. To znači deljeni sistem fajlova, NFS ili slično; obično blok skladište to ne podržava.
Ako klaster nema deljeno skladište, ostaje vezivanje poda za određeni node i lokalni direktorijum:
volumes:
- name: modeli
hostPath:
path: /data/modeli
type: Directory
Radi, ali pod tada ne može da se preseli — što oduzima glavni razlog zbog kog si uopšte u Kubernetesu.
Podsetnik na volumene stoji u posebnom tekstu.
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-qwen3
namespace: ai
spec:
replicas: 1
selector:
matchLabels:
app: vllm-qwen3
template:
metadata:
labels:
app: vllm-qwen3
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
args:
- --model=Qwen/Qwen3-8B-AWQ
- --served-model-name=qwen3
- --host=0.0.0.0
- --port=8000
- --gpu-memory-utilization=0.90
- --max-model-len=16384
- --download-dir=/modeli
ports:
- containerPort: 8000
name: http
resources:
limits:
nvidia.com/gpu: 1
memory: 16Gi
requests:
cpu: "4"
memory: 16Gi
env:
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token
key: token
volumeMounts:
- name: modeli
mountPath: /modeli
- name: deljena-memorija
mountPath: /dev/shm
startupProbe:
httpGet:
path: /health
port: 8000
failureThreshold: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 30
failureThreshold: 3
volumes:
- name: modeli
persistentVolumeClaim:
claimName: modeli
- name: deljena-memorija
emptyDir:
medium: Memory
sizeLimit: 8Gi
Četiri stvari koje se razlikuju
nvidia.com/gpu: 1 ide samo pod limits. Kartica je nedeljiva — ne postoji „pola kartice". Vrednost pod requests Kubernetes sam popunjava; ako je navedeš, mora biti ista.
startupProbe je obavezan. Podizanje vLLM-a sa preuzimanjem modela traje minutima. Bez ove probe livenessProbe ubije pod usred učitavanja, pa se sve vrti u krug. Vrednost failureThreshold: 60 daje deset minuta.
Deljena memorija. Ekvivalent oznake --ipc=host iz Docker postavke — bez nje vLLM puca pri učitavanju, sa porukom koja ni na šta ne upućuje.
Direktorijum za preuzimanje mora biti na volumenu, ne u sloju kontejnera. Inače svaki restart znači ponovno preuzimanje.
Token
$ kubectl create secret generic hf-token \
--from-literal=token=hf_xxxxxxxxxxxxx \
-n ai
Više o tajnama u posebnom tekstu. Token ne stavljaj u manifest koji ide u git.
Servis i pristup
apiVersion: v1
kind: Service
metadata:
name: vllm-qwen3
namespace: ai
spec:
selector:
app: vllm-qwen3
ports:
- port: 8000
targetPort: 8000
type: ClusterIP
$ kubectl port-forward -n ai svc/vllm-qwen3 8000:8000 $ curl http://localhost:8000/v1/models
Za pristup spolja ide ingress, sa istim podešavanjima kao Nginx proxy:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ai
namespace: ai
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
cert-manager.io/cluster-issuer: letsencrypt
spec:
ingressClassName: nginx
tls:
- hosts: [ai.primer.rs]
secretName: ai-tls
rules:
- host: ai.primer.rs
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vllm-qwen3
port:
number: 8000
Bez proxy-buffering: "off" nema streaminga, a bez dugačkog roka duži odgovori pucaju — iste dve greške kao kod običnog proxy-ja.
Raspoređivanje po node-ovima
U mešovitom klasteru pod mora da završi tamo gde ima kartica:
$ kubectl label node node-01 gpu=nvidia-3090 $ kubectl label node node-02 gpu=nvidia-a100
spec:
nodeSelector:
gpu: nvidia-a100
Obrnuto — da obični podovi ne završe na skupim node-ovima:
$ kubectl taint node node-02 gpu=true:NoSchedule
spec:
tolerations:
- key: gpu
operator: Equal
value: "true"
effect: NoSchedule
Ovaj par je ono što čini GPU klaster upotrebljivim. Bez taint-a, prvi deployment koji naiđe zauzme node sa karticom, a tvoj model čeka.
Model preko više kartica
args:
- --tensor-parallel-size=2
resources:
limits:
nvidia.com/gpu: 2
Obe kartice moraju biti na istom node-u — Kubernetes ne deli jedan pod preko dva servera. Za modele koji ne staju na jedan node treba znatno složenija postavka.
Skaliranje
Podrazumevani HPA gleda procesor, što je za inferenciju beskorisno. Skalira se po dužini reda čekanja:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-qwen3
namespace: ai
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-qwen3
minReplicas: 1
maxReplicas: 3
metrics:
- type: Pods
pods:
metric:
name: vllm_num_requests_waiting
target:
type: AverageValue
averageValue: "3"
behavior:
scaleDown:
stabilizationWindowSeconds: 600
Traži prometheus-adapter, koji metrike iz Prometheusa izlaže Kubernetesu.
Dva ograničenja koja moraš imati u vidu. Prvo, skaliranje radi samo dok ima slobodnih kartica — nema elastičnosti kao kod procesorskih poslova. Drugo, novi pod se diže minutima jer učitava model, pa dugačak stabilizationWindowSeconds sprečava da se podovi dižu i gase u krug.
Nadzor
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
Uz DCGM exporter kao DaemonSet dobijaš iste metrike kao u prethodnom tekstu, samo po node-u:
$ kubectl top nodes $ kubectl describe node node-01 | grep -A5 'Allocated resources'
Ovo drugo pokazuje koliko je kartica zauzeto, što je podatak koji kubectl top ne daje.
Praktični scenariji
Scenario 1: pod stoji u stanju Pending
$ kubectl describe pod -n ai -l app=vllm-qwen3 | tail -20
Warning FailedScheduling 0/3 nodes are available:
3 Insufficient nvidia.com/gpu
Nema slobodne kartice, ili plugin ne radi. Proveri kroz kubectl get nodes sa kolonom za GPU.
Scenario 2: pod se stalno restartuje
$ kubectl logs -n ai -l app=vllm-qwen3 --previous --tail=50
Ako logovi prestaju usred učitavanja modela, startupProbe je prekratak. Podigni failureThreshold.
Scenario 3: model se preuzima pri svakom podizanju
--download-dir ne pokazuje na volumen, ili volumen nije prikačen. Proveri kroz kubectl describe pod.
Scenario 4: vLLM puca sa greškom o deljenoj memoriji
Nedostaje emptyDir sa medium: Memory na /dev/shm.
Scenario 5: dva poda na istoj kartici
Ne bi trebalo da je moguće ako je nvidia.com/gpu zadat. Ako se ipak desi, neki pod koristi hostPath ka uređaju umesto resursa.
Scenario 6: streaming ne radi kroz ingress
Nedostaje proxy-buffering: "off". Ista greška kao kod običnog proxy-ja.
Kratka referenca
nvidia-device-plugin— prijavljuje karticu kao resursnvidia-ctk runtime configure --runtime=containerd— na svakom node-ukubectl get nodes -o custom-columns=...allocatable.nvidia\.com/gpu— proveranvidia.com/gpu: 1— samo podlimits, kartica je nedeljivastartupProbesa velikimfailureThreshold— obavezanemptyDirna/dev/shm— ekvivalent za--ipc=hostReadWriteManyPVC — više podova čita iste modelenodeSelector+taint— GPU node-ovi samo za GPU poslove--tensor-parallel-size— kartice moraju biti na istom node-uproxy-buffering: "off"u ingressu — inače nema streamingavllm_num_requests_waiting— metrika za HPAkubectl describe node | grep Allocated— zauzetost kartica
Vežba
- Instaliraj device plugin i potvrdi da node prijavljuje karticu kao resurs.
- Postavi deployment sa vLLM-om i prati podizanje kroz
kubectl logs -f. - Namerno postavi
startupProbena trideset sekundi i posmatraj petlju restarta. - Označi node i dodaj taint, pa proveri gde se pod raspoređuje.
- Podigni broj replika iznad broja kartica i pročitaj poruku iz
kubectl describe pod. - Izloži servis kroz ingress i potvrdi da streaming radi.
Sledeći tekst u serijalu: Bezbednost: prompt injection i curenje podataka
Povezano:
- AI na Linux serveru — pregled celog serijala
- Monitoring inferencije — prethodni tekst
- Ollama u Dockeru sa GPU passthrough-om — sloj ispod ovoga
- kubectl — rad sa Kubernetes klasterom — osnovne komande
- Deployment i StatefulSet — osnovni pojmovi
Comments
Post a Comment