Guides IA · 12 min de lecture

Qu'est-ce qu'un KV cache ? Comment les LLM réutilisent les états d'attention

Découvrez ce qu'est un KV cache, comment il accélère le décodage des LLM, évolue avec le contexte et influe sur les coûts des API.

Illustration d'une séquence de tuiles colorées représentant des tokens alimentant des blocs de cache empilés qui émettent un flux lumineux plus rapide

Un KV cache (cache Key-Value) est une optimisation de l’inférence dans laquelle un grand modèle de langage stocke, pour chaque couche, les tenseurs d’attention Key et Value calculés pour les tokens déjà traités. Lors du décodage autorégressif, le modèle réutilise ces états au lieu de recalculer tous les tokens précédents à chaque étape, ce qui réduit les calculs redondants à mesure que les séquences s’allongent.

Que vous utilisiez des API hébergées ou exécutiez des modèles à poids ouverts sur vos propres GPU, le KV cache joue un rôle central dans la latence, la pression sur la mémoire et le débit. Comprendre son fonctionnement explique pourquoi le temps jusqu’au premier token diffère de la vitesse de streaming, pourquoi les longs contextes consomment autant de VRAM et pourquoi la mise en cache des prompts entre requêtes API exige des préfixes identiques.

Comment fonctionne l’attention sans KV cache ?

Sans KV cache, un Transformer autorégressif doit recalculer depuis le début les projections d’attention de toute la séquence chaque fois qu’il génère un nouveau token dans un modèle d’IA.

Dans chaque couche du décodeur Transformer, l’auto-attention projette chaque représentation de token d’entrée en trois vecteurs :

  • Query ($Q$) : représente la position actuelle qui recherche le contexte pertinent.
  • Key ($K$) : représente ce que chaque position propose pour la correspondance avec les Query.
  • Value ($V$) : contient les informations agrégées une fois les poids d’attention calculés à partir des produits scalaires Query-Key.

Les modèles de langage à décodeur seul générant le texte un token à la fois, la génération du token $t+1$ ne nécessite que le vecteur Query du dernier token pour porter attention aux Key et Value des tokens $1$ à $t$. Si l’environnement d’exécution élimine ces Key et Value après l’émission du token $t$, l’étape $t+1$ doit faire repasser chaque token du préfixe dans toutes les couches pour recréer exactement les mêmes tenseurs $K$ et $V$. À mesure que le prompt et la sortie s’allongent, ce travail répété augmente de façon quadratique sur l’ensemble de la génération.

Pourquoi l’attention causale rend-elle le KV caching possible ?

Le masquage causal garantit que les tenseurs Key et Value d’un token dépendent uniquement des tokens qui le précèdent, jamais de ceux générés ensuite.

Dans un décodeur causal (de gauche à droite), le token $i$ ne peut porter attention à aucune position $j > i$. Puisque l’information ne circule que vers l’avant dans la séquence :

  1. La représentation du token $i$ à la couche $l$ est entièrement fixée une fois les tokens $1 \dots i$ traités.
  2. L’ajout du token $i+1$ à la fin de la séquence ne modifie les états cachés, Key ou Value des positions $1 \dots i$ dans aucune couche.
  3. L’environnement d’exécution peut ajouter les vecteurs $K_{i+1}$ et $V_{i+1}$ nouvellement calculés à un tampon par couche, le KV cache, et ne transmettre que le tout dernier token à l’étape de décodage suivante.

Comme l’expliquent le guide KV cache de Hugging Face Transformers et la présentation de l’architecture du cache Transformers, la mise en cache transforme chaque étape de décodage : au lieu de parcourir toute la séquence, elle met à jour un seul token de manière incrémentale en lisant les Key et Value précédents en mémoire.

Comment Prefill et Decode utilisent-ils différemment le KV cache ?

L’inférence d’un LLM se divise en deux phases distinctes, Prefill et Decode, qui interagissent avec le KV cache de façons fondamentalement différentes et rencontrent des goulots d’étranglement matériels différents.

Phase d’inférence Effet sur le KV cache Principal goulot d’étranglement matériel Mesure visible par l’utilisateur
Prefill (traitement du prompt) Calcule $K$ et $V$ en parallèle pour tous les tokens du prompt et remplit le cache initial dans chaque couche. Limité par le calcul (grandes multiplications matrice-matrice sur tout le prompt). Temps jusqu’au premier token (TTFT).
Decode (génération de tokens) Calcule $Q$, $K$ et $V$ pour un nouveau token à la fois, ajoute la nouvelle entrée $K$/$V$ et lit tout l’historique en cache. Limité par la bande passante mémoire (chargement des poids du modèle et des états KV grandissants à chaque étape). Latence entre tokens / vitesse de streaming de la sortie.

Comme l’explique NVIDIA dans son article technique sur Prefill, Decode et le KV caching, la phase Prefill traite tous les tokens d’entrée simultanément. Comme le prompt complet est connu à l’avance, les GPU peuvent regrouper les projections en opérations matricielles denses, saturer les unités de calcul et remplir le KV cache en un seul passage. Les longs prompts, comme les contextes multidocuments dans les pipelines de génération augmentée par récupération (RAG), augmentent directement le travail de Prefill et le temps jusqu’au premier token.

Après l’émission du premier token de sortie, l’inférence entre en phase Decode. Chaque étape traite un seul nouveau token par séquence, ajoute ses vecteurs Key et Value au cache et lit tous les tenseurs $K$ et $V$ précédemment stockés dans la mémoire à haute bande passante (HBM) du GPU pour calculer l’attention. L’intensité arithmétique par étape étant faible tandis que les lectures mémoire augmentent avec la longueur de séquence et la taille du batch, le décodage est rapidement limité par la bande passante mémoire et la VRAM disponible plutôt que par les FLOPs bruts.

Quelle quantité de mémoire utilise un KV cache ?

L’empreinte mémoire d’un KV cache augmente linéairement avec la longueur de séquence, la taille du batch, le nombre de couches et le nombre de têtes d’attention Key-Value.

À partir des dimensions des tenseurs dans les caches Transformer standards, où chaque couche stocke un tenseur Key et un tenseur Value structurés selon le batch, les têtes KV, la longueur de séquence et la dimension des têtes, la taille du cache par séquence s’exprime ainsi :

bytes ≈ 2 × layers × kv_heads × head_dim × seq_len × bytes_per_element

Chaque terme de cette formule constitue un levier pour les architectes de modèles et les ingénieurs d’inférence qui cherchent à maîtriser la mémoire :

  • 2 : tient compte du stockage des tenseurs Key (K) et Value (V).
  • layers : nombre total de blocs décodeurs Transformer dans le modèle.
  • kv_heads et head_dim : nombre de têtes d’attention Key/Value par couche et dimension de chaque tête.
  • seq_len : nombre total de tokens en cache (tokens du prompt et tokens générés jusqu’ici).
  • bytes_per_element : précision numérique des tenseurs stockés (par exemple, 2 octets pour FP16/BF16, 1 octet pour les formats 8 bits ou une fraction plus petite avec une quantification du KV cache sur 4 bits comme NVFP4).

Réduire kv_heads : MHA, MQA et GQA

Dans la Multi-Head Attention classique (MHA), chaque tête Query possède sa propre tête Key et sa propre tête Value (kv_heads = query_heads). Avec l’augmentation des fenêtres de contexte et des tailles de batch, le stockage de projections K et V distinctes pour chaque tête est devenu un goulot d’étranglement mémoire majeur.

Deux changements d’architecture réduisent directement kv_heads dans la formule ci-dessus :

En plus de GQA, les moteurs d’inférence réduisent bytes_per_element en quantifiant les tenseurs K et V en cache dans des formats de moindre précision, ce qui permet d’accueillir des contextes plus longs ou des batches simultanés plus grands dans la mémoire GPU.

Comment les moteurs de serving gèrent-ils la mémoire du KV cache à grande échelle ?

Les moteurs de serving en production gèrent la mémoire du KV cache en divisant les tenseurs contigus en blocs de taille fixe afin que les GPU puissent traiter davantage de requêtes simultanées sans gaspiller de VRAM.

Dans les implémentations naïves, l’environnement de serving préalloue un bloc contigu de mémoire GPU dimensionné selon la longueur maximale possible de chaque requête. Les longueurs de sortie réelles étant inconnues à l’avance, cette préallocation provoque une forte fragmentation mémoire interne et externe : des emplacements réservés restent inutilisés tandis que de nouvelles requêtes ne peuvent pas s’exécuter.

Kwon et al. ont résolu ce goulot d’étranglement avec PagedAttention dans Efficient Memory Management for Large Language Model Serving with PagedAttention (arXiv:2309.06180), le mécanisme central de vLLM. Inspiré de la mémoire virtuelle et de la pagination des systèmes d’exploitation :

  • Blocs non contigus de taille fixe : PagedAttention divise le KV cache de chaque requête en blocs de taille fixe contenant les Key et Value d’un nombre défini de tokens, associés au moyen d’une table de correspondance entre blocs logiques et physiques.
  • Allocation à la demande : de nouveaux blocs physiques ne sont alloués qu’à mesure que de nouveaux tokens sont générés, éliminant presque toute fragmentation interne.
  • Partage de préfixe au niveau des blocs : lorsque plusieurs requêtes partagent un préfixe de prompt identique, comme un prompt système ou des exemples few-shot communs, leurs tables de blocs logiques peuvent pointer vers les mêmes blocs KV physiques en mémoire, avec copie à l’écriture uniquement lorsque les séquences divergent.

Dans l’évaluation de l’article vLLM (arXiv:2309.06180), cette gestion paginée des KV a atteint un débit 2–4× supérieur à FasterTransformer et Orca pour une latence comparable, en accueillant des batches nettement plus grands dans la même mémoire GPU. Les piles de serving modernes, notamment vLLM et le système de réutilisation du KV cache de NVIDIA TensorRT-LLM, utilisent une gestion des KV par blocs pour réutiliser les préfixes communs et maximiser les requêtes simultanées.

Quelle différence entre KV cache et mise en cache des prompts ?

Un KV cache est le mécanisme d’inférence sous-jacent propre à une requête qui réutilise les états d’attention entre les étapes de décodage. La mise en cache des prompts est une fonction de serving et d’API entre requêtes qui conserve et réutilise ces blocs KV entre des appels distincts partageant un préfixe identique.

Les développeurs rencontrant souvent ces deux termes lorsqu’ils optimisent la latence et les factures, il est utile de distinguer ce qui se passe dans un seul appel de génération de ce qui se passe entre plusieurs requêtes API :

Dimension KV cache (décodage dans une requête) Mise en cache des prompts (réutilisation de préfixe entre requêtes)
Portée Une requête, sur les étapes de génération des tokens. Plusieurs requêtes pendant une courte période de conservation.
Ce qui est évité Le recalcul des tokens précédents dans la même réponse lors du décodage de chaque nouveau token. Un nouveau passage Prefill sur les préfixes de prompt statiques répétés entre appels distincts.
Cycle de vie Créé pendant Prefill, enrichi pendant Decode, libéré ou évincé à la fin de la requête. Conservé dans un stockage par blocs (mémoire GPU ou hôte) avec une politique d’éviction et une TTL pour les appels futurs.
Visibilité sur la facture Intégré à l’inférence standard ; pas de ligne de facturation distincte. Souvent proposé par les fournisseurs d’API sous forme de tarif réduit pour les tokens d’entrée en cache et de TTFT plus faible.

L’attention causale impose une règle stricte à la mise en cache des prompts : le préfixe doit correspondre exactement dès le tout premier token. Les tenseurs Key et Value de chaque token dépendant de la séquence exacte des tokens qui le précèdent, modifier ne serait-ce qu’un horodatage, un identifiant utilisateur ou une instruction au début d’un prompt invalide les états $K$ et $V$ en cache de tous les tokens suivants.

Pour les règles de conservation, les seuils minimaux de tokens et les remises propres à chaque fournisseur, consultez notre guide complet sur la mise en cache des prompts et sa tarification.

Conseils pratiques pour les développeurs et les équipes

Comprendre le comportement du KV cache permet directement d’améliorer l’architecture des prompts, de stabiliser la latence et de réduire les dépenses d’infrastructure :

  1. Placez le contenu statique tout au début du prompt : placez les instructions système, les schémas d’outils et les documents de référence communs avant toute entrée utilisateur dynamique ou métadonnée propre à une requête. Un préfixe identique permet aux API hébergées et aux moteurs locaux comme vLLM ou TensorRT-LLM de réutiliser les blocs KV en cache au lieu d’effectuer des passages Prefill redondants.
  2. Surveillez l’effet de la longueur du contexte sur les requêtes simultanées avec des GPU auto-hébergés : la taille du KV cache augmentant linéairement avec seq_len, doubler la longueur moyenne du contexte réduit de moitié le nombre de requêtes simultanées que la mémoire GPU peut contenir. Pour les modèles à poids ouverts auto-hébergés, privilégiez les architectures utilisant Grouped-Query Attention (GQA) et les environnements prenant en charge l’allocation paginée et la quantification des KV.
  3. Réduisez le contexte inutile même avec de grandes fenêtres de contexte : fournir de longs historiques à chaque tour augmente la latence Prefill, la pression sur la mémoire KV et les factures de tokens. Associez la mise en cache des préfixes à une récupération ciblée et à des résumés périodiques, des techniques centrales présentées dans notre guide pour réduire les coûts des API d’IA et notre comparatif des prix des API d’IA.

Questions fréquentes

Un KV cache change-t-il la qualité de sortie du modèle ?

Un KV cache standard conservé dans la précision d’inférence native du modèle, comme FP16 ou BF16, est mathématiquement équivalent au recalcul du préfixe à chaque étape, car le masquage causal empêche les tokens futurs de modifier les vecteurs Key et Value passés. Des compromis sur la qualité n’apparaissent qu’en cas de quantification avec perte du KV cache, comme une compression sur 8 bits ou 4 bits, ou d’éviction de tokens anciens pour économiser de la mémoire.

Pourquoi un KV cache ne peut-il pas réutiliser les tokens au milieu d’un prompt si le début a changé ?

Dans un Transformer causal, la représentation de chaque token intègre une attention sur tous les tokens précédents de la séquence. Si les dix premiers tokens d’un prompt changent, les états cachés, et donc les projections Key et Value, changeront également à partir du onzième token. Ces états dépendant du préfixe exact qui les précède, les moteurs de serving ne peuvent réutiliser les blocs KV en cache entre requêtes que si le préfixe correspond depuis le début.

Le KV cache contient-il les poids du modèle ?

Non. Les poids du modèle sont des paramètres statiques appris pendant l’entraînement et partagés entre toutes les requêtes traitées par le GPU. Le KV cache contient des tenseurs d’activation dynamiques propres à chaque requête (les projections Key et Value des tokens actuellement dans le contexte). Pendant l’inférence, la mémoire GPU totale doit contenir les poids statiques du modèle, les activations temporaires de l’environnement et les KV cache dynamiques de toutes les requêtes actives du batch.

Pourquoi la génération de tokens supplémentaires ralentit-elle lorsque de nombreuses requêtes sont simultanées ?

Pendant la phase Decode, chaque étape de génération d’un token nécessite de lire les tenseurs Key et Value stockés pour tous les tokens précédents de chaque requête active du batch. Lorsque les séquences s’allongent et que le nombre de requêtes simultanées augmente, le volume de données KV transférées depuis la mémoire GPU à chaque étape augmente, saturant la bande passante mémoire et obligeant l’ordonnanceur à limiter la taille du batch ou à attendre les lectures mémoire.

Sources

Écrit par

Chunyu Cai

Chunyu Cai est le directeur général de Lotook, LLC et un entrepreneur qui écrit sur les modèles d’IA, les agents et leur influence sur le travail au quotidien. Chunyu Cai est titulaire d’un master de l’établissement Imperial College London.