Aller au contenu
13/30Chapitre 13 sur 30

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é.

TEXT
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 2N2N 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écution

Pour 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 :

generate.pyPYTHON
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 d=64d = 64, une étape de génération calculée des deux façons :

tokens dans le contextetout recalculeravec un cacheratiomatrice de scores
1280,59 ms0,062 ms10x65 536 B vs 512 B
2561,20 ms0,163 ms7x262 144 B vs 1 024 B
5127,03 ms0,078 ms90x1 048 576 B vs 2 048 B
102417,31 ms0,114 ms152x4 194 304 B vs 4 096 B
204859,83 ms0,214 ms279x16 777 216 B vs 8 192 B
4096236,18 ms0,284 ms832x67 108 864 B vs 16 384 B

La colonne de droite en est la cause. Recalculer construit la matrice d’attention complète n×nn \times n à chaque étape — le O(n2)O(n^2) de l’encadré sur la notation asymptotique du chapitre 9, payé une fois par token. Avec le cache, vous construisez à la place une ligne 1×n1 \times n : à 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 TT tokens depuis un départ à froid :

tokens générésavec un cacheen recalculantratio
1282,6 M192,0 M73x
51223,1 M7,36 G318x
2048293,7 M392,6 G1 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, O(T2)O(T^2) contre O(T3)O(T^3), 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 cache21,8 Mo
en recalculant181,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érentes

Regardez encore l’exécution rapide : son premier token ne s’est pas comporté comme les quarante-sept autres.

TEXT
prefill, 40 prompt tokens : 1.0224 s   ->  25.6 ms per token
decode,  47 steps         : 0.1665 s mean per step

Le 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 PP tokens :

tokens de promptsecondesms par token
160,351521,97
320,525416,42
641,049116,39
1281,655212,93
2563,096512,10

Decode, un token face à un cache de CC :

tokens en cachems pour un token
16110,05
6497,57
256108,53
1024103,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 1/decode step1/\text{decode step}, 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 é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 :

bytes per token=2×L×Hkv×dhead×bytes per element\text{bytes per token} = 2 \times L \times H_{kv} \times d_{\text{head}} \times \text{bytes per element}

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 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 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 :

TEXT
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/token

Exact, et cela reste exact pour toutes les formes testées :

batchcontextecache mesurépréditmémoire de travail au pic
15126,0 Mo6,0 Mo15,4 Mo
116 384192,0 Mo192,0 Mo207,3 Mo
165 536768,0 Mo768,0 Mo793,7 Mo
84 096384,0 Mo384,0 Mo401,5 Mo
322 048768,0 Mo768,0 Mo794,2 Mo
641 024768,0 Mo768,0 Mo797,0 Mo
128512768,0 Mo768,0 Mo816,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.

Le 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 HkvH_{kv} 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 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 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 contexteun utilisateur8 utilisateurs64 utilisateurs
4 0000,49 Go3,91 Go31,2 Go
32 0003,91 Go31,25 Go250,0 Go
128 00015,62 Go125,00 Go1 000,0 Go
1 000 000122,07 Go976,56 Go7 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 descend

Le 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 :

batchlatence par étapedébitlatence vs B=1
10,1286 s7,78 tok/s1,00x
20,1839 s10,88 tok/s1,43x
40,1909 s20,95 tok/s1,49x
80,2781 s28,76 tok/s2,16x
160,3430 s46,64 tok/s2,67x
320,6302 s50,78 tok/s4,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 gagne

La façon naïve de faire du batching consiste à collecter BB 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 :

politiquetemps réeldébitlatence moyenne par requêteslot-steps gaspillés
batches statiques de 8176,9 s10,6 tok/s83,2 s3 214
continu, 8 slots109,0 s17,2 tok/s8,1 s0

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é.

L’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 :

quantize.pyPYTHON
qmax  = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax                        
Wq    = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale                                  # dequantized

Choisissez 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 WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert :

schémaerreur relative moyennepire matrice
INT8, une échelle pour toute la matrice0,04000,1487
INT8, une échelle par ligne de sortie0,01000,0149
INT4, une échelle pour toute la matrice0,60260,9931
INT4, une échelle par ligne de sortie0,17900,2589
INT4, une échelle par groupe de 1280,13230,1992
NF4, une échelle par bloc de 640,09520,1205
INT3, une échelle par groupe de 1280,30440,4123
INT2, une échelle par groupe de 1280,77900,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 :

TEXT
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 4+16/128=4.1254 + 16/128 = 4.125 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.

L’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 :

coucheplus grand |h|plus grand |h| de la dimension médianeratiodimensions au-dessus de 6x la médiane
16,190,33918x2
41543,481,550996x34
81571,631,4981049x36
121575,031,5461019x34
161579,601,617977x32
201577,982,361668x24
24204,4410,76019x12

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 :

TEXT
     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 | #                                        1

Neuf 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émaerreur relativeniveaux entiers distincts utilisés, tenseur entier
une échelle pour tout le tenseur0,108314 sur 256
une échelle par token (par ligne)0,0433158
tenseur entier, 1 dimension outlier gardée en fp320,044248
tenseur entier, 4 dimensions outlier gardées en fp320,027957
tenseur entier, 16 dimensions outlier gardées en fp320,0085102

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 :

TEXT
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 levels

Un 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émaerreur moyenne des poidsperplexitébatterie de questionsaccord avec fp32
fp32 (référence)0,000023,0813/16100,0 %
INT8 par tenseur0,040023,5813/16
INT8 par ligne0,010022,9613/1698,6 %
INT4 par tenseur0,6026365 416 0000/16
INT4 par ligne0,179046,186/1658,3 %
INT4 groupe 1280,132331,0810/1671,5 %
NF4 bloc 640,095224,5511/1684,7 %
INT3 groupe 1280,3044213,090/165,6 %
INT2 groupe 1280,779026 325 4360/160,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.

Le 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 γ\gamma tokens coûte une passe avant sur γ\gamma positions — un produit matrice-matrice, à peine plus cher que la passe sur une seule. Donc :

Un petit modèle bon marché génère γ\gamma tokens candidats de façon autorégressive.

Le grand modèle exécute une passe avant sur les γ\gamma 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 α\alpha, 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 draftacceptationplus longue séquence acceptéetokens attendus par passe cible, γ=4\gamma = 4
fp32 (la cible elle-même)100,0 %485,00
INT8 par ligne98,6 %484,86
NF4 bloc 6484,7 %203,69
INT4 groupe 12871,5 %132,85
INT4 par ligne58,3 %72,24
INT3 groupe 1285,6 %21,06
INT2 groupe 1280,0 %01,00

Les tokens acceptés attendus par passe de vérification, à longueur de draft γ\gamma, sont

E[tokens]=1αγ+11α\mathbb{E}[\text{tokens}] = \frac{1 - \alpha^{\gamma+1}}{1 - \alpha}

et l’accélération nette divise cela par le coût propre du draft, une fraction cc de la cible par token :

acceptationc=0.05c=0.05, γ=4\gamma=4c=0.1c=0.1, γ=4\gamma=4c=0.2c=0.2, γ=4\gamma=4c=0.1c=0.1, γ=8\gamma=8
30 %1,19x1,02x0,79x0,79x
50 %1,61x1,38x1,08x1,11x
70 %2,31x1,98x1,54x1,78x
90 %3,41x2,93x2,28x3,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 γ\gamma tokens est presque jamais atteinte. À 90 % d’acceptation, γ=8\gamma = 8 vaut 3,40x ; à 30 %, il vaut 0,79x : même configuration, gain ou perte selon un nombre mesuré sur votre trafic.

La 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 :

TEXT
"She poured the milk into the"
  ' jug' 0.1355   ' cup' 0.1051   ' bowl' 0.0605   ' large' 0.0380   ' milk' 0.0360

Le 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 TT 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 à T=1T = 1 à 1,50 à T=2T = 2 — 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.

Tout ce chapitre se résume maintenant à une somme :

memory=N×bytes per weightfixed+T×2LHkvdhead×bytesgrows with every token+runtime overheadcall it 1.5 GB\text{memory} = \underbrace{N \times \text{bytes per weight}}_{\text{fixed}} + \underbrace{T \times 2 L H_{kv} d_{\text{head}} \times \text{bytes}}_{\text{grows with every token}} + \underbrace{\text{runtime overhead}}_{\text{call it 1.5 GB}}

TT 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èleprécisionpoidslibre après overheadtokens de contexte qui tiennent
7Bfp1613,0 Gone tient pas
7Bint86,5 Gone tient pas
7Bint4 (g128)3,4 Go3,1 Go25 710
13Bint4 (g128)6,2 Go0,3 Go337
70Bint4 (g128)33,6 Gone tient pas

16 Go

modèleprécisionpoidslibre après overheadtokens de contexte qui tiennent
7Bfp1613,0 Go1,5 Go11 972
7Bint86,5 Go8,0 Go65 378
7Bint4 (g128)3,4 Go11,1 Go91 246
13Bint812,1 Go2,4 Go3 136
13Bint4 (g128)6,2 Go8,3 Go10 822

24 Go

modèleprécisionpoidslibre après overheadtokens de contexte qui tiennent
7Bfp1613,0 Go9,5 Go77 508
7Bint86,5 Go16,0 Go130 914
7Bint4 (g128)3,4 Go19,1 Go156 782
13Bint812,1 Go10,4 Go13 622
13Bint4 (g128)6,2 Go16,3 Go21 308
70Bint4 (g128)33,6 Gone 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.

Il 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.


Deux 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 n×nn \times n 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

  7. 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).

  8. Frantar, E., Ashkboos, S., Hoefler, T. et Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022).

  9. Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023).

  10. 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.

  11. 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 ».

  12. 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.

Prêt à laisser LIA choisir à votre place ?

Créez avec tous les modèles d'IA au même endroit — commencez gratuitement dès aujourd'hui.