KV Cache Optimization
L’optimisation du KV-cache (Key-Value cache) regroupe les techniques qui réduisent la consommation mémoire GPU du cache d’attention des LLM pendant l’inférence, permettant de traiter des contextes plus longs, des batchs plus grands, et un throughput plus élevé sur le même matériel.
- Catégorie
- Optimisation d’inférence LLM / Gestion mémoire GPU
- Problème résolu
- Le KV-cache consomme 60-80 % de la mémoire GPU allouée à l’inférence, limitant le throughput
- Technique phare
- PagedAttention (vLLM) : réduit le gaspillage mémoire de ~70 % à < 4 %, throughput 2-4× supérieur
- Quantification KV
- NVFP4 (Blackwell) : réduit le KV-cache de 50 %, < 1 % de perte de précision
- Offloading
- GPU → CPU RAM → SSD, via LMCache ou KVSwap
- Outils
- vLLM, TensorRT-LLM, SGLang, LMCache
Le KV-cache : pourquoi il existe et pourquoi il pose problème
Un transformer génère du texte token par token. Pour chaque nouveau token, le mécanisme d’auto-attention a besoin de « regarder » tous les tokens précédents. Sans cache, le modèle devrait recalculer les vecteurs Key et Value de tous les tokens précédents à chaque étape de génération. Pour une séquence de 10 000 tokens, cela signifierait recalculer 10 000 × 10 000 paires d’attention, une complexité quadratique O(n²) qui rendrait l’inférence impraticablement lente.
Le KV-cache élimine ce recalcul. À chaque étape de génération, les vecteurs K et V du nouveau token sont calculés et ajoutés au cache. Les K et V des tokens précédents sont simplement lus depuis le cache. Le modèle ne calcule l’attention que entre le nouveau token et les K/V cachés, ce qui ramène la complexité à O(n) par token généré.
Le problème : la mémoire. Le KV-cache grossit linéairement avec la longueur du contexte et le nombre de couches du modèle. La formule de taille :
Taille KV-cache = 2 × nb_couches × nb_têtes × dim_tête × nb_tokens × octets_par_valeur
# Exemple : Llama 3 70B, contexte 8K tokens, FP16
# 2 × 80 couches × 8 têtes GQA × 128 dim × 8192 tokens × 2 octets
# ≈ 20 Go par requête
# Pour un batch de 32 requêtes simultanées :
# 32 × 20 Go = 640 Go de KV-cache
Pour un modèle de 70B paramètres avec un contexte de 8K tokens, le KV-cache consomme environ 20 Go par requête. Avec un batch de 32, c’est 640 Go, soit plus que la VRAM de la plupart des configurations GPU. Le KV-cache dépasse souvent la taille des poids du modèle eux-mêmes en consommation mémoire. C’est le goulot d’étranglement principal de l’inférence LLM à grande échelle.
PagedAttention : la révolution vLLM
PagedAttention, introduit par vLLM en 2023, est l’optimisation la plus impactante de l’histoire de l’inférence LLM. Le principe est directement inspiré de la gestion de la mémoire virtuelle dans les systèmes d’exploitation.
Le problème de la fragmentation
Sans PagedAttention, chaque requête reçoit un bloc contigu de mémoire GPU pour son KV-cache, dimensionné pour la longueur maximale possible de la séquence. Si le contexte maximum est 8K tokens mais que la requête n’en utilise que 2K, 75 % de la mémoire allouée est gaspillée. De plus, les séquences de longueurs variables créent de la fragmentation : des « trous » de mémoire inutilisable entre les blocs alloués. En production, 60 à 80 % de la mémoire KV-cache est gaspillée par la fragmentation et la sur-allocation.
La solution : pagination
PagedAttention découpe le KV-cache en blocs de taille fixe (« pages »), exactement comme la mémoire virtuelle d’un OS. Les séquences voient une mémoire logique continue, mais les blocs physiques peuvent être non contigus et alloués dynamiquement. Quand une séquence a besoin de plus de mémoire, de nouvelles pages sont allouées à la demande. Quand une séquence se termine, ses pages sont libérées et réutilisables immédiatement.
Résultat : le gaspillage mémoire passe de ~70 % à moins de 4 %. Le throughput augmente de 2 à 4× sur le même matériel, car plus de requêtes peuvent être traitées simultanément avec la mémoire libérée.
# vLLM avec PagedAttention (activé par défaut)
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
tensor_parallel_size=4, # 4 GPU en parallèle
gpu_memory_utilization=0.90, # Utiliser 90 % de la VRAM
max_model_len=32768, # Contexte max
enable_prefix_caching=True, # Activer le prefix caching
)
# PagedAttention gère automatiquement l'allocation/libération des pages
Partage de préfixes
PagedAttention permet aussi le partage de pages entre requêtes qui ont un préfixe commun. Si 100 requêtes partagent le même system prompt de 2 000 tokens, les pages correspondant à ce préfixe sont stockées une seule fois et référencées par les 100 requêtes. C’est l’implémentation technique du prefix caching côté serveur d’inférence.
Quantification du KV-cache
Réduire la précision numérique des vecteurs K et V dans le cache réduit directement sa taille en mémoire. C’est complémentaire à PagedAttention : PagedAttention élimine le gaspillage, la quantification réduit la taille utile.
| Format | Précision | Réduction mémoire | Perte de qualité | Matériel requis |
|---|---|---|---|---|
| FP16 / BF16 | 16 bits | Référence | Aucune | Tout GPU moderne |
| FP8 | 8 bits | 50 % | Négligeable (< 0,5 %) | H100, Blackwell, Ada Lovelace |
| NVFP4 | 4 bits | 75 % (vs FP16) | < 1 % sur les benchmarks majeurs | Blackwell (B200, RTX 50xx) |
| MXFP4 | 4 bits | 75 % | ~5 % sur MMLU (moins précis que NVFP4) | Blackwell |
| INT8 | 8 bits | 50 % | Variable (1-3 %) | Ampere+ (A100, H100) |
NVIDIA a introduit NVFP4 sur l’architecture Blackwell, un format 4 bits spécifique qui utilise des facteurs d’échelle FP8 par bloc pour une meilleure précision que le MXFP4. Les benchmarks montrent moins de 1 % de perte de qualité sur LiveCodeBench, MMLU-PRO, et Ruler 64K (contexte long), tout en doublant la capacité de contexte disponible.
La quantification du KV-cache se distingue de la quantification des poids du modèle. Quantifier les poids (AWQ, GPTQ, bitsandbytes NF4) réduit la taille du modèle en mémoire, libérant indirectement de l’espace pour le KV-cache. Quantifier le KV-cache directement réduit la taille du cache lui-même. Les deux techniques se cumulent.
Stratégies d’éviction du KV-cache
Quand la mémoire GPU est pleine, le système doit décider quels éléments du KV-cache supprimer pour faire place aux nouveaux. C’est le problème de l’éviction du cache. Les politiques classiques (LRU, LFU) ne sont pas optimales pour les LLM car les patterns d’attention sont spécifiques.
H2O (Heavy-Hitter Oracle). Modélise l’éviction comme un problème d’optimisation sous-modulaire. Les tokens qui reçoivent le plus d’attention (« heavy hitters ») sont conservés, les tokens peu regardés sont évincés. Gains de throughput > 40 % sur les modèles Llama.
SnapKV. Identifie les tokens les plus importants en analysant les patterns d’attention des couches profondes, puis compresse le KV-cache en ne gardant que ces tokens clés. Particulièrement efficace pour les longs contextes où la majorité des tokens sont peu pertinents pour la requête courante.
Streaming LLM. Pour la génération de longueur théoriquement infinie, maintient uniquement les premiers tokens (les « attention sinks », typiquement les 4-8 premiers tokens qui reçoivent une attention disproportionnée) et les tokens récents dans une fenêtre glissante. Le contexte intermédiaire est supprimé. La mémoire reste fixe quelle que soit la longueur de la génération, mais la qualité se dégrade pour les tâches nécessitant des dépendances à longue portée.
PagedEviction. Algorithme d’éviction par blocs conçu spécifiquement pour PagedAttention. Au lieu d’évincer token par token, il identifie et supprime des blocs entiers de pages à faible importance, sans modifier les kernels CUDA.
Éviction guidée par l’entropie. Alloue le budget de cache de manière non uniforme entre les couches. Les couches dont l’attention est « diffuse » (entropie élevée) reçoivent plus de cache. Les couches dont l’attention est concentrée sur quelques tokens reçoivent moins de cache, car la perte d’information est moindre.
Offloading : GPU → CPU → SSD
Quand la VRAM GPU ne suffit pas, le KV-cache peut être déchargé vers des niveaux de mémoire moins coûteux mais plus lents :
| Niveau de stockage | Capacité typique | Bande passante | Latence ajoutée | Cas d’usage |
|---|---|---|---|---|
| VRAM GPU (HBM) | 40-80 Go par GPU | 2-3 To/s (HBM3) | Référence | Cache actif, requêtes en cours |
| RAM CPU (DDR5) | 256-2048 Go | 50-100 Go/s | 10-50 ms par retrieval | Cache de sessions inactives, conversations en pause |
| SSD NVMe | 1-16 To | 5-14 Go/s (PCIe 5.0) | 50-200 ms par retrieval | Cache froid, archivage longue durée |
LMCache est l’outil de référence pour l’offloading de KV-cache. Il s’intègre avec vLLM et permet de stocker le KV-cache dans un système multi-niveaux (GPU → CPU → disque). Les benchmarks montrent des réductions de latence (TTFT) de 3 à 10× pour les workloads à long contexte, en évitant le recalcul du prefill quand le cache peut être récupéré depuis le CPU ou le disque.
Le pattern typique : les requêtes actives ont leur KV-cache en VRAM GPU. Quand une session est inactive (l’utilisateur ne parle plus), le cache est déplacé vers la RAM CPU. Si l’utilisateur revient, le cache est rechargé en GPU (10-50 ms de latence) au lieu de recalculer tout le prefill (plusieurs secondes). Pour les sessions très longues ou les archives, le cache peut descendre jusqu’au SSD.
Optimisations au niveau du modèle
Certaines optimisations sont intégrées dans l’architecture du modèle et réduisent structurellement la taille du KV-cache.
GQA (Grouped-Query Attention). Au lieu d’avoir un jeu de K/V par tête d’attention (Multi-Head Attention classique), GQA partage les K/V entre plusieurs têtes. Llama 3 utilise GQA avec 8 groupes KV pour 64 têtes d’attention, réduisant le KV-cache de 8× par rapport au MHA. C’est l’optimisation architecturale la plus impactante et elle est adoptée par quasiment tous les modèles modernes.
MQA (Multi-Query Attention). Cas extrême de GQA : un seul jeu de K/V partagé entre toutes les têtes. Réduction maximale du KV-cache mais perte de qualité potentielle. Utilisé par certains modèles de code (StarCoder).
MLA (Multi-Head Latent Attention). Technique utilisée par DeepSeek-V3 qui compresse les K/V dans un espace latent de dimension réduite avant de les stocker. Le KV-cache est significativement plus petit, et les K/V complets sont reconstruits à la volée lors de l’attention. Cela a contribué à l’efficacité remarquable de DeepSeek sur les longs contextes (128K tokens).
FlashAttention : l’autre pilier
FlashAttention n’optimise pas directement le KV-cache, mais il optimise la façon dont le cache est lu pendant le calcul de l’attention. En réorganisant les accès mémoire pour minimiser les transferts entre la mémoire HBM (lente, grande) et les registres/SRAM du GPU (rapide, petite), FlashAttention réduit le nombre de lectures/écritures et accélère le calcul d’attention de 2 à 4×.
FlashAttention est complémentaire à PagedAttention. vLLM utilise les deux simultanément : PagedAttention gère l’allocation mémoire du KV-cache, FlashAttention optimise les opérations d’attention sur ce cache.
Guide pratique : optimiser le KV-cache
Arbre de décision
Votre modèle est-il déjà en GQA/MQA ?
├── Non → Envisagez un modèle plus récent avec GQA (Llama 3, Mistral, Qwen)
└── Oui → Continuez
Utilisez-vous vLLM ou TensorRT-LLM ?
├── Non → Migrez vers vLLM (PagedAttention activé par défaut)
└── Oui → PagedAttention est déjà actif
La VRAM est-elle le goulot d'étranglement ?
├── Non → Augmentez le batch size jusqu'à saturation GPU
└── Oui →
├── Activez la quantification KV (FP8 sur H100, NVFP4 sur Blackwell)
├── Activez le prefix caching (--enable-prefix-caching dans vLLM)
├── Réduisez max_model_len au minimum nécessaire
└── Si toujours insuffisant → Offloading KV vers CPU (LMCache)
Métriques à surveiller
KV cache utilization : pourcentage de la mémoire KV-cache effectivement utilisée (vs allouée). Avec PagedAttention, visez > 96 %.
Throughput (tokens/s) : le nombre total de tokens générés par seconde sur toutes les requêtes du batch. C’est la métrique d’efficacité principale.
TTFT (Time To First Token) : impacté par le prefill et la récupération du KV-cache (si offloading ou prefix caching).
Batch size effectif : le nombre de requêtes traitées simultanément. Plus le KV-cache est optimisé, plus le batch peut être grand, et plus le throughput est élevé.
GPU memory utilization : pourcentage de VRAM utilisée. Visez 85-95 %. En dessous de 80 %, vous sous-utilisez le GPU. Au-dessus de 95 %, vous risquez des OOM (Out of Memory).
Bonnes pratiques
Utilisez vLLM ou TensorRT-LLM. PagedAttention est la baseline non négociable pour l’inférence en production. HuggingFace Transformers natif gaspille 60 à 80 % de la mémoire KV-cache. La migration vers vLLM apporte un gain de throughput de 2 à 24× selon le scénario, pour un effort d’intégration minimal.
Activez le prefix caching si vos prompts partagent des préfixes. Le prefix caching de vLLM partage automatiquement les blocs de KV-cache entre requêtes avec des préfixes communs. Pour les workloads avec un system prompt constant (chatbots, RAG), le gain mémoire est proportionnel au nombre de requêtes concurrentes × taille du préfixe partagé.
Quantifiez le KV-cache sur H100+. La quantification FP8 du KV-cache (supportée nativement par TensorRT-LLM et vLLM sur H100) réduit la taille du cache de 50 % avec une perte de qualité négligeable. Sur Blackwell, NVFP4 offre une réduction supplémentaire de 50 % (75 % total vs FP16).
Dimensionnez max_model_len au minimum nécessaire. La mémoire réservée pour le KV-cache est proportionnelle à max_model_len. Si vos requêtes n’excèdent jamais 8K tokens, ne configurez pas 128K. Chaque token de contexte inutilisé réserve de la mémoire qui pourrait servir à augmenter le batch size.
Monitorez et itérez. Les benchmarks publiés ne reflètent pas votre workload réel. Mesurez le throughput, la latence, et l’utilisation mémoire sur VOS requêtes, avec VOTRE distribution de longueurs de prompt. Ajustez les paramètres (gpu_memory_utilization, max_model_len, quantification) en fonction des données réelles.
Questions fréquentes sur l’optimisation du KV-cache
Qu’est-ce que le KV-cache et pourquoi est-il si important ?
Le KV-cache stocke les vecteurs Key et Value calculés par le mécanisme d’attention du transformer pour chaque token déjà traité. Sans KV-cache, le modèle devrait recalculer l’attention sur tous les tokens précédents à chaque nouveau token généré, une opération en O(n²) qui rendrait l’inférence impraticablement lente. Le KV-cache ramène la complexité à O(n) par token, ce qui rend la génération en temps réel possible. Le problème est que le KV-cache consomme énormément de mémoire GPU : pour un modèle 70B avec un contexte de 8K, c’est environ 20 Go par requête.
Qu’est-ce que PagedAttention et pourquoi l’utiliser ?
PagedAttention (implémenté dans vLLM) applique le concept de mémoire virtuelle paginée au KV-cache. Au lieu d’allouer un bloc contigu pour chaque requête (gaspillant 60-80 % de la mémoire), il découpe le cache en petites pages allouées dynamiquement. Le gaspillage tombe à moins de 4 %, permettant de traiter 2 à 4× plus de requêtes simultanément sur le même GPU. C’est l’optimisation la plus impactante pour l’inférence LLM en production. Elle est activée par défaut dans vLLM et TensorRT-LLM.
Quelle est la différence entre quantifier les poids et quantifier le KV-cache ?
La quantification des poids (AWQ, GPTQ, bitsandbytes NF4) réduit la taille du modèle en mémoire, libérant indirectement de l’espace pour le KV-cache. La quantification du KV-cache (FP8, NVFP4) réduit directement la taille du cache lui-même. Les deux sont complémentaires et se cumulent. Pour un modèle 70B : poids en NF4 (~18 Go au lieu de ~35 Go en FP16) + KV-cache en FP8 (50 % de réduction) = un modèle qui tourne confortablement sur 4× A100 80 Go avec des batchs de 16+ requêtes.
Que faire quand la VRAM GPU ne suffit pas pour le KV-cache ?
Quatre solutions, par ordre de préférence : quantifier le KV-cache (FP8 ou NVFP4 si Blackwell), activer le prefix caching pour partager les blocs communs entre requêtes, réduire max_model_len au minimum réellement utilisé, et en dernier recours, offloader le KV-cache vers la RAM CPU (via LMCache ou KVSwap). L’offloading ajoute 10-50 ms de latence par retrieval, mais permet de gérer des contextes bien plus longs que la VRAM seule. Pour les cas extrêmes, l’offloading vers SSD est possible mais la latence (50-200 ms) peut devenir perceptible.
Le KV-cache est-il lié au prompt caching des fournisseurs (Anthropic, OpenAI) ?
Oui, c’est le même concept. Le prompt caching proposé par Anthropic et OpenAI est un service managé qui stocke le KV-cache de vos prompts côté fournisseur pour le réutiliser lors de requêtes ultérieures. L’optimisation du KV-cache (cet article) est la version « sous le capot » appliquée par ceux qui self-hostent leurs modèles. Le context caching de Google Gemini est la même chose avec un TTL et un stockage explicites. Dans tous les cas, c’est le KV-cache (les vecteurs Key-Value du mécanisme d’attention) qui est stocké et réutilisé.