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
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 DCGM | Description | Seuil d'alerte |
|---|---|---|
| DCGM_FI_DEV_GPU_UTIL | Utilisation GPU (%) | < 20% sur 30min (idle) / > 95% sur 5min (saturation) |
| DCGM_FI_DEV_FB_USED | VRAM utilisée (MiB) | > 90% de la VRAM totale |
| DCGM_FI_DEV_POWER_USAGE | Consommation (W) | > 95% TDP (ex. 710W pour H100) |
| DCGM_FI_DEV_GPU_TEMP | Température (°C) | > 83°C (throttling imminent) |
| DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL | Débit NVLink (MB/s) | Chute > 50% (défaut liaison) |
| DCGM_FI_DEV_XID_ERRORS | Erreurs 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
readinessProbeavecinitialDelaySeconds: 90est critique pour éviter que le kubelet tue les pods vLLM pendant leur démarrage. maxUnavailable: 0dans 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.