Aller au contenu
Développement medium

vLLM : servir un LLM en production sur GPU

16 min de lecture

logo vllm

vLLM est le moteur d'inférence de référence pour servir un LLM sur GPU en production. Là où Ollama ou llama.cpp visent l'usage local, vLLM est conçu pour le débit : traiter des dizaines de requêtes concurrentes sans effondrer la latence. Ce guide montre comment le déployer avec Docker, l'interroger via son API compatible OpenAI, régler ses paramètres selon votre GPU, et mesurer son débit réel avec l'outil de bench officiel. Public visé : SRE et ingénieurs plateforme disposant d'un GPU NVIDIA. Les mesures de ce guide ont été relevées sur un H100.

  • Comprendre ce que PagedAttention et le continuous batching apportent au serving LLM.
  • Déployer vLLM avec Docker et son API compatible OpenAI.
  • Régler les paramètres clés (--max-model-len, --gpu-memory-utilization, --tensor-parallel-size) selon votre GPU.
  • Mesurer le débit et la latence réels avec vllm bench serve.
  • Durcir le déploiement pour un usage production (clé d'API, redémarrage, reverse proxy).

vLLM exige un GPU NVIDIA : il n'a pas d'équivalent du mode CPU d'Ollama. Pour suivre ce guide, vous avez besoin de :

  • Un GPU NVIDIA architecture Ampere ou plus récente (A100, L40S, H100, RTX 30/40), avec assez de VRAM pour le modèle visé — comptez environ 30 Go pour un modèle 14B en bfloat16.
  • Un driver NVIDIA récent et le NVIDIA Container Toolkit installé, pour exposer le GPU à Docker.
  • Docker avec le plugin Compose.

Côté concepts, ce guide suppose acquises les notions des guides Comprendre l'inférence d'un LLM et Servir un LLM : batching et débitprefill, decode, KV cache, continuous batching, PagedAttention. Si vous n'avez pas de GPU, le guide llama.cpp : serveur d'inférence couvre le serving sur CPU. Pour situer vLLM parmi les autres moteurs, voir le comparatif des backends d'inférence.

Pourquoi vLLM : PagedAttention et continuous batching

Section intitulée « Pourquoi vLLM : PagedAttention et continuous batching »

Servir un LLM, c'est facile pour une requête isolée. Le problème surgit avec la charge concurrente : plusieurs utilisateurs en même temps, des longueurs de prompt variées, des réponses qui se terminent à des moments différents. Deux innovations de vLLM règlent ce problème.

La première est PagedAttention. Pendant la génération, le modèle conserve un cache des clés et valeurs d'attention (le KV cache) pour chaque séquence en cours. Géré naïvement, ce cache fragmente la mémoire GPU : on réserve un bloc contigu par séquence, dimensionné au pire cas, et la VRAM se gaspille. PagedAttention applique au KV cache la même idée que la mémoire virtuelle paginée d'un système d'exploitation : le cache est découpé en pages de taille fixe, allouées à la demande. Résultat, vLLM tient beaucoup plus de séquences simultanées dans la même VRAM.

La seconde est le continuous batching. Un serveur classique forme un lot fixe de requêtes, attend qu'elles soient toutes terminées, puis passe au lot suivant — une requête courte coincée avec des longues attend pour rien. vLLM travaille token par token : à chaque itération, il retire les séquences finies et injecte les nouvelles requêtes dans le lot en cours. Le GPU ne reste jamais inactif, et le débit agrégé grimpe fortement.

L'équipe vLLM publie l'image officielle vllm/vllm-openai, qui démarre directement un serveur d'API compatible OpenAI. C'est l'approche la plus reproductible : aucune gestion de dépendances Python, CUDA ou PyTorch sur l'hôte.

  1. Vérifier que Docker voit le GPU.

    Fenêtre de terminal
    docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi -L

    La sortie doit afficher votre GPU, par exemple GPU 0: NVIDIA H100 PCIe. Si la commande échoue, c'est le NVIDIA Container Toolkit qui manque ou est mal configuré.

  2. Décrire la stack dans un fichier compose.yml.

    Cet exemple sert le modèle Qwen2.5-14B-Instruct, sous licence Apache 2.0 et sans restriction d'accès sur Hugging Face.

    services:
    vllm:
    image: vllm/vllm-openai:latest
    container_name: vllm-server
    restart: unless-stopped
    runtime: nvidia
    ipc: host
    ports:
    - "8000:8000"
    volumes:
    - hf-cache:/root/.cache/huggingface
    command:
    - --model
    - Qwen/Qwen2.5-14B-Instruct
    - --served-model-name
    - qwen2.5-14b
    - --max-model-len
    - "8192"
    - --gpu-memory-utilization
    - "0.90"
    deploy:
    resources:
    reservations:
    devices:
    - driver: nvidia
    count: all
    capabilities: [gpu]
    healthcheck:
    test: ["CMD-SHELL", "curl -fsS http://localhost:8000/health || exit 1"]
    interval: 15s
    timeout: 5s
    retries: 40
    start_period: 600s
    volumes:
    hf-cache:

    Trois points méritent un mot. ipc: host donne au conteneur la mémoire partagée dont PyTorch a besoin. Le volume hf-cache persiste les poids du modèle : ils ne seront téléchargés qu'une fois. Le start_period de 600 s laisse au premier démarrage le temps de télécharger le modèle et de compiler les kernels.

  3. Démarrer le serveur et suivre le chargement.

    Fenêtre de terminal
    docker compose up -d
    docker compose logs -f vllm

    Le premier démarrage est long : vLLM télécharge le modèle (environ 28 Go pour un 14B), le charge en VRAM, puis compile ses kernels CUDA et capture les CUDA graphs. Comptez de dix minutes à un quart d'heure. Cette compilation est mise en cache : les démarrages suivants sont rapides.

  4. Vérifier que le serveur est prêt.

    Fenêtre de terminal
    curl -fsS http://localhost:8000/health
    docker inspect -f '{{.State.Health.Status}}' vllm-server

    Le healthcheck doit afficher healthy. Les logs se terminent alors par Application startup complete.

vLLM expose la même API REST qu'OpenAI. Tout client ou bibliothèque qui sait parler à l'API OpenAI fonctionne en pointant simplement l'URL de base vers votre serveur. L'endpoint /v1/models liste les modèles servis.

Fenêtre de terminal
curl -fsS http://localhost:8000/v1/models

L'endpoint /v1/chat/completions génère une réponse. Le champ model reprend le nom déclaré dans --served-model-name.

Fenêtre de terminal
curl -fsS http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen2.5-14b",
"messages": [
{"role": "user", "content": "En une phrase, quel est le rôle de PagedAttention dans vLLM ?"}
],
"temperature": 0.2,
"max_tokens": 90
}'

La réponse est un objet JSON dont choices[0].message.content contient le texte généré. Comme l'API est compatible, le SDK Python openai fonctionne en changeant base_url :

from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="aucune-cle")
reponse = client.chat.completions.create(
model="qwen2.5-14b",
messages=[{"role": "user", "content": "Résume le continuous batching en deux phrases."}],
temperature=0.2,
)
print(reponse.choices[0].message.content)

Les valeurs par défaut conviennent pour démarrer, mais quatre paramètres déterminent le comportement réel du serveur. Le tableau ci-dessous résume leur effet ; les sections suivantes du guide les mettent en pratique.

ParamètreRôleConseil
--max-model-lenLongueur de contexte maximale (tokens)Réduisez-le si la VRAM manque ; chaque token coûte du KV cache
--gpu-memory-utilizationFraction de VRAM allouée à vLLM0.90 est un bon défaut ; baissez si le GPU est partagé
--max-num-seqsSéquences concurrentes dans un lotMontez-le pour le débit, baissez-le pour la latence
--tensor-parallel-sizeNombre de GPU sur lesquels répartir le modèle1 par GPU unique ; N pour un modèle trop gros pour une seule carte

Le compromis central est entre contexte et concurrence. La VRAM non occupée par les poids du modèle sert de KV cache partagé entre les séquences. Un --max-model-len élevé réserve plus de cache par séquence, donc moins de séquences simultanées. Inversement, un contexte court libère du cache pour traiter plus de requêtes en parallèle.

Annoncer « c'est rapide » ne vaut rien sans mesure. vLLM intègre vllm bench serve, un outil qui injecte une charge contrôlée et rapporte débit et latence. Il distingue deux métriques de latence essentielles : le TTFT (Time To First Token, délai avant le premier token, ce que l'utilisateur perçoit comme « ça démarre ») et le TPOT (Time Per Output Token, cadence de génération ensuite).

La commande suivante envoie 200 requêtes de 1024 tokens d'entrée et 256 de sortie. Le --tokenizer pointe le dépôt Hugging Face réel, car le nom servi (qwen2.5-14b) n'est pas un identifiant de modèle valide pour charger le tokenizer.

Fenêtre de terminal
docker exec vllm-server vllm bench serve \
--backend vllm \
--base-url http://localhost:8000 \
--model qwen2.5-14b \
--tokenizer Qwen/Qwen2.5-14B-Instruct \
--dataset-name random \
--num-prompts 200 \
--random-input-len 1024 \
--random-output-len 256 \
--request-rate inf

L'option --request-rate change tout. À inf, les 200 requêtes partent d'un coup : on mesure le débit plafond. À une valeur finie (par exemple 8), elles arrivent progressivement : on mesure la latence en service réaliste. Voici les deux mesures relevées sur un H100 PCIe 80 Go avec vLLM 0.21 et Qwen2.5-14B-Instruct.

ScénarioDébit totalTTFT médianTPOT médian
Débit plafond (--request-rate inf)8 354 tok/s7 983 ms74,6 ms
Charge modérée (--request-rate 8)7 916 tok/s230 ms53,2 ms

La lecture est instructive. Entre les deux scénarios, le débit total bouge à peine (8 354 contre 7 916 tokens/seconde), mais le TTFT médian s'effondre de près de 8 secondes à 230 millisecondes. À pleine charge, les requêtes font la queue : le débit est maximal mais chaque utilisateur attend. À charge modérée, le continuous batching maintient un débit quasi identique tout en gardant une latence faible.

La conclusion pour le dimensionnement est nette : viser un taux de requêtes en deçà du plafond préserve la latence perçue. Le bon point de fonctionnement n'est pas le débit maximal, mais le débit le plus élevé qui tient votre budget de TTFT.

Le compose.yml ci-dessus est déjà proche d'un déploiement réel. La directive restart: unless-stopped relance le conteneur après un crash ou un redémarrage de l'hôte, à condition que le service Docker soit lui-même activé au boot (sudo systemctl enable docker). Inutile d'écrire un service systemd dédié : Docker assure le cycle de vie.

Trois points de sécurité sont non négociables. D'abord, exiger une clé d'API : ajoutez --api-key à la commande vLLM, et chaque requête devra porter l'en-tête Authorization: Bearer <clé>. Cette clé est un secret — passez-la par un fichier .env non versionné. Ensuite, ne jamais exposer le port 8000 directement sur Internet : un serveur vLLM ouvert est une ressource de calcul GPU offerte à n'importe qui. Enfin, placez vLLM derrière un reverse proxy qui termine le TLS et applique un rate limiting.

Les incidents au démarrage de vLLM se résument à quelques causes récurrentes. Le tableau relie le symptôme observé à sa cause et à la correction.

SymptômeCause probableSolution
CUDA out of memory au chargementModèle trop gros pour la VRAMBaisser --max-model-len, ou répartir avec --tensor-parallel-size, ou choisir un modèle quantifié
is not a valid model identifierModèle à accès restreint sans tokenAccepter la licence sur Hugging Face et fournir HF_TOKEN
Démarrage très long la première foisTéléchargement du modèle + compilation des kernelsNormal ; le cache rend les démarrages suivants rapides
failed to set up container networkingRéseau Docker non recréé après un redémarrage du démonRelancer docker compose up -d
GPU absent dans le conteneurNVIDIA Container Toolkit manquant ou non configuréInstaller le toolkit et relancer nvidia-ctk runtime configure
Latence qui explose sous chargeTrop de requêtes simultanéesBaisser --max-num-seqs ou lisser le trafic en amont

Pour observer le serveur en fonctionnement, docker compose logs -f vllm affiche chaque requête traitée, et nvidia-smi montre l'occupation VRAM et l'utilisation GPU en temps réel.

  • vLLM sert un LLM sur GPU NVIDIA avec un objectif de débit, pas d'usage local mono-utilisateur.
  • PagedAttention pagine le KV cache et continuous batching garde le GPU occupé : ensemble, ils maximisent le débit concurrent.
  • L'image Docker vllm/vllm-openai donne un serveur compatible OpenAI sans gérer CUDA sur l'hôte.
  • Le premier démarrage est long (téléchargement + compilation) ; le cache rend les suivants rapides.
  • vllm bench serve distingue TTFT et TPOT : le bon point de fonctionnement vise le débit qui respecte votre budget de latence, pas le débit maximal.
  • En production, clé d'API, reverse proxy et TLS sont obligatoires — un GPU exposé se fait détourner.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn