🧠 LLM Expliqué

Guide visuel avec fil rouge — Suivez "Le chat mange" à travers le modèle

🎯 Notre fil rouge tout au long du guide

Nous suivons la phrase "Le chat mange" à travers un LLaMA 70B pour comprendre chaque étape.

ModèleLLaMA 70B
Couches80
Dimension8192
Têtes64 Q / 8 KV
Vocab128K
VRAM140 GB FP16

📚 Table des matières

Cliquez sur une section pour y accéder directement

🔄 Pipeline

Vue d'ensemble du flux

✂️ Tokenizer

Texte → IDs

📐 Embedding

IDs → Vecteurs

🏗️ Transformer

Architecture d'une couche

👁️ Attention

Q, K, V expliqués

🧠 FFN

Le "cerveau" du modèle

📚 Layers

80 couches en profondeur

📊 Logits

Vecteur → Probabilités

🎲 Sampling

Température, top-p

💾 KV Cache

Éviter les recalculs

🧩 MoE

Mixture of Experts

🔧 Functions

Tool calling, MCP

🧠 Reasoning

o1, R1, CoT

🔢 Précision

FP16, INT8, INT4

💾 Mémoire

Calculs VRAM

🎓 Training

SFT, RLHF, DPO

⚡ Optim

Flash Attention, vLLM

🔄 Pipeline Complet

Le chemin du texte à la réponse générée

📍 Notre exemple complet
"Le chat mange" PREFILL (parallèle) "sa" DECODE (séquentiel) "pâtée." <EOS>
ENTRÉE TOKENIZE EMBED 80 LAYERS Attention + FFN LOGITS 🎲 PREFILL (compute-bound) GPU saturé, parallèle sur tous tokens DECODE (memory-bound) Bande passante limitante, 1 token/pass <EOS> = FIN
💡 L'essentiel

Prefill = traiter tout le prompt en parallèle, GPU saturé (compute-bound). Decode = générer un token à la fois, on recharge 140 GB de poids par token (memory-bound). Le goulot d'étranglement n'est pas le calcul mais la bande passante mémoire (~3 TB/s sur H100).

📖 Glossaire
Prefill
— Traitement initial parallèle
Decode
— Génération séquentielle
TTFT
— Time To First Token
Memory-bound
— Limité par bande passante

🔍 Les 9 étapes du pipeline

"Le chat mange" (user)
        ↓
1. TOKENIZER : texte → IDs [2356, 7541, 64867]
        ↓
2. EMBEDDING : IDs → vecteurs [3 × 8192]
        ↓
3. POSITIONAL : + position (RoPE)
        ↓
4. PREFILL (parallèle, compute-bound) :
   └→ 80 couches × [Attention → FFN]
   └→ Stocke K,V dans le cache
        ↓
5. LOGITS : dernier vecteur → 128K scores
        ↓
6. SOFTMAX : scores → probabilités
        ↓
7. SAMPLING : top-p + température → "sa"
        ↓
8. DECODE (séquentiel, memory-bound) :
   └→ "sa" traverse 80 couches
   └→ KV cache réutilisé (pas recalculé)
   └→ K,V de "sa" ajoutés au cache
   └→ → "pâtée" → "." → <EOS>
        ↓
9. DETOKENIZER : IDs → "sa pâtée."

⚡ Pourquoi le decode est lent (memory-bound)

Pour générer 1 seul token, il faut :

• Charger 140 GB de poids (70B × 2 bytes)
• Faire très peu de calcul (1 token × 8192 dims)

Ratio calcul/transfert très faible !

H100 : 3 TB/s bande passante
→ 140 GB / 3 TB/s ≈ 47 ms minimum par token
→ ~21 tokens/sec théorique (1 batch)

Avec batching (8 requêtes) :
→ Même 140 GB chargés, 8× plus de calcul
→ ~168 tokens/sec (8 × 21)

Le batching amortit le coût mémoire — c'est pour ça que vLLM bat llama.cpp en throughput.

✂️ Tokenization

Transformer le texte en nombres

📍 Notre exemple
"Le chat mange" ["Le", "chat", "mange"] [2356, 7541, 64867]
"Le chat mange" BPE TOKENIZER vocab: 128K 2356 "Le" 7541 "chat" 64867 "mange" pos 0 pos 1 pos 2
💡 L'essentiel

Le tokenizer découpe le texte en sous-mots (tokens) via l'algorithme BPE (Byte Pair Encoding). Chaque token reçoit un ID unique dans un vocabulaire de 128K entrées. Un mot peut devenir 1 ou plusieurs tokens : "aujourd'hui" → 3-4 tokens, "the" → 1 token.

📖 Glossaire
BPE
— Byte Pair Encoding
Token
— Unité de base (mot/sous-mot)
Vocab
— Dictionnaire complet
<BOS>
— Begin Of Sequence

🏷️ Tokens spéciaux

En plus des tokens de texte, il existe des tokens de contrôle :

TokenRôleExemple
<BOS>Begin Of SequenceDémarre la génération
<EOS>End Of SequenceArrête la génération
<PAD>PaddingRemplissage pour batch
<UNK>UnknownToken inconnu (rare)
[INST]...[/INST]InstructionDélimite user/assistant

<EOS> est crucial : c'est lui qui dit au modèle de s'arrêter. Sans lui, le modèle continuerait indéfiniment.

🇫🇷 Pourquoi le français consomme ~1.5× plus de tokens ?

Les tokenizers sont entraînés sur des corpus majoritairement anglais.

"hello" → 1 token     "bonjour" → 2 tokens (bon+jour)
"today" → 1 token     "aujourd'hui" → 3-4 tokens

Conséquence : contexte 8K tokens ≈ 6000 mots anglais mais ~4000 mots français.

📐 Embedding

Transformer les IDs en vecteurs sémantiques

📍 Notre exemple
[2356, 7541, 64867] table[ID] 3 vecteurs de 8192 dimensions
2356 7541 64867 EMBEDDING TABLE [128K × 8192] lookup: table[ID] +RoPE Matrice [3 × 8192] [0.12, -0.45, 0.78, ...] [0.84, 0.23, -0.56, ...] [-0.33, 0.91, 0.15, ...]
💡 L'essentiel

L'embedding est une simple lookup dans une table : table[ID] = vecteur. Chaque token devient un vecteur de 8192 dimensions. Les mots similaires ont des vecteurs proches dans cet espace. On ajoute le positional encoding (RoPE) pour que le modèle sache l'ordre des mots.

📖 Glossaire
Embedding
— Vecteur dense d'un token
d_model
— Dimension (8192)
RoPE
— Rotary Position Encoding
Lookup
— Accès par index

🎯 Pourquoi pas juste les IDs ?

Les IDs sont arbitraires : "chat" = 7541, "chien" = 9871 — aucune relation mathématique !

Avec embeddings :
"chat"   → [0.8, 0.3, -0.2, ...]  ┐
"chien"  → [0.7, 0.4, -0.1, ...]  ┘ PROCHES !
"voiture"→ [-0.5, 0.9, 0.2, ...]    LOIN

Le fameux : roi - homme + femme ≈ reine

🏗️ Architecture Transformer

La structure d'une couche

📍 Notre exemple
Matrice [3×8192] 80 couches identiques (Attention+FFN) Matrice [3×8192] transformée
Input [3×8192] COUCHE N (×80) LayerNorm ATTENTION LayerNorm FFN Residual 📊 Répartition params Attn ~20% FFN ~80% • Attention : tokens se parlent • FFN : chaque token "réfléchit"
💡 L'essentiel

Une couche = Attention → FFN, toujours dans cet ordre. L'Attention permet aux tokens de communiquer entre eux (20% des params). Le FFN transforme chaque token individuellement — c'est là que sont stockées les "connaissances" (80% des params). La connexion résiduelle (+) préserve l'info originale à travers les 80 couches.

📖 Glossaire
Attention
— Communication inter-tokens
FFN
— Feed-Forward Network
LayerNorm
— Normalisation
Residual
— x + f(x)

👁️ Mécanisme d'Attention

Comment les tokens se regardent

📍 Notre exemple
"mange" cherche son sujet score élevé avec "chat" (85%) récupère l'info de "chat"
"mange" Q "Qui fait?" K "Je suis..." V "Contenu" Q × Kᵀ / √d "Le" 0.1 "chat" 0.85 ✓ "mange" 0.05 softmax Poids 5% 90% 5% × V Output ≈ V(chat) ⚠️ Causal Mask : "mange" ne voit pas le futur
💡 L'essentiel

Q (Query) = ce que le token cherche. K (Key) = l'étiquette de chaque token. V (Value) = le contenu à récupérer. Le score Q × K détermine "qui regarde qui". Softmax transforme en probabilités. Résultat = somme pondérée des V. Le causal mask empêche de voir les tokens futurs.

📖 Glossaire
Query
— "Ce que je cherche"
Key
— "Ce que je suis"
Value
— "Ce que je donne"
Causal
— Masque empêchant le futur

🧠 Multi-Head Attention

Plusieurs "têtes" regardent en parallèle, chacune spécialisée :

Tête 1 : Relations sujet-verbe
Tête 2 : Coréférences (sa → chat)
Tête 3 : Proximité syntaxique
LLaMA 70B : 64 têtes × 128 dimensions

⚡ GQA : Grouped-Query Attention

TypeQ headsKV headsKV Cache
MHA6464100%
GQA64812.5%
MQA6411.5%

GQA = compromis optimal, utilisé par LLaMA 2+.

🧠 Feed-Forward Network (FFN)

Le "cerveau" qui stocke les connaissances

📍 Notre exemple
Vecteur "mange" [8192] expansion [28672] SwiGLU contraction [8192] enrichi
Input [8192] × W1 Hidden [28672] ×3.5 + SwiGLU × W2 Output [8192] 🔥 80% des params ! W1: 8192 × 28672 W2: 28672 × 8192 ≈ 705M params/couche
💡 L'essentiel

Le FFN expand la dimension ×3.5 (8192 → 28672), applique l'activation SwiGLU, puis contracte. Chaque token passe individuellement (pas de communication). C'est là que sont stockées les "connaissances" : chaque neurone encode un micro-concept (capitales, syntaxe Python, termes médicaux...).

📖 Glossaire
FFN/MLP
— Feed-Forward Network
SwiGLU
— Activation avec gate
Expansion
— ×3.5 la dimension
Neurone
— Unité de la couche cachée

🧠 Que font les neurones ?

Chaque neurone de la couche cachée est un détecteur de patterns :

Neurone 4821 : s'active sur "capitale de..."
Neurone 12045 : s'active sur du code Python
Neurone 8923 : s'active sur des termes médicaux
Neurone 15678 : s'active sur des négations

Le modèle a appris ces patterns pendant le pre-training. C'est pourquoi le FFN représente 80% des paramètres : c'est la "mémoire" du modèle.

⚙️ SwiGLU vs ReLU

SwiGLU (Swish-Gated Linear Unit) est l'activation moderne :

ReLU(x)   = max(0, x)            → coupe les négatifs
SwiGLU(x) = swish(xW₁) ⊙ (xW₃)  → gate + smooth

• Plus expressif que ReLU
• Meilleure propagation du gradient
• Utilisé par LLaMA, Mistral, Gemma

📊 Calcul des paramètres FFN

LLaMA 70B FFN (par couche) :
W1 (gate)  : 8192 × 28672 = 235M params
W2 (down)  : 28672 × 8192 = 235M params
W3 (up)    : 8192 × 28672 = 235M params
Total FFN  : 705M params/couche

80 couches × 705M = 56B params (80% du total)

📚 Traversée des 80 Couches

Spécialisation en profondeur

📍 Notre exemple
"Le chat mange" traverse 80 couches : syntaxe → grammaire → sémantique → raisonnement
x Couches 1-20 🔤 Syntaxe locale "le" → nom probable +Δ₁ Couches 20-40 📖 Grammaire "chat" = sujet +Δ₂ Couches 40-60 💡 Sémantique "mange" → nourriture +Δ₃ Couches 60-80 🧠 Raisonnement "quoi répondre?" +Δ₄ Logits Sortie = x + Δ₁ + Δ₂ + Δ₃ + ... + Δ₈₀ (residual stream)
💡 L'essentiel

Les couches se spécialisent : les basses traitent la syntaxe, les hautes le raisonnement. L'info circule via le residual stream : chaque couche ajoute son Δ sans perdre l'info précédente. Sortie finale = accumulation de contributions, pas remplacement.

📖 Glossaire
Profondeur
— Nombre de couches (80)
Residual
— "Rivière" d'info continue
Delta (Δ)
— Contribution d'une couche

🌊 Le Residual Stream = une rivière

Imagine le flux d'information comme une rivière qui traverse 80 villages (couches) :

                    ┌─────────┐
Embedding ─────────▶│ Couche 1│──┬──────────────▶
     (source)       │ Attn+FFN│  │ +Δ₁ (affluent)
                    └─────────┘  │
                                 ▼
                    ┌─────────┐
              ─────▶│ Couche 2│──┬──────────────▶
                    │ Attn+FFN│  │ +Δ₂ (affluent)
                    └─────────┘  │
                                 ▼
                         ...80 couches...
                                 │
                                 ▼
                              Logits

Chaque couche est comme un affluent qui enrichit la rivière sans remplacer ce qui coule déjà. La rivière principale (residual) transporte l'info originale + toutes les contributions.

🔍 Pourquoi c'est crucial ?

Sans connexions résiduelles, l'info de l'embedding serait perdue après quelques couches (vanishing gradient). Avec elles :

• L'info originale traverse directement
• Chaque couche AJOUTE au lieu de REMPLACER
• Gradient stable pour le training
• Le modèle peut "skip" des couches inutiles

📊 Ce que fait chaque "zone" de couches

CouchesRôleExemple
1-20Syntaxe locale"le" → article, nom attendu
20-40Structure grammaticale"chat" = sujet de "mange"
40-60Sémantique"mange" → contexte nourriture
60-80Raisonnement haut niveauQu'est-ce qui suit logiquement ?

📊 Logits & Softmax

Du vecteur aux probabilités

📍 Notre exemple
Vecteur "mange" [8192] LM Head 128K logits softmax "sa": 62%
Dernier vect. [8192] LM Head 128K Logits "sa": 8.2 "la": 6.1 "une": 5.8 softmax Probabilités "sa": 62% "la": 18% "une": 12% 🎲 "sa" P(i) = e^(logit_i) / Σ e^(logit_j)
💡 L'essentiel

Le dernier vecteur est projeté vers 128K scores bruts (logits) — un par token du vocabulaire. Softmax transforme ces scores en probabilités (somme = 100%). Gros scores → grosses probas, petits → quasi-zéro. Puis vient le sampling pour choisir.

📖 Glossaire
Logit
— Score brut (peut être négatif)
Softmax
— Scores → probabilités
LM Head
— Projection [d → vocab]
Greedy
— Toujours prendre le max

🎲 Sampling

Choisir le prochain token

📍 Notre exemple
Probas [62%, 18%, 12%...] température 0.7 top-p 0.9 tirage "sa" ✓
Original sa 62% T=0.7 Après T sa 77% top-p Après top-p éliminé 🎲 Tirage : "sa" 🌡️ T basse → amplifie écarts 🎯 Top-p → garde 90% cumulé 🎲 Tirage pondéré
💡 L'essentiel

Température : ajuste les écarts. Basse (0.2) = déterministe, haute (1.5) = créatif/risqué. Top-p (nucleus) : garde les tokens jusqu'à p% cumulé, élimine les absurdités. Ensemble : température redistribue, top-p filtre → créatif mais pas fou.

📖 Glossaire
Température
— logits/T avant softmax
Top-p
— Garde tokens cumulant p%
Top-k
— Garde les k meilleurs
Greedy
— T=0, toujours le max

🎛️ Combinaisons recommandées

UsageTempératureTop-p
Code / Maths0.0 - 0.30.9
Assistants0.70.9
Créatif1.0 - 1.30.95

💾 KV Cache

Éviter les recalculs pendant la génération

📍 Notre exemple
Prefill : K₁₋₃, V₁₋₃ stockés génère "sa" ajoute K₄, V₄ génère "pâtée"...
KV Cache K₁, V₁ "Le" ✓ K₂, V₂ "chat" ✓ K₃, V₃ "mange" ✓ Nouveau : "sa" Calcule Q₄, K₄, V₄ Q₄ × K₁₋₃ Ajoute K₄, V₄ ⚠️ Pourquoi pas Q ? • Q = "Ce que JE cherche" • K = "Ce que je SUIS" • V = "Ce que je DONNE" → Nouveau interroge K,V anciens
💡 L'essentiel

Pour générer le token N, l'attention regarde les tokens 1 à N-1. Sans cache : recalculer K,V pour TOUS (O(N²)). Avec cache : K,V stockés, on calcule seulement pour le nouveau (O(N)). On stocke K et V, pas Q (le nouveau génère sa propre Q).

📖 Glossaire
KV Cache
— Stockage K,V passés
Prefill
— Cache initial du prompt
GQA
— Réduit KV (8 au lieu de 64)
Paged
— Gestion par pages (vLLM)

📏 Calcul de la taille

KV par token = 2 × layers × kv_heads × head_dim × bytes
LLaMA 70B, contexte 4K :
Par token : 2 × 80 × 8 × 128 × 2 = 327 KB
4096 tokens : 327 KB × 4096 ≈ 1.3 GB
8 requêtes batch : 1.3 GB × 8 ≈ 10 GB

🧩 Mixture of Experts (MoE)

Plus de paramètres, même compute

📍 Notre exemple
"mange" Router choisit Expert 2 + Expert 5 0.7×E2 + 0.3×E5 output
Token ROUTER softmax(x·Wg) Expert 2 w=0.7 Expert 5 w=0.3 ... Σ Output Mixtral 8×7B Total: 47B Actif: 13B Top-2
💡 L'essentiel

Au lieu d'un seul gros FFN, on a 8 experts spécialisés. Un router sélectionne les 2 meilleurs pour chaque token. Avantage : plus de paramètres (47B) mais même compute (13B actifs). Inconvénient : tous les poids en VRAM quand même.

📖 Glossaire
MoE
— Mixture of Experts
Router
— Sélectionne les experts
Top-K
— K experts par token (souvent 2)
Sparse
— Pas tout activé

🔧 Function Calling, Tools, MCP & Agents

Le LLM qui interagit avec le monde

📍 Notre exemple
"Météo à Paris ?" LLM génère JSON système appelle API résultat → LLM répond
USER "Météo Paris demain?" 🧠 LLM → get_weather() OUTPUT JSON {"fn":"get_weather"...} ⚡ SYSTÈME Appelle API "18°C, nuageux" 🛠️ Tools get_weather() search_web() run_code() définis en JSON
💡 L'essentiel

Tool = une fonction que le LLM peut appeler. Function Calling = le LLM génère un JSON structuré pour déclencher un tool. MCP = protocole standardisé pour exposer des tools. Agent = LLM + tools + boucle d'exécution autonome.

📖 Glossaire
Tool
— Fonction externe appelable
Function Call
— JSON généré par le LLM
MCP
— Model Context Protocol
Agent
— LLM autonome + boucle

🛠️ Tool (Outil)

Un Tool est simplement une fonction que le LLM peut décider d'utiliser :

Exemples de tools :
• get_weather(city) → API météo
• search_web(query) → Moteur de recherche
• run_python(code) → Exécution de code
• read_file(path)  → Lecture de fichier
• send_email(to, subject, body) → Envoi d'email

Le tool est décrit au LLM (nom, paramètres, description) mais exécuté par le système hôte.

📞 Function Calling

Function Calling = la capacité du LLM à générer un appel de fonction structuré :

User : "Quelle météo à Paris demain ?"

LLM génère (au lieu de texte) :
{
  "function": "get_weather",
  "arguments": {
    "city": "Paris",
    "date": "2025-01-02"
  }
}

→ Le système exécute get_weather("Paris", "2025-01-02")
→ Résultat renvoyé au LLM
→ LLM formule la réponse finale

C'est un mode de sortie du LLM, pas une exécution. Le LLM ne fait que générer du JSON.

🔌 MCP — Model Context Protocol

MCP (Anthropic) est un protocole standardisé pour exposer des tools :

Avant MCP :
• Chaque app définit ses tools différemment
• Intégration custom à chaque fois
• Pas de réutilisation

Avec MCP :
┌─────────────┐     ┌─────────────┐
│  Claude.ai  │────▶│ MCP Server  │──▶ Google Drive
│  ou autre   │     │ (standard)  │──▶ Slack
│   client    │     │             │──▶ GitHub
└─────────────┘     └─────────────┘──▶ Base de données

• Un serveur MCP = N tools exposés
• Un client MCP = peut utiliser N serveurs
• Protocol JSON-RPC standardisé

Analogie : MCP est aux tools ce que USB est aux périphériques — un standard de connexion.

🤖 Agent

Un Agent = LLM + Tools + Boucle autonome :

Agent vs Chatbot simple :

CHATBOT :
User → LLM → Réponse (1 tour)

AGENT :
User → LLM → "Je dois chercher X"
         ↓
      Tool: search(X)
         ↓
      LLM → "Maintenant je dois lire Y"
         ↓
      Tool: read(Y)
         ↓
      LLM → "J'ai assez d'info, voici la réponse"
         ↓
      Réponse finale

L'agent décide seul quels tools utiliser et dans quel ordre. Il boucle jusqu'à avoir la réponse.

📊 Comparaison résumée

ConceptC'est quoi ?Qui l'exécute ?
ToolUne fonction disponibleLe système hôte
Function CallJSON généré par LLMLLM génère, système exécute
MCPProtocole standardServeur MCP
AgentLLM + tools + boucleAutonome (multi-tours)

🔄 Exemple concret d'Agent

User : "Trouve les 3 meilleurs restos italiens près de chez moi 
        et réserve une table pour 2 demain soir"

Agent (Claude Code, Cursor, etc.) :

1. 🧠 "Je dois d'abord localiser l'utilisateur"
   → Tool: get_location() → "Paris 11ème"

2. 🧠 "Maintenant chercher les restos"
   → Tool: search_restaurants("italien", "Paris 11") 
   → [Lista Ristorante, Ober Mamma, Pink Mamma...]

3. 🧠 "Je dois vérifier les notes"
   → Tool: get_reviews(["Lista", "Ober Mamma", "Pink Mamma"])
   → Scores et avis

4. 🧠 "Maintenant réserver"
   → Tool: book_table("Ober Mamma", "2025-01-02", "20:00", 2)
   → Confirmation #12345

5. 🧠 "J'ai tout, je peux répondre"
   → "J'ai réservé chez Ober Mamma demain 20h pour 2.
      Confirmation #12345. Bon appétit !"

5 appels de tools, décidés autonomement par l'agent.

🧠 Reasoning

Les modèles qui "réfléchissent" (o1, R1)

📍 Notre exemple
"17×28 ?" <think> 17×30-17×2 = 510-34 </think> "476" ✓
LLM Standard "17×28?" "476" 1 forward → réponse ❌ Souvent faux Reasoning (o1, R1) "17×28?" <think> 17×30-17×2=510-34=476 </think> "476" ✓ 💡 Pourquoi ? Plus de tokens = plus de compute Test-time scaling ⚠️ Plus cher !
💡 L'essentiel

Les modèles reasoning génèrent une chaîne de pensée (CoT) avant de répondre. Plus de tokens générés = plus de "compute à l'inférence" = meilleures réponses sur des problèmes complexes. Le CoT peut être visible (R1) ou caché (o1). C'est une nouvelle dimension de scaling.

📖 Glossaire
CoT
— Chain of Thought
Test-time
— Compute à l'inférence
o1/o3
— OpenAI reasoning
R1
— DeepSeek reasoning (open)

🔢 Formats de Précision

FP32, FP16, INT8, INT4...

📍 Notre exemple
70B params × 2 bytes (FP16) = 140 GB | × 0.5 byte (INT4) = 35 GB
FP32 4 bytes FP16 2 bytes BF16 2 bytes FP8 1 byte INT8 1 byte INT4 0.5 byte Mémoire pour 70B : FP32: 280 GB FP16: 140 GB INT8: 70 35
💡 L'essentiel

FP16/BF16 : standard pour l'inférence (2 bytes/param). INT8 : divise par 2 avec peu de perte. INT4/NF4 : divise par 4, perte qualité mesurable mais 70B tourne sur 2× RTX 4090 ! La quantization est un trade-off mémoire vs qualité.

📖 Glossaire
FP16
— 16 bits float (±65K)
BF16
— Brain Float (range FP32)
GPTQ/AWQ
— Méthodes quantization
GGUF
— Format llama.cpp

💾 Mémoire GPU

Combien de VRAM faut-il ?

📍 Notre exemple
LLaMA 70B FP16 : 140 GB poids + ~10 GB KV cache (8 req × 4K) = ~150 GB VRAM
2× H100 = 160 GB VRAM Poids : 140 GB (87.5%) KV 10G libre Poids = params × bytes/param KV = f(batch, seq_len)
💡 L'essentiel

Poids = params × bytes (70B × 2 = 140 GB, pas 70 !). KV Cache grandit avec contexte × batch. Solutions : Quantization (INT4 → 35 GB), Tensor Parallelism (split sur N GPUs), Offloading (CPU/disk).

📖 Glossaire
VRAM
— Mémoire GPU
Tensor Parallel
— Split matrices
Pipeline Parallel
— Split couches
Offload
— Décharge vers CPU

📊 VRAM selon précision

PrécisionBytes/param70B13B
FP324280 GB52 GB
FP16/BF162140 GB26 GB
INT8170 GB13 GB
INT40.535 GB6.5 GB

🎓 Training & Fine-tuning

Du pre-training au déploiement optimisé

📍 Pipeline complet
Pre-train SFT RLHF/DPO LoRA (optionnel) Merge AWQ/GPTQ Deploy
1 PRE-TRAIN Next-token 2 SFT Instructions 3 RLHF Alignement 4 LoRA Adapters 5 QUANT AWQ/GPTQ 6 DEPLOY vLLM Coût : Pre-train = millions $ | SFT = milliers $ | LoRA = centaines $ | Quant = heures Mémoire : Full FT = 10-20× modèle | QLoRA = ~1× modèle (en 4-bit)
💡 L'essentiel

Pre-training : prédire le token suivant sur TB de texte. SFT : fine-tuning instruction→réponse. RLHF/DPO : aligner sur préférences humaines. LoRA : adapter sans toucher les poids. Quantization : compresser pour le déploiement.

📖 Glossaire
SFT
— Supervised Fine-Tuning
RLHF
— RL from Human Feedback
DPO
— Direct Preference Optim
LoRA
— Low-Rank Adaptation

🔧 LoRA — Low-Rank Adaptation

Problème : Fine-tuner 70B params = modifier 70 milliards de poids → impossible en mémoire.

Solution : Ajouter des petites matrices (adapters) sans toucher les poids originaux :

W' = W + ΔW = W + (A × B)
W : poids originaux (frozen)
A : matrice (d × r)  ← r = rang (64-128)
B : matrice (r × d)

Pour une couche 4096×4096 = 16.7M params :

ApprocheParams entraînésRatio
Full fine-tune16.7M100%
LoRA r=64524K3%

Pourquoi ça marche ? Les modifications pour adapter un LLM vivent dans un sous-espace de faible dimension.

⚡ QLoRA — Quantized LoRA

Charge le modèle base en 4-bit (frozen) + adapters LoRA en FP16 (trainable) :

Mémoire = Base(4-bit frozen) + LoRA(FP16 trainable)
70B en QLoRA ≈ 35-40 GB (vs 140+ GB full fine-tune)

→ Fine-tuner LLaMA 70B sur 1× A100 80GB devient possible !

📉 Quantization — Compresser pour l'inference

Réduire FP16 → INT4 divise la mémoire par 4, mais quantization naïve = perte de qualité.

Observation clé : tous les poids ne sont pas égaux. Certains ont un impact énorme sur les activations, d'autres très peu.

🎯 AWQ — Activation-aware Weight Quantization

Identifie les poids saillants via les activations sur un dataset de calibration :

Saillance(w) = |w| × ||activation||

Les poids importants (~1%) sont protégés de la quantization agressive.

Processus :
1. Charger modèle FP16
2. Passer dataset de calibration
3. Mesurer activations par couche
4. Identifier poids saillants
5. Quantifier en préservant les importants
6. Sauvegarder INT4

⚠️ Calibration importante : utiliser des samples représentatifs de ton use-case !

⚙️ GPTQ — Post-Training Quantization

Quantifie couche par couche avec compensation d'erreur (méthode OBQ/OBS) :

Pour chaque couche :
1. Quantifier un groupe de poids
2. Mesurer l'erreur introduite
3. Ajuster les poids restants pour compenser
4. Répéter

Plus lent à générer qu'AWQ, mais souvent légèrement meilleure qualité.

📊 Comparaison AWQ vs GPTQ

AspectAWQGPTQ
Vitesse quantizationRapideLent
QualitéTrès bonneExcellente
Inference speedOptimisé batchStandard
CalibrationCritiqueMoins critique
Support vLLMNatif (GEMM)Natif

Recommandation : AWQ pour vLLM en production, GPTQ si qualité max requise.

🔄 Pipeline complet fine-tune + deploy

Modèle base (FP16, 140GB)
        │
        ▼ QLoRA fine-tuning
   ┌─────────┐
   │  LoRA   │ ← Entraîne adapters sur ta tâche
   │ adapters│   Base frozen en 4-bit
   └────┬────┘
        │
        ▼ Merge
   ┌─────────┐
   │  Merge  │ ← Fusionne adapters dans le modèle
   └────┬────┘
        │
        ▼ Quantization
   ┌─────────┐
   │   AWQ   │ ← Avec calibration sur ton domaine
   └────┬────┘
        │
        ▼
Modèle final (INT4, 35GB)
        │
        ▼ Serving
   ┌─────────┐
   │  vLLM   │ ← Inference optimisée
   └─────────┘

⚡ Optimisations

Techniques pour aller plus vite

📍 Résumé
Flash Attention (2-4×) | Speculative Decode (2-3×) | Paged Attention (+24× throughput) | Continuous Batching

⚡ Flash Attention

Calcul par blocs en SRAM, évite matrice N×N en HBM. 2-4× plus rapide, contextes plus longs.

🔮 Speculative Decode

Petit modèle "devine" K tokens, gros vérifie en batch. 2-3× speedup, qualité identique.

📄 Paged Attention

KV Cache par pages (comme mémoire virtuelle). +24× throughput via vLLM.

📦 Continuous Batching

Insertion dynamique dès qu'une requête finit. GPU toujours occupé.

💾 GQA

Partage K,V entre têtes Q. KV Cache ÷4 à ÷8, qualité ~identique.

📉 Quantization

GPTQ, AWQ, NF4. Mémoire ÷2 à ÷8, trade-off qualité.

💡 L'essentiel

L'inférence est memory-bound (on attend les données). Les optimisations ciblent : réduire les accès mémoire (Flash Attention), paralléliser la vérification (Speculative), mieux gérer la mémoire (Paged), maximiser l'utilisation GPU (Continuous Batching).

📖 Glossaire
vLLM
— Serveur optimisé (Paged)
TensorRT-LLM
— NVIDIA optimizations
SRAM
— Cache ultra-rapide GPU
HBM
— High Bandwidth Memory

⚡ Le problème : la matrice N×N

L'attention standard calcule une matrice Q × Kᵀ de taille N × N (N = longueur séquence) :

Contexte 32K tokens :
Matrice attention = 32K × 32K = 1 milliard de floats
En FP16 = 2 GB juste pour cette matrice !

→ Doit être stockée en HBM (lent)
→ Limite la longueur de contexte

💡 La solution : calcul par blocs

Flash Attention ne matérialise jamais la matrice complète :

Au lieu de :
1. Calculer toute la matrice Q×Kᵀ (N×N en HBM)
2. Appliquer softmax
3. Multiplier par V

Flash Attention :
1. Charger un bloc de Q, K, V en SRAM (20MB, ultra-rapide)
2. Calculer attention sur ce bloc
3. Accumuler le résultat
4. Passer au bloc suivant

→ Jamais plus de quelques MB en mémoire
→ 2-4× plus rapide
→ Contextes 100K+ possibles

📊 Hiérarchie mémoire GPU

TypeTailleBande passanteUsage
Registres~KB~20 TB/sCalculs immédiats
SRAM20 MB~19 TB/sFlash Attention
HBM80 GB~3 TB/sPoids, KV Cache

Flash Attention exploite le SRAM (6× plus rapide que HBM) en ne gardant que des blocs.

📄 Le problème : fragmentation du KV Cache

Sans Paged Attention, chaque requête réserve un bloc contigu de mémoire pour son KV Cache :

Requête A : [████████████████________] 16K alloués, 12K utilisés
Requête B : [████████____] 12K alloués, 8K utilisés
Requête C : [████████████████████____] 20K alloués, 18K utilisés

→ Gaspillage énorme (fragmentation interne)
→ Impossible de servir plus de requêtes

💡 La solution : mémoire virtuelle pour LLM

Paged Attention alloue le KV Cache par pages (comme un OS) :

Pages de 16 tokens chacune :

Requête A : page 0 → page 3 → page 7 → page 12
Requête B : page 1 → page 5 → page 8
Requête C : page 2 → page 4 → page 6 → page 9 → page 11

→ Pas de fragmentation
→ Allocation dynamique
→ Pages libérées dès que la requête finit
→ +24× throughput vs naïf !

🚀 vLLM = Paged Attention + Continuous Batching

Continuous Batching :
• Requête A finit → sa place est immédiatement réutilisée
• Nouvelle requête D s'insère sans attendre le batch

llama.cpp : 1 requête à la fois (simple, CPU-friendly)
vLLM      : N requêtes en parallèle (optimisé serveur)

Pour du self-hosting en production → vLLM
Pour du dev local → llama.cpp / Ollama