Rendre l’inférence moins chère : KV cache, batching et quantification
Même modèle, même question, sortie identique : 8,8 s ou 78,9 s. Puis INT4 mesuré de trois façons, pas affirmé.
Dans cet article
Le même modèle, sur la même machine, répondant à la même question avec les mêmes 48 tokens. Les deux sorties sont identiques token par token — vérifié, pas supposé.
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)Un seul argument a changé : use_cache=False. Rien dans le modèle, le prompt, le sampling ou l’arithmétique n’est différent, et la seconde exécution n’est pas plus précise pour autant. Elle est neuf fois plus lente pour rien.
Voilà la forme de ce chapitre. Tout ce qu’il contient — le cache, le batch, les poids quantifiés — tente soit d’arrêter de payer pour un travail qui ne change pas la réponse, soit de découvrir ce que coûte une réponse moins chère. Le chapitre 10 a établi le barème du training. Voici le barème du côté que vous payez indéfiniment : un modèle déployé dépense environ FLOPs pour chaque token qu’il émet, à chaque requête, pour le reste de sa vie.
Où est passé le temps de la seconde exécution
Lien vers la section : Où est passé le temps de la seconde exécutionPour générer un token, un transformer decoder-only prend toute la séquence jusqu’ici, la fait passer dans chaque couche et lit la distribution de probabilité à la dernière position. Puis il ajoute le token choisi et recommence. Cette description est correcte, et c’est ce que fait l’exécution lente.
Elle est aussi énormément gaspilleuse, à cause du masque causal du chapitre 9. Les vecteurs key et value de la position 7 sont calculés à partir de l’entrée de la position 7 et des positions précédentes. Quand la position 8 arrive, la position 7 ne peut pas la voir — c’est cela, causal — donc les key et value de la position 7 sont exactement les mêmes nombres qu’avant. L’exécution lente les recalcule pourtant, à chaque étape.
Alors stockez-les. Ce stockage est le key-value cache, l’optimisation la plus déterminante du serving de modèles de langage :
out = model(prompt_ids, use_cache=True) # prefill: the whole prompt
past = out.past_key_values
nxt = out.logits[:, -1].argmax(-1, keepdim=True)
for _ in range(n - 1):
out = model(nxt, past_key_values=past, use_cache=True)
past = out.past_key_values
nxt = out.logits[:, -1].argmax(-1, keepdim=True)Regardez ce qui est donné au modèle dans la boucle : nxt, un seul token. Pas la séquence. La query du nouveau token fait attention à toutes les keys en cache, et les keys en cache n’allaient jamais changer. Ce n’est pas une approximation — le contrôle de sortie identique ci-dessus est justement le point. Le cache n’échange pas de la qualité contre de la vitesse ; il supprime une arithmétique redondante.
Pour voir le scaling proprement, retirez le transformer et mesurez une seule tête d’attention avec , une étape de génération calculée des deux façons :
| tokens dans le contexte | tout recalculer | avec un cache | ratio | matrice de scores |
|---|---|---|---|---|
| 128 | 0,59 ms | 0,062 ms | 10x | 65 536 B vs 512 B |
| 256 | 1,20 ms | 0,163 ms | 7x | 262 144 B vs 1 024 B |
| 512 | 7,03 ms | 0,078 ms | 90x | 1 048 576 B vs 2 048 B |
| 1024 | 17,31 ms | 0,114 ms | 152x | 4 194 304 B vs 4 096 B |
| 2048 | 59,83 ms | 0,214 ms | 279x | 16 777 216 B vs 8 192 B |
| 4096 | 236,18 ms | 0,284 ms | 832x | 67 108 864 B vs 16 384 B |
La colonne de droite en est la cause. Recalculer construit la matrice d’attention complète à chaque étape — le de l’encadré sur la notation asymptotique du chapitre 9, payé une fois par token. Avec le cache, vous construisez à la place une ligne : à 4 096 tokens, 67 Mo de scores contre 16 Ko.
Compter les multiplications-accumulations plutôt que les millisecondes retire la machine de l’argument. Pour générer tokens depuis un départ à froid :
| tokens générés | avec un cache | en recalculant | ratio |
|---|---|---|---|
| 128 | 2,6 M | 192,0 M | 73x |
| 512 | 23,1 M | 7,36 G | 318x |
| 2048 | 293,7 M | 392,6 G | 1 336x |
À chaque étape, la version avec cache est linéaire dans le contexte et celle sans cache est quadratique ; sommée sur une génération, contre , avec un ratio qui croît sans limite. L’écart par neuf de l’ouverture a été mesuré sur 48 tokens — moins que la première ligne de ce tableau.
Le cache change aussi ce qui doit tenir en mémoire. Sur un GPU d’ordinateur portable de 8 Go générant 256 tokens en fp16, en prenant le pic de l’allocateur et en soustrayant les poids résidents :
| mémoire de travail au pic | |
|---|---|
| avec un cache | 21,8 Mo |
| en recalculant | 181,7 Mo |
8,3 fois plus de mémoire, dépensée pour produire les mêmes tokens plus lentement. C’est la promesse faite au chapitre 5, venue d’une direction inattendue : là-bas, l’autodiff en mode inverse devait garder chaque intermédiaire vivant pour la passe arrière, et les activations dominaient la mémoire du training. À l’inférence, il n’y a pas de passe arrière et rien à retenir pour elle — donc ce qui domine la mémoire à la place, c’est le cache, et c’est un choix délibéré plutôt qu’un coût inévitable.
Prefill et decode sont deux machines différentes
Lien vers la section : Prefill et decode sont deux machines différentesRegardez encore l’exécution rapide : son premier token ne s’est pas comporté comme les quarante-sept autres.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepLe prompt a coûté 25,6 ms par token et chaque token généré 166 ms. Même modèle, même matériel, mêmes poids, un écart par token d’un facteur six — et dans le sens que la plupart des gens n’attendent pas. Le prompt est la partie bon marché. La génération se divise en deux phases dont la physique est réellement différente :
Une passe avant sur tout le prompt. Chaque token est traité en parallèle, donc chaque matrice de poids est chargée depuis la mémoire une seule fois et multipliée par une matrice de centaines de vecteurs de tokens — un produit matrice-matrice, avec beaucoup d’arithmétique par octet déplacé, exactement ce pour quoi un GPU est fait. Le prefill est compute-bound, et son coût est à peu près linéaire dans la longueur du prompt.
Une passe avant par token, batch de un et séquence de un. Chaque matrice de poids est toujours chargée intégralement depuis la mémoire, puis multipliée par un seul vecteur — un produit matrice-vecteur, avec presque aucune arithmétique par octet déplacé. Le decode est memory-bandwidth-bound, et son coût par token dépend à peine de la longueur du contexte.
Les deux moitiés se mesurent. Prefill, une passe sur tokens :
| tokens de prompt | secondes | ms par token |
|---|---|---|
| 16 | 0,3515 | 21,97 |
| 32 | 0,5254 | 16,42 |
| 64 | 1,0491 | 16,39 |
| 128 | 1,6552 | 12,93 |
| 256 | 3,0965 | 12,10 |
Decode, un token face à un cache de :
| tokens en cache | ms pour un token |
|---|---|
| 16 | 110,05 |
| 64 | 97,57 |
| 256 | 108,53 |
| 1024 | 103,86 |
Lisez le second tableau deux fois. Passer de 16 tokens de contexte à 1 024 — soixante-quatre fois plus d’historique à parcourir avec l’attention — n’a changé le coût d’une étape d’aucune quantité mesurable. L’attention contre le cache est un vrai travail, mais il est noyé par le coût fixe consistant à faire passer un demi-milliard de poids dans le bus mémoire pour produire un vecteur. Ce coût fixe explique toute la section suivante.
Ces deux phases sont à l’origine des deux nombres que tout système de serving rapporte. Le time to first token est essentiellement le prefill, et il croît avec le prompt, d’où la lenteur ressentie au démarrage d’une longue conversation. Les tokens par seconde sont , et ils sont à peu près constants, d’où une réponse qui s’écoule ensuite régulièrement. Un chat qui démarre lentement puis streame sans à-coups n’est pas une astuce de rendu. Ce sont ces deux tableaux.
Le cache est aussi la facture
Lien vers la section : Le cache est aussi la factureLe cache échange de l’arithmétique contre de la mémoire, et la mémoire qu’il demande n’est pas petite. Pour chaque token du contexte, chaque couche conserve un vecteur key et un vecteur value par tête key-value :
Le 2 correspond aux keys et aux values ; tout le reste vient de l’architecture. Pour le modèle mesuré dans tout ce chapitre — 24 couches, 14 têtes query, 2 têtes key-value, dimension de tête 64 — en fp16, cela fait octets par token.
Les formules de ce domaine ont l’habitude de se tromper d’un facteur deux, donc vérifiez-la contre l’allocateur plutôt que d’y croire :
KV cache tensors per layer: (1, 2, 295, 64) float16
measured: 3,624,960 bytes for 295 tokens = 12,288 bytes/token
formula : 2 * 24 * 2 * 64 * 2 = 12,288 bytes/tokenExact, et cela reste exact pour toutes les formes testées :
| batch | contexte | cache mesuré | prédit | mémoire de travail au pic |
|---|---|---|---|---|
| 1 | 512 | 6,0 Mo | 6,0 Mo | 15,4 Mo |
| 1 | 16 384 | 192,0 Mo | 192,0 Mo | 207,3 Mo |
| 1 | 65 536 | 768,0 Mo | 768,0 Mo | 793,7 Mo |
| 8 | 4 096 | 384,0 Mo | 384,0 Mo | 401,5 Mo |
| 32 | 2 048 | 768,0 Mo | 768,0 Mo | 794,2 Mo |
| 64 | 1 024 | 768,0 Mo | 768,0 Mo | 797,0 Mo |
| 128 | 512 | 768,0 Mo | 768,0 Mo | 816,4 Mo |
Les trois dernières lignes méritent un deuxième regard. Trente-deux utilisateurs avec 2 048 tokens chacun, soixante-quatre avec 1 024, cent vingt-huit avec 512 — le cache fait 768 Mo dans tous les cas, parce que les trois contiennent 65 536 tokens. Le cache dépend uniquement du nombre total de tokens résidents, pas de leur distribution entre utilisateurs. Ce fait est la base de la section sur le batching.
D’où viennent MQA et GQA
Lien vers la section : D’où viennent MQA et GQALe chapitre 9 a introduit l’attention multi-query et grouped-query et a reporté la raison à ce chapitre. La raison est cette formule, et précisément le qu’elle contient.
L’attention multi-head standard donne à chaque tête query ses propres têtes key et value. Le modèle ici a 14 têtes query ; avec une attention multi-head complète, son cache serait de octets par token — 84 Ko au lieu de 12 Ko, exactement sept fois plus, le ratio entre têtes query et têtes key-value.
L’attention multi-query1 pousse cela à la limite : toutes les têtes query partagent une seule tête key-value. L’attention grouped-query2 est le compromis qui a gagné — une poignée de têtes key-value, chacune partagée par un groupe de têtes query — parce que la perte de qualité de MQA était réelle et celle de GQA ne l’est pas. Aucune ne gagne d’arithmétique. Elles existent pour diviser cette formule par un entier, et elles se sont répandues dans l’industrie dès que les longs contextes ont fait du cache la contrainte bloquante.
Et il le devient vite. Pour un modèle de classe 7B avec 32 couches et 8 têtes key-value de dimension 128, le cache fait 128 Ko par token en fp16 :
| tokens de contexte | un utilisateur | 8 utilisateurs | 64 utilisateurs |
|---|---|---|---|
| 4 000 | 0,49 Go | 3,91 Go | 31,2 Go |
| 32 000 | 3,91 Go | 31,25 Go | 250,0 Go |
| 128 000 | 15,62 Go | 125,00 Go | 1 000,0 Go |
| 1 000 000 | 122,07 Go | 976,56 Go | 7 812,5 Go |
Les poids de ce modèle font eux-mêmes 13,0 Go en fp16, chiffre du tableau en fin de chapitre. Donc à un contexte de 128 000 tokens, le cache d’un seul utilisateur est plus grand que le modèle. C’est l’arithmétique que le chapitre 16 transforme en argent, et c’est pourquoi une longue conversation n’est pas seulement lente : elle occupe une tranche fixe d’une machine tant que la requête est vivante.
Batching : le nombre qui monte et celui qui descend
Lien vers la section : Batching : le nombre qui monte et celui qui descendLe decode est memory-bound : les poids sont tirés dans le bus pour produire un token, et les unités arithmétiques attendent. Ajoutez donc plus de travail dans la même étape. Exécutez plusieurs requêtes à la fois, et les poids, lus une seule fois, les servent toutes. Mesuré sur le même modèle, chaque requête conservant un cache de 64 tokens et décodant un token :
| batch | latence par étape | débit | latence vs B=1 |
|---|---|---|---|
| 1 | 0,1286 s | 7,78 tok/s | 1,00x |
| 2 | 0,1839 s | 10,88 tok/s | 1,43x |
| 4 | 0,1909 s | 20,95 tok/s | 1,49x |
| 8 | 0,2781 s | 28,76 tok/s | 2,16x |
| 16 | 0,3430 s | 46,64 tok/s | 2,67x |
| 32 | 0,6302 s | 50,78 tok/s | 4,90x |
Lisez les deux colonnes de droite l’une contre l’autre, car elles sont tout l’enjeu. Passer d’une requête à seize multiplie le débit par 6,0 et multiplie l’attente de chaque requête individuelle par 2,67. Le batch a rendu le serveur meilleur et chaque utilisateur pire.
Ce n’est pas un bug à régler ; c’est le compromis lui-même, et il a un nom de chaque côté. La latence est ce que vit une personne qui attend une réponse. Le débit est ce par quoi la facture est divisée. Aucun réglage n’améliore les deux.
Notez aussi où cela s’arrête. De 16 à 32, le débit gagne 9 % tandis que la latence double presque : l’étape a cessé d’être memory-bound et est devenue compute-bound, et au-delà de ce coude le batch n’achète plus rien. Chaque déploiement a un tel coude ; son emplacement doit être mesuré sur le vôtre, mais son existence n’est pas à prouver.
Le batching statique gaspille la plupart de ce qu’il gagne
Lien vers la section : Le batching statique gaspille la plupart de ce qu’il gagneLa façon naïve de faire du batching consiste à collecter requêtes, les exécuter ensemble et répondre quand toutes sont terminées. Mais elles ne se terminent pas ensemble : certaines réponses font vingt tokens et d’autres cinq cents. Un batch fixe tourne jusqu’à ce que son membre le plus long termine, et chaque requête terminée continue d’occuper son slot, en contribuant du padding, jusqu’à ce moment.
Prenez 64 requêtes avec une asymétrie réaliste des longueurs de sortie — médiane 18 tokens, maximum 231, 1 874 au total — et simulez les deux politiques au coût mesuré par étape pour huit slots :
| politique | temps réel | débit | latence moyenne par requête | slot-steps gaspillés |
|---|---|---|---|---|
| batches statiques de 8 | 176,9 s | 10,6 tok/s | 83,2 s | 3 214 |
| continu, 8 slots | 109,0 s | 17,2 tok/s | 8,1 s | 0 |
Le débit s’améliore de 1,6x. La latence moyenne s’améliore de plus de dix fois, parce qu’avec le batching statique, une requête terminée en quatre étapes attend toujours un voisin à 231 tokens avant que quiconque n’en entende parler.
Le continuous batching3 est la solution, et elle est aussi simple qu’elle en a l’air : le batch n’est pas un groupe mais un ensemble de slots, et un slot libéré admet la requête suivante dans la file dès l’étape suivante. Le scheduler travaille à la granularité d’un token plutôt que d’une requête. Toute stack de serving en production fait cela aujourd’hui.
Il a une seconde moitié : le cache. Les slots qui arrivent et partent fragmentent la mémoire du cache, et réserver pour chaque slot son contexte maximal possible gaspille la majeure partie de la réservation. PagedAttention4 emprunte la réponse aux systèmes d’exploitation : stocker le cache dans des blocs de taille fixe avec une table de blocs par séquence, afin que le cache d’une séquence puisse être physiquement dispersé tout en restant logiquement contigu — ce qui permet aussi à deux séquences ayant un préfixe commun de partager les blocs qui le contiennent. C’est la base de vLLM, et la raison pour laquelle un moteur de serving est un allocateur mémoire avec un transformer attaché.
Quantification, et la première chose qui casse
Lien vers la section : Quantification, et la première chose qui casseL’autre moitié de la facture, ce sont les poids eux-mêmes. Un demi-milliard de paramètres à quatre octets chacun fait 1,98 Go ; à deux octets, 0,99 Go ; à un octet, 0,49 Go. Moins de bits par poids réduit le modèle sur disque, le réduit en mémoire et — puisque le decode est bandwidth-bound — rend chaque étape plus rapide, car il y a moins d’octets à déplacer.
Le schéma le plus simple est la quantification symétrique par maximum absolu, et il tient en trois lignes :
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedChoisissez une échelle pour que le plus grand poids corresponde au plus grand entier, divisez, arrondissez, stockez les entiers et l’échelle. Reconstruisez en multipliant dans l’autre sens. Il n’y a rien d’intelligent là-dedans, et cela fonctionne — jusqu’au moment où cela ne fonctionne plus.
Mesuré sur les vrais poids du modèle : les 168 matrices de projection, 357,8 millions de paramètres, erreur relative :
| schéma | erreur relative moyenne | pire matrice |
|---|---|---|
| INT8, une échelle pour toute la matrice | 0,0400 | 0,1487 |
| INT8, une échelle par ligne de sortie | 0,0100 | 0,0149 |
| INT4, une échelle pour toute la matrice | 0,6026 | 0,9931 |
| INT4, une échelle par ligne de sortie | 0,1790 | 0,2589 |
| INT4, une échelle par groupe de 128 | 0,1323 | 0,1992 |
| NF4, une échelle par bloc de 64 | 0,0952 | 0,1205 |
| INT3, une échelle par groupe de 128 | 0,3044 | 0,4123 |
| INT2, une échelle par groupe de 128 | 0,7790 | 0,8076 |
La quatrième ligne est l’effondrement. Une erreur relative de 0,99 sur la pire matrice signifie que la reconstruction ne conserve presque rien de l’original — la matrice a été remplacée par du bruit d’une magnitude à peu près correcte. La cause est visible dans la même expérience sur une seule matrice :
model.layers.12.mlp.down_proj.weight (896 x 4864)
mean |w| 0.01386 std 0.01822 max |w| 0.43945 max/std 24.1
weights beyond 6 sigma: 692 of 4,358,144 (0.016 %)Un poids sur six mille se trouve au-delà de six écarts types, et le plus grand est à 24. Avec une seule échelle pour toute la matrice, ce poids fixe le pas pour les 4,3 millions de poids. À 8 bits, il y a 256 pas et le poids typique tombe encore sur un pas significatif. À 4 bits, il y en a 16, l’extrême est réservé à une valeur que presque rien n’a, et les poids ordinaires — c’est-à-dire tous — s’arrondissent sur deux ou trois niveaux distincts.
Tout ce qui suit cette ligne est la même réparation à différentes granularités : donner à l’échelle un territoire plus petit. Par ligne de sortie, l’erreur est divisée par 3,4 ; par groupe de 128 poids consécutifs, elle est encore divisée. Le coût est de la comptabilité — une échelle 16 bits par groupe de 128 fait bits par poids au lieu de 4 — et elle rachète l’essentiel de l’écart.
NF4 attaque le problème par l’autre côté.5 Les niveaux n’ont pas besoin d’être espacés régulièrement. Les poids d’un bloc sont approximativement distribués normalement, donc choisissez les seize niveaux comme les quantiles d’une distribution normale : denses près de zéro, là où les poids se trouvent réellement, clairsemés dans les queues, là où ils ne sont pas. Même quatre bits, même scaling par bloc, sur un bloc plus petit — 4,25 bits par poids contre 4,125 pour group-128 — et l’erreur mesurée tombe de 0,1323 à 0,0952, soit 28 % de moins. Une partie vient du bloc plus fin, le reste du placement des niveaux là où se trouve la masse, et séparer les deux demanderait une troisième ligne.
Les outlier features
Lien vers la section : Les outlier featuresL’encadré sur la virgule flottante du chapitre 2 se terminait par une promesse : ce chapitre quantifierait des poids sur 8 et 4 bits et trouverait une poignée d’outlier features refusant d’être compressées. Les voici, et elles expliquent pourquoi « arrondir simplement les nombres » n’allait jamais fonctionner sur les activations.
Les poids ci-dessus se comportaient mal. Les activations jouent dans une autre ligue. Prenez un prompt ordinaire de 84 tokens, capturez le flux résiduel à chaque couche et mesurez la plus grande magnitude atteinte par chacune des 896 dimensions :
| couche | plus grand |h| | plus grand |h| de la dimension médiane | ratio | dimensions au-dessus de 6x la médiane |
|---|---|---|---|---|
| 1 | 6,19 | 0,339 | 18x | 2 |
| 4 | 1543,48 | 1,550 | 996x | 34 |
| 8 | 1571,63 | 1,498 | 1049x | 36 |
| 12 | 1575,03 | 1,546 | 1019x | 34 |
| 16 | 1579,60 | 1,617 | 977x | 32 |
| 20 | 1577,98 | 2,361 | 668x | 24 |
| 24 | 204,44 | 10,760 | 19x | 12 |
La dimension 62 atteint 1 579,6 tandis que la dimension médiane ne dépasse jamais 1,6. Ce n’est pas un accident d’un token ou d’une couche : la même dimension est là à la couche 4 et toujours là à la couche 20, avec presque la même valeur. Ce sont les outlier features,6 et elles sont systématiques — une propriété du modèle entraîné, pas de l’entrée.
L’histogramme de ces 896 maxima par dimension à la couche 16 rend la forme impossible à manquer :
0 - 1 | ######################################## 254
1 - 2 | ######################################## 283
2 - 4 | ######################################## 226
4 - 8 | ######################################## 93
8 - 16 | ################## 18
16 - 32 | ######### 9
32 - 64 | ####### 7
64 - 128 | ##### 5
128 - 256 | 0
256 - 512 | 0
512 - 1024 | 0
1024 - 4096 | # 1Neuf cents dimensions en un tas bien rangé sous 8, absolument rien pendant trois octaves, puis une seule dimension tout au bout. Quantifiez maintenant ce tenseur en INT8 et comptez ce qui se passe :
| schéma | erreur relative | niveaux entiers distincts utilisés, tenseur entier |
|---|---|---|
| une échelle pour tout le tenseur | 0,1083 | 14 sur 256 |
| une échelle par token (par ligne) | 0,0433 | 158 |
| tenseur entier, 1 dimension outlier gardée en fp32 | 0,0442 | 48 |
| tenseur entier, 4 dimensions outlier gardées en fp32 | 0,0279 | 57 |
| tenseur entier, 16 dimensions outlier gardées en fp32 | 0,0085 | 102 |
Quatorze niveaux sur 256. L’échelle a été fixée par 1 579,6, donc chaque pas fait 12,44 de large, et l’activation typique — magnitude médiane 0,26, quatre-vingt-dix-neuvième percentile 2,51 — n’a nulle part où atterrir. Par dimension, c’est encore plus net :
single tensor-wide scale = 12.4378
dim 826 (max |h| = 4.77): 1 distinct level out of 256
dim 336 (max |h| = 1.62): 1 distinct level out of 256
dim 96 (max |h| = 0.69): 1 distinct level out of 256
after excluding the top 4 dimensions, scale = 0.5749 (22x smaller)
dim 826: 8 levels dim 336: 4 levels dim 96: 3 levelsUn niveau. Toute la dimension, chaque token, quantifiée au même nombre. Huit bits ont été alloués et environ zéro ont été utilisés, et le modèle qui lit ces activations reçoit une constante.
Cette mesure justifie toutes les techniques réellement utilisées :
Gardez les outliers hors du coup. LLM.int8()6 décompose la multiplication matricielle : les dimensions aux magnitudes extrêmes sont calculées en 16 bits, tout le reste en INT8, puis les deux moitiés sont sommées. Le tableau ci-dessus est le reçu — retirer quatre dimensions réduit l’erreur d’un facteur proche de quatre. SmoothQuant7 déplace plutôt la difficulté : diviser les activations par un facteur par canal et multiplier la colonne de poids correspondante par ce facteur, ce qui laisse le produit inchangé et déplace l’outlier du tenseur qui ne peut pas l’absorber vers celui qui le peut.
Choisissez l’arrondi, ne vous contentez pas d’arrondir. Rien ci-dessus ne demande à quoi sert la matrice. GPTQ8 quantifie colonne par colonne et, après chacune, ajuste les colonnes restantes en précision complète pour compenser l’erreur déjà commise — en minimisant l’erreur de la sortie de la couche sur de vraies entrées plutôt que celle de ses poids. AWQ9 remarque qu’une petite fraction des canaux de poids compte bien plus que le reste, les trouve à partir des statistiques d’activation, puis les met à l’échelle vers le haut avant de quantifier afin qu’ils tombent sur des niveaux plus fins. Les deux ont besoin d’un jeu de calibration ; aucune n’a besoin de gradients.
Afficher les détails
GGUF, et ce qu’un format de fichier vient faire ici.
GGUF n’est pas une méthode de quantification ; c’est le conteneur utilisé par llama.cpp, et la confusion dans les comparaisons gguf vs gptq vient du fait qu’on traite les deux comme des choses du même type. GGUF contient les tenseurs, le tokenizer, les métadonnées d’architecture et le template de chat dans un seul fichier memory-mappable, et embarque en son sein une famille de schémas par blocs — des noms comme Q4_K_M encodent les bits par poids, la taille de bloc et le fait que certains tenseurs soient conservés à plus haute précision.
La différence d’ingénierie qui compte : GPTQ et AWQ produisent des poids optimisés pour un kernel GPU, tandis que les schémas de GGUF se décodent à faible coût sur CPU avec le fichier mappé plutôt que chargé. C’est pourquoi le même « modèle 7B 4-bit » nominal existe dans les deux mondes à des tailles et des qualités différentes, et pourquoi la comparaison honnête n’est jamais le format : c’est la mesure ci-dessous, exécutée sur votre propre tâche.
Ce que coûte vraiment la quantification, mesuré
Lien vers la section : Ce que coûte vraiment la quantification, mesuréPresque tous les articles sur la quantification s’arrêtent à la section précédente : ils expliquent la méthode, citent un ratio de compression et affirment que la qualité est « largement préservée ». Le chapitre 4 parlait de ne pas se tromper soi-même, alors vérifions.
Même modèle, poids quantifiés en place avec chaque schéma, puis trois mesures : perplexité sur 2 048 tokens de prose anglaise mise de côté — ici, le brouillon de ce cours, raison pour laquelle le dépôt substitue un livre fixe du domaine public et imprime un tableau de même forme avec des nombres différents — une batterie de 16 questions factuelles courtes à réponses connues en greedy decoding, et la fraction de tokens sur lesquels le modèle quantifié est d’accord avec celui en précision complète à contexte identique.
| schéma | erreur moyenne des poids | perplexité | batterie de questions | accord avec fp32 |
|---|---|---|---|---|
| fp32 (référence) | 0,0000 | 23,08 | 13/16 | 100,0 % |
| INT8 par tenseur | 0,0400 | 23,58 | 13/16 | — |
| INT8 par ligne | 0,0100 | 22,96 | 13/16 | 98,6 % |
| INT4 par tenseur | 0,6026 | 365 416 000 | 0/16 | — |
| INT4 par ligne | 0,1790 | 46,18 | 6/16 | 58,3 % |
| INT4 groupe 128 | 0,1323 | 31,08 | 10/16 | 71,5 % |
| NF4 bloc 64 | 0,0952 | 24,55 | 11/16 | 84,7 % |
| INT3 groupe 128 | 0,3044 | 213,09 | 0/16 | 5,6 % |
| INT2 groupe 128 | 0,7790 | 26 325 436 | 0/16 | 0,0 % |
Quatre éléments de ce tableau méritent d’être dits clairement.
INT8 fait correctement est gratuit. INT8 par ligne marque 22,96 contre 23,08 pour la référence — un écart d’une partie sur deux cents, du bruit, à lire comme « identique ». Le sens du bruit n’est pas stable : sur le corpus public du dépôt, les deux mêmes schémas donnent 22,24 contre 22,18, moitié moins d’écart et dans l’autre sens. Il est d’accord avec le modèle en précision complète sur 142 des 144 tokens générés. Un quart de la mémoire face à la référence fp32, la moitié face au fp16 que vous déploieriez réellement, et aucun coût détectable. INT8 fait négligemment est presque gratuit aussi : une échelle par matrice coûte 0,5 point de perplexité et aucune réponse de la batterie. Huit bits pardonnent assez pour que la granularité compte à peine, exactement la raison pour laquelle on généralise d’INT8 à INT4 et on se fait mal.
INT4 avec une échelle par tenseur détruit le modèle. Perplexité 365 millions : pas dégradé, annihilé. La granularité devient alors tout le jeu — par tenseur 365 416 000, par ligne 46,18, par groupe de 128 31,08, NF4 24,55. Les mêmes quatre bits par poids, un facteur de quinze millions entre le pire et le meilleur.
La perplexité est un instrument grossier et la batterie un instrument encore plus grossier. Entre NF4 et INT4 group-128, l’écart de perplexité est de 6,5 points et la batterie diffère d’une question — et l’intervalle de confiance du chapitre 4 dit qu’une question sur seize ne distingue absolument rien. Il existe une démonstration plus nette que l’intervalle : exécutez la même batterie avec la pénalité de répétition stock du modèle désactivée, ce qui est ce que greedy decoding signifie réellement, et ces deux lignes échangent leurs places. Une question sur seize n’est pas un petit effet, c’est une absence d’effet. L’avertissement du chapitre 8 s’applique aussi : la perplexité n’est comparable qu’entre modèles partageant un tokenizer, donc un nombre issu de l’article de quelqu’un d’autre ne peut pas être comparé au vôtre.
La colonne d’accord est la plus fine des trois, et presque gratuite : exécutez le modèle en précision complète en greedy, puis demandez au modèle quantifié, à chaque position, ce qu’il aurait choisi avec le même préfixe. Elle a 144 observations indépendantes au lieu de 16, n’a pas besoin de vérité terrain et se dégrade en douceur là où la batterie se dégrade par sauts. C’est aussi exactement la quantité dont la section suivante a besoin.
C’est la promesse faite par le chapitre 1 à propos de ce chapitre, arrivée à l’heure : les mathématiques disent qu’un modèle 4-bit est possible, et l’ingénierie décide s’il est utilisable.
Speculative decoding
Lien vers la section : Speculative decodingLe chapitre 12 l’a annoncé et a laissé la facture ici.
L’idée vient directement de la séparation prefill/decode. Vérifier une séquence proposée de tokens coûte une passe avant sur positions — un produit matrice-matrice, à peine plus cher que la passe sur une seule. Donc :
Un petit modèle bon marché génère tokens candidats de façon autorégressive.
Le grand modèle exécute une passe avant sur les candidats à la fois, produisant ce qu’il aurait dit à chaque position.
Gardez le plus long préfixe sur lequel les deux sont d’accord, plus le token que le grand modèle fournit gratuitement au premier désaccord. Jetez le reste et recommencez.
La distribution de sortie est inchangée. Avec greedy decoding, c’est évident — un token n’est accepté que si la cible l’aurait produit. Avec sampling, il faut une règle d’acceptation modifiée, et Leviathan et al. prouvent que la distribution résultante est exactement celle de la cible.10 C’est la deuxième optimisation exacte de ce chapitre.
Tout dépend donc du taux d’acceptation , qui est mesurable — c’est la colonne d’accord ci-dessus, raison pour laquelle elle a été calculée. En utilisant chaque modèle quantifié comme draft pour la cible en précision complète, sur 144 positions générées :
| modèle draft | acceptation | plus longue séquence acceptée | tokens attendus par passe cible, |
|---|---|---|---|
| fp32 (la cible elle-même) | 100,0 % | 48 | 5,00 |
| INT8 par ligne | 98,6 % | 48 | 4,86 |
| NF4 bloc 64 | 84,7 % | 20 | 3,69 |
| INT4 groupe 128 | 71,5 % | 13 | 2,85 |
| INT4 par ligne | 58,3 % | 7 | 2,24 |
| INT3 groupe 128 | 5,6 % | 2 | 1,06 |
| INT2 groupe 128 | 0,0 % | 0 | 1,00 |
Les tokens acceptés attendus par passe de vérification, à longueur de draft , sont
et l’accélération nette divise cela par le coût propre du draft, une fraction de la cible par token :
| acceptation | , | , | , | , |
|---|---|---|---|---|
| 30 % | 1,19x | 1,02x | 0,79x | 0,79x |
| 50 % | 1,61x | 1,38x | 1,08x | 1,11x |
| 70 % | 2,31x | 1,98x | 1,54x | 1,78x |
| 90 % | 3,41x | 2,93x | 2,28x | 3,40x |
L’entrée en gras est celle à retenir : speculative decoding peut ralentir la génération. À 30 % d’acceptation avec un draft coûtant un cinquième de la cible, vous payez cinq passes avant et gardez 1,4 token. La dernière colonne est l’autre piège : un draft plus long n’aide que lorsque l’acceptation est élevée, car la queue d’une supposition de tokens est presque jamais atteinte. À 90 % d’acceptation, vaut 3,40x ; à 30 %, il vaut 0,79x : même configuration, gain ou perte selon un nombre mesuré sur votre trafic.
Distillation, et ce que porte un soft label
Lien vers la section : Distillation, et ce que porte un soft labelLa quantification réduit un modèle en stockant la même fonction avec moins de bits. La distillation le réduit en entraînant un modèle plus petit à imiter un plus grand11 — une idée antérieure au deep learning de presque une décennie.12
La partie subtile est ce que l’élève apprend. Pas la bonne réponse : il aurait pu être entraîné directement dessus. Ce que le professeur ajoute, c’est la distribution entière. Demandez au modèle ce qui suit une phrase et regardez au-delà de l’argmax :
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360Le hard label dit jug et rien d’autre. Le soft label dit jug, et aussi que cup était presque aussi bon, bowl plausible, et large — un adjectif, une continuation grammaticale complètement différente — encore vivant. C’est l’argument original : c’est un 7, mais il ressemble beaucoup à un 1, et la ressemblance est une information que le hard label jette.
C’est aussi pourquoi la distillation utilise une température. Diviser les logits par avant le softmax aplatit la distribution et augmente le poids relatif des suivants : sur cette phrase, le ratio entre le top token et le troisième passe de 2,24 à à 1,50 à — la racine carrée du premier, ce que diviser les logits par deux fait à un ratio. Même ordre, plus d’attention de la loss sur les quasi-réussites. Le gradient de l’élève transporte l’incertitude du professeur, pas seulement son verdict.
Ce qui tient dans 8, 16 et 24 Go
Lien vers la section : Ce qui tient dans 8, 16 et 24 GoTout ce chapitre se résume maintenant à une somme :
où est le total des tokens résidents sur toutes les requêtes concurrentes. Application : les lignes 7B et 70B supposent 8 têtes key-value de dimension 128, la ligne 13B une attention multi-head complète avec 40 têtes, comme ces générations de modèles ont été construites — et cela se voit.
8 Go
| modèle | précision | poids | libre après overhead | tokens de contexte qui tiennent |
|---|---|---|---|---|
| 7B | fp16 | 13,0 Go | ne tient pas | — |
| 7B | int8 | 6,5 Go | ne tient pas | — |
| 7B | int4 (g128) | 3,4 Go | 3,1 Go | 25 710 |
| 13B | int4 (g128) | 6,2 Go | 0,3 Go | 337 |
| 70B | int4 (g128) | 33,6 Go | ne tient pas | — |
16 Go
| modèle | précision | poids | libre après overhead | tokens de contexte qui tiennent |
|---|---|---|---|---|
| 7B | fp16 | 13,0 Go | 1,5 Go | 11 972 |
| 7B | int8 | 6,5 Go | 8,0 Go | 65 378 |
| 7B | int4 (g128) | 3,4 Go | 11,1 Go | 91 246 |
| 13B | int8 | 12,1 Go | 2,4 Go | 3 136 |
| 13B | int4 (g128) | 6,2 Go | 8,3 Go | 10 822 |
24 Go
| modèle | précision | poids | libre après overhead | tokens de contexte qui tiennent |
|---|---|---|---|---|
| 7B | fp16 | 13,0 Go | 9,5 Go | 77 508 |
| 7B | int8 | 6,5 Go | 16,0 Go | 130 914 |
| 7B | int4 (g128) | 3,4 Go | 19,1 Go | 156 782 |
| 13B | int8 | 12,1 Go | 10,4 Go | 13 622 |
| 13B | int4 (g128) | 6,2 Go | 16,3 Go | 21 308 |
| 70B | int4 (g128) | 33,6 Go | ne tient pas | — |
Regardez la ligne 13B dans le tableau 8 Go. Les poids tiennent — 6,2 Go sur 8 — donc, selon la façon habituelle de parler, un modèle 13B « tourne sur une carte 8 Go ». Il dispose de 337 tokens de contexte, ce qui n’est pas une conversation mais à peine un prompt. « Est-ce que ça tient ? » est la mauvaise question. La bonne est « avec combien de contexte, et pour combien d’utilisateurs à la fois ? ».
Regardez aussi les deux lignes int8 à 16 Go. Le 7B obtient 65 378 tokens et le 13B 3 136 — un écart d’un facteur vingt dû à 5,6 Go de poids supplémentaires, parce que le 13B ici a une attention multi-head et que son cache coûte 800 Ko par token contre 128 Ko pour le 7B. Deux modèles de taille similaire, l’un inutilisable en long context, pour une raison qui n’apparaît dans le titre d’aucune model card.
Où cela mène ensuite
Lien vers la section : Où cela mène ensuiteIl y a treize chapitres, c’était un perceptron avec deux poids et un biais. C’est maintenant un transformer qui a été conçu, entraîné, aligned, à qui l’on a appris à dépenser du compute sur les questions difficiles, et servi à un coût mesuré par token — sans boîte encore fermée à l’intérieur.
Cela se termine ici, et cela se termine volontairement.
Le chapitre 14 commence avec le modèle ailleurs. Pas dans votre processus, pas dans votre mémoire, pas dans une variable que vous pouvez imprimer : sur une machine que vous n’administrez pas, derrière une clé API, un port et une facture. Tout ce qui a été mesuré ici se produit encore — le prefill tourne toujours avant le premier token, le cache grandit toujours avec la conversation, le batch dans lequel vous êtes appartient toujours à quelqu’un d’autre et décide toujours de votre latence — mais désormais vous l’observez à travers un flux de Server-Sent Events, un finish_reason, et un HTTP 429 avec un header Retry-After. Les questions changent avec le point de vue : non plus comment ce gradient est-il calculé, mais pourquoi ma facture a-t-elle triplé. Le langage change aussi, et le chapitre 14 explique cette règle plutôt que de l’annoncer — jusqu’ici, le code contenait des poids, des gradients, des logits et des octets de tokenizer ; à partir de là, il contient une connexion, une reprise, une annulation et un état accumulé. Les treize chapitres derrière vous ne sont pas jetés au passage. Ils décrivent ce qui tourne de l’autre côté du port.
Sources et méthode
Lien vers la section : Sources et méthodeDeux omissions sont délibérées. FlashAttention (Dao et al., arXiv:2205.14135) n’est pas une attention différente — elle calcule la même fonction en découpant l’opération en tuiles afin que la matrice de scores ne soit jamais écrite en mémoire, raison pour laquelle les 67 Mo du deuxième tableau de ce chapitre sont plus petits en pratique que ce que l’arithmétique suggère. Et les kernels eux-mêmes sont délégués : le cours 10 du CS336 de Stanford couvre les systèmes d’inférence avec une profondeur que ce chapitre ne tente pas d’atteindre, et le dépôt llama.cpp ainsi que la spécification GGUF sont les sources primaires pour le côté CPU.
Références
Lien vers la section : Références-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). L’article est largement un argument de bande passante mémoire, et se lit comme tel. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Inclut la recette d’uptraining qui convertit un checkpoint multi-head existant, raison pour laquelle GQA s’est diffusé si vite. ↩
-
Yu, G.-I., Jeong, J. S., Kim, G.-W., Kim, S. et Chun, B.-G. Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. Introduit le scheduling au niveau de l’itération — continuous batching — et le batching sélectif. ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. L’article sur lequel vLLM est construit ; la §3 développe entièrement l’analogie avec les systèmes d’exploitation. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. et Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 est défini en §3 ; les seize valeurs de niveaux utilisées dans la mesure ci-dessus sont celles dérivées par cet article. ↩
-
Dettmers, T., Lewis, M., Belkada, Y. et Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). L’analyse des outlier features en §4 est la source du phénomène mesuré ci-dessus, y compris le constat que les outliers émergent systématiquement à l’échelle. ↩ ↩2
-
Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J. et Han, S. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438 (2022). ↩
-
Frantar, E., Ashkboos, S., Hoefler, T. et Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022). ↩
-
Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023). ↩
-
Leviathan, Y., Kalman, M. et Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). Le théorème 1 prouve que la distribution de sortie est inchangée ; Chen et al. (arXiv:2302.01318) ont publié la même idée indépendamment. ↩
-
Hinton, G., Vinyals, O. et Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). La température et l’argument de la « dark knowledge ». ↩
-
Buciluă, C., Caruana, R. et Niculescu-Mizil, A. Model Compression. KDD 2006. La distillation, neuf ans plus tôt, pour des ensembles plutôt que des transformers. ↩