Aller au contenu principal

Performance

Le cache KV (Key-Value) expliqué

Le cache KV expliqué : calculer sa taille, le compresser (GQA, MLA, FP8), et l'impact du prefix caching sur les architectures hybrides.

Par Antoine Michéa, Fondateur & ingénieur infrastructure · · mis à jour le

Le cache KV est une optimisation fondamentale qui évite de recalculer les vecteurs Key et Value des tokens précédents à chaque étape de génération, accélérant drastiquement l’inférence. C’est aussi le poste mémoire qui décide de la longueur de contexte qu’on peut tenir.

Le problème de la génération autorégressive

Un LLM génère du texte un token à la fois. À chaque nouveau token, le mécanisme d’attention doit considérer tous les tokens précédents. Sans optimisation, cela implique de recalculer les mêmes valeurs encore et encore.

Étape 1: "Bonjour"          → Calcule K,V pour "Bonjour"
Étape 2: "Bonjour je"       → Recalcule K,V pour "Bonjour" + calcule pour "je"
Étape 3: "Bonjour je suis"  → Recalcule K,V pour "Bonjour", "je" + calcule "suis"
...
→ Complexité quadratique O(n²) en nombre de tokens

La solution : le cache KV

L’idée est simple : stocker (cacher) les vecteurs Key et Value une fois calculés, puis les réutiliser aux étapes suivantes. Seul le nouveau token nécessite un calcul.

Étape 1: "Bonjour"  → Calcule K₁,V₁ → Cache: [K₁,V₁]
Étape 2: + "je"     → Calcule K₂,V₂ → Cache: [K₁,K₂], [V₁,V₂]
Étape 3: + "suis"   → Calcule K₃,V₃ → Cache: [K₁,K₂,K₃], [V₁,V₂,V₃]
...
→ Complexité linéaire O(n) par nouveau token

Le cache KV échange mémoire contre calcul : l’inférence devient linéaire au lieu de quadratique, mais le cache peut devenir volumineux pour de longues séquences.

Calcul de la taille du cache KV

La taille du cache dépend de plusieurs facteurs architecturaux du modèle :

Taille_KV = 2 × n_layers × n_heads × head_dim × seq_len × batch_size × bytes
Modèle Couches Têtes Cache/token (FP16) Cache 4K tokens
Qwen3-4B 36 8 (GQA) ~144 Ko ~0,6 Go
Qwen3.6-27B (dense GQA) 64 8 (GQA) ~262 Ko ~1,0 Go
Qwen3.8-27B (hybride GDN) 64 dont 16 en attention complète 4 (GQA) ~64 Ko (32 Ko en FP8) ~0,26 Go
Qwen3.6-35B-A3B (MoE) 48 4 (GQA) ~98 Ko ~0,4 Go
Llama 4 Scout (17B/109B MoE) 48 8 (GQA) ~196 Ko ~0,8 Go
DeepSeek V3 (MoE 671B) 61 MLA — ~70 Ko / token ~0,3 Go

À noter : un MoE comme Qwen3.6-35B-A3B a un cache KV plus petit qu’un dense de taille proche, parce qu’il a moins de têtes K/V et moins de couches d’attention par token.

Optimisations du cache KV

1. Quantification du cache

Au lieu de stocker les K/V en FP16, on peut les quantifier, réduisant la mémoire de 2x à 4x. C’est l’optimisation la plus accessible.

# llama.cpp
./llama-cli -m model.gguf \
    --cache-type-k q8_0 \    # Quantifie les Keys
    --cache-type-v q8_0 \    # Quantifie les Values
    --flash-attn             # Requis pour quantifier V
# vLLM
vllm serve model --kv-cache-dtype fp8_e5m2

En pratique, q8_0 (llama.cpp) et fp8_e5m2 (vLLM) sont le bon défaut : ~41,7 Kio/token, ce qui double le contexte tenable à VRAM constante. À titre d’exemple, lors de nos tests cela permet de tenir 82 227 tokens dans 3,3 Gio de cache. Les variantes plus agressives (q4_0, 3 bits) sont à éviter : la perte de qualité devient visible, surtout en extraction de données et en tool-calling.

Et le cache en 4 bits « natif » ?

Les formats 4 bits micro-scalés (NVFP4 et apparentés) changent un peu la donne : ce ne sont pas 4 bits bruts, mais 4 bits plus un facteur d’échelle par bloc de seize valeurs, ce qui récupère l’essentiel de la plage dynamique. Certains moteurs les proposent désormais pour le cache lui-même, avec un gain annoncé de l’ordre de 13 % de mémoire par token par rapport au 8 bits.

Nous restons prudents, pour une raison précise : le cache tolère beaucoup moins la quantification que les poids. Les poids encaissent bien 4 bits ; le cache encaisse bien 8 bits ; et 4 bits est exactement la zone où ça commence à coûter. Deux mécanismes l’expliquent — les vecteurs Key contiennent des dimensions à valeurs extrêmes que la quantification par blocs écrase mal, et sur contexte long l’erreur ne s’annule pas : elle se propage dans le softmax de l’attention sur des milliers de positions.

⚠️ Et surtout, le test habituel ne le mesure pas. Le needle in a haystack — retrouver une chaîne unique dans un long document — est le test le plus facile qui soit : il suffit que l’aiguille se distingue grossièrement du foin, ce qu’un cache dégradé réussit encore. Ce qui casse en premier est ailleurs : la discrimination entre passages similaires, le raisonnement à plusieurs sauts reliant deux éléments distants, et la stabilité du tool-calling quand les arguments doivent être recopiés depuis loin dans le contexte. Un checkpoint qui annonce « needle réussi à 184 000 tokens » n’a donc pas démontré grand-chose sur ces trois points.

Si vous l’évaluez, faites-le avec plusieurs aiguilles concurrentes à distinguer, une question croisant deux passages éloignés, et surtout une comparaison A/B en greedy (température 0) contre le 8 bits sur les mêmes prompts. C’est la seule façon de voir une dégradation fine plutôt que de constater qu’un test facile passe encore.

2. Grouped-Query Attention (GQA)

GQA partage les vecteurs K/V entre plusieurs têtes Query. Au lieu d’avoir 32 K/V pour 32 têtes, on peut n’en avoir que 8, réduisant le cache de 4x. Tous les modèles 2026 (Qwen3.6, Llama 4, Gemma 3) utilisent GQA.

3. MLA — Multi-head Latent Attention (DeepSeek V2/V3)

MLA compresse K et V dans un espace latent de très basse dimension (typiquement 512), puis projette à la volée vers les K/V de chaque tête lors du calcul d’attention.

MHA (Llama 2)        : cache 2 × n_heads × head_dim       par token
                       Llama 2 70B → ~2,5 Mo / token

GQA (Llama 3, Qwen3) : cache 2 × n_kv_heads × head_dim    par token
                       Qwen3.6-27B → ~262 Ko / token (×10 vs MHA)

Hybride linéaire      : SEULES les couches d'attention complète ont un cache ;
(Qwen3.8 GDN,          les autres portent un état récurrent de taille CONSTANTE
 Kimi K3 KDA)          Qwen3.8-27B → ~64 Ko / token (16 couches sur 64)

MLA (DeepSeek V3)    : cache d_latent + d_rope            par token
                       DeepSeek V3 671B → ~70 Ko / token (×35 vs MHA équivalente)

Le cache KV de DeepSeek V3 est plus petit qu’un Llama 8B alors que le modèle pèse 671B paramètres. Cela permet des contextes très longs (128K) sur du matériel modeste, au prix d’une décompression supplémentaire à chaque step — opération fusionnée avec FA3/FA4 pour rester quasi gratuite.

Empiriquement, K et V sont fortement redondants : leur rang effectif est très inférieur à n_heads × head_dim. MLA exploite cette redondance via une décomposition de bas rang, similaire à l’idée de LoRA pour le fine-tuning.

4. Sliding Window Attention (SWA)

Limite l’attention aux W derniers tokens uniquement. Le cache ne dépasse jamais W tokens, permettant des contextes théoriquement infinis. Mistral Small 3.2 alterne SWA / attention complète tous les 4 layers pour préserver la qualité tout en bornant le cache.

5. PagedAttention (vLLM)

Inspiré de la pagination mémoire des OS, PagedAttention alloue le cache par blocs non-contigus, évitant le gaspillage de mémoire pré-allouée. C’est ce qui permet à vLLM de supporter des centaines de requêtes concurrentes sans réservation pessimiste.

6. KIVI et quantification 2-bit du KV cache

KIVI (2024) propose une quantification asymétrique du KV cache : K en per-channel et V en per-token. Cette asymétrie reflète l’observation que K a des outliers stables par channel, alors que V est plus uniforme.

  • 2-bit KIVI : -7x mémoire vs FP16, perte de qualité < 1 % en perplexité.
  • 4-bit KIVI : -4x mémoire, perte indétectable sur la plupart des benchmarks.
  • Supporté nativement dans vLLM 0.7+ et SGLang.

7. Prefix caching (cache de préfixe)

Les serveurs modernes (vLLM, SGLang, TensorRT-LLM) supportent le prefix caching : quand plusieurs requêtes partagent un même préfixe — prompt système, contexte RAG, conversation multi-tour — le cache KV de ce préfixe est calculé une seule fois puis réutilisé. SGLang l’implémente sous forme de radix tree : à chaque requête, le serveur retrouve le plus long préfixe déjà calculé et ne préfille que le delta.

C’est le cas d’usage du chat et des agents, parce qu’un dialogue est par construction une suite de préfixes emboîtés :

Tour 1 : [système 20k] + [Q1]                          → tout à calculer
Tour 2 : [système 20k] + [Q1] + [R1] + [Q2]            → seuls [R1]+[Q2] sont neufs
Tour 3 : [système 20k] + [Q1] + [R1] + [Q2] + [R2] + [Q3]

Nos mesures, sur un agent dont le prompt système pèse 19 000 tokens, trois tours de conversation :

Sans prefix caching Avec
Tour 1 (à froid) 2 118 ms 2 133 ms
Tour 2 2 078 ms 295 ms
Tour 3 2 014 ms 312 ms
Tokens réutilisés 0 19 072

Soit un facteur 7 sur tous les tours après le premier. Pour un assistant conversationnel, c’est la différence entre une réponse qui se fait attendre et une réponse immédiate.

8. Le piège du prefix caching sur les architectures hybrides

Voici ce que la documentation dit rarement, et que nous avons dû mesurer nous-mêmes.

Sur un modèle à attention linéaire hybride (Qwen3.8 avec GDN, Kimi K3 avec KDA), la majorité des couches n’ont pas de cache KV du tout : elles portent un état récurrent de taille constante. Or un état récurrent ne se découpe pas en tranches réutilisables comme un cache KV. Pour reprendre une conversation à un point donné, il faut avoir sauvegardé l’état de ces couches à ce point précis — ce qui coûte de la mémoire, prise sur le même budget que le contexte.

Sur notre carte de 32 Go, la cartographie mesurée est sans appel :

Budget alloué aux états Contexte servable Requêtes en parallèle Chat (tour 2)
minimal, sans cache 220 211 8 2 078 ms
minimal, avec cache 225 196 1 295 ms
intermédiaire 207 750 2 295 ms
généreux (retenu) 187 812 4 294 ms

Le prefix caching et le parallélisme puisent dans la même réserve d’états. Nous avons retenu la dernière ligne : 33 000 tokens de contexte et quatre slots de concurrence échangés contre un chat sept fois plus réactif — parce que notre usage réel, ce sont des agents et des conversations, pas des documents uniques de 200 000 tokens. Un autre usage justifierait l’arbitrage inverse ; l’important est de savoir qu’il existe et de le mesurer plutôt que de le subir.

Deux pièges que nous avons découverts depuis

Une requête consomme plusieurs emplacements d’état, même sans décodage spéculatif. Nous avions supposé qu’une requête en occupait un seul dès lors qu’aucun brouillon n’était généré. C’est faux : chez nous elle en occupe cinq, parce que le cache de préfixes en réserve pour conserver l’état des tours précédents. Conséquence directe : le paramètre qui pilote réellement le nombre d’utilisateurs simultanés n’est pas celui qui porte le nom évident, mais celui qui dimensionne la réserve d’états. Le réglage nommé « nombre maximum de requêtes » ne fixe rien du tout — seul le message affiché au démarrage du serveur dit la vérité, et il faut le relire après chaque changement.

Et il faut laisser de la marge libre. Sur ces architectures, les couches récurrentes allouent de la mémoire à la volée pendant le service, pas seulement au démarrage : quelques dizaines de mégaoctets ici, quelques centaines là. Nous avons appris ça de la pire façon — en poussant le contexte au maximum, nous avons ramené la mémoire libre sous le gigaoctet, et le serveur s’est mis à mourir après plusieurs heures de fonctionnement normal. Pas au démarrage, pas sous forte charge : après des heures, sur une allocation de 24 Mo qui ne trouvait plus de place.

La règle que nous appliquons désormais : remplir un GPU à ras bord n’est pas une optimisation, c’est une panne différée. Nous laissons délibérément plus de 2 Go inutilisés, et nous avons payé ce filet en parallélisme plutôt qu’en contexte.

Combiner les techniques

En production sur Qwen3.8-27B, la combinaison attention hybride + KV en FP8 + prefix caching ramène le cache à 32 Kio par token — contre plus de 40 Kio pour un dense classique de la génération précédente, et bien davantage en FP16 naïf. C’est ce qui permet de servir 185 000 tokens de contexte sur une seule RTX 5090 de 32 Go, décodage spéculatif inclus.

Sur RTX 3090 (24 Go), la même logique permet de servir un 27B quantifié avec un contexte de 136k tokens, là où un cache FP16 naïf saturerait la carte bien avant.

Le point à retenir : ce n’est plus la taille des poids qui décide du contexte servable, c’est la structure du cache. Un modèle hybride bien quantifié tient sur une carte grand public un contexte que son prédécesseur dense réservait au datacenter.

Pour aller plus loin

← Toutes les découvertes