Aller au contenu principal
Le KV-Cache des LLM expliqué aux développeurs
Retour au blog
IA

Le KV-Cache des LLM expliqué aux développeurs

Patrice Huetz6 avril 202611 min

Tu as déjà envoyé un prompt de 100 000 tokens à Claude ou GPT et remarqué que la première réponse prend 15 secondes, mais les suivantes arrivent en 2 secondes ? Ce n'est pas de la magie. C'est le KV-cache. Et si tu développes des applications LLM sans comprendre comment il fonctionne, tu brûles de l'argent et de la mémoire GPU sans le savoir.

Le problème fondamental : l'attention recalcule tout

Pour comprendre le KV-cache, il faut d'abord comprendre le mécanisme d'attention des Transformers. Je vais simplifier volontairement — on n'est pas dans un papier de recherche, on est entre développeurs.

Quand un Transformer génère un token, il doit « regarder » tous les tokens précédents. C'est le mécanisme d'attention : chaque token pose une question (Query), et les tokens précédents répondent avec des clés (Keys) et des valeurs (Values).

Concrètement, pour générer le token N+1 :

  1. 1.Le modèle calcule un vecteur Query pour le nouveau token
  2. 2.Il compare ce Query à tous les Keys des tokens 1 à N
  3. 3.Les scores d'attention pondèrent les Values correspondantes
  4. 4.La somme pondérée des Values produit la représentation du nouveau token

Le problème ? Sans cache, à chaque nouveau token généré, le modèle recalcule les Keys et Values de tous les tokens précédents. Pour une séquence de 1 000 tokens, le 1 000e token nécessite le recalcul des K et V des 999 tokens avant lui. Le 1 001e les recalcule aussi. Et ainsi de suite.

C'est du O(n²) en calcul. Catastrophique.

ℹ️
L'attention n'est pas le seul composant d'un Transformer, mais c'est le plus coûteux. Les couches feed-forward, la normalisation et les embeddings sont des opérations en O(n) — linéaires. C'est l'attention qui crée le goulot d'étranglement quadratique.

La solution : mettre les K et V en cache

L'idée est simple : pourquoi recalculer ce qui ne change pas ? Les Keys et Values des tokens déjà traités sont déterministes — ils ne changent pas quand on ajoute un nouveau token. Alors on les stocke en mémoire.

C'est le KV-cache : un tableau en mémoire GPU qui contient les tenseurs Keys et Values de chaque token, pour chaque couche et chaque tête d'attention du modèle.

Avec le KV-cache :

  1. 1.Pour le token 1 : calculer Q, K, V. Stocker K et V.
  2. 2.Pour le token 2 : calculer Q, K, V. Récupérer K₁, V₁ du cache. Stocker K₂, V₂.
  3. 3.Pour le token N : calculer Q, K, V. Récupérer K₁...N₋₁, V₁...N₋₁ du cache.

La génération passe de O(n²) à O(n) par token. C'est pour ça que la première réponse est lente (phase de « prefill » — remplissage du cache) et les suivantes sont rapides (phase de « decode » — lecture du cache).

La formule qui fait mal : combien de mémoire ?

Voici la formule exacte de la taille du KV-cache :

Mémoire KV-cache = 2 × L × H × D × S × B

Où :

  • 2 : on stocke les Keys ET les Values
  • L : nombre de couches (layers) du modèle
  • H : nombre de têtes d'attention (heads)
  • D : dimension par tête (head_dim)
  • S : longueur de la séquence (nombre de tokens en cache)
  • B : taille en bytes par paramètre (2 pour FP16, 4 pour FP32)
⚠️
Cette formule est par requête. Si tu sers 10 utilisateurs en parallèle, multiplie par 10. C'est souvent là que les déploiements explosent — le modèle tient en mémoire, mais le KV-cache de 32 requêtes simultanées ne tient pas.

Exemple concret : Llama 3.1 8B à 128K tokens

Prenons les spécifications de Llama 3.1 8B :

  • Couches (L) : 32
  • Têtes KV (H) : 8 (Grouped Query Attention — j'y reviendrai)
  • Dimension par tête (D) : 128
  • Précision : FP16 (B = 2 bytes)

Pour une séquence de 128 000 tokens :

Mémoire = 2 × 32 × 8 × 128 × 128 000 × 2
        = 2 × 32 × 8 × 128 × 128 000 × 2
        = 16 777 216 000 bytes
        ≈ 16 Go

Seize gigaoctets. Pour une seule requête. Le modèle lui-même pèse environ 16 Go en FP16. Donc à 128K tokens de contexte, le KV-cache prend autant de mémoire que le modèle.

Et Llama 8B utilise le Grouped Query Attention (GQA) qui réduit le nombre de têtes KV à 8 au lieu de 32. Avec l'attention standard (Multi-Head Attention), ce serait 64 Go de KV-cache. Pour un modèle de 8 milliards de paramètres.

Pour Llama 3.1 70B (80 couches, 8 têtes KV, 128 dim) à 128K tokens :

Mémoire = 2 × 80 × 8 × 128 × 128 000 × 2 ≈ 32 Go

Trente-deux gigaoctets de cache. Par requête.

💡
En production, la règle empirique est : prévois autant de mémoire GPU pour le KV-cache que pour le modèle lui-même. Si ton modèle pèse 40 Go, prévois 80 Go de VRAM totale pour servir des contextes longs. C'est pour ça que les H100 (80 Go) sont si demandés.

Grouped Query Attention : la première optimisation

Le Grouped Query Attention (GQA) est la raison pour laquelle les modèles récents sont viables avec de longs contextes. Au lieu d'avoir une paire K/V par tête d'attention, GQA partage les K/V entre plusieurs têtes.

Llama 3.1 8B a 32 têtes d'attention (Query) mais seulement 8 têtes KV. Chaque tête KV est partagée par 4 têtes Query. Résultat : le KV-cache est 4 fois plus petit.

MHA : 32 têtes Q × 32 têtes KV → ratio 1:1
GQA : 32 têtes Q × 8 têtes KV  → ratio 4:1  (4x moins de cache)
MQA : 32 têtes Q × 1 tête KV   → ratio 32:1 (32x moins de cache)

Le Multi-Query Attention (MQA) pousse le concept à l'extrême avec une seule tête KV, mais la qualité en souffre. GQA est le compromis qui a gagné : réduction significative de la mémoire avec une perte de qualité négligeable.

Flash Attention : vitesse sans réduire la mémoire

Flash Attention (Tri Dao, 2022) ne réduit pas la taille du KV-cache — il accélère le calcul de l'attention. Le truc ? Il exploite la hiérarchie mémoire du GPU.

Le problème : l'attention standard matérialise une matrice N×N en HBM (la mémoire principale du GPU). Pour N = 128K, c'est une matrice de 128K × 128K = 16 milliards d'éléments. La lire et l'écrire en HBM est lent.

Flash Attention découpe le calcul en blocs qui tiennent dans le SRAM (la mémoire rapide du GPU, typiquement 20 Mo). Il ne matérialise jamais la matrice N×N complète. Résultat :

  • 2-4x plus rapide que l'attention standard
  • Mémoire O(N) au lieu de O(N²) pour la matrice intermédiaire
  • Numériquement exact (pas une approximation)

Flash Attention 2 et 3 améliorent encore les performances avec un meilleur partitionnement du travail entre les threads du GPU.

ℹ️
Flash Attention est maintenant intégré par défaut dans PyTorch (via `torch.nn.functional.scaled_dot_product_attention`), Hugging Face Transformers et vLLM. Tu n'as rien à configurer — il est activé automatiquement si ton GPU le supporte (Ampere+).

PagedAttention : la révolution vLLM

PagedAttention (Kwon et al., 2023) résout un problème différent : le gaspillage mémoire du KV-cache.

Le problème : quand tu alloues le KV-cache pour une requête, tu dois réserver la mémoire pour la longueur maximale possible (disons 4096 tokens). Mais la plupart des requêtes n'utilisent que 200-500 tokens. Résultat : 80-90 % de la mémoire KV-cache est allouée mais vide.

PagedAttention emprunte le concept de pagination de la mémoire virtuelle des systèmes d'exploitation. Au lieu d'allouer un bloc contigu pour chaque requête, il découpe le KV-cache en « pages » de taille fixe (typiquement 16 tokens). Les pages sont allouées à la demande et peuvent être non contiguës en mémoire.

Bénéfices :

  • Utilisation mémoire quasi parfaite : plus de fragmentation
  • 3-5x plus de requêtes simultanées à mémoire GPU constante
  • Partage de cache : les préfixes communs (system prompt) partagent les mêmes pages

vLLM a été construit autour de PagedAttention. C'est pour ça qu'il domine le serving de LLM en production. Si tu sers un modèle en production et que tu n'utilises pas vLLM (ou TGI/TensorRT-LLM qui implémentent des techniques similaires), tu gaspilles entre 60 et 80 % de ta VRAM.

Quantification du KV-cache

La quantification réduit la précision des tenseurs pour économiser de la mémoire. Pour le KV-cache, passer de FP16 (2 bytes) à INT8 (1 byte) divise la mémoire par deux. Passer à INT4 la divise par quatre.

Mais attention : quantifier le KV-cache est plus risqué que quantifier les poids du modèle. Les valeurs du cache ont une distribution très différente des poids — elles contiennent des outliers qui sont critiques pour l'attention. Quantifier agressivement (INT4) dégrade la qualité.

Les techniques modernes comme KV-Quant et KIVI appliquent une quantification par canal avec calibration, préservant les outliers. Résultat : INT4 sur le KV-cache avec une perte de qualité < 1 % sur les benchmarks.

Mémoire avec quantification INT4 :
Llama 8B @ 128K tokens : 16 Go → 4 Go
Llama 70B @ 128K tokens : 32 Go → 8 Go

La combinaison GQA + PagedAttention + quantification INT4 rend les contextes de 128K tokens praticables sur du hardware accessible (une seule A100 80 Go pour Llama 70B).

💡
Si tu utilises vLLM en production, active la quantification du KV-cache avec `--kv-cache-dtype fp8_e5m2`. C'est un format 8 bits optimisé pour les GPU Hopper (H100) qui offre le meilleur compromis qualité/mémoire. Pour les GPU Ampere (A100), utilise `int8`.

Le prefix caching : ne pas recalculer le system prompt

Un pattern omniprésent : toutes tes requêtes commencent par le même system prompt de 2 000 tokens. Sans optimisation, le KV-cache de ces 2 000 tokens est recalculé pour chaque requête.

Le prefix caching (ou prompt caching) stocke le KV-cache du préfixe commun et le réutilise entre les requêtes. Anthropic l'expose directement dans l'API Claude sous le nom « prompt caching ». OpenAI le fait automatiquement.

En self-hosting avec vLLM, active-le avec --enable-prefix-caching. Le gain est immédiat :

  • Réduction du TTFT (Time To First Token) de 50-80 %
  • Réduction de la consommation GPU proportionnelle à la taille du préfixe

Pour les applications RAG où chaque requête inclut des documents de contexte récurrents, le prefix caching est transformationnel.

Ce que ça signifie pour toi, développeur

Pourquoi tout ceci devrait t'intéresser si tu ne fais pas de la recherche en IA ?

1. Comprendre tes factures API. La tarification au token des APIs reflète directement le coût du KV-cache. Les tokens d'entrée coûtent moins que les tokens de sortie parce que le prefill est parallélisable (un seul calcul matriciel pour tout le prompt) alors que le decode est séquentiel (un calcul par token généré).

2. Optimiser tes prompts. Un system prompt de 10 000 tokens multiplié par 100 000 requêtes/jour = un milliard de tokens de KV-cache recalculés si le prefix caching n'est pas actif. Structure tes prompts pour maximiser le préfixe commun.

3. Choisir le bon hardware. La VRAM est le goulot d'étranglement pour les contextes longs, pas les FLOPS. Un GPU avec 80 Go de VRAM lente (A100 PCIe) est souvent préférable à un GPU avec 24 Go de VRAM rapide (RTX 4090) pour servir des LLM en production.

4. Comprendre les limites de contexte. Quand un modèle annonce 128K tokens de contexte, c'est une promesse de mémoire GPU, pas de capacité cognitive. Le modèle « peut » traiter 128K tokens — mais la qualité de l'attention se dégrade sur les positions éloignées (le fameux « lost in the middle »).

Résumé visuel

Requête arrive (2000 tokens)
    ↓
[Prefill] Calcul K,V pour les 2000 tokens → stockage en cache
    ↓ (lent : 2-5 secondes)
[Decode] Token 2001 : Q × K_cache → attention → V_cache → sortie
    ↓ (rapide : 20-50 ms/token)
[Decode] Token 2002 : idem + K/V du token 2001 ajoutés au cache
    ↓
... jusqu'à token de fin ou limite de contexte

Le KV-cache est le composant invisible qui rend les LLM utilisables. Sans lui, chaque token généré prendrait autant de temps que le prefill initial. Une réponse de 500 tokens prendrait 500 fois 3 secondes = 25 minutes. Au lieu de 10 secondes.

J'ai creusé en détail le KV-cache, les architectures de mémoire des LLM et les techniques d'optimisation avancées (TurboQuant, context engineering, frameworks Mem0/Letta/Zep) dans mon guide technique.

La Mémoire des Machines
La Mémoire des Machines

Du KV-Cache au Context Engineering.

Découvrir →
🔒

Soutenez mon travail sur Patreon

Accès anticipé aux articles, contenu exclusif, et la satisfaction de soutenir un auteur indépendant.

Rejoindre — à partir de 3€/mois

Commentaires

Chargement des commentaires...

Laisser un commentaire