Moteurs d'inférence OSS — vLLM, TGI, llama.cpp, SGLang, MLC-LLM
TL;DR Quatre familles d'engines dominent en 2026 : vLLM (PagedAttention, throughput champion, multi-LoRA), TGI (Hugging Face, intégration HF Hub native, prod-grade), SGLang (RadixAttention, leader pour structured output + agents complexes), llama.cpp (CPU/edge/Apple Silicon, GGUF). À côté : MLC LLM (cross-platform compile), TensorRT-LLM (NVIDIA, perf max mais lock-in). Pour 90% des cas FR self-host : vLLM sur Scaleway/OVH GPU. Critères de choix : throughput vs latence, multi-LoRA, structured output, prefix caching, GPU vs CPU. Prix GPU FR : H100 ≈ 3-3.5€/h, A100 ≈ 2.2-2.4€/h. Throughput typique : Mistral Small sur 1× H100 = 300-600 req/s sur prompts courts.
🧠 Mental model
┌─────────────────────────────────────┐
│ Tu veux servir un LLM open-source │
└────────────────┬────────────────────┘
│
┌────────────────────┼─────────────────────────────┐
│ │ │
┌────────▼────────┐ ┌────────▼─────────┐ ┌────────▼─────────┐
│ GPU datacenter │ │ GPU on-prem │ │ CPU / Edge │
│ Scaleway, OVH, │ │ (banque, santé) │ │ (laptop, mobile)│
│ AWS, GCP │ │ │ │ │
└────────┬────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
┌────────▼────────────────────▼─────────┐ ┌────────▼─────────┐
│ vLLM (throughput champion) │ │ llama.cpp │
│ TGI (HF native, prod-grade) │ │ GGUF quant │
│ SGLang (structured, agents) │ │ Ollama wrapper │
│ TensorRT-LLM (NVIDIA stack max perf) │ │ MLX (Apple) │
└───────────────────────────────────────┘ └──────────────────┘
PagedAttention (vLLM) :
┌─────────────────────────────────────────────────────────┐
│ KV cache géré comme une mémoire virtuelle paginée │
│ Pas de fragmentation, batching dynamique, prefix shared │
│ → throughput × 5-10 vs naïve HF generate() │
└─────────────────────────────────────────────────────────┘Analogie : comparé à l'inférence naïve (HF generate()), un moteur comme vLLM c'est passer d'une cuisine domestique (1 plat à la fois) à une cuisine de restaurant gastro : batching dynamique de plusieurs commandes, partage des préparations communes (prefix cache), mise en place efficace (paged KV cache).
🛠️ Code minimal
vLLM en server OpenAI-compatible
# 1× H100, Mistral Small, batching dynamique + prefix caching
python -m vllm.entrypoints.openai.api_server \
--model mistralai/Mistral-Small-Instruct-2502 \
--served-model-name mistral-small \
--tensor-parallel-size 1 \
--max-model-len 32768 \
--gpu-memory-utilization 0.92 \
--dtype bfloat16 \
--enable-prefix-caching \
--enable-lora --max-loras 4 --max-lora-rank 64 \
--port 8000# client.py — exactement la même API qu'OpenAI
from openai import OpenAI
client = OpenAI(base_url="http://vllm-svc:8000/v1", api_key="not-needed")
resp = client.chat.completions.create(
model="mistral-small",
messages=[{"role": "user", "content": "..."}],
extra_body={"lora_modules": "cv-extractor-v1"}, # multi-LoRA serving
)TGI (Hugging Face)
docker run --gpus all -p 8080:80 \
-v $HOME/data:/data \
ghcr.io/huggingface/text-generation-inference:3.0 \
--model-id mistralai/Mistral-Small-Instruct-2502 \
--max-input-length 8192 \
--max-total-tokens 16384 \
--quantize bitsandbytes-nf4llama.cpp / Ollama
# llama.cpp direct (CPU + Metal sur Mac)
./llama-server -m models/mistral-small-Q5_K_M.gguf \
--port 8080 --ctx-size 8192 --n-gpu-layers 35
# Ollama (wrapper convivial)
ollama serve
ollama pull mistral-small:24b-instruct-2502-q5_K_M
curl http://localhost:11434/api/chat -d '{"model":"mistral-small","messages":[...]}'SGLang (structured output, agents)
import sglang as sgl
@sgl.function
def extract_invoice(s, text):
s += "Extrait les données de cette facture: " + text + "\n"
s += "Numéro: " + sgl.gen("num", regex=r"FAC-\d{6}") + "\n"
s += "Montant: " + sgl.gen("amount", regex=r"\d+[\.,]\d{2}") + "\n"
s += "Date: " + sgl.gen("date", regex=r"\d{2}/\d{2}/\d{4}")
# Le moteur garantit le format via grammar-constrained decoding🎬 Cas d'usage concrets
1. Banque privée Paris — Mistral Large self-host souveraineté
Banque privée 800 collaborateurs. Politique : aucune donnée client ne quitte le datacenter français. Stack : vLLM + Mistral Large 2 (123B) sur 4× H100 (NVLink) chez OVH Strasbourg. Throughput : 80 req/s, latence p50 1.8s. Multi-LoRA pour 3 départements (banque privée, IPO, M&A) avec adapters distincts. Coût : 11k€/mois GPU + 2k€/mois ops = 13k€/mois pour ~ 5M req/mois (équivalent ~ 60k€/mois en API GPT-4o). ROI : 565k€/an.
2. Llama 4 pour assistant interne ministère
Ministère (DINUM) déploie un assistant Llama 4 Maverick (17B) pour 12 000 agents (rédaction notes, recherche dans circulaires, synthèse). Stack : TGI sur infra SecNumCloud chez Outscale (FR). 8× A100 répartis sur 4 nœuds, autoscale via Kubernetes. Authentification SSO ministériel + audit log complet. Coût total annuel : 280k€ infra + 120k€ ops = 400k€ pour 12K utilisateurs = 33€/agent/an. Comparaison Microsoft Copilot M365 : 350€/agent/an → 4M€/an.
3. llama.cpp embarqué dans app desktop juridique (offline)
Editeur logiciel juridique (Wolters Kluwer-like) intègre Mistral Small Q4_K_M (14 GB GGUF) directement dans son app desktop Windows/Mac via llama.cpp. Cas d'usage : avocats en déplacement (TGV, audience) sans réseau. Modèle tourne sur M3 Pro / RTX 4060 mobile à 15-25 tok/s. Avantages : zéro coût récurrent, data 100% locale, conformité maximale. Limite : qualité < Sonnet, mais suffisant pour assistance rédactionnelle.
🛠️ Exemple end-to-end — vLLM Mistral Small sur Scaleway H100 avec auto-scaling
Contexte : startup legaltech FR, 30k req/jour, doit servir un Mistral Small fine-tuné (LoRA mergée). Objectif : haute dispo, autoscale, coût maîtrisé, benchmark vs API Mistral.
Étape 1 — Architecture
┌──────────────────────────────────────┐
│ Frontend (Next.js sur Vercel) │
└──────────────────┬───────────────────┘
│
┌──────────────────▼───────────────────┐
│ API Gateway (Scaleway Functions) │
│ - auth JWT │
│ - rate limit │
│ - logging audit │
└──────────────────┬───────────────────┘
│
┌──────────────────▼───────────────────┐
│ Load Balancer (Scaleway LB) │
└──────┬────────────────────┬──────────┘
│ │
┌────────▼────────┐ ┌────────▼────────┐
│ vLLM Pod 1 │ │ vLLM Pod 2 │ ← HPA autoscale
│ H100 80GB │ │ H100 80GB │ 1-4 replicas
│ Mistral-S FT │ │ Mistral-S FT │
└─────────────────┘ └─────────────────┘
│ │
┌──────▼────────────────────▼──────┐
│ Prometheus + Grafana + Loki │
└──────────────────────────────────┘Étape 2 — Image Docker
FROM vllm/vllm-openai:v0.7.2
# Modèle pré-baked (merge LoRA done in CI)
COPY models/mistral-small-legal-merged /models/legal
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8000
ENTRYPOINT ["/entrypoint.sh"]# entrypoint.sh
#!/bin/bash
exec python -m vllm.entrypoints.openai.api_server \
--model /models/legal \
--served-model-name legal-assistant \
--max-model-len 16384 \
--gpu-memory-utilization 0.90 \
--dtype bfloat16 \
--enable-prefix-caching \
--max-num-seqs 256 \
--port 8000Étape 3 — Manifests Kubernetes (Scaleway Kapsule)
# vllm-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-legal
namespace: ai-inference
spec:
replicas: 2
selector:
matchLabels: {app: vllm-legal}
template:
metadata:
labels: {app: vllm-legal}
spec:
nodeSelector:
k8s.scaleway.com/pool-name: gpu-h100
tolerations:
- key: nvidia.com/gpu
operator: Exists
containers:
- name: vllm
image: rg.fr-par.scw.cloud/legal/vllm-legal:1.4.0
resources:
limits:
nvidia.com/gpu: "1"
memory: 80Gi
ports: [{containerPort: 8000}]
readinessProbe:
httpGet: {path: /health, port: 8000}
initialDelaySeconds: 90
periodSeconds: 10
livenessProbe:
httpGet: {path: /health, port: 8000}
initialDelaySeconds: 120
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: vllm-legal
spec:
selector: {app: vllm-legal}
ports: [{port: 80, targetPort: 8000}]
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-legal-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-legal
minReplicas: 1
maxReplicas: 4
metrics:
- type: Pods
pods:
metric:
name: vllm_num_requests_running # exposé par vLLM metrics
target:
type: AverageValue
averageValue: "80"Étape 4 — Monitoring
# servicemonitor.yml (Prometheus operator)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: vllm-legal
spec:
selector:
matchLabels: {app: vllm-legal}
endpoints:
- port: http
path: /metrics
interval: 15sDashboards Grafana clés : vllm:request_success_total, vllm:time_to_first_token_seconds, vllm:e2e_request_latency_seconds, vllm:gpu_cache_usage_perc.
Étape 5 — Benchmark vs API Mistral
Test charge avec vllm benchmarks/benchmark_serving.py sur 5000 prompts du dataset prod (avg 800 tokens in, 400 out).
Setup | TTFT p50 | TTFT p95 | Throughput | Coût/1K req
---------------------------------|----------|----------|------------|-------------
Mistral API mistral-small | 340ms | 780ms | - | 3.20€
vLLM self-host 1× H100 | 180ms | 410ms | 380 req/s | 0.62€
vLLM self-host 2× H100 (LB) | 175ms | 395ms | 720 req/s | 0.65€Break-even : 230K req/mois (en dessous, API Mistral plus avantageux car GPU H100 a un coût plancher de ~ 2 300€/mois 24/7).
Étape 6 — Plan de continuité
- Fallback : si tous les pods vLLM down, route automatique vers Mistral API (clé secrète stockée Vault).
- Sauvegarde modèle : LoRA adapter + base model versionnés sur S3 Scaleway, possibilité de re-déployer < 15 min.
- DR multi-AZ : 1 pod par-1, 1 pod par-2 (Paris et Amsterdam).
- Runbook on-call : si OOM, scale down
max-num-seqs, kill pods. Si latence p95 > 2s, scale up replicas.
🎯 Patterns courants
- vLLM par défaut : 90% des projets se trouvent bien avec vLLM. Bench les autres seulement si besoin spécifique.
- Prefix caching ON :
--enable-prefix-cachingpour partager system prompts longs entre requêtes, gain ×3 sur prompts répétitifs. max-num-seqsà régler : trop bas = sous-utilisation, trop haut = OOM. Commencer à 128 et monter.- Tensor parallelism seulement si le modèle ne tient pas sur 1 GPU.
--tensor-parallel-size 4ajoute de la latence sur les modèles < 30B. - Quantization : AWQ ou GPTQ 4-bit pour servir Llama 70B sur 1× H100. Légère perte qualité (1-2%).
- Speculative decoding : vLLM 0.7+ supporte un draft model (petit) qui propose tokens validés par le big model. Gain 1.5-2× latence sur outputs longs.
- Continuous batching : c'est ce que vLLM fait nativement, ne pas le désactiver.
- GGUF + llama.cpp pour CPU ou Mac : Q5_K_M est le meilleur ratio qualité/taille.
- SGLang quand structured output critique : grammar-constrained decoding garanti 100% format JSON/regex.
🔄 Versions & écosystème 2026
- vLLM 0.7 (mai 2026) : multi-LoRA prod-ready, speculative decoding, prefix caching, AWQ/GPTQ/FP8 quant, support Llama 4, DeepSeek-V3, Mistral.
- TGI 3.0 : intégration HF Hub native, support TPU, meilleure UX que vLLM mais throughput légèrement inférieur.
- SGLang 0.4 : RadixAttention (prefix tree shared), leader sur agents complexes et workflows multi-tour, supporte chunked prefill.
- llama.cpp : standard de facto pour CPU/edge, support Metal (Apple), Vulkan, ROCm. GGUF universel.
- MLC LLM : compile pour iOS, Android, web (WebGPU), Vulkan.
- TensorRT-LLM 0.18 : performance max sur Hopper/Blackwell mais lock-in NVIDIA, complexe à compiler.
- LMDeploy (Shanghai AI Lab) : alternative chinoise crédible, support InternLM, Qwen.
- Modal / RunPod / Lambda : platforms managed pour héberger vLLM sans gérer Kubernetes.
Prix GPU FR 2026 (référence) :
| GPU | Scaleway | OVH | AWS | Outscale | Modal |
|---|---|---|---|---|---|
| L4 24GB | 0.90€/h | 1.05€/h | 1.50€/h | - | 1.20$/h |
| L40S 48GB | 1.60€/h | 1.85€/h | 2.40€/h | 1.95€/h | 2.10$/h |
| A100 80GB | 2.40€/h | 2.20€/h | 4.10€/h | 2.50€/h | 3.10$/h |
| H100 80GB | 3.20€/h | 3.50€/h | 6.50€/h | 3.40€/h | 5.20$/h |
| H200 141GB | 4.50€/h | - | 8.00€/h | - | 6.80$/h |
⚠️ Pitfalls
- Démarrer un pod vLLM = 90-120s (chargement modèle). Probes readiness
initialDelaySeconds: 90minimum. - GPU OOM silencieux :
--gpu-memory-utilization 0.95peut crasher selon le modèle. Rester à 0.88-0.92. max-model-lentrop élevé : réserve du KV cache pour rien. Mettre la valeur réelle nécessaire.- Mixer dtype bf16 et fp16 : certaines couches FT-LoRA en fp16, base en bf16 → outputs corrompus. Toujours uniformiser.
- Pas de tokenizer côté client :
prompt_tokensmal compté = facturation client incorrecte. Toujours utiliser tokenizer du modèle. - llama.cpp + context long : Q4 quant + ctx 32K = qualité dégrade beaucoup. Préférer Q5_K_M ou Q6_K si VRAM le permet.
- TGI ne supporte plus tous les modèles : depuis v3.0, focus HF-friendly. Vérifier compat avant migration.
- Multi-LoRA et latency spike : premier appel à un adapter = chargement, +200ms. Pré-warmer les adapters au démarrage.
- Oublier de monitorer GPU util : 30% GPU util = sous-batching. Augmenter
max-num-seqsou consolider trafic. - Pas de circuit breaker en front : un pod vLLM OOM → 500 cascades → retries → DDoS interne. Toujours timeout client + retry exponential backoff.
💰 Pricing / ROI client
Coût mensuel d'un setup vLLM prod typique (2× H100 Scaleway) :
| Poste | Coût/mois |
|---|---|
| 2× H100 24/7 | 4 660€ |
| LB + bande passante | 200€ |
| Stockage modèles S3 | 50€ |
| Monitoring (Grafana Cloud) | 150€ |
| Total infra | 5 060€ |
Pour 230K req/mois minimum → break-even vs API Mistral. Pour 1M req/mois → économie ~ 20k€/mois vs API.
Tarification mission "self-host LLM" (freelance senior FR) :
- Audit infra + choix engine : 5j × 1200€ = 6 000€
- Setup K8s + vLLM + LoRA merge : 10j × 1200€ = 12 000€
- Monitoring + alerting : 4j × 1200€ = 4 800€
- Bench + tuning + handover : 5j × 1200€ = 6 000€
- Total : 28 800€ + run rate 1.5k€/mois pour maintenance
🧪 Testing / Eval
vllm benchmark_serving.py: outil officiel pour latence/throughput.locustouk6: simulation utilisateurs réels, ramp-up progressif.- Eval qualité avant rollout : 200 prompts hold-out, comparer outputs vLLM vs API référence.
- Chaos engineering : kill un pod en plein run, vérifier que LB route et que retry client fonctionne.
- GPU stress test :
nvidia-smi dmonpendant 1h de charge max, vérifier pas de throttling thermal. - Audit log : chaque requête loggée avec input/output/latence/user_id pour replay et debug.
🔁 Quand utiliser / éviter
vLLM quand :
- Throughput max sur GPU (la plupart des cas).
- Multi-LoRA serving (SaaS multi-tenants).
- Tu connais Python/K8s.
TGI quand :
- Tu vis dans l'écosystème Hugging Face (HF Hub, HF Spaces).
- Tu veux UX simple, moins de tuning manuel.
SGLang quand :
- Structured output critique (JSON/regex strict, function calling complexe).
- Agents avec branches conditionnelles, RadixAttention apporte.
llama.cpp quand :
- CPU only ou Apple Silicon.
- Edge / desktop / mobile / offline.
- Modèles ≤ 30B Q5_K_M.
Évite self-host quand :
- Volume < 100K req/mois : API providers moins chers.
- Pas d'équipe ops compétente : la complexité K8s + GPU + monitoring est réelle.
- Modèle frontier nécessaire : un frontier propriétaire (Claude Opus 4.8, GPT-class) reste au-dessus de tout OSS pour le reasoning complexe et l'extended thinking. Pour ces appels, garde un fallback API (voir le pattern hybride ci-dessous).
🧭 Tableau de décision engine (la grille qu'un staff utilise vraiment)
Le choix se réduit rarement à « lequel est le plus rapide ». Il se fait sur 6 axes que tu pondères selon le contrat client. Voici la grille mentale comparative :
| Axe | vLLM | TGI | SGLang | llama.cpp | TensorRT-LLM |
|---|---|---|---|---|---|
| Throughput batch GPU | ★★★★★ | ★★★★ | ★★★★★ | ★★ | ★★★★★ |
| Latence p50 mono-requête | ★★★ | ★★★ | ★★★★ | ★★ | ★★★★★ |
| Multi-LoRA chaud | ★★★★★ | ★★★ | ★★★★ | ✗ | ★★ (compile/adapter) |
| Structured / grammar decoding | ★★★ (xgrammar) | ★★★ | ★★★★★ (RadixAttn + jump-forward) | ★★ (GBNF) | ★★ |
| CPU / edge / Apple | ✗ | ✗ | ✗ | ★★★★★ | ✗ |
| Coût d'exploitation (setup/ops) | ★★★★ | ★★★★★ | ★★★ | ★★★★★ | ★★ (compile lock-in) |
Comment lire la grille : un même client peut avoir besoin de deux engines. Exemple récurrent : SGLang sur le chemin « extraction structurée stricte » (factures, KYC) où le format JSON/regex est un SLA contractuel, et vLLM sur le chemin « chat libre » où seul le throughput compte. Ne cherche pas l'engine universel ; segmente le trafic par profil de requête.
Le piège du benchmark unique : un throughput « 600 req/s » n'a aucun sens hors distribution de longueurs. Le facteur dominant est (input_len, output_len). Un engine champion sur prompts courts (in 200 / out 50) peut être médiocre sur du long-contexte (in 8000 / out 2000) car le KV cache explose et le batch size effectif s'effondre. Toujours benchmarker sur ta vraie distribution de tokens, pas sur ShareGPT par défaut.
🔬 Structured output : ce qui se passe vraiment sous le capot
C'est la zone où un senior se démarque d'un junior qui copie response_format. La contrainte de format ne se fait pas par re-prompting — elle se fait par masquage du vocabulaire à chaque pas de décodage.
À chaque token généré :
1. Le modèle produit un vecteur de logits (taille = vocab, ~32k-128k).
2. Une grammaire (regex/JSON-schema compilée en automate) calcule
l'ensemble des tokens LÉGAUX dans l'état courant.
3. Tous les autres logits → -inf (masque)
4. On échantillonne uniquement parmi les tokens valides.
→ Le format est garanti par construction, pas par espoir.Trois implémentations à connaître en entretien :
- Outlines / xgrammar (utilisé par vLLM) : compile le schéma en FSM, masque les logits. Coût : compilation de la grammaire (cache-la si réutilisée).
- RadixAttention + jump-forward decoding (SGLang) : quand la grammaire impose plusieurs tokens déterministes d'affilée (ex. les guillemets et la clé
"montant":), SGLang les émet d'un coup sans appeler le modèle → gain réel de latence, pas seulement de validité. C'est la raison technique pour laquelle SGLang gagne sur le structured. - GBNF (llama.cpp) : grammaire BNF, même principe, côté edge.
Tradeoff à énoncer : le masquage garantit la syntaxe (le JSON parse) mais pas la sémantique (le montant peut être faux). Tu as toujours besoin d'une validation métier en aval. Et une grammaire trop contraignante peut dégrader la qualité (le modèle est forcé dans un chemin qu'il n'aurait pas choisi). Mesure-le.
🔭 Observabilité — les 4 métriques qui pilotent une flotte GPU
Un dashboard vLLM qui n'affiche que « req/s » est un dashboard junior. Les quatre signaux qui prédisent réellement la santé :
| Métrique vLLM | Ce qu'elle révèle | Seuil d'alerte typique |
|---|---|---|
vllm:gpu_cache_usage_perc | Saturation KV cache → preemption imminente | > 90% soutenu → preemptions, latence chaotique |
vllm:num_requests_waiting | File d'attente, batch saturé | > 0 soutenu → scale up replicas |
vllm:time_to_first_token_seconds | Ressenti UX (streaming) | p95 > 1s → enquête prefill |
vllm:num_preemptions_total | KV cache a évincé des séquences (recompute) | toute hausse = perte de throughput |
Le signal sous-estimé : num_preemptions. Quand le KV cache est plein, vLLM préempte des requêtes en cours et les recalcule plus tard. Le throughput affiché reste « correct » mais tu brûles du compute en double et la latence p99 part en vrille. Un junior augmente gpu-memory-utilization pour « avoir plus de mémoire » et aggrave tout ; un senior baisse max-num-seqs ou ajoute une replica pour faire retomber les preemptions à zéro. La preemption est le canari de la mine GPU.
Coût observé, pas estimé : logge pour chaque requête prompt_tokens, completion_tokens, l'engine, l'adapter LoRA et la latence. Le coût réel par requête se reconstruit a posteriori ; ne fais jamais confiance à une moyenne théorique pour facturer un client.
🔌 Le pattern hybride OSS + API frontier (à avoir en tête)
Le faux dilemme « self-host ou API ». En prod sérieuse c'est un routeur :
# Routeur de complexité : OSS pour le volume, frontier pour le hard reasoning.
# vLLM/SGLang exposent une API OpenAI-compatible → un seul SDK côté client.
async def route(task: Task) -> str:
if task.requires_deep_reasoning or task.is_high_stakes:
# fallback frontier (Anthropic) : extended thinking ADAPTATIF.
# NB: la forme budget_tokens est SUPPRIMÉE — on utilise thinking adaptatif
# + output_config effort (low/medium/high).
return await call_frontier(task) # ex. claude-opus-4-8
# 95% du trafic : modèle OSS self-host, coût marginal ~ électricité GPU
return await call_vllm(task) # mistral-small fine-tunéRègle de dimensionnement : si > 90% du trafic est « routine », le self-host OSS porte le volume et l'API frontier ne traite que la longue traîne difficile — tu captures l'économie du self-host et la qualité frontier là où elle compte. C'est l'archi qui gagne les missions.
🏋️ Exercices
Progression : de « ça tourne » à « défends le chiffre devant le CTO et casse-le avant qu'il ne te casse ».
Exercice 1 — Lancer et instrumenter (échauffement)
Objectif : démarrer un vLLM OpenAI-compatible sur un Mistral Small (ou un 7B si pas de GPU, sur RunPod/Modal), et scrapper /metrics dans un Prometheus local. Indice/Solution : vllm serve mistralai/Mistral-Small-Instruct-2502 --enable-prefix-caching --port 8000, puis un prometheus.yml qui scrape :8000/metrics. Vérifie que tu vois vllm:gpu_cache_usage_perc bouger sous charge. Sans cette métrique tu pilotes à l'aveugle.
Exercice 2 — Le benchmark honnête
Objectif : prouver (ou démentir) le « 380 req/s » de ce fichier sur ta distribution de tokens, pas sur ShareGPT. Indice/Solution : extrais 1000 prompts réels, mesure leur distribution (input_len, output_len), génère un dataset synthétique de même profil, lance benchmark_serving.py. Fais varier --max-num-seqs (64 → 512) et trace throughput vs p95. Tu dois trouver le coude où p95 explose pendant que le throughput plafonne : c'est ton max-num-seqs de prod. Défends le chiffre avec la courbe, pas avec un nombre nu.
Exercice 3 — Multi-LoRA sans cold-start
Objectif : servir 3 adapters LoRA distincts sur une seule replica vLLM, sans que le premier appel à un adapter froid paie +200ms. Indice/Solution : --enable-lora --max-loras 4, charge les 3 adapters, puis envoie un prompt de warm-up à chacun au démarrage (dans l'entrypoint, après le /health ready). Mesure la latence du 1er vrai appel avant/après warm-up. Bonus : route l'adapter via le champ model de la requête et vérifie l'isolation (l'adapter A ne fuit pas dans la réponse de l'adapter B).
Exercice 4 — Structured output : garantie vs qualité
Objectif : montrer empiriquement que la contrainte grammaticale garantit la syntaxe mais peut dégrader la sémantique. Indice/Solution : sur 200 factures, compare (a) prompt libre + parsing best-effort, (b) JSON-schema contraint via vLLM/xgrammar, (c) SGLang avec regex + jump-forward. Mesure trois choses : % JSON parsable, exactitude des champs (vs ground truth), et latence p50. Tu dois observer que (b/c) atteignent 100% de parsabilité mais que l'exactitude n'augmente pas forcément — parfois baisse si la grammaire force des champs. Conclusion à écrire : la grammaire ne remplace pas la validation métier.
Exercice 5 — Casse-le : la tempête de preemptions (production-grade)
Objectif : provoquer délibérément l'effondrement KV cache, l'observer, puis le réparer sans toucher au GPU. Indice/Solution : monte --gpu-memory-utilization 0.97 et --max-num-seqs 512, envoie une charge avec des outputs longs (max_tokens 2000). Observe vllm:num_preemptions_total grimper et la p99 exploser. Répare : baisse max-num-seqs, réserve de la marge KV, et ajoute un max_tokens côté gateway. Le livrable est un mini post-mortem : symptôme (p99), cause (preemption), fix (back-pressure), prévention (alerte sur preemptions != 0).
Exercice 6 — Défends le break-even devant le CTO (architecture)
Objectif : construire le modèle de coût complet self-host vs API et déterminer le volume de bascule, avec le pattern hybride. Indice/Solution : tableur avec coût plancher GPU 24/7 (2× H100 ≈ 5 060€/mois ici), coût marginal par requête self-host (~0), prix API par requête, et taux d'utilisation GPU réel (pas 100% !). Trace le coût total des deux courbes en fonction du volume ; le croisement est ton break-even (ici ~230K req/mois). Puis ajoute la couche hybride : 90% OSS + 10% frontier, et montre que le coût mixte bat les deux extrêmes. Sois prêt à justifier l'hypothèse d'utilisation GPU — c'est là qu'un CTO attaque.
🎤 En entretien
- « Pourquoi vLLM est-il plus rapide qu'un
model.generate()HF naïf ? » — PagedAttention gère le KV cache comme de la mémoire virtuelle paginée (zéro fragmentation) + continuous batching (on ajoute/retire des séquences dans le batch à chaque step au lieu d'attendre la fin du batch) → throughput ×5-10. - « Comment garantis-tu un output 100% JSON valide ? » — Pas par re-prompting : par décodage contraint par grammaire (logits masqués à -inf sur les tokens illégaux à chaque pas). Mais ça garantit la syntaxe, pas la sémantique → validation métier en aval obligatoire.
- « Self-host ou API : comment tu tranches ? » — Modèle de coût : le self-host a un coût plancher GPU 24/7, l'API un coût linéaire au volume ; le break-even est typiquement autour de 200-300K req/mois sur une instance saturée. En dessous, API. Au-dessus, hybride : OSS pour le volume routine, API frontier (ex. claude-opus-4-8) pour la longue traîne « hard reasoning ».
- « Ta p99 de latence explose alors que le throughput a l'air bon. Diagnostic ? » — Premier réflexe :
vllm:num_preemptions_totaletgpu_cache_usage_perc. Si le KV cache sature, vLLM préempte et recalcule → throughput moyen OK mais p99 chaotique. Fix : baissermax-num-seqsou ajouter une replica, pas montergpu-memory-utilization.
🔗 Liens
- vLLM docs et benchmarks (github.com/vllm-project/vllm)
- TGI (github.com/huggingface/text-generation-inference)
- SGLang (github.com/sgl-project/sglang)
- llama.cpp (github.com/ggerganov/llama.cpp)
- Anyscale blog — "Continuous batching" article fondateur
- ArtificialAnalysis.ai — benchmarks comparatifs perf
- Fichier voisin :
02-lora-peft.md(modèles à servir) - Fichier voisin :
05-self-host.md(production K8s + sécurité) - Fichier voisin :
06-mlx-local.md(edge / Mac local)