vLLM : Servir des LLM à haute performance
Qu’est-ce que vLLM ?
vLLM (Very Large Language Model) est un framework open-source d’inférence haute performance pour les LLM. Développé par UC Berkeley, il utilise des techniques innovantes comme PagedAttention et continuous batching pour servir des modèles avec un throughput 10-24x supérieur aux implémentations classiques.
Compatible OpenAI API : Drop-in replacement pour vos applications existantes.
Servir des LLM en production est un défi majeur : latence élevée, faible throughput, coûts GPU astronomiques. Les frameworks traditionnels (HuggingFace Transformers, PyTorch) ne sont pas optimisés pour l’inférence à grande échelle.

Le problème : goulots d’étranglement de l’inférence
# Approche naïve avec HuggingFace
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
# Problème 1 : Batch size limité
# → Mémoire GPU saturée rapidement par KV cache
# Problème 2 : Pas de batching dynamique
# → GPU idle pendant les réponses courtes
# Problème 3 : Gestion inefficace de la mémoire
# → Fragmentation, allocation statique
# Résultat : Throughput faible (~10-20 req/s sur A100)
vLLM résout ces problèmes :
| Métrique | HuggingFace | vLLM | Amélioration |
|---|---|---|---|
| Throughput | 10-20 req/s | 100-250 req/s | ✅ 10-24x |
| Latence | Élevée | Faible | ✅ 2-3x plus rapide |
| Utilisation GPU | 30-50% | 80-95% | ✅ 2x meilleur |
| Batch size | Limité (4-8) | Dynamique (100+) | ✅ Flexible |
PagedAttention : L’innovation clé
Le problème du KV Cache
Les Transformers stockent les key/value tensors des tokens précédents (KV cache) pour éviter de recalculer à chaque nouveau token.
# Génération autoregressive classique
def generate_token(model, past_tokens):
"""
Génère le prochain token
"""
# Problème : KV cache grossit linéairement avec la séquence
# Pour un modèle 7B avec batch de 32 :
# - Séquence de 2048 tokens
# - KV cache ≈ 2048 * 32 * 4096 (hidden) * 2 (K+V) * 2 bytes (FP16)
# - Total ≈ 1 GB par requête !
# Avec batch de 32 requêtes : 32 GB juste pour KV cache
# → Laisse peu de mémoire pour le modèle lui-même
outputs = model(past_tokens, use_cache=True)
kv_cache = outputs.past_key_values # Énorme !
return next_token, kv_cache
Problèmes traditionnels :
- Fragmentation : Allocation mémoire inefficace
- Over-allocation : Alloue max_seq_len dès le départ
- Pas de partage : Impossible de partager KV cache entre requêtes similaires
PagedAttention : mémoire virtuelle pour KV Cache
vLLM emprunte le concept de mémoire virtuelle des OS modernes.
# Concept PagedAttention
class PagedAttention:
"""
Gère le KV cache comme des pages mémoire
"""
def __init__(self, block_size=16):
"""
Args:
block_size: Nombre de tokens par bloc (page)
"""
self.block_size = block_size
self.blocks = [] # Pool de blocs physiques
self.block_table = {} # Mapping logique → physique
def allocate_blocks(self, sequence_id, num_tokens):
"""
Alloue des blocs pour une séquence
"""
num_blocks_needed = (num_tokens + self.block_size - 1) // self.block_size
allocated_blocks = []
for _ in range(num_blocks_needed):
# Récupère un bloc libre du pool
if self.blocks:
block = self.blocks.pop()
else:
block = self.create_new_block()
allocated_blocks.append(block)
# Mapping logique
self.block_table[sequence_id] = allocated_blocks
return allocated_blocks
def share_blocks(self, seq1_id, seq2_id):
"""
Partage des blocs entre séquences (ex: même prompt)
"""
# Si seq2 a le même préfixe que seq1
# → Partage les blocs du préfixe commun (copy-on-write)
shared_blocks = self.block_table[seq1_id][:num_common_blocks]
new_blocks = self.allocate_blocks(seq2_id, remaining_tokens)
self.block_table[seq2_id] = shared_blocks + new_blocks
# Économie de mémoire massive pour batch avec même prompt !
Avantages :
✅ Zero fragmentation : Blocs de taille fixe
✅ Allocation dynamique : Alloue seulement ce qui est nécessaire
✅ Partage mémoire : Copy-on-write pour prompts communs
✅ Swapping : Peut déplacer KV cache vers CPU si nécessaire
Comparaison visuelle
# Approche classique
# Séquence 1 : [████████████████░░░░░░░░] 16 tokens utilisés, 8 gaspillés
# Séquence 2 : [██████░░░░░░░░░░░░░░░░░░] 6 tokens utilisés, 18 gaspillés
# → Fragmentation énorme !
# PagedAttention
# Séquence 1 : [████|████|████|████] 4 blocs de 4 tokens chacun
# Séquence 2 : [████|██░░] 1.5 blocs
# → Zero gaspillage, allocation précise
Continuous Batching
Batching Statique vs Dynamique
# Batching statique (classique)
def static_batching(requests):
"""
Attend d'avoir un batch complet avant de traiter
"""
batch = []
while len(batch) < BATCH_SIZE:
batch.append(wait_for_request()) # Attend...
# Traite tout le batch
# Problème : Certaines requêtes finissent plus tôt
# → GPU idle en attendant la plus longue
results = model.generate(batch, max_length=512)
return results
# Continuous batching (vLLM)
def continuous_batching(scheduler):
"""
Ajoute/retire des requêtes dynamiquement
"""
active_batch = []
while True:
# Ajoute de nouvelles requêtes si de la place
while scheduler.has_pending() and can_fit_more():
active_batch.append(scheduler.get_next())
# Génère 1 token pour toutes les requêtes actives
outputs = model.generate_one_token(active_batch)
# Retire les requêtes terminées
active_batch = [
req for req in active_batch
if not req.is_finished(outputs)
]
# GPU toujours occupé à 100% !
Avantage clé : Le GPU génère continuellement, jamais idle.
Installation et démarrage rapide
Installation
# Installation via pip
pip install vllm
# Ou depuis source pour dernières features
git clone https://github.com/vllm-project/vllm.git
cd vllm
pip install -e .
# Vérifier CUDA
python -c "import torch; print(torch.cuda.is_available())"
Inférence Python
from vllm import LLM, SamplingParams
# Initialiser le modèle
llm = LLM(
model="meta-llama/Llama-2-7b-chat-hf",
tensor_parallel_size=1, # Nombre de GPUs
gpu_memory_utilization=0.9, # Utilise 90% de la VRAM
max_model_len=4096 # Context window
)
# Paramètres de génération
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=256,
stop=["</s>"]
)
# Prompts
prompts = [
"Explique la relativité en termes simples :",
"Écris une fonction Python pour trier une liste :",
"Quels sont les avantages de l'IA ?"
]
# Génération (batching automatique !)
outputs = llm.generate(prompts, sampling_params)
# Afficher les résultats
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
print(f"Prompt: {prompt}")
print(f"Réponse: {generated_text}\n")
Performance :
- Llama-2-7B sur 1x A100
- Batch de 100 requêtes : ~150 tokens/sec
- vs ~15-20 tokens/sec avec HuggingFace
Serveur API compatible OpenAI
# Lancer le serveur vLLM
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-chat-hf \
--host 0.0.0.0 \
--port 8000 \
--tensor-parallel-size 1
# Le serveur expose une API compatible OpenAI !
Client Python :
from openai import OpenAI
# Pointer vers votre serveur vLLM local
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed" # vLLM ne nécessite pas de clé
)
# Utiliser comme l'API OpenAI standard
response = client.chat.completions.create(
model="meta-llama/Llama-2-7b-chat-hf",
messages=[
{"role": "system", "content": "Tu es un assistant utile."},
{"role": "user", "content": "Explique les trous noirs."}
],
temperature=0.7,
max_tokens=300
)
print(response.choices[0].message.content)
Avantage : Remplacez base_url et vos apps existantes fonctionnent immédiatement !
# Migration d'OpenAI vers vLLM local
# Avant
client = OpenAI(api_key="sk-...") # API OpenAI
# Après (zero code change)
client = OpenAI(
base_url="http://your-vllm-server:8000/v1",
api_key="not-needed"
)
# Tout le reste du code reste identique !
Configuration avancée
Optimisation GPU
from vllm import LLM, SamplingParams
llm = LLM(
model="mistralai/Mixtral-8x7B-Instruct-v0.1",
# Parallélisme tensoriel (multi-GPU)
tensor_parallel_size=4, # Distribue le modèle sur 4 GPUs
# Utilisation mémoire
gpu_memory_utilization=0.95, # Utilise 95% de la VRAM
# Context window
max_model_len=32768, # Mixtral supporte 32K tokens
# Quantization
quantization="awq", # Active AWQ 4-bit
# Optimisations
enforce_eager=False, # Utilise CUDA graphs (plus rapide)
enable_prefix_caching=True, # Cache les préfixes communs
)
Multi-GPU et Tensor Parallelism
# Distribuer un gros modèle sur plusieurs GPUs
# Llama-2-70B nécessite ~140 GB en FP16
# → Ne tient pas sur 1 GPU A100 (80 GB)
llm = LLM(
model="meta-llama/Llama-2-70b-chat-hf",
tensor_parallel_size=2, # Distribue sur 2x A100
dtype="float16",
gpu_memory_utilization=0.9
)
# vLLM découpe automatiquement :
# - GPU 0 : Couches 0-39
# - GPU 1 : Couches 40-79
# Communication automatique via NCCL
Configuration optimale par modèle :
| Modèle | Taille | GPUs Recommandés | Config |
|---|---|---|---|
| Llama-2-7B | ~14 GB | 1x A10/A100 | tensor_parallel_size=1 |
| Llama-2-13B | ~26 GB | 1x A100 | tensor_parallel_size=1 |
| Llama-2-70B | ~140 GB | 2x A100 80GB | tensor_parallel_size=2 |
| Mixtral 8x7B | ~90 GB | 2x A100 | tensor_parallel_size=2 |
| Llama-3.1-405B | ~810 GB | 8x A100 | tensor_parallel_size=8 |
Quantization intégrée
# vLLM supporte plusieurs méthodes de quantization
# AWQ (4-bit)
llm = LLM(
model="TheBloke/Llama-2-7B-Chat-AWQ",
quantization="awq",
dtype="float16"
)
# GPTQ (4-bit)
llm = LLM(
model="TheBloke/Llama-2-7B-Chat-GPTQ",
quantization="gptq",
dtype="float16"
)
# SqueezeLLM
llm = LLM(
model="squeeze-ai-lab/sq-llama-2-7b-w4a16",
quantization="squeezellm",
dtype="float16"
)
# Économie mémoire : 4-bit ≈ 4x moins de VRAM
# Llama-2-70B : ~140 GB → ~35 GB avec AWQ !
Prefix Caching
# Économiser du calcul pour prompts avec préfixes communs
llm = LLM(
model="meta-llama/Llama-2-7b-chat-hf",
enable_prefix_caching=True # Active le cache de préfixes
)
# Système prompt commun
system_prompt = """Tu es un expert en Python.
Réponds toujours avec du code propre et documenté."""
prompts = [
f"{system_prompt}\n\nÉcris une fonction de tri.",
f"{system_prompt}\n\nÉcris une fonction de recherche.",
f"{system_prompt}\n\nÉcris une fonction de hash."
]
# vLLM détecte le préfixe commun (system_prompt)
# → Calcule KV cache UNE SEULE FOIS
# → Partage entre les 3 requêtes
# → Accélération massive !
outputs = llm.generate(prompts, sampling_params)
Économie :
- Sans prefix caching : 3 × coût system_prompt
- Avec prefix caching : 1 × coût system_prompt + 3 × coût requête unique
- Gain : ~2-3x pour prompts longs
Benchmarks et performances
Comparaison Throughput
# Benchmark vLLM vs HuggingFace vs TensorRT-LLM
import time
from vllm import LLM, SamplingParams
def benchmark(framework, model_name, num_requests=100):
"""
Benchmark throughput
"""
prompts = [
"Écris une histoire courte sur l'IA."
] * num_requests
start = time.time()
if framework == "vllm":
llm = LLM(model=model_name)
outputs = llm.generate(prompts, SamplingParams(max_tokens=100))
elif framework == "huggingface":
# Implémentation HuggingFace standard
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
for prompt in prompts:
inputs = tokenizer(prompt, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=100)
end = time.time()
throughput = num_requests / (end - start)
return throughput
# Résultats (Llama-2-7B sur A100)
results = {
"vLLM": benchmark("vllm", "meta-llama/Llama-2-7b-hf"),
"HuggingFace": benchmark("huggingface", "meta-llama/Llama-2-7b-hf"),
}
print(f"vLLM: {results['vLLM']:.1f} req/s")
print(f"HuggingFace: {results['HuggingFace']:.1f} req/s")
print(f"Speedup: {results['vLLM'] / results['HuggingFace']:.1f}x")
# Résultats typiques :
# vLLM: 120-150 req/s
# HuggingFace: 8-12 req/s
# Speedup: 10-15x
Résultats réels
Configuration : Llama-2-13B, A100 80GB, batch requests variées
| Métrique | HuggingFace | Text-Gen-Inference | vLLM |
|---|---|---|---|
| Throughput | 12 req/s | 45 req/s | 142 req/s |
| Latence P50 | 850ms | 320ms | 180ms |
| Latence P99 | 2100ms | 780ms | 420ms |
| GPU Util | 35% | 62% | 88% |
| Max Batch | 8 | 32 | 128 |
Observations :
- vLLM : 3x faster que Text-Gen-Inference
- vLLM : 12x faster que HuggingFace
- Utilisation GPU quasi-optimale
Scaling multi-GPU
# Performance scaling avec tensor parallelism
# Llama-2-70B sur différentes configs GPU
configs = {
"1x A100": {
"impossible": "Modèle trop gros (140 GB > 80 GB)"
},
"2x A100": {
"throughput": "45 req/s",
"latency_p50": "280ms",
"cost_per_hour": "$6.00"
},
"4x A100": {
"throughput": "82 req/s", # Pas 2x (overhead communication)
"latency_p50": "160ms",
"cost_per_hour": "$12.00"
},
"8x A100": {
"throughput": "135 req/s",
"latency_p50": "95ms",
"cost_per_hour": "$24.00"
}
}
# Scaling efficiency
# 2→4 GPUs : 1.82x throughput (91% efficiency)
# 4→8 GPUs : 1.65x throughput (82% efficiency)
# Sweet spot : 2-4 GPUs pour la plupart des cas
Déploiement en production
Docker Deployment
# Dockerfile pour serveur vLLM
FROM nvidia/cuda:12.1.0-devel-ubuntu22.04
# Installer Python et dépendances
RUN apt-get update && apt-get install -y \
python3.10 \
python3-pip \
git
# Installer vLLM
RUN pip install vllm
# Télécharger le modèle (optionnel, peut être fait au runtime)
RUN python3 -c "from huggingface_hub import snapshot_download; \
snapshot_download('meta-llama/Llama-2-7b-chat-hf')"
# Exposer le port
EXPOSE 8000
# Commande de démarrage
CMD ["python3", "-m", "vllm.entrypoints.openai.api_server", \
"--model", "meta-llama/Llama-2-7b-chat-hf", \
"--host", "0.0.0.0", \
"--port", "8000"]
Build et run :
# Build l'image
docker build -t vllm-server .
# Run avec GPU
docker run --gpus all -p 8000:8000 vllm-server
# Test
curl http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Llama-2-7b-chat-hf",
"prompt": "Bonjour, comment vas-tu ?",
"max_tokens": 100
}'
Kubernetes Deployment
# vllm-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-llama2
spec:
replicas: 2 # 2 instances pour load balancing
selector:
matchLabels:
app: vllm-llama2
template:
metadata:
labels:
app: vllm-llama2
spec:
containers:
- name: vllm
image: vllm-server:latest
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: 1 # 1 GPU par pod
memory: "32Gi"
requests:
nvidia.com/gpu: 1
memory: "16Gi"
env:
- name: CUDA_VISIBLE_DEVICES
value: "0"
args:
- "--model"
- "meta-llama/Llama-2-7b-chat-hf"
- "--tensor-parallel-size"
- "1"
- "--gpu-memory-utilization"
- "0.9"
---
apiVersion: v1
kind: Service
metadata:
name: vllm-service
spec:
selector:
app: vllm-llama2
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: LoadBalancer
Deploy :
kubectl apply -f vllm-deployment.yaml
# Vérifier
kubectl get pods
kubectl get svc
# Accéder au service
export SERVICE_IP=$(kubectl get svc vllm-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl http://$SERVICE_IP/v1/models
Monitoring et observability
# vLLM expose des métriques Prometheus
from prometheus_client import start_http_server, Counter, Histogram
# Métriques custom
request_counter = Counter('vllm_requests_total', 'Total requests')
latency_histogram = Histogram('vllm_latency_seconds', 'Request latency')
# Middleware pour tracking
class MonitoredVLLM:
def __init__(self, model_name):
self.llm = LLM(model=model_name)
@latency_histogram.time()
def generate(self, prompts, sampling_params):
request_counter.inc(len(prompts))
outputs = self.llm.generate(prompts, sampling_params)
return outputs
# Démarrer serveur Prometheus
start_http_server(9090)
# vLLM natif expose aussi :
# - vllm_num_requests_running
# - vllm_num_requests_waiting
# - vllm_gpu_cache_usage_perc
# - vllm_num_preemptions_total
# Scrape config Prometheus
"""
scrape_configs:
- job_name: 'vllm'
static_configs:
- targets: ['localhost:8000']
"""
Dashboard Grafana :
{
"panels": [
{
"title": "Requests per Second",
"targets": [{
"expr": "rate(vllm_requests_total[1m])"
}]
},
{
"title": "P95 Latency",
"targets": [{
"expr": "histogram_quantile(0.95, vllm_latency_seconds)"
}]
},
{
"title": "GPU Cache Usage",
"targets": [{
"expr": "vllm_gpu_cache_usage_perc"
}]
}
]
}
Cas d’usage avancés
RAG avec vLLM
from vllm import LLM, SamplingParams
from sentence_transformers import SentenceTransformer
import numpy as np
class RAGSystem:
"""
Système RAG optimisé avec vLLM
"""
def __init__(self, llm_model="meta-llama/Llama-2-7b-chat-hf"):
# Embeddings
self.embedder = SentenceTransformer('all-MiniLM-L6-v2')
# vLLM pour génération
self.llm = LLM(
model=llm_model,
gpu_memory_utilization=0.9,
enable_prefix_caching=True # Cache le contexte récupéré
)
self.sampling_params = SamplingParams(
temperature=0.7,
max_tokens=512
)
# Documents (simulation)
self.documents = []
self.doc_embeddings = None
def add_documents(self, docs):
"""Ajoute des documents à la base"""
self.documents.extend(docs)
self.doc_embeddings = self.embedder.encode(docs)
def retrieve(self, query, top_k=5):
"""Récupère les documents pertinents"""
query_emb = self.embedder.encode([query])[0]
# Similarité cosinus
similarities = np.dot(self.doc_embeddings, query_emb)
top_indices = np.argsort(similarities)[-top_k:][::-1]
return [self.documents[i] for i in top_indices]
def answer(self, questions):
"""
Répond à plusieurs questions (batching automatique)
"""
prompts = []
for question in questions:
# Récupérer contexte
context_docs = self.retrieve(question, top_k=3)
context = "\n\n".join(context_docs)
# Construire prompt
prompt = f"""Contexte :
{context}
Question : {question}
Réponds en te basant uniquement sur le contexte fourni."""
prompts.append(prompt)
# vLLM batch automatiquement
outputs = self.llm.generate(prompts, self.sampling_params)
return [output.outputs[0].text for output in outputs]
# Utilisation
rag = RAGSystem()
# Ajouter documents
rag.add_documents([
"Paris est la capitale de la France.",
"La Tour Eiffel mesure 330 mètres.",
"Le Louvre est le plus grand musée du monde."
])
# Poser plusieurs questions en parallèle
questions = [
"Quelle est la capitale de la France ?",
"Quelle est la hauteur de la Tour Eiffel ?",
"Quel est le plus grand musée ?"
]
answers = rag.answer(questions)
for q, a in zip(questions, answers):
print(f"Q: {q}\nA: {a}\n")
Multi-LoRA Serving
vLLM peut servir plusieurs adaptateurs LoRA depuis un seul modèle de base.
from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest
# Modèle de base
llm = LLM(
model="meta-llama/Llama-2-7b-hf",
enable_lora=True, # Active le support LoRA
max_lora_rank=64
)
# Plusieurs adaptateurs LoRA pour différents domaines
lora_adapters = {
"legal": LoRARequest("legal-lora", 1, "/path/to/legal-lora"),
"medical": LoRARequest("medical-lora", 2, "/path/to/medical-lora"),
"code": LoRARequest("code-lora", 3, "/path/to/code-lora"),
}
# Requêtes avec différents LoRA
prompts_and_loras = [
("Rédige un contrat de bail", lora_adapters["legal"]),
("Explique le diabète", lora_adapters["medical"]),
("Écris une fonction de tri", lora_adapters["code"]),
]
# vLLM charge/décharge dynamiquement les LoRA
outputs = llm.generate(
[p for p, _ in prompts_and_loras],
SamplingParams(max_tokens=200),
lora_request=[lora for _, lora in prompts_and_loras]
)
# Un seul modèle de base, plusieurs spécialisations !
# Économie massive de mémoire GPU
Speculative Decoding
vLLM supporte le speculative decoding pour accélérer la génération.
from vllm import LLM, SamplingParams
# Modèle principal (gros, lent)
main_llm = LLM(
model="meta-llama/Llama-2-70b-chat-hf",
tensor_parallel_size=2
)
# Modèle draft (petit, rapide)
draft_llm = LLM(
model="meta-llama/Llama-2-7b-chat-hf",
tensor_parallel_size=1
)
# Speculative decoding :
# 1. Draft model génère K tokens rapidement
# 2. Main model vérifie en parallèle
# 3. Accepte les bons, rejette les mauvais
# 4. Continue avec les tokens acceptés
# Résultat : 2-3x speedup sur génération longue !
# Configuration
llm = LLM(
model="meta-llama/Llama-2-70b-chat-hf",
tensor_parallel_size=2,
speculative_model="meta-llama/Llama-2-7b-chat-hf",
num_speculative_tokens=5 # Génère 5 tokens spéculatifs
)
outputs = llm.generate(prompts, sampling_params)
# Accélération sans perte de qualité !
Comparaison avec alternatives
vLLM vs Text-Generation-Inference (TGI)
| Fonctionnalité | vLLM | TGI (HuggingFace) |
|---|---|---|
| PagedAttention | ✅ Oui | ❌ Non |
| Continuous Batching | ✅ Oui | ✅ Oui |
| Throughput | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Latence | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Support modèles | Large | Très large |
| Quantization | AWQ, GPTQ, SqueezeLLM | GPTQ, bitsandbytes |
| Multi-LoRA | ✅ Oui | ❌ Non |
| Prefix caching | ✅ Oui | ❌ Non |
| OpenAI API | ✅ Oui | ✅ Oui |
| Facilité setup | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Verdict : vLLM pour maximum performance, TGI pour facilité d’utilisation.
vLLM vs TensorRT-LLM
| Fonctionnalité | vLLM | TensorRT-LLM (NVIDIA) |
|---|---|---|
| Performance | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Flexibilité | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Setup | Simple | Complexe |
| GPU support | NVIDIA | NVIDIA uniquement |
| Open-source | ✅ Apache 2.0 | ✅ Apache 2.0 |
| PyTorch compat | ✅ Natif | ❌ Nécessite conversion |
Verdict : vLLM pour développement rapide, TensorRT-LLM pour dernière performance absolue.
Limitations et considérations
Mémoire GPU minimum
# Exigences mémoire approximatives (FP16)
requirements = {
"Llama-2-7B": {
"model": "14 GB",
"kv_cache": "6-10 GB (selon batch)",
"total_min": "24 GB",
"gpu_recommended": "A10/A100"
},
"Llama-2-13B": {
"model": "26 GB",
"kv_cache": "8-15 GB",
"total_min": "40 GB",
"gpu_recommended": "A100"
},
"Llama-2-70B": {
"model": "140 GB",
"kv_cache": "30-50 GB",
"total_min": "180 GB",
"gpu_recommended": "2x A100 80GB"
}
}
# Si pas assez de VRAM :
# → Utiliser quantization (AWQ/GPTQ)
# → Réduire max_model_len
# → Réduire gpu_memory_utilization
Latence vs throughput
# Trade-off latence vs throughput
# Configuration low-latency (chatbot interactif)
llm_low_latency = LLM(
model="meta-llama/Llama-2-7b-chat-hf",
max_num_seqs=4, # Petit batch
gpu_memory_utilization=0.6
)
# → Latence : 80-120ms
# → Throughput : 20-30 req/s
# Configuration high-throughput (batch processing)
llm_high_throughput = LLM(
model="meta-llama/Llama-2-7b-chat-hf",
max_num_seqs=256, # Gros batch
gpu_memory_utilization=0.95
)
# → Latence : 300-500ms
# → Throughput : 150-200 req/s
# Choisir selon cas d'usage !
Support des modèles
vLLM supporte la plupart des architectures, mais pas toutes :
Supportés ✅ :
- Llama 1/2/3 (toutes variantes)
- Mistral, Mixtral
- GPT-2, GPT-J, GPT-NeoX
- OPT, BLOOM
- Falcon, Baichuan, Qwen
- Phi, Gemma
Non supportés ❌ :
- Modèles multimodaux (vision + texte) - support limité
- Certains modèles avec architectures custom
Ressources et outils
Documentation et tutoriels
# Documentation officielle
https://docs.vllm.ai/
# GitHub
https://github.com/vllm-project/vllm
# Discord communautaire
https://discord.gg/vllm
# Papier académique (PagedAttention)
https://arxiv.org/abs/2309.06180
Outils complémentaires
# vLLM + LangChain
from langchain.llms import VLLM
llm = VLLM(
model="meta-llama/Llama-2-7b-chat-hf",
trust_remote_code=True,
max_new_tokens=512,
temperature=0.7
)
# Utiliser dans chains LangChain
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
prompt = PromptTemplate.from_template("Résume ce texte : {text}")
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run(text="Long texte à résumer...")
Benchmarking scripts
# Script de benchmark complet
import time
import numpy as np
from vllm import LLM, SamplingParams
def comprehensive_benchmark(
model_name,
num_prompts=100,
prompt_length=50,
output_length=100
):
"""
Benchmark complet d'un modèle
"""
# Générer prompts
prompts = [
f"Prompt {i}: " + "test " * prompt_length
for i in range(num_prompts)
]
# Initialiser vLLM
llm = LLM(model=model_name, gpu_memory_utilization=0.9)
sampling_params = SamplingParams(
max_tokens=output_length,
temperature=0.7
)
# Warmup
_ = llm.generate(prompts[:5], sampling_params)
# Benchmark
latencies = []
for i in range(0, num_prompts, 10):
batch = prompts[i:i+10]
start = time.time()
outputs = llm.generate(batch, sampling_params)
end = time.time()
batch_latency = (end - start) / len(batch)
latencies.append(batch_latency)
return {
"model": model_name,
"throughput_req_s": num_prompts / sum(latencies),
"latency_p50": np.percentile(latencies, 50),
"latency_p95": np.percentile(latencies, 95),
"latency_p99": np.percentile(latencies, 99),
}
# Run benchmark
results = comprehensive_benchmark(
"meta-llama/Llama-2-7b-chat-hf",
num_prompts=100
)
print(f"""
Modèle : {results['model']}
Throughput : {results['throughput_req_s']:.1f} req/s
Latence P50 : {results['latency_p50']*1000:.0f}ms
Latence P95 : {results['latency_p95']*1000:.0f}ms
Latence P99 : {results['latency_p99']*1000:.0f}ms
""")
Conclusion
vLLM est devenu l’outil de référence pour servir des LLM en production :
✅ Performances exceptionnelles :
- 10-24x throughput vs solutions classiques
- Utilisation GPU > 90%
- Latence minimale
✅ Innovations techniques :
- PagedAttention : gestion mémoire révolutionnaire
- Continuous batching : GPU toujours occupé
- Prefix caching : économies massives
✅ Production-ready :
- API compatible OpenAI
- Multi-GPU support
- Monitoring intégré
- Docker/K8s friendly
Quand utiliser vLLM ?
✅ Idéal si :
- Vous servez des LLM en production
- Vous avez besoin de throughput élevé
- Vous voulez optimiser les coûts GPU
- Vous utilisez Llama, Mistral, Mixtral, etc.
⚠️ Alternatives si :
- Modèle très custom/expérimental → HuggingFace
- Performance absolue critique → TensorRT-LLM
- Développement rapide sans GPU → Text-Gen-Inference
Prochaines étapes
- Installer vLLM et tester avec votre modèle
- Benchmarker vs votre solution actuelle
- Optimiser la configuration (batch size, mémoire, etc.)
- Déployer en production avec monitoring
Pour aller plus loin :