On lance un grand modèle de langage, tout fonctionne à merveille pendant les premières secondes, puis, d’un coup, les tokens s’arrêtent. Le débit s’effondre, et la mémoire du GPU vire au rouge vif. Pourtant, le matériel n’a pas changé, les poids du modèle n’ont pas grossi, mais le système se met à ramer comme s’il tournait sur une machine d’il y a dix ans. On a souvent tendance à accuser la complexité intrinsèque de l’architecture du modèle. Pourtant, le véritable goulot d’étranglement de l’IA moderne ne réside pas dans le modèle lui-même, mais dans sa mémoire à court terme.
C’est ici qu’intervient le KV Cache (le cache Key-Value). S’il est le secret d’une génération rapide, il représente aussi une taxe mémoire colossale qui peut dévorer la VRAM et se transformer en un véritable gouffre financier en cycles de calcul gâchés. Pour faire tourner ces modèles à grande échelle ou les exploiter sur une carte grand public, il faut comprendre ces défis de mémoire et maîtriser les leviers de quantification à notre disposition.
Les points clés à retenir
- Le KV Cache est indispensable mais gourmand : Il évite de recalculer l’historique de la conversation à chaque nouveau token, mais il consomme une part immense de la VRAM.
- La fragmentation gaspille nos ressources : Les systèmes traditionnels allouent une mémoire maximale fixe par requête, ce qui crée un gaspillage massif (souvent plus de 80 %).
- La pagination et la quantification sauvent la mise : Des techniques comme l’attention paginée (paged attention) et la quantification du KV Cache (en 8-bit ou 4-bit) permettent de réduire drastiquement l’empreinte mémoire sans sacrifier la précision.
- L’offloading hybride pour les modèles MoE : Pour les modèles de type Mixture of Experts (MoE), on peut déporter les experts peu sollicités en calcul vers la mémoire RAM système et ne garder que l’attention sur le GPU.
La phase de pré-remplissage face au décodage
Le problème commence avec la façon dont les LLM traitent l’information lors de l’inférence. Le modèle ne voit pas nos mots comme du simple texte, mais comme des vecteurs dans un espace à haute dimension. Le traitement se déroule en deux étapes distinctes : le pré-remplissage (pre-fill) et le décodage (decode).
Lorsque je soumets une longue requête, je remarque un léger temps de latence. C’est la phase de pré-remplissage. Le modèle traite l’intégralité de l’entrée à travers toutes les couches de son transformeur pour construire une représentation mathématique du texte. Cette étape est fortement limitée par la puissance de calcul (compute-bound).
Mais dès que le premier token est généré, on bascule dans la phase de décodage. Cette fois, le processus est limité par la bande passante mémoire (memory-bound). Pour chaque nouveau token produit, le système doit retourner chercher dans la mémoire du GPU tout le contexte accumulé depuis le début de la conversation.
Pourquoi stocker des clés et des valeurs, mais pas de requêtes ?
Pour comprendre la mécanique sous-jacente, il faut descendre au niveau des matrices. Lorsqu’un transformeur génère un token, il calcule trois matrices spécifiques : la requête (Query – Q), la clé (Key – K) et la valeur (Value – V).
- La requête représente le token actuel qui cherche à savoir ce qui est pertinent pour lui dans le passé.
- Les clés et les valeurs fournissent les réponses issues du contexte passé.
Dans un système naïf sans cache, si l’on génère un texte de mille tokens, le millième token devrait recalculer les clés et les valeurs pour les 999 tokens précédents. Cela engendre une courbe de calcul quadratique qui finirait par paralyser n’importe quelle machine. Le KV Cache résout ce problème en stockant les matrices clés et valeurs des étapes précédentes. Le modèle n’a ainsi qu’à effectuer les calculs pour l’unique nouveau token généré.
On me pose souvent la question : pourquoi ne pas mettre aussi les requêtes (Query) en cache ? La réponse tient à la façon dont le modèle prédit le mot suivant. Pour obtenir le token suivant, le système n’a besoin que de la toute dernière ligne de sa matrice de contexte. La requête d’un ancien token ne sera plus jamais utilisée pour prédire les tokens futurs. Mettre en cache les requêtes passées serait donc un pur gaspillage d’espace. Nous n’avons besoin que des matrices de clés et de valeurs passées, combinées à la requête du token actif.
Le coût de la VRAM et le piège de la fragmentation
Cette efficacité de calcul se paye cher en VRAM. Si l’on prend un modèle comme Llama 13B, ses poids occupent environ 26 gigaoctets de mémoire (ce qui représente déjà 65 % d’une carte A100). Les 35 % restants doivent servir à accueillir le KV Cache de chaque requête active.
Le problème est que les architectures traditionnelles gèrent cet espace de manière très inefficace. Elles réservent souvent un bloc de mémoire géant et contigu pour chaque requête, basé sur la longueur maximale possible de la réponse. C’est un peu comme si l’on réservait un étage entier d’un hôtel pour un seul client, au cas où il déciderait d’inviter une centaine d’amis plus tard (une pratique bien peu rentable). Si mon contexte maximal est fixé à 2 000 tokens mais que l’utilisateur n’en génère que 200, 80 % de la mémoire réservée reste vide et totalement inutilisable pour d’autres requêtes.
Ce phénomène s’appelle la fragmentation interne. Elle s’accompagne souvent d’une fragmentation externe : des espaces vides se créent entre différentes allocations, de sorte qu’il reste de la mémoire disponible sur le papier, mais aucun bloc contigu n’est assez grand pour accueillir une nouvelle requête.
Pour contourner ce problème, les moteurs d’inférence modernes comme vLLM ou SGLang intègrent une technique appelée l’attention paginée (paged attention). Inspirée de la gestion de la mémoire des systèmes d’exploitation, l’attention paginée découpe le KV Cache en petits blocs physiques de taille fixe (généralement 16 tokens). Ces pages peuvent être dispersées n’importe où dans la mémoire du GPU et ne sont allouées qu’au fur et à mesure des besoins. Cela élimine la fragmentation et permet même à différentes requêtes de partager les mêmes blocs de KV Cache, ce qui améliore radicalement le débit.
La quantification du KV Cache pour les configurations modestes
Quand on fait tourner des modèles localement sur une carte grand public, le KV Cache est le véritable mur invisible. On voit parfois des modèles afficher fièrement des fenêtres de contexte de 100 000 tokens, mais c’est un chiffre de laboratoire impossible à faire entrer sur du matériel grand public standard.
Heureusement, nous disposons d’un levier puissant : la quantification du KV Cache. Tout comme nous compressons les poids d’un modèle (en passant de 16-bit à des précisions inférieures comme le 4-bit pour que le modèle tienne en mémoire), nous pouvons appliquer ce procédé aux valeurs stockées dans le KV Cache.
Passer à une quantification en 8-bit (Q8) permet de diviser par deux l’espace requis avec une perte de qualité presque imperceptible. On peut descendre plus bas, mais une réduction trop agressive commence à dégrader les capacités de raisonnement du modèle sur les longues conversations.
La quantification consiste à abandonner la précision 16-bit utilisée lors de l’entraînement pour adopter une représentation plus brute lors de l’inférence. C’est grâce à cela qu’on arrive à faire tenir un modèle de 20 gigaoctets sur une carte de 12 gigaoctets. Sur des plateformes comme HuggingFace, on croise souvent une multitude de formats : Q4, Q5, KM ou IQ.
- Les étiquettes Q4, Q5 ou Q8 indiquent le nombre de bits alloués par poids.
- Le K dans les “K-quants” fait référence à une méthode intelligente qui attribue une précision plus élevée aux poids les plus importants, et une précision plus faible aux poids secondaires.
- Les quants IQ (Importance Matrix) utilisent une matrice d’importance et une table de correspondance pour compresser encore plus le modèle à qualité équivalente. C’est une option idéale si la VRAM est très limitée mais que l’on dispose d’un processeur robuste pour encaisser le surplus de calcul.
- Pour une carte graphique standard, le compromis idéal se situe souvent autour du format Q4 KM, qui offre un excellent équilibre entre vitesse de génération et fidélité.
Dense vs MoE : optimiser l’usage de la mémoire RAM
Si le modèle refuse toujours de charger, il faut analyser sa structure. La distinction entre un modèle dense et un modèle de type Mixture of Experts (MoE) s’avère alors capitale.
Un modèle dense sollicite chaque neurone de son réseau pour chaque token généré. Avec ce type d’architecture, la VRAM est une limite stricte : dès que l’on commence à déporter une partie d’un modèle dense vers la mémoire vive (RAM) du système, la vitesse s’effondre.
À l’inverse, un modèle MoE n’active qu’une petite portion de ses couches pour générer un token donné. Comme une grande partie du modèle reste inactive à chaque étape, nous pouvons réaliser une optimisation chirurgicale : nous extrayons uniquement les sections où se trouvent les experts (qui pèsent lourd en mémoire mais demandent peu de calcul) pour les envoyer vers la RAM système, tout en conservant les couches d’attention (très gourmandes en calcul) directement sur le GPU.
Cette approche change complètement la donne. Il n’est plus nécessaire de stocker l’intégralité du modèle dans la VRAM, seules les couches d’attention y résident. Ainsi, avec 64 gigaoctets de RAM système rapide et seulement 8 gigaoctets de mémoire GPU, on peut faire tourner un modèle massif de 120 milliards de paramètres à une vitesse tout à fait exploitable, car il ne sollicite qu’environ 5 milliards de paramètres à la fois. Dans cette configuration d’offloading, la priorité matérielle n’est plus seulement la quantité de VRAM, mais bien la bande passante de la RAM système et la qualité du processeur.
Trois paramètres avancés à ajuster
Pour optimiser la production ou affiner un environnement de recherche, il existe trois leviers de configuration précis pour maximiser les performances du KV Cache :
- Le taux d’utilisation de la mémoire GPU (GPU Memory Utilization) : Ce paramètre définit la fraction de VRAM restante allouée spécifiquement au cache. La valeur par défaut est souvent fixée à 0,9, mais on peut la pousser jusqu’à 0,95 sur des charges de travail stables pour traiter davantage de requêtes simultanées.
- Le cache de préfixe (Prefix Caching) : Très utile pour les requêtes partageant une même consigne système ou un même pipeline RAG. Les requêtes pointent vers les mêmes blocs de mémoire physique, ce qui évite de recalculer la phase de pré-remplissage et réduit considérablement le temps d’attente avant le premier token.
- Le pré-remplissage par morceaux (Chunked Pre-fill) : Cette option évite les saccades de génération lorsqu’une longue requête arrive en cours de traitement. Elle découpe la nouvelle requête en morceaux pour les insérer de manière fluide dans les ressources de calcul restantes, entre les étapes de décodage des autres utilisateurs.
En comprenant ces mécanismes, on réalise que le KV Cache transforme ce qui aurait pu être un cauchemar de calcul exponentiel en un processus linéaire et maîtrisable. Que l’on choisisse d’utiliser vLLM pour maximiser le débit sur des modèles denses, ou SGLang pour des architectures MoE complexes, maîtriser la gestion de cette mémoire permet de s’approprier ces technologies sur son propre matériel sans dépendre systématiquement d’infrastructures cloud coûteuses.
Parfois, il ne s’agit pas d’acheter un GPU plus grand, mais simplement d’apprendre à mieux utiliser la mémoire que l’on possède déjà.