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 resurs
  • nvidia-ctk runtime configure --runtime=containerd — na svakom node-u
  • kubectl get nodes -o custom-columns=...allocatable.nvidia\.com/gpu — provera
  • nvidia.com/gpu: 1samo pod limits, kartica je nedeljiva
  • startupProbe sa velikim failureThreshold — obavezan
  • emptyDir na /dev/shm — ekvivalent za --ipc=host
  • ReadWriteMany PVC — više podova čita iste modele
  • nodeSelector + taint — GPU node-ovi samo za GPU poslove
  • --tensor-parallel-size — kartice moraju biti na istom node-u
  • proxy-buffering: "off" u ingressu — inače nema streaminga
  • vllm_num_requests_waiting — metrika za HPA
  • kubectl describe node | grep Allocated — zauzetost kartica

Vežba

  1. Instaliraj device plugin i potvrdi da node prijavljuje karticu kao resurs.
  2. Postavi deployment sa vLLM-om i prati podizanje kroz kubectl logs -f.
  3. Namerno postavi startupProbe na trideset sekundi i posmatraj petlju restarta.
  4. Označi node i dodaj taint, pa proveri gde se pod raspoređuje.
  5. Podigni broj replika iznad broja kartica i pročitaj poruku iz kubectl describe pod.
  6. Izloži servis kroz ingress i potvrdi da streaming radi.

Sledeći tekst u serijalu: Bezbednost: prompt injection i curenje podataka

Povezano:

Comments

Popular posts from this blog

Početak u Linuxu — šta je i zašto se uči

Konverzija tipova podataka u Pythonu

groupadd