Solution française • Hébergement souverain • Conformité européenne Blog IA souveraine

Kubernetes + GPU — déployer un LLM en production en France (guide complet 2026)

Kubernetes est devenu la plateforme d'orchestration standard pour les workloads LLM en production. Mais déployer un LLM sur GPU avec K8s comporte des subtilités absentes des workloads CPU classiques : device plugin NVIDIA, gestion des ressources VRAM, autoscaling basé sur des métriques GPU custom, et rolling updates sans interruption de service. Ce guide couvre l'intégralité de la chaîne, du nœud bare metal jusqu'aux alertes Prometheus.

Prérequis matériels et OS

Avant d'aborder la configuration Kubernetes, les nœuds GPU doivent satisfaire des prérequis précis. Un nœud mal configuré provoquera des erreurs cryptiques au moment du scheduling des pods — mieux vaut vérifier méthodiquement.

Prérequis nœud GPU (Worker Node)

# OS recommandé : Ubuntu 22.04 LTS (Jammy)
lsb_release -a
# Ubuntu 22.04.4 LTS

# Vérification GPU détecté
nvidia-smi
# Driver version: 550.90.07 (minimum requis pour H100)
# CUDA Version: 12.4

# Installer les drivers NVIDIA (méthode recommandée : repo officiel)
add-apt-repository ppa:graphics-drivers/ppa
apt-get update
apt-get install -y nvidia-driver-550-server nvidia-utils-550-server

# Activer le persistence mode (évite les cold starts)
nvidia-smi -pm 1

# Désactiver le GSP firmware si problèmes de perf
nvidia-smi --gom=0

# Vérifier CUDA toolkit
nvcc --version
# Cuda compilation tools, release 12.4

# Container runtime : containerd avec NVIDIA runtime
apt-get install -y nvidia-container-toolkit
nvidia-ctk runtime configure --runtime=containerd
systemctl restart containerd
550+Driver NVIDIA min. (H100)
12.4Version CUDA recommandée
1.7+Version containerd requise
K8s 1.29+Version Kubernetes min.

NVIDIA Device Plugin Kubernetes

Le Device Plugin NVIDIA est le pont entre Kubernetes et les GPU physiques. Il expose les GPU comme des ressources Kubernetes schedulables (nvidia.com/gpu), permet de les allouer par pod, et collecte les informations de santé matérielle.

# Installation via Helm (méthode recommandée)
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update

# Déploiement avec time-slicing activé (partage GPU entre pods)
helm install nvidia-device-plugin nvdp/nvidia-device-plugin \
  --namespace kube-system \
  --set gfd.enabled=true \
  --set migStrategy=mixed \
  --set config.default.sharing.timeSlicing.replicas=4 \
  --version 0.15.0

# Vérification : les GPU doivent apparaître comme ressources
kubectl get nodes -o json | jq '.items[].status.capacity | select(has("nvidia.com/gpu"))'
# {"nvidia.com/gpu": "4"}

# Labels automatiques ajoutés par GFD (GPU Feature Discovery)
kubectl get node gpu-node-01 --show-labels | grep nvidia
# nvidia.com/cuda.driver.major=550
# nvidia.com/cuda.runtime.major=12
# nvidia.com/gpu.memory=81559
# nvidia.com/gpu.product=H100-SXM5-80GB

Node labels et taints pour isolation GPU

# Taint les nœuds GPU pour réserver à l'inférence LLM
kubectl taint nodes gpu-node-01 gpu-node-02 \
  gpu-workload=llm-inference:NoSchedule

# Label pour node affinity sélective
kubectl label node gpu-node-01 \
  node-role=gpu-inference \
  gpu-model=H100 \
  gpu-memory=80GB

# Vérification scheduling
kubectl describe node gpu-node-01 | grep -A5 Taints

Manifest YAML complet vLLM

Voici un manifest de production complet pour déployer vLLM avec ELODIE 32B sur Kubernetes, incluant les resource requests/limits GPU, les probes de santé, le volume pour les modèles, et l'intégration avec Vault pour les secrets.

# vllm-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-elodie-32b
  namespace: llm-production
  labels:
    app: vllm
    model: elodie-32b
    version: "0.4.3"
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0  # Zéro downtime
  selector:
    matchLabels:
      app: vllm
      model: elodie-32b
  template:
    metadata:
      labels:
        app: vllm
        model: elodie-32b
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8000"
        prometheus.io/path: "/metrics"
    spec:
      # Tolérer les taints GPU
      tolerations:
      - key: "gpu-workload"
        operator: "Equal"
        value: "llm-inference"
        effect: "NoSchedule"
      # Affinité nœuds H100
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: gpu-model
                operator: In
                values: ["H100"]
      # Anti-affinity : 2 replicas sur 2 nœuds différents
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: vllm
            topologyKey: kubernetes.io/hostname
      initContainers:
      - name: model-loader
        image: alpine:3.19
        command: ["/bin/sh", "-c"]
        args:
        - |
          if [ ! -f /models/elodie-32b/config.json ]; then
            echo "Downloading model from MinIO..."
            mc alias set minio http://minio.storage:9000 \
              "${MINIO_ACCESS_KEY}" "${MINIO_SECRET_KEY}"
            mc mirror minio/models/elodie-32b /models/elodie-32b/
          fi
        volumeMounts:
        - name: model-storage
          mountPath: /models
        - name: minio-creds
          mountPath: /secrets
      containers:
      - name: vllm
        image: vllm/vllm-openai:v0.4.3
        args:
        - "--model"
        - "/models/elodie-32b"
        - "--tensor-parallel-size"
        - "2"
        - "--dtype"
        - "bfloat16"
        - "--max-model-len"
        - "16384"
        - "--max-num-seqs"
        - "256"
        - "--gpu-memory-utilization"
        - "0.90"
        - "--enable-prefix-caching"
        - "--enable-chunked-prefill"
        - "--served-model-name"
        - "elodie-32b"
        - "--host"
        - "0.0.0.0"
        - "--port"
        - "8000"
        ports:
        - containerPort: 8000
          name: http
          protocol: TCP
        resources:
          requests:
            nvidia.com/gpu: "2"
            cpu: "16"
            memory: "64Gi"
          limits:
            nvidia.com/gpu: "2"
            cpu: "32"
            memory: "128Gi"
        env:
        - name: VLLM_API_KEY
          valueFrom:
            secretKeyRef:
              name: vllm-secrets
              key: api-key
        - name: CUDA_VISIBLE_DEVICES
          value: "0,1"
        - name: NCCL_DEBUG
          value: "WARN"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 120
          periodSeconds: 30
          timeoutSeconds: 10
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /v1/models
            port: 8000
          initialDelaySeconds: 90
          periodSeconds: 15
          timeoutSeconds: 5
          failureThreshold: 5
        volumeMounts:
        - name: model-storage
          mountPath: /models
        - name: shm
          mountPath: /dev/shm
      volumes:
      - name: model-storage
        persistentVolumeClaim:
          claimName: model-storage-pvc
      - name: shm
        emptyDir:
          medium: Memory
          sizeLimit: 16Gi
      - name: minio-creds
        secret:
          secretName: minio-credentials
---
apiVersion: v1
kind: Service
metadata:
  name: vllm-elodie-32b
  namespace: llm-production
  labels:
    app: vllm
    model: elodie-32b
spec:
  selector:
    app: vllm
    model: elodie-32b
  ports:
  - name: http
    port: 80
    targetPort: 8000
    protocol: TCP
  type: ClusterIP

HorizontalPodAutoscaler GPU

L'HPA standard de Kubernetes scale sur CPU/mémoire. Pour les GPU, il faut utiliser des métriques custom via l'adaptateur Prometheus. La métrique clé est l'utilisation GPU (DCGM_FI_DEV_GPU_UTIL) ou la longueur de la queue vLLM.

# prometheus-adapter-config.yaml — règles de métriques custom
apiVersion: v1
kind: ConfigMap
metadata:
  name: adapter-config
  namespace: monitoring
data:
  config.yaml: |
    rules:
    - seriesQuery: 'vllm:num_requests_waiting{namespace!="",pod!=""}'
      resources:
        overrides:
          namespace: {resource: "namespace"}
          pod: {resource: "pod"}
      name:
        matches: "^vllm:num_requests_waiting$"
        as: "vllm_queue_length"
      metricsQuery: 'avg_over_time(vllm:num_requests_waiting{<<.LabelMatchers>>}[2m])'
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: vllm-hpa
  namespace: llm-production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: vllm-elodie-32b
  minReplicas: 1
  maxReplicas: 4
  metrics:
  - type: Pods
    pods:
      metric:
        name: vllm_queue_length
      target:
        type: AverageValue
        averageValue: "10"  # Scale up si queue > 10 requêtes en attente
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Pods
        value: 1
        periodSeconds: 120  # Max 1 pod toutes les 2 min (cold start long)
    scaleDown:
      stabilizationWindowSeconds: 300  # Attendre 5 min avant scale down
      policies:
      - type: Percent
        value: 25
        periodSeconds: 60

Cold start des pods vLLM

Un pod vLLM avec ELODIE 32B met 90 à 150 secondes à être prêt (chargement modèle en VRAM). L'HPA doit en tenir compte avec une stabilizationWindowSeconds de scale-up de 60s minimum et un initialDelaySeconds sur la readinessProbe d'au moins 90s. Sans cela, le kubelet redémarre le pod en boucle avant même qu'il soit prêt.

Monitoring : DCGM + Prometheus

Le stack de monitoring GPU se compose de trois éléments : DCGM Exporter (métriques matérielles GPU), les métriques intégrées vLLM (/metrics endpoint), et Prometheus pour la collecte centralisée.

# dcgm-exporter.yaml — DaemonSet sur tous les nœuds GPU
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: dcgm-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: dcgm-exporter
  template:
    metadata:
      labels:
        app: dcgm-exporter
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9400"
    spec:
      tolerations:
      - key: "gpu-workload"
        operator: "Exists"
        effect: "NoSchedule"
      nodeSelector:
        node-role: gpu-inference
      containers:
      - name: dcgm-exporter
        image: nvcr.io/nvidia/k8s/dcgm-exporter:3.3.5-3.4.0-ubuntu22.04
        ports:
        - containerPort: 9400
          name: metrics
        securityContext:
          runAsNonRoot: false
          privileged: true
        volumeMounts:
        - name: pod-gpu-resources
          mountPath: /var/lib/kubelet/pod-resources
      volumes:
      - name: pod-gpu-resources
        hostPath:
          path: /var/lib/kubelet/pod-resources

Métriques GPU clés à monitorer

Métrique DCGMDescriptionSeuil d'alerte
DCGM_FI_DEV_GPU_UTILUtilisation GPU (%)< 20% sur 30min (idle) / > 95% sur 5min (saturation)
DCGM_FI_DEV_FB_USEDVRAM utilisée (MiB)> 90% de la VRAM totale
DCGM_FI_DEV_POWER_USAGEConsommation (W)> 95% TDP (ex. 710W pour H100)
DCGM_FI_DEV_GPU_TEMPTempérature (°C)> 83°C (throttling imminent)
DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTALDébit NVLink (MB/s)Chute > 50% (défaut liaison)
DCGM_FI_DEV_XID_ERRORSErreurs matérielles GPU> 0 (critique immédiat)
# prometheus-rules-llm.yaml — Alertes critiques
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: llm-gpu-alerts
  namespace: monitoring
spec:
  groups:
  - name: llm.gpu
    interval: 30s
    rules:
    - alert: GPUMemoryHigh
      expr: |
        (DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL) > 0.90
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "GPU VRAM > 90% sur {{ $labels.instance }}"
        description: "VRAM utilisée : {{ $value | humanizePercentage }}"
    - alert: GPUMemoryCritical
      expr: |
        (DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL) > 0.97
      for: 1m
      labels:
        severity: critical
      annotations:
        summary: "VRAM critique — OOM imminent"
    - alert: vLLMHighLatency
      expr: |
        histogram_quantile(0.95,
          rate(vllm:e2e_request_latency_seconds_bucket[5m])
        ) > 5
      for: 3m
      labels:
        severity: warning
      annotations:
        summary: "Latence P95 vLLM > 5s"
    - alert: GPUXidError
      expr: DCGM_FI_DEV_XID_ERRORS > 0
      for: 0m
      labels:
        severity: critical
      annotations:
        summary: "Erreur matérielle GPU XID détectée — intervention requise"

Secrets management (Vault + Kubernetes)

Les secrets des pods vLLM (clé API, credentials MinIO, certificats TLS) ne doivent jamais être stockés en clair dans les manifests YAML. Deux approches : Kubernetes Secrets chiffrés (simple) ou HashiCorp Vault (enterprise-grade).

# Option 1 : Kubernetes Secrets avec chiffrement etcd
# Activer l'encryption at rest dans kube-apiserver
# /etc/kubernetes/encryption-config.yaml
cat << EOF > /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: $(head -c 32 /dev/urandom | base64)
      - identity: {}
EOF

# Option 2 : Vault Agent Injector (recommandé production)
# Installation Vault via Helm
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault \
  --namespace vault \
  --set "server.ha.enabled=true" \
  --set "server.ha.replicas=3" \
  --set "injector.enabled=true"

# Annotation sur les pods pour injection automatique
# (ajoutée dans le deployment YAML ci-dessus)
kubectl annotate pod vllm-elodie-32b-xxx \
  vault.hashicorp.com/agent-inject="true" \
  vault.hashicorp.com/role="llm-production" \
  vault.hashicorp.com/agent-inject-secret-api-key="secret/llm/api-key"

Rolling updates sans coupure

Mettre à jour vLLM ou changer de version de modèle sans interruption de service nécessite une stratégie précise, car le cold start d'un pod vLLM est long (90-150s).

# Procédure de rolling update sans downtime

# 1. Préparer la nouvelle image
docker pull vllm/vllm-openai:v0.4.4
docker tag vllm/vllm-openai:v0.4.4 registry.interne/vllm:v0.4.4
docker push registry.interne/vllm:v0.4.4

# 2. Patcher le deployment (déclenche le rolling update)
kubectl set image deployment/vllm-elodie-32b \
  vllm=registry.interne/vllm:v0.4.4 \
  -n llm-production

# 3. Surveiller le rollout (maxUnavailable: 0 garantit zéro downtime)
kubectl rollout status deployment/vllm-elodie-32b \
  -n llm-production --timeout=600s

# 4. Rollback si problème détecté
kubectl rollout undo deployment/vllm-elodie-32b -n llm-production

# 5. Vérification santé post-update
kubectl get pods -n llm-production -l app=vllm -o wide
curl -H "Authorization: Bearer ${API_KEY}" \
  http://vllm-service/v1/models

Multi-tenancy et quotas

Lorsque plusieurs équipes partagent l'infrastructure LLM, il faut isoler les workloads et contrôler la consommation via ResourceQuota et LimitRange Kubernetes.

# namespace-equipe-finance.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: llm-finance
  labels:
    team: finance
    gpu-access: "true"
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: gpu-quota-finance
  namespace: llm-finance
spec:
  hard:
    requests.nvidia.com/gpu: "2"   # Max 2 GPU pour l'équipe finance
    limits.nvidia.com/gpu: "2"
    requests.cpu: "32"
    requests.memory: "128Gi"
    pods: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: gpu-limits
  namespace: llm-finance
spec:
  limits:
  - type: Container
    default:
      nvidia.com/gpu: "0"
    defaultRequest:
      nvidia.com/gpu: "0"
    max:
      nvidia.com/gpu: "2"
---
# NetworkPolicy : isolation entre namespaces
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: llm-isolation
  namespace: llm-finance
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: llm-gateway  # Seul le gateway peut appeler
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          name: llm-storage  # Accès vectorDB et MinIO

Ce qu'il faut retenir

  • Le NVIDIA Device Plugin + GFD sont indispensables : sans eux, Kubernetes ne voit pas les GPU et ne peut pas les scheduler.
  • La readinessProbe avec initialDelaySeconds: 90 est critique pour éviter que le kubelet tue les pods vLLM pendant leur démarrage.
  • maxUnavailable: 0 dans la stratégie RollingUpdate garantit zéro downtime mais nécessite des nœuds GPU disponibles pour le pod surge.
  • DCGM Exporter en DaemonSet + PrometheusRule pour les alertes XID et VRAM > 90% : ces deux métriques couvrent 80% des incidents de production.
  • ResourceQuota par namespace est le mécanisme de chargeback naturel : mesurez la consommation GPU par équipe, facturez en interne.

Déploiement LLM Kubernetes géré

Nos ingénieurs DevOps déploient, configurent et maintiennent vos LLM souverains sur Kubernetes — on-premise ou cloud privé français. SLA 99.9%, monitoring 24/7, mises à jour sans interruption.

Démarrer le déploiement →

FAQ

Peut-on partager un GPU entre plusieurs pods vLLM ?

Oui, via le time-slicing NVIDIA (configuré dans le Device Plugin avec sharing.timeSlicing.replicas). En time-slicing, un GPU H100 peut apparaître comme 4 GPU logiques. Attention : il n'y a pas d'isolation VRAM entre les slices — si un pod consomme trop, les autres en pâtissent. Pour un LLM de production, évitez le time-slicing sur les nœuds serving. Réservez-le aux nœuds de développement.

Comment éviter que deux replicas vLLM se partagent le même nœud GPU ?

Utilisez un podAntiAffinity avec topologyKey: kubernetes.io/hostname et requiredDuringSchedulingIgnoredDuringExecution (hard constraint). Cela force chaque replica sur un nœud différent. Si vous avez moins de nœuds GPU que de replicas configurés, le pod restera en Pending — le scheduler Kubernetes ne violera pas la contrainte hard.

vLLM supporte-t-il le MIG (Multi-Instance GPU) sur H100 ?

Oui. Sur H100, vous pouvez créer jusqu'à 7 instances MIG (chacune avec une partition dédiée de VRAM et compute). vLLM détecte les instances MIG comme des GPU séparés. Configuration recommandée : 7×10GB pour des modèles 7B, ou 3×20GB pour des modèles 13B. Activez migStrategy: single dans le Device Plugin et configurez les profils MIG avec nvidia-smi mig -cgip -i 0 -p 19,9.