Avant de comprendre pourquoi vLLM est devenu la référence pour le serving d’inférence LLM, il faut d’abord essayer de servir un modèle sans lui. Ce chapitre construit un serveur d’inférence minimal avec PyTorch + transformers + FastAPI, expose une API compatible OpenAI, et révèle exactement pourquoi cette approche naïve ne passe pas à l’échelle — un client à la fois ça tient, deux clients simultanés c’est le crash GPU.
TL;DR
Ce deuxième chapitre de la formation vLLM par xavki construit un serveur d’inférence LLM en ~65 lignes avec PyTorch, Hugging Face transformers et FastAPI, sans vLLM. L’objectif est pédagogique : exposer une API `/v1/chat/completions` compatible OpenAI, comprendre le mécanisme de génération autogressive, et surtout mettre en évidence les quatre problèmes fondamentaux que vLLM résout — fragmentation du KV cache, absence de batching, saturation mémoire, et allocation fixe. Le chapitre montre qu’un deuxième client concurrent provoque un crash CUDA Out of Memory.
La vidéo
Cette vidéo fait partie de la playlist vLLM — Tutorials par xavki (8 épisodes).
- Titre : vLLM — 02. L’inférence sans vLLM ? pytorch + transformer
- Chaîne : xavki
- Playlist : Formation vLLM
- Durée : ∼15 min
Inférence PyTorch sans vLLM, c’est quoi ?
L’inférence LLM “naïve” consiste à charger un modèle causal (GPT-2, Llama, Mistral) avec PyTorch et Hugging Face `transformers`, puis à l’exposer via une API HTTP. Rien de plus. Pas de PagedAttention, pas de continuous batching, pas de scheduler, pas de file d’attente, pas de métriques.
Le code du serveur est dans le dépôt, fichier `02-minimal-pytorch-inference/server.py` :
import torch
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from transformers import AutoModelForCausalLM, AutoTokenizer
app = FastAPI(title="Naive OpenAI-compatible inference API")
MODEL_ID = "gpt2"
device = "cuda" if torch.cuda.is_available() else "cpu"
torch_dtype = torch.float16 if device == "cuda" else torch.float32
tokeniser = AutoTokenizer.from_pretrained(MODEL_ID)
if tokeniser.pad_token is None:
tokeniser.pad_token = tokeniser.eos_token
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=torch_dtype)
model.to(device)
model.eval()
class Message(BaseModel):
role: str
content: str
class ChatCompletionRequest(BaseModel):
model: str = MODEL_ID
messages: list[Message]
temperature: float = 0.7
max_tokens: int = 50
@app.post("/v1/chat/completions")
async def chat_completions(req: ChatCompletionRequest):
prompt = render_prompt(req.messages)
inputs = tokeniser(prompt, return_tensors="pt", padding=True,
truncation=True, max_length=512).to(device)
with torch.no_grad():
output = model.generate(**inputs, max_new_tokens=req.max_tokens)
input_length = inputs["input_ids"].shape[1]
generated_tokens = output[0][input_length:]
generated_text = tokeniser.decode(generated_tokens, skip_special_tokens=True)
return {
"choices": [{"message": {"role": "assistant", "content": generated_text}}],
"usage": {
"prompt_tokens": input_length,
"completion_tokens": len(generated_tokens),
},
}
Ce code fait 5 choses :
- Charge GPT-2 avec HuggingFace `transformers`
- Tokenize le prompt en `input_ids`
- Génère token par token avec `model.generate()`
- Décode les tokens en texte
- Expose via FastAPI sur `/v1/chat/completions`
Le vrai problème de ce serveur naïf
Ce chapitre existe pour faire l’expérience directe de l’échec. Le chapitre 01 pose les concepts. Le chapitre 02 les concrétise en code qui plante.
Le problème n’est pas “comment écrire un serveur d’inférence” — c’est trivial. Le problème est pourquoi ce serveur ne tient pas la charge :
1. Le KV cache est réalloué à chaque requête
Chaque appel recrée un KV cache complet depuis zéro. Aucune réutilisation entre requêtes, même avec un préfixe system identique. La mémoire se fragmente.
2. Pas de batching
Les requêtes sont traitées une par une. Pendant qu’une requête génère ses tokens, le GPU reste inactif pour toutes les autres.
3. Pas de file d’attente
FastAPI lance un thread par requête. Tous les threads se battent pour le GPU. Pas de fairness, pas de backpressure.
4. Allocation mémoire fixe
`max_length=512` force une allocation pour le pire cas. Si le prompt fait 10 tokens, le KV cache pré-alloue quand même pour 512 tokens — jusqu’à 98% de gaspillage.
Le crash OOM en conditions réelles
# Terminal 1 (fonctionne)
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "gpt2", "messages": [{"role": "user", "content": "Hello"}]}'
# Terminal 2 (en simultané — crash garanti)
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "gpt2", "messages": [{"role": "user", "content": "Tell me a story"}]}'
Résultat :
RuntimeError: CUDA out of memory.
Tried to allocate 2.54 GiB (GPU 0; 23.03 GiB total capacity;
20.14 GiB already allocated; 1.25 GiB free)
Deux utilisateurs simultanés suffisent à saturer un GPU L4 (24 GB). C’est le moment où la vidéo bascule vers “voilà pourquoi vLLM existe”.
Comparaison : PyTorch naïf vs vLLM

| Aspect | PyTorch naïf | vLLM |
|---|---|---|
| Code serveur | ~65 lignes (server.py) | 1 commande : `vllm serve` |
| KV cache | Pré-alloué, fragmenté | Paginé, à la demande, 0 gaspillage |
| Batching | Séquentiel uniquement | Continu, niveau itération |
| Concurrence | Threads × modèle = OOM | Scheduler + block management |
| Débit | ∼100 tok/s (GPT-2) | ∼2000 tok/s (Mistral 7B) |
| Mémoire | 60-80% de gaspillage | ∼100% efficace |
| API OpenAI | Forme compatible, backend naïf | Moteur complet `/v1/…` |
| Métriques | Aucune | Endpoint `/metrics` |
| Multi-GPU | Implémentation manuelle | `–tensor-parallel-size 2` |
| Streaming | Non | Oui |
| File d’attente | Non | Oui (scheduler) |
Le modèle mental
Trois concepts suffisent :
- Génération autogressive — le modèle prédit un token à la fois, en boucle. Chaque nouveau token dépend des précédents.
- KV cache — les clés/valeurs d’attention des tokens déjà générés sont stockés en mémoire GPU. Sans cache, chaque nouveau token recalcule tout depuis le début.
- Format OpenAI — l’API `/v1/chat/completions` est le standard de facto.
Reproduire l’expérience
Installation
pip install torch transformers fastapi uvicorn pydantic requests
Lancement
python 02-minimal-pytorch-inference/server.py
Test
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt2",
"messages": [{"role": "user", "content": "The future of AI is"}],
"max_tokens": 50
}'
Liens utiles
| Ressource | URL |
|---|---|
| Playlist formation vLLM | YouTube |
| Dépôt du chapitre | GitLab |
| server.py | GitLab |
| Chapitre 01 — Introduction | GitLab |
| Documentation vLLM | docs.vllm.ai |
| GitHub vLLM | GitHub |
| Article PagedAttention | blog.vllm.ai |
| Paper vLLM (SOSP 2023) | arXiv |
| Documentation PyTorch | pytorch.org |
| Hugging Face Transformers | huggingface.co |
| Chaîne xavki | YouTube |
| Blog xavki | xavki.blog |
FAQ
Pourquoi GPT-2 plutôt que Mistral ou Llama ?
GPT-2 est petit (~500 Mo) et se charge rapidement. Le code fonctionne avec n’importe quel modèle causal AutoModelForCausalLM.
Ce serveur supporte-t-il le streaming ?
Non. L’ajout du streaming nécessite `StreamingResponse` de FastAPI. C’est un des nombreux apports de vLLM.
Le crash OOM est-il inévitable avec 2 clients ?
Oui, sur GPU 24 GB avec GPT-2 en float16. La première requête alloue 512 tokens de KV cache. La deuxième tente de faire de même et la mémoire ne suffit pas. Avec Mistral 7B, le crash arrive dès la première requête.
Différence avec l’API OpenAI réelle ?
La forme HTTP est identique. La différence est sous le capot : moteurs optimisés, batching, caching, scaling automatique.
La suite ?
Le chapitre 03 introduit KV Cache, PagedAttention, Continuous Batching — les briques que vLLM ajoute.
Puis-je utiliser ce serveur en production ?
Non. C’est un outil pédagogique. En production : `vllm serve` (chapitre 05) ou Docker Compose / Kubernetes (chapitres 34-35).
Conclusion
Ce chapitre 02 est une démonstration volontairement frustrante : on écrit un serveur fonctionnel en quelques dizaines de lignes, on le teste, ça marche — puis on ajoute un deuxième utilisateur et tout s’effondre. La leçon : la forme de l’API (OpenAI-compatible) n’est pas le problème difficile. Ce qui est difficile, c’est la gestion mémoire, le scheduling, le batching, la concurrence — tout ce que vLLM apporte.
Le code est dans `02-minimal-pytorch-inference/`. Les slides (`slides.md`) se lisent avec `mdp`. La vidéo montre l’exécution en conditions réelles.
Le prochain chapitre (`03-notions-definitions`) pose les définitions de KV Cache, PagedAttention et Continuous Batching — les briques qui transforment ce serveur naïf en moteur de production.