Article

vLLM ch.02 — Inférence PyTorch : pourquoi ça plante sans vLLM ?

Diagramme comparatif : architecture inférence naïve PyTorch vs vLLM avec GPU, KV cache, OOM, scheduler

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 :

  1. Charge GPT-2 avec HuggingFace `transformers`
  2. Tokenize le prompt en `input_ids`
  3. Génère token par token avec `model.generate()`
  4. Décode les tokens en texte
  5. Expose via FastAPI sur `/v1/chat/completions`
Découvrez  Ponytail : faire ecrire moins de code aux agents IA sans supprimer les garde-fous

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

Architecture comparée PyTorch naïf vs vLLM avec GPU, KV cache, OOM, scheduler
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)
Découvrez  vLLM : introduction pratique au serving LLM, à l’inférence et au KV cache

Le modèle mental

Trois concepts suffisent :

  1. Génération autogressive — le modèle prédit un token à la fois, en boucle. Chaque nouveau token dépend des précédents.
  2. 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.
  3. 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.

Explorer les formations Xavki

Pour apprendre dans l ordre, repartez depuis la roadmap ou une playlist thematique.