Zum Inhalt springen
13/30Kapitel 13 von 30

Inference günstig machen: KV cache, Batching und Quantisierung

Dasselbe model antwortet byte-identisch in 8,8 oder 78,9 Sekunden. Dann INT4, dreifach gemessen statt nur behauptet.

Auf dieser Seite

Dasselbe model, auf derselben Maschine, beantwortet dieselbe Frage mit denselben 48 tokens. Die beiden Ausgaben sind token für token identisch — geprüft, nicht angenommen.

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)

Ein Argument hat sich geändert: use_cache=False. Am model, am prompt, am Sampling oder an der Arithmetik ist nichts anders, und der zweite Lauf ist für seinen Aufwand nicht genauer. Er ist ohne jeden Nutzen neunmal langsamer.

Das ist die Form dieses Kapitels. Alles darin — der Cache, der batch, die quantisierten Gewichte — ist ein Versuch, nicht mehr für Arbeit zu bezahlen, die die Antwort nicht verändert, oder herauszufinden, was eine billigere Antwort kostet. Kapitel 10 hat die Preisliste fürs Training aufgestellt. Das hier ist die Preisliste für die Seite, für die du für immer zahlst: Ein deploytes model verbraucht ungefähr 2N2N FLOPs für jeden token, den es ausgibt, bei jeder Anfrage, für den Rest seiner Lebensdauer.

Wo die Zeit des zweiten Laufs geblieben ist

Link zum Abschnitt: Wo die Zeit des zweiten Laufs geblieben ist

Um einen token zu generieren, nimmt ein decoder-only transformer die gesamte bisherige Sequenz, lässt sie durch jede Schicht laufen und liest die Wahrscheinlichkeitsverteilung an der letzten Position ab. Dann hängt er den gewählten token an und macht es noch einmal. Diese Beschreibung ist korrekt, und genau das tut der langsame Lauf.

Sie ist aber auch enorm verschwenderisch, und der Grund ist die kausale Maske aus Kapitel 9. Die key- und value-Vektoren von Position 7 werden aus dem Input von Position 7 und den Positionen davor berechnet. Wenn Position 8 hinzukommt, kann Position 7 sie nicht sehen — genau das bedeutet kausal —, also sind key und value von Position 7 exakt dieselben Zahlen wie zuvor. Der langsame Lauf berechnet sie trotzdem neu, bei jedem Schritt.

Also speichere sie. Dieser Speicher ist der key-value cache, die folgenreichste Optimierung im Serving von Sprachmodellen:

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)

Sieh dir an, was innerhalb der Schleife ins model gefüttert wird: nxt, ein token. Nicht die Sequenz. Die query des neuen tokens attendiert gegen jeden gecachten key, und die gecachten keys hätten sich nie geändert. Das ist keine Näherung — genau darum geht es bei der Identitätsprüfung oben. Der Cache tauscht keine Qualität gegen Geschwindigkeit; er löscht redundante Arithmetik.

Um die Skalierung sauber zu sehen, nimm den transformer heraus und miss einen einzelnen attention head mit d=64d = 64, einen Generierungsschritt auf beide Arten berechnet:

tokens im contextalles neu berechnenmit CacheVerhältnisscore-Matrix
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

Die rechte Spalte ist die Ursache. Neu berechnen baut bei jedem Schritt die volle n×nn \times n attention-Matrix auf — das O(n2)O(n^2) aus der Box zur asymptotischen Notation in Kapitel 9, bezahlt einmal pro token. Mit dem Cache baust du stattdessen eine 1×n1 \times n-Zeile: bei 4.096 tokens 67 MB scores gegenüber 16 KB.

Multiply-Accumulates statt Millisekunden zu zählen, nimmt die Maschine aus dem Argument. Um TT tokens aus einem Kaltstart zu generieren:

generierte tokensmit Cacheneu berechnenVerhältnis
1282,6 M192,0 M73x
51223,1 M7,36 G318x
2048293,7 M392,6 G1.336x

Pro Schritt ist die gecachte Version linear im context und die ungecachte quadratisch; über eine Generation aufsummiert steht O(T2)O(T^2) gegen O(T3)O(T^3), wobei das Verhältnis unbegrenzt wächst. Der neunfache Unterschied am Anfang wurde über 48 tokens gemessen — noch unter der ersten Zeile dieser Tabelle.

Der Cache verändert auch, was im Speicher liegen muss. Auf einer 8-GB-Laptop-GPU, die 256 tokens in fp16 generiert, mit dem Peak des Allocators minus residente Gewichte:

Peak-Arbeitsspeicher
mit Cache21,8 MB
neu berechnen181,7 MB

8,3-mal mehr Speicher, ausgegeben, um dieselben tokens langsamer zu produzieren. Das ist das Versprechen aus Kapitel 5, das aus einer unerwarteten Richtung eintrifft: Dort musste Reverse-Mode-Autodiff jedes Zwischenresultat für den backward pass am Leben halten, und Aktivierungen dominierten den Trainingsspeicher. Bei Inference gibt es keinen backward pass und nichts, was dafür behalten werden müsste — also dominiert stattdessen der Cache den Speicher, und er ist eine bewusste Entscheidung statt unvermeidbarer Kosten.

Prefill und Decode sind zwei verschiedene Maschinen

Link zum Abschnitt: Prefill und Decode sind zwei verschiedene Maschinen

Sieh noch einmal auf den schnellen Lauf: Sein erster token verhielt sich anders als die übrigen siebenundvierzig.

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

Der prompt kostete 25,6 ms pro token und jeder generierte token 166 ms. Dasselbe model, dieselbe Hardware, dieselben Gewichte, ein sechsfacher Unterschied pro token — und zwar in die Richtung, die die meisten nicht erwarten. Der prompt ist der billige Teil. Generation teilt sich in zwei Phasen mit wirklich unterschiedlicher Physik:

Ein forward pass über den gesamten prompt. Jeder token wird parallel verarbeitet, also wird jede Gewichtsmatrix einmal aus dem Speicher geladen und mit einer Matrix aus Hunderten token-Vektoren multipliziert — ein Matrix-Matrix-Produkt mit viel Arithmetik pro bewegtem Byte, genau wofür eine GPU gebaut ist. Prefill ist rechenlimitiert, und seine Kosten sind ungefähr linear in der prompt-Länge.

Ein forward pass pro token, batch von eins und Sequenz von eins. Jede Gewichtsmatrix wird weiterhin vollständig aus dem Speicher geladen und mit einem einzelnen Vektor multipliziert — ein Matrix-Vektor-Produkt mit fast keiner Arithmetik pro bewegtem Byte. Decode ist speicherbandbreitenlimitiert, und seine Kosten pro token hängen kaum von der Länge des context ab.

Beide Hälften sind messbar. Prefill, ein Durchlauf über PP tokens:

prompt tokensSekundenms pro token
160,351521,97
320,525416,42
641,049116,39
1281,655212,93
2563,096512,10

Decode, ein token gegen einen Cache von CC:

gecachte tokensms für einen token
16110,05
6497,57
256108,53
1024103,86

Lies die zweite Tabelle zweimal. Von 16 tokens context auf 1.024 — vierundsechzigmal mehr Verlauf, gegen den attendiert werden muss — hat die Kosten eines Schritts um nichts Messbares verändert. Attention gegen den Cache ist echte Arbeit, aber sie wird von den Fixkosten überlagert, eine halbe Milliarde Gewichte durch den Speicherbus zu ziehen, um einen Vektor zu produzieren. Diese Fixkosten sind der Grund für alles im nächsten Abschnitt.

Diese zwei Phasen sind der Ursprung der zwei Zahlen, die jedes Serving-System meldet. Time to first token ist im Wesentlichen Prefill und wächst mit dem prompt, weshalb ein langes Gespräch langsam startet. Tokens pro Sekunde ist 1/decode step1/\text{decode step} und ungefähr konstant, weshalb die Antwort danach gleichmäßig fließt. Ein Chat, der langsam beginnt und dann flüssig streamt, ist kein Rendering-Trick. Es sind diese zwei Tabellen.

Der Cache tauscht Arithmetik gegen Speicher, und der Speicherbedarf ist nicht klein. Für jeden token im context hält jede Schicht einen key-Vektor und einen value-Vektor pro key-value head:

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}

Die 2 steht für keys und values; alles andere ist Architektur. Für das model, das in diesem Kapitel durchgehend gemessen wird — 24 Schichten, 14 query heads, 2 key-value heads, head-Dimension 64 — sind das in fp16 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 Bytes pro token.

Formeln in diesem Feld liegen gern um einen Faktor zwei daneben, also prüfe es gegen den Allocator, statt es zu glauben:

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

Exakt, und es bleibt über jede ausprobierte Form hinweg exakt:

batchcontextgemessener CachevorhergesagtPeak-Arbeitsspeicher
15126,0 MB6,0 MB15,4 MB
116.384192,0 MB192,0 MB207,3 MB
165.536768,0 MB768,0 MB793,7 MB
84.096384,0 MB384,0 MB401,5 MB
322.048768,0 MB768,0 MB794,2 MB
641.024768,0 MB768,0 MB797,0 MB
128512768,0 MB768,0 MB816,4 MB

Die letzten drei Zeilen verdienen einen zweiten Blick. Zweiunddreißig Nutzer mit je 2.048 tokens, vierundsechzig mit 1.024, einhundertachtundzwanzig mit 512 — der Cache ist in jedem Fall 768 MB, weil alle drei 65.536 tokens halten. Der Cache hängt nur von der Gesamtzahl residenter tokens ab, nicht davon, wie sie auf Nutzer verteilt sind. Diese Tatsache ist das Fundament des Abschnitts zu Batching.

Kapitel 9 hat multi-query und grouped-query attention eingeführt und den Grund auf dieses Kapitel verschoben. Der Grund ist diese Formel, und konkret das HkvH_{kv} darin.

Standard-multi-head attention gibt jedem query head seine eigenen key- und value heads. Das model hier hat 14 query heads; mit voller multi-head attention wäre sein Cache 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 Bytes pro token — 84 KB statt 12 KB, exakt siebenmal mehr, das Verhältnis von query heads zu key-value heads.

Multi-query attention1 treibt das ans Limit: Alle query heads teilen sich einen einzigen key-value head. Grouped-query attention2 ist der Kompromiss, der gewonnen hat — eine Handvoll key-value heads, jeweils von einer Gruppe query heads geteilt —, weil der Qualitätsverlust von MQA real war und der von GQA nicht. Keines davon kauft Arithmetik. Sie existieren, um diese Formel durch eine ganze Zahl zu teilen, und sie verbreiteten sich in der Branche in dem Moment, in dem lange Kontexte den Cache zur bindenden Beschränkung machten.

Was schnell passiert. Für ein model der 7B-Klasse mit 32 Schichten und 8 key-value heads der Dimension 128 beträgt der Cache in fp16 128 KB pro token:

context tokensein Nutzer8 Nutzer64 Nutzer
4.0000,49 GB3,91 GB31,2 GB
32.0003,91 GB31,25 GB250,0 GB
128.00015,62 GB125,00 GB1.000,0 GB
1.000.000122,07 GB976,56 GB7.812,5 GB

Die eigenen Gewichte dieses model sind in fp16 13,0 GB, die Zahl in der Tabelle am Ende dieses Kapitels. Bei einem context von 128.000 tokens ist also der Cache eines einzelnen Nutzers größer als das model. Das ist die Arithmetik, die Kapitel 16 in Geld übersetzt, und es ist der Grund, warum ein langes Gespräch nicht nur langsam ist — es belegt einen festen Anteil einer Maschine, solange die Anfrage lebt.

Batching: die Zahl, die steigt, und die Zahl, die fällt

Link zum Abschnitt: Batching: die Zahl, die steigt, und die Zahl, die fällt

Decode ist speicherlimitiert: Die Gewichte werden durch den Bus gezogen, um einen token zu produzieren, und die Recheneinheiten warten. Also pack mehr Arbeit in denselben Schritt. Lass mehrere Anfragen gleichzeitig laufen, und die Gewichte, einmal gelesen, bedienen sie alle. Gemessen am selben model, wobei jede Anfrage einen 64-token-Cache hält und einen token decodiert:

batchLatenz pro SchrittDurchsatzLatenz 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

Lies die beiden rechten Spalten gegeneinander, denn sie sind der ganze Punkt. Von einer Anfrage auf sechzehn zu gehen, multipliziert den Durchsatz mit 6,0 und die Wartezeit für jede einzelne Anfrage mit 2,67. Der batch hat den Server besser und jeden Nutzer schlechter gemacht.

Das ist kein Bug, den man wegoptimiert; es ist der Trade-off selbst, und er hat auf jeder Seite einen Namen. Latenz ist das, was ein Mensch erlebt, der auf eine Antwort wartet. Durchsatz ist das, wodurch die Rechnung geteilt wird. Keine Einstellung verbessert beides.

Beachte auch, wo es aufhört. Von 16 auf 32 gewinnt der Durchsatz 9 %, während sich die Latenz fast verdoppelt: Der Schritt ist nicht mehr speicherlimitiert, sondern rechenlimitiert, und jenseits dieses Knicks bringt der batch nichts mehr. Jedes Deployment hat so einen Knick; seine Lage musst du auf deinem messen, aber seine Existenz nicht.

Static batching verschwendet den Großteil dessen, was es gewinnt

Link zum Abschnitt: Static batching verschwendet den Großteil dessen, was es gewinnt

Die naive Art zu batchen ist, BB Anfragen zu sammeln, sie zusammen auszuführen und zurückzugeben, wenn alle fertig sind. Aber sie werden nicht zusammen fertig: Manche Antworten haben zwanzig tokens, andere fünfhundert. Ein fester batch läuft, bis sein längstes Mitglied fertig ist, und jede fertige Anfrage belegt bis dahin weiter ihren Slot und trägt Padding bei.

Nimm 64 Anfragen mit realistisch schiefer Verteilung der Ausgabelängen — Median 18 tokens, längste 231, insgesamt 1.874 — und simuliere beide Strategien mit den gemessenen Kosten pro Schritt für acht Slots:

StrategieWall ClockDurchsatzmittlere Latenz pro Anfrageverschwendete Slot-Schritte
statische Batches à 8176,9 s10,6 tok/s83,2 s3.214
kontinuierlich, 8 Slots109,0 s17,2 tok/s8,1 s0

Der Durchsatz verbessert sich um 1,6x. Die mittlere Latenz verbessert sich um mehr als das Zehnfache, weil bei static batching eine Anfrage, die nach vier Schritten fertig war, trotzdem auf einen 231-token-Nachbarn wartet, bevor irgendjemand davon hört.

Continuous batching3 ist die Lösung, und sie ist so einfach, wie sie klingt: Der batch ist keine Gruppe, sondern eine Menge von Slots, und ein Slot, der frei wird, nimmt im allernächsten Schritt die nächste wartende Anfrage auf. Der Scheduler arbeitet in der Granularität eines tokens statt einer Anfrage. Jeder produktive Serving-Stack macht das inzwischen.

Es hat eine zweite Hälfte, den Cache. Slots, die kommen und gehen, hinterlassen fragmentierten Cache-Speicher, und jeden Slot für seinen maximal möglichen context zu reservieren, verschwendet den Großteil der Reservierung. PagedAttention4 übernimmt die Antwort von Betriebssystemen: Speichere den Cache in Blöcken fester Größe mit einer Blocktabelle pro Sequenz, sodass der Cache einer Sequenz physisch verstreut liegen kann, während er logisch zusammenhängend bleibt — was außerdem zwei Sequenzen mit gemeinsamem Präfix erlaubt, die Blöcke zu teilen, die es enthalten. Darauf baut vLLM auf, und deshalb ist eine Serving-Engine ein Speicherallocator mit angehängtem transformer.

Die andere Hälfte der Rechnung sind die Gewichte selbst. Eine halbe Milliarde Parameter zu je vier Bytes sind 1,98 GB; zu zwei Bytes 0,99 GB; zu einem Byte 0,49 GB. Weniger Bits pro Gewicht verkleinern das model auf der Festplatte, verkleinern es im Speicher und machen — weil Decode bandbreitenlimitiert ist — jeden Schritt schneller, da weniger Bytes bewegt werden müssen.

Das einfachste Schema ist symmetrische Absolute-Maximum-Quantisierung, und es passt in drei Zeilen:

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

Wähle einen Scale, sodass das größte Gewicht auf die größte Ganzzahl abgebildet wird, teile, runde, speichere die Ganzzahlen und den Scale. Rekonstruiere durch Zurückmultiplizieren. Daran ist nichts clever, und es funktioniert — bis es das nicht mehr tut.

Gemessen an den echten Gewichten des model: alle 168 Projektionsmatrizen, 357,8 Millionen Parameter, relativer Fehler WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

Schemamittlerer relativer Fehlerschlechteste Matrix
INT8, ein Scale für die ganze Matrix0,04000,1487
INT8, ein Scale pro Ausgabezeile0,01000,0149
INT4, ein Scale für die ganze Matrix0,60260,9931
INT4, ein Scale pro Ausgabezeile0,17900,2589
INT4, ein Scale pro Gruppe von 1280,13230,1992
NF4, ein Scale pro Block von 640,09520,1205
INT3, ein Scale pro Gruppe von 1280,30440,4123
INT2, ein Scale pro Gruppe von 1280,77900,8076

Die vierte Zeile ist der Kollaps. Ein relativer Fehler von 0,99 auf der schlechtesten Matrix bedeutet, dass die Rekonstruktion im Wesentlichen nichts vom Original behält — die Matrix wurde durch Rauschen ungefähr der richtigen Größenordnung ersetzt. Die Ursache ist im selben Experiment an einer einzelnen Matrix sichtbar:

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

Ein Gewicht von sechstausend liegt jenseits von sechs Standardabweichungen, und das größte liegt 24 entfernt. Mit einem einzigen Scale für die ganze Matrix legt dieses eine Gewicht die Schrittweite für alle 4,3 Millionen fest. Bei 8 Bits gibt es 256 Schritte, und ein typisches Gewicht landet immer noch auf einem sinnvollen. Bei 4 Bits gibt es 16, der äußerste ist für einen Wert reserviert, den fast nichts hat, und die gewöhnlichen Gewichte — also alle — runden auf zwei oder drei verschiedene Level.

Alles nach dieser Zeile ist dieselbe Reparatur in unterschiedlichen Granularitäten: Gib dem Scale ein kleineres Territorium. Pro Ausgabezeile teilt den Fehler durch 3,4; pro Gruppe von 128 aufeinanderfolgenden Gewichten teilt ihn noch einmal. Der Preis ist Buchhaltung — ein 16-Bit-Scale pro Gruppe von 128 sind 4+16/128=4.1254 + 16/128 = 4.125 Bits pro Gewicht statt 4 — und er kauft den größten Teil der Lücke zurück.

NF4 geht es von der anderen Seite an.5 Die Level müssen nicht gleichmäßig verteilt sein. Gewichte innerhalb eines Blocks sind näherungsweise normalverteilt, also wähle die sechzehn Level als Quantile einer Normalverteilung: dicht bei null, wo die Gewichte tatsächlich sind, dünn in den Rändern, wo sie nicht sind. Dieselben vier Bits, dasselbe Block-Scaling, bei kleinerem Block — 4,25 Bits pro Gewicht gegenüber 4,125 bei Gruppe 128 — und der gemessene Fehler fällt von 0,1323 auf 0,0952, 28 % niedriger. Ein Teil davon ist der feinere Block, der Rest die Platzierung der Level dort, wo die Masse liegt; die beiden zu trennen, bräuchte eine dritte Zeile.

Die Floating-Point-Box aus Kapitel 2 endete mit einem Versprechen: dass dieses Kapitel Gewichte auf 8 und 4 Bits quantisieren und eine Handvoll Outlier-Features finden würde, die sich nicht zusammendrücken lassen. Hier sind sie, und sie erklären, warum „einfach die Zahlen runden“ bei Aktivierungen nie funktionieren konnte.

Die Gewichte oben verhielten sich schlecht. Die Aktivierungen spielen in einer anderen Liga. Nimm einen gewöhnlichen 84-token-prompt, zeichne den residual stream in jeder Schicht auf und miss die größte Magnitude, die jede der 896 Dimensionen erreicht:

Schichtgrößtes |h|größtes |h| der Median-DimensionVerhältnisDimensionen über 6x Median
16,190,33918x2
41543,481,550996x34
81571,631,4981049x36
121575,031,5461019x34
161579,601,617977x32
201577,982,361668x24
24204,4410,76019x12

Dimension 62 erreicht 1.579,6, während die Median-Dimension nie über 1,6 hinauskommt. Es ist kein Zufall eines tokens oder einer Schicht: Dieselbe Dimension ist in Schicht 4 da und immer noch in Schicht 20, mit fast demselben Wert. Das sind die Outlier-Features,6 und sie sind systematisch — eine Eigenschaft des trainierten model, nicht des Inputs.

Das Histogramm dieser 896 Maxima pro Dimension in Schicht 16 macht die Form unverkennbar:

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

Neunhundert Dimensionen in einem ordentlichen Haufen unter 8, drei Oktaven lang gar nichts, dann eine einzelne Dimension ganz am Ende. Quantisiere diesen Tensor nun auf INT8 und zähle, was passiert:

Schemarelativer Fehlergenutzte verschiedene Ganzzahl-Level, ganzer Tensor
ein Scale für den ganzen Tensor0,108314 von 256
ein Scale pro token (pro Zeile)0,0433158
ganzer Tensor, 1 Outlier-Dimension in fp32 gehalten0,044248
ganzer Tensor, 4 Outlier-Dimensionen in fp32 gehalten0,027957
ganzer Tensor, 16 Outlier-Dimensionen in fp32 gehalten0,0085102

Vierzehn Level von 256. Der Scale wurde durch 1.579,6 gesetzt, also ist jeder Schritt 12,44 breit, und die typische Aktivierung — Median-Magnitude 0,26, neunundneunzigstes Perzentil 2,51 — hat keinen Platz zum Landen. Pro Dimension ist es noch krasser:

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

Ein Level. Die ganze Dimension, jeder token, auf dieselbe Zahl quantisiert. Acht Bits wurden zugewiesen und ungefähr null wurden genutzt, und das model, das diese Aktivierungen liest, bekommt eine Konstante.

Diese Messung ist die Rechtfertigung für jede Technik, die Menschen tatsächlich verwenden:

Halt die Outlier heraus. LLM.int8()6 zerlegt die Matrixmultiplikation: Die Dimensionen mit extremen Magnituden werden in 16 Bits berechnet, alles andere in INT8, und die Hälften werden summiert. Die Tabelle oben ist die Quittung — vier Dimensionen zu entfernen, senkt den Fehler um fast den Faktor vier. SmoothQuant7 verlagert stattdessen die Schwierigkeit: Teile die Aktivierungen durch einen Faktor pro Kanal und multipliziere die passende Gewichtsspalte damit, wodurch das Produkt unverändert bleibt und der Outlier aus dem Tensor, der ihn nicht aufnehmen kann, in den verschoben wird, der es kann.

Wähle die Rundung, runde nicht einfach. Nichts oben fragt, wofür die Matrix da ist. GPTQ8 quantisiert Spalte für Spalte und passt nach jeder Spalte die verbleibenden Full-Precision-Spalten an, um den bereits verursachten Fehler zu kompensieren — minimiert also den Fehler des Outputs der Schicht auf realen Inputs statt den ihrer Gewichte. AWQ9 stellt fest, dass ein kleiner Teil der Gewichtskanäle weit wichtiger ist als der Rest, findet sie aus Aktivierungsstatistiken und skaliert sie vor dem Quantisieren hoch, damit sie auf feineren Leveln landen. Beide brauchen ein Kalibrierungsset; keines braucht Gradienten.

Details anzeigen

GGUF, und was ein Dateiformat damit zu tun hat.

GGUF ist keine Quantisierungsmethode; es ist der Container, den llama.cpp verwendet, und die Verwirrung in gguf vs gptq-Vergleichen entsteht dadurch, dass beides als dieselbe Art von Ding behandelt wird. GGUF hält Tensoren, tokenizer, Architektur-Metadaten und Chat-Template in einer memory-mappable Datei und trägt eine Familie von Blockschemata in sich — Namen wie Q4_K_M codieren Bits pro Gewicht, Blockgröße und ob manche Tensoren mit höherer Präzision gehalten werden.

Der technische Unterschied, der zählt: GPTQ und AWQ erzeugen Gewichte, die für einen GPU-Kernel optimiert sind, während GGUF-Schemata auf einer CPU mit gemappter statt geladener Datei billig decodiert werden. Deshalb existiert dasselbe nominelle „4-Bit-7B-model“ in beiden Welten mit unterschiedlichen Größen und unterschiedlicher Qualität, und deshalb ist der ehrliche Vergleich nie das Format — es ist die Messung unten, ausgeführt auf deiner eigenen Aufgabe.

Was Quantisierung wirklich kostet, gemessen

Link zum Abschnitt: Was Quantisierung wirklich kostet, gemessen

Fast jeder Artikel über Quantisierung endet beim vorherigen Abschnitt: Er erklärt die Methode, zitiert ein Kompressionsverhältnis und behauptet, die Qualität sei „weitgehend erhalten“. Kapitel 4 handelte davon, sich nicht selbst zu täuschen, also finden wir es heraus.

Dasselbe model, Gewichte mit jedem Schema in place quantisiert, dann drei Messungen: Perplexity auf 2.048 tokens zurückgehaltener englischer Prosa — hier der Entwurf dieses Kurses, weshalb das Repository ein festes gemeinfreies Buch einsetzt und eine Tabelle derselben Form mit anderen Zahlen ausgibt —, eine Batterie aus 16 kurzen Faktfragen mit bekannten Antworten unter greedy decoding, und der Anteil der tokens, bei denen das quantisierte model bei identischem context mit dem Full-Precision-model übereinstimmt.

Schemamittlerer GewichtsfehlerPerplexityFragenbatteriestimmt mit fp32 überein
fp32 (Referenz)0,000023,0813/16100,0 %
INT8 pro Tensor0,040023,5813/16
INT8 pro Zeile0,010022,9613/1698,6 %
INT4 pro Tensor0,6026365.416.0000/16
INT4 pro Zeile0,179046,186/1658,3 %
INT4 Gruppe 1280,132331,0810/1671,5 %
NF4 Block 640,095224,5511/1684,7 %
INT3 Gruppe 1280,3044213,090/165,6 %
INT2 Gruppe 1280,779026.325.4360/160,0 %

Vier Dinge in dieser Tabelle sollte man klar aussprechen.

Richtig gemachtes INT8 ist kostenlos. INT8 pro Zeile erreicht 22,96 gegenüber 23,08 der Referenz — eine Lücke von einem Teil in zweihundert, also Rauschen, und sollte als „identisch“ gelesen werden. In welche Richtung das Rauschen zeigt, ist nicht stabil: Auf dem gemeinfreien Korpus des Repository kommen dieselben zwei Schemata auf 22,24 gegenüber 22,18, halb so weit auseinander und in die andere Richtung. Es stimmt mit dem Full-Precision-model bei 142 von 144 generierten tokens überein. Ein Viertel des Speichers gegenüber der fp32-Referenz, die Hälfte gegenüber dem fp16, das du tatsächlich deployen würdest, und keine messbaren Kosten. Nachlässig gemachtes INT8 ist ebenfalls fast kostenlos: Ein Scale pro Matrix kostet 0,5 Perplexity-Punkte und keine Antworten in der Batterie. Acht Bits sind verzeihend genug, dass Granularität kaum zählt; genau deshalb verallgemeinern Leute von INT8 auf INT4 und laufen gegen die Wand.

INT4 mit einem Scale pro Tensor zerstört das model. Perplexity 365 Millionen: nicht degradiert, vernichtet. Danach ist Granularität das ganze Spiel — pro Tensor 365.416.000, pro Zeile 46,18, pro Gruppe von 128 31,08, NF4 24,55. Dieselben vier Bits pro Gewicht, ein Faktor von fünfzehn Millionen zwischen schlechtestem und bestem Fall.

Perplexity ist ein grobes Instrument und die Batterie ein noch gröberes. Zwischen NF4 und Gruppe-128-INT4 beträgt die Perplexity-Lücke 6,5 Punkte und die Batterie unterscheidet sich um eine Frage — und das Konfidenzintervall aus Kapitel 4 sagt, dass eine Frage von sechzehn überhaupt nichts unterscheidet. Es gibt eine schärfere Demonstration als das Intervall: Schalte die stock repetition penalty des model aus, was greedy decoding tatsächlich bedeutet, und diese beiden Zeilen tauschen die Plätze. Eine Frage von sechzehn ist kein kleiner Effekt, es ist kein Effekt. Auch die Warnung aus Kapitel 8 gilt: Perplexity ist nur zwischen Modellen vergleichbar, die einen tokenizer teilen, also kann eine Zahl aus dem Write-up eines anderen nicht mit deiner verglichen werden.

Die Übereinstimmungsspalte ist die schärfste der drei, und fast kostenlos: Lass das Full-Precision-model greedy laufen und frage dann das quantisierte model an jeder Position, was es bei demselben Präfix gewählt hätte. Sie hat 144 unabhängige Beobachtungen statt 16, braucht keine Ground Truth und degradiert glatt, wo die Batterie in Sprüngen degradiert. Sie ist außerdem genau die Größe, die der nächste Abschnitt braucht.

Das ist das Versprechen, das Kapitel 1 über dieses Kapitel gemacht hat, pünktlich eingelöst: Die Mathematik sagt, dass ein 4-Bit-model möglich ist, und die Technik entscheidet, ob es brauchbar ist.

Kapitel 12 hat es angekündigt und die Rechnung hierher verschoben.

Die Idee kommt direkt aus der Prefill/Decode-Trennung. Eine vorgeschlagene Sequenz von γ\gamma tokens zu verifizieren, kostet einen forward pass über γ\gamma Positionen — ein Matrix-Matrix-Produkt, kaum teurer als der Durchlauf über eine. Also:

Ein kleines, billiges model generiert γ\gamma Kandidaten-tokens autoregressiv.

Das große model führt einen forward pass über alle γ\gamma Kandidaten auf einmal aus und erzeugt, was es an jeder Position gesagt hätte.

Behalte das längste Präfix, bei dem beide übereinstimmen, plus den token, den das große model bei der ersten Abweichung kostenlos liefert. Verwirf den Rest und beginne von vorn.

Die Ausgabeverteilung bleibt unverändert. Bei greedy decoding ist das offensichtlich — ein token wird nur akzeptiert, wenn das Target ihn erzeugt hätte. Beim Sampling braucht es eine modifizierte Akzeptanzregel, und Leviathan et al. beweisen, dass die resultierende Verteilung exakt die des Targets ist.10 Das ist die zweite exakte Optimierung in diesem Kapitel.

Alles hängt daher an der Akzeptanzrate α\alpha, die messbar ist — sie ist die Übereinstimmungsspalte oben, weshalb sie dort berechnet wurde. Mit jedem quantisierten model als Draft für das Full-Precision-Target, über 144 generierte Positionen:

Draft-modelAkzeptanzlängster akzeptierter Lauferwartete tokens pro Target-Pass, γ=4\gamma = 4
fp32 (das Target selbst)100,0 %485,00
INT8 pro Zeile98,6 %484,86
NF4 Block 6484,7 %203,69
INT4 Gruppe 12871,5 %132,85
INT4 pro Zeile58,3 %72,24
INT3 Gruppe 1285,6 %21,06
INT2 Gruppe 1280,0 %01,00

Die erwarteten akzeptierten tokens pro Verifikationspass bei Draft-Länge γ\gamma sind

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

und der Netto-Speedup teilt das durch die eigenen Kosten des Drafts, einen Bruchteil cc des Targets pro token:

Akzeptanzc=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

Der fette Eintrag ist der, den man sich merken sollte: speculative decoding kann Generation langsamer machen. Bei 30 % Akzeptanz mit einem Draft, der ein Fünftel des Targets kostet, bezahlst du für fünf forward passes und behältst 1,4 tokens. Die letzte Spalte ist die andere Falle — ein längerer Draft hilft nur bei hoher Akzeptanz, weil der Schwanz eines γ\gamma-token-Vorschlags fast nie erreicht wird. Bei 90 % Akzeptanz ist γ=8\gamma = 8 3,40x wert und bei 30 % 0,79x: dieselbe Konfiguration, Gewinn oder Verlust je nach einer Zahl, die auf deinem Traffic gemessen wird.

Distillation, und was ein Soft Label trägt

Link zum Abschnitt: Distillation, und was ein Soft Label trägt

Quantisierung schrumpft ein model, indem sie dieselbe Funktion in weniger Bits speichert. Distillation schrumpft es, indem ein kleineres model trainiert wird, ein größeres zu imitieren11 — eine Idee, die Deep Learning um fast ein Jahrzehnt vorausgeht.12

Der subtile Teil ist, woraus der Student lernt. Nicht aus der richtigen Antwort: Darauf hätte er direkt trainiert werden können. Was der Teacher hinzufügt, ist die gesamte Verteilung. Frag das model, was auf eine Phrase folgt, und schau über den argmax hinaus:

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

Das Hard Label sagt jug und sonst nichts. Das Soft Label sagt jug, und außerdem, dass cup fast genauso gut war, bowl plausibel, und large — ein Adjektiv, eine völlig andere grammatische Fortsetzung — immer noch im Rennen. Das ist das ursprüngliche Argument: Das ist eine 7, aber sie sieht ziemlich nach einer 1 aus, und die Ähnlichkeit ist Information, die das Hard Label wegwirft.

Deshalb verwendet Distillation auch eine Temperatur. Die logits vor dem softmax durch TT zu teilen, flacht die Verteilung ab und erhöht das relative Gewicht der Nachzügler: Bei dieser Phrase fällt das Verhältnis zwischen dem Top-token und dem dritten von 2,24 bei T=1T = 1 auf 1,50 bei T=2T = 2 — die Quadratwurzel des ersten, was genau das Teilen der logits durch zwei mit einem Verhältnis macht. Gleiche Reihenfolge, mehr attention des Loss auf die knappen Verlierer. Der gradient des Studenten trägt die Unsicherheit des Teachers und nicht nur sein Urteil.

Alles in diesem Kapitel ist jetzt eine Summe:

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}}

wobei TT die gesamten residenten tokens über alle gleichzeitigen Anfragen hinweg sind. Angewendet: Die 7B- und 70B-Zeilen nehmen 8 key-value heads der Dimension 128 an, die 13B-Zeile volle multi-head attention mit 40 heads, wie diese model-Generationen gebaut wurden — und man sieht es.

8 GB

modelprecisionGewichtefrei nach Overheadcontext tokens, die passen
7Bfp1613,0 GBpasst nicht
7Bint86,5 GBpasst nicht
7Bint4 (g128)3,4 GB3,1 GB25.710
13Bint4 (g128)6,2 GB0,3 GB337
70Bint4 (g128)33,6 GBpasst nicht

16 GB

modelprecisionGewichtefrei nach Overheadcontext tokens, die passen
7Bfp1613,0 GB1,5 GB11.972
7Bint86,5 GB8,0 GB65.378
7Bint4 (g128)3,4 GB11,1 GB91.246
13Bint812,1 GB2,4 GB3.136
13Bint4 (g128)6,2 GB8,3 GB10.822

24 GB

modelprecisionGewichtefrei nach Overheadcontext tokens, die passen
7Bfp1613,0 GB9,5 GB77.508
7Bint86,5 GB16,0 GB130.914
7Bint4 (g128)3,4 GB19,1 GB156.782
13Bint812,1 GB10,4 GB13.622
13Bint4 (g128)6,2 GB16,3 GB21.308
70Bint4 (g128)33,6 GBpasst nicht

Sieh dir die 13B-Zeile in der 8-GB-Tabelle an. Die Gewichte passen — 6,2 GB von 8 —, also „läuft ein 13B-model auf einer 8-GB-Karte“, wie man üblicherweise sagt. Es hat 337 tokens context, was kein Gespräch ist, sondern kaum ein prompt. „Passt es?“ ist die falsche Frage. Die richtige lautet: „Mit wie viel context, und für wie viele Nutzer gleichzeitig?“

Sieh dir auch die zwei 16-GB-int8-Zeilen an. Das 7B bekommt 65.378 tokens und das 13B bekommt 3.136 — ein zwanzigfacher Unterschied durch 5,6 GB zusätzliche Gewichte, weil das 13B hier multi-head attention hat und sein Cache 800 KB pro token kostet gegenüber 128 KB beim 7B. Zwei models ähnlicher Größe, eines unbrauchbar für langen context, aus einem Grund, der in keiner model card als Überschrift erscheint.

Vor dreizehn Kapiteln war das ein Perzeptron mit zwei Gewichten und einem Bias. Jetzt ist es ein transformer, der entworfen, trainiert, aligned wurde, gelernt hat, Rechenleistung für schwierige Fragen auszugeben, und zu gemessenen Kosten pro token bereitgestellt wird — ohne dass darin noch eine ungeöffnete Box übrig wäre.

Das endet hier, und es endet mit Absicht.

Kapitel 14 beginnt mit dem model an einem anderen Ort. Nicht in deinem Prozess, nicht in deinem Speicher, nicht in einer Variablen, die du ausgeben kannst: auf einer Maschine, die du nicht administrierst, hinter einem API key, einem Port und einer Rechnung. Alles, was hier gemessen wurde, passiert weiterhin — Prefill läuft immer noch vor dem ersten token, der Cache wächst immer noch mit dem Gespräch, der batch, in dem du steckst, gehört immer noch jemand anderem und entscheidet immer noch deine Latenz —, aber von nun an beobachtest du es durch einen Strom von Server-Sent Events, einen finish_reason und ein HTTP 429 mit einem Retry-After-Header. Die Fragen ändern sich mit dem Blickwinkel: nicht wie wird dieser gradient berechnet, sondern warum hat sich meine Rechnung verdreifacht. Auch die Sprache ändert sich, und Kapitel 14 erklärt diese Regel, statt sie zu verkünden — bis hierhin hielt der Code Gewichte, Gradienten, logits und tokenizer-Bytes; von dort an hält er eine Verbindung, einen Retry, eine Cancellation und angesammelten Zustand. Die dreizehn Kapitel hinter dir werden beim Übergang nicht verworfen. Sie sind die Beschreibung dessen, was auf der anderen Seite des Ports läuft.


Zwei Auslassungen sind bewusst. FlashAttention (Dao et al., arXiv:2205.14135) ist keine andere attention — es berechnet dieselbe Funktion, indem es die Operation tiled, sodass die n×nn \times n score-Matrix nie in den Speicher geschrieben wird, weshalb die 67 MB in der zweiten Tabelle dieses Kapitels in der Praxis kleiner sind, als die Arithmetik nahelegt. Und die Kernel selbst werden delegiert: Lecture 10 von Stanfords CS336 behandelt Inference-Systeme in einer Tiefe, die dieses Kapitel nicht versucht, und das llama.cpp-Repository sowie die GGUF-Spezifikation sind die Primärquellen für die CPU-Seite.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Das Paper ist im Wesentlichen ein Speicherbandbreiten-Argument und liest sich auch so.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Enthält das Uptraining-Rezept, das einen bestehenden multi-head Checkpoint konvertiert, weshalb GQA sich so schnell verbreitet hat.

  3. Yu, G.-I., Jeong, J. S., Kim, G.-W., Kim, S. and Chun, B.-G. Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. Führt Scheduling auf Iterationsebene — continuous batching — und selektives Batching ein.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. Das Paper, auf dem vLLM basiert; §3 ist die Betriebssystem-Analogie vollständig ausgeführt.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 wird in §3 definiert; die sechzehn Level-Werte, die in der Messung oben verwendet wurden, sind die, die dieses Paper herleitet.

  6. Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). Die Outlier-Feature-Analyse in §4 ist die Quelle des oben gemessenen Phänomens, einschließlich der Erkenntnis, dass Outlier systematisch mit Skalierung entstehen. 2

  7. Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J. and Han, S. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438 (2022).

  8. Frantar, E., Ashkboos, S., Hoefler, T. and 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. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). Theorem 1 ist der Beweis, dass die Ausgabeverteilung unverändert bleibt; Chen et al. (arXiv:2302.01318) veröffentlichten dieselbe Idee unabhängig.

  11. Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Die Temperatur und das „dark knowledge“-Argument.

  12. Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, neun Jahre früher, für Ensembles statt transformers.

Bereit, LIA die Wahl zu überlassen?

Bau mit jedem KI-Modell an einem Ort — starte heute kostenlos.