Inference goedkoop maken: KV cache, batching en quantization
Hetzelfde model antwoordt in 8,8 en 78,9 seconden identiek. Daarna INT4, op drie manieren gemeten in plaats van beweerd.
Op deze pagina
Hetzelfde model, op dezelfde machine, dat dezelfde vraag beantwoordt met dezelfde 48 tokens. De twee outputs zijn identiek, token voor token — gecontroleerd, niet aangenomen.
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)Eén argument veranderde: use_cache=False. Niets aan het model, de prompt, de sampling of de rekenkunde is anders, en de tweede run is niet nauwkeuriger voor al die moeite. Hij is negen keer trager voor niets.
Dat is de vorm van dit hoofdstuk. Alles erin — de cache, de batch, de gequantizeerde weights — is een poging om niet te betalen voor werk dat het antwoord niet verandert, of om uit te vinden wat een goedkoper antwoord kost. Hoofdstuk 10 zette de prijslijst voor training neer. Dit is de prijslijst voor de kant waarvoor je voor altijd betaalt: een gedeployed model besteedt grofweg FLOPs voor elke token die het uitstuurt, bij elke request, voor de rest van zijn leven.
Waar de tijd van de tweede run bleef
Link naar de sectie: Waar de tijd van de tweede run bleefOm een token te genereren neemt een decoder-only transformer de hele sequence tot dan toe, voert die door elke layer en leest de kansverdeling af op de laatste positie. Daarna voegt hij de gekozen token toe en doet hij het opnieuw. Die beschrijving klopt, en dat is wat de trage run doet.
Het is ook enorm verspillend, en de reden is het causal mask uit Hoofdstuk 9. De key- en value-vectoren van positie 7 worden berekend uit de input van positie 7 en de posities ervoor. Wanneer positie 8 arriveert, kan positie 7 die niet zien — dat is wat causal betekent — dus de key en value van positie 7 zijn exact dezelfde getallen als eerder. De trage run berekent ze toch opnieuw, bij elke stap.
Sla ze dus op. Die opslag is de key-value cache, de meest bepalende optimalisatie in language-model serving:
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)Kijk wat binnen de loop aan het model wordt gevoerd: nxt, één token. Niet de sequence. De query van de nieuwe token attends against elke cached key, en de cached keys zouden nooit veranderen. Dit is geen benadering — de identieke-outputcheck hierboven is precies het punt. De cache ruilt geen kwaliteit voor snelheid; hij verwijdert redundante rekenkunde.
Om de schaal helder te zien, haal je de transformer weg en time je één attention head met , één generation-stap op beide manieren berekend:
| tokens in context | alles herberekenen | met een cache | verhouding | score matrix |
|---|---|---|---|---|
| 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 |
De rechterkolom is de oorzaak. Herberekenen bouwt bij elke stap de volledige attention matrix — de uit het asymptotische-notatievak van Hoofdstuk 9, betaald per token. Met de cache bouw je in plaats daarvan een rij: bij 4.096 tokens, 67 MB aan scores tegenover 16 KB.
Multiply-accumulates tellen in plaats van milliseconden haalt de machine uit het argument. Om tokens vanuit een koude start te genereren:
| gegenereerde tokens | met een cache | herberekenen | verhouding |
|---|---|---|---|
| 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 |
Per stap is de cached versie lineair in de context en de uncached versie kwadratisch; opgeteld over een generation, tegenover , waarbij de verhouding onbeperkt groeit. Het negenvoudige verschil in de opening werd gemeten over 48 tokens — korter dan de eerste rij van die tabel.
De cache verandert ook wat er in memory moet staan. Op een laptop-GPU van 8 GB die 256 tokens in fp16 genereert, met de piek van de allocator minus de resident weights:
| piek working memory | |
|---|---|
| met een cache | 21,8 MB |
| herberekenen | 181,7 MB |
8,3 keer meer memory, besteed om dezelfde tokens trager te produceren. Dit is de belofte uit Hoofdstuk 5, maar dan vanuit een onverwachte richting: daar moest reverse-mode autodiff elke intermediate in leven houden voor de backward pass, en activations domineerden training memory. Bij inference is er geen backward pass en hoeft daar niets voor bewaard te blijven — dus wat memory in plaats daarvan domineert is de cache, en dat is een bewuste keuze in plaats van een onvermijdelijke kost.
Prefill en decode zijn twee verschillende machines
Link naar de sectie: Prefill en decode zijn twee verschillende machinesKijk opnieuw naar de snelle run: de eerste token gedroeg zich anders dan de andere zevenenveertig.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepDe prompt kostte 25,6 ms per token en elke gegenereerde token kostte 166 ms. Zelfde model, zelfde hardware, zelfde weights, een zesvoudig verschil per token — en het gaat de kant op die de meeste mensen niet verwachten. De prompt is het goedkope deel. Generation valt uiteen in twee fasen met werkelijk verschillende fysica:
Prefill
Link naar de sectie: PrefillEén forward pass over de hele prompt. Elke token wordt parallel verwerkt, dus elke weight matrix wordt één keer uit memory geladen en vermenigvuldigd met een matrix van honderden token-vectoren — een matrix-matrixproduct, met veel rekenwerk per verplaatste byte, precies waar een GPU voor gebouwd is. Prefill is compute-bound, en de kost is grofweg lineair in de promptlengte.
Eén forward pass per token, batch van één en sequence van één. Elke weight matrix wordt nog steeds volledig uit memory geladen, en vermenigvuldigd met een enkele vector — een matrix-vectorproduct, met bijna geen rekenwerk per verplaatste byte. Decode is memory-bandwidth-bound, en de kost per token hangt nauwelijks af van de lengte van de context.
Beide helften zijn meetbaar. Prefill, één pass over tokens:
| prompt tokens | seconden | ms per 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, één token tegen een cache van :
| cached tokens | ms voor één token |
|---|---|
| 16 | 110,05 |
| 64 | 97,57 |
| 256 | 108,53 |
| 1024 | 103,86 |
Lees de tweede tabel twee keer. Van 16 tokens context naar 1.024 gaan — vierenzestig keer meer geschiedenis om over te attend — veranderde de kost van een stap met niets meetbaars. Attention tegen de cache is echt werk, maar het valt in het niet bij de vaste kost van een half miljard weights door de memory bus slepen om één vector te produceren. Die vaste kost is de reden voor alles in de volgende sectie.
Deze twee fasen zijn de oorsprong van de twee getallen die elk serving-systeem rapporteert. Time to first token is in wezen prefill, en groeit met de prompt, waardoor een lang gesprek traag aanvoelt bij de start. Tokens per second is , en is grofweg constant, waardoor het antwoord daarna gelijkmatig binnenstroomt. Een chat die traag start en daarna soepel streamt is geen renderingtruc. Het zijn deze twee tabellen.
De cache is ook de rekening
Link naar de sectie: De cache is ook de rekeningDe cache ruilt rekenkunde voor memory, en de memory die hij wil is niet klein. Voor elke token in de context houdt elke layer één key-vector en één value-vector per key-value head vast:
De 2 is voor keys en values; al het andere is de architectuur. Voor het model dat in dit hoofdstuk wordt gemeten — 24 layers, 14 query heads, 2 key-value heads, head dimension 64 — is dat in fp16 bytes per token.
Formules in dit veld hebben de gewoonte er een factor twee naast te zitten, dus controleer het tegen de allocator in plaats van het te geloven:
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, en het blijft exact over elke geteste shape:
| batch | context | gemeten cache | voorspeld | piek working memory |
|---|---|---|---|---|
| 1 | 512 | 6,0 MB | 6,0 MB | 15,4 MB |
| 1 | 16.384 | 192,0 MB | 192,0 MB | 207,3 MB |
| 1 | 65.536 | 768,0 MB | 768,0 MB | 793,7 MB |
| 8 | 4.096 | 384,0 MB | 384,0 MB | 401,5 MB |
| 32 | 2.048 | 768,0 MB | 768,0 MB | 794,2 MB |
| 64 | 1.024 | 768,0 MB | 768,0 MB | 797,0 MB |
| 128 | 512 | 768,0 MB | 768,0 MB | 816,4 MB |
De laatste drie rijen verdienen een tweede blik. Tweeëndertig gebruikers met elk 2.048 tokens, vierenzestig met 1.024, honderdtwintig plus acht met 512 — de cache is telkens 768 MB, omdat alle drie 65.536 tokens bevatten. De cache hangt alleen af van het totale aantal resident tokens, niet van hoe ze over gebruikers verdeeld zijn. Dat feit is de basis van de sectie over batching.
Waar MQA en GQA vandaan komen
Link naar de sectie: Waar MQA en GQA vandaan komenHoofdstuk 9 introduceerde multi-query en grouped-query attention en stelde de reden uit tot dit hoofdstuk. De reden is die formule, en specifiek de erin.
Standaard multi-head attention geeft elke query head zijn eigen key- en value-heads. Het model hier heeft 14 query heads; met volledige multi-head attention zou zijn cache bytes per token zijn — 84 KB in plaats van 12 KB, exact zeven keer meer, de verhouding tussen query heads en key-value heads.
Multi-query attention1 voert dit tot de limiet: alle query heads delen één key-value head. Grouped-query attention2 is het compromis dat won — een handvol key-value heads, elk gedeeld door een groep query heads — omdat het kwaliteitsverlies van MQA echt was en dat van GQA niet. Geen van beide koopt rekenwerk. Ze bestaan om die formule door een integer te delen, en ze verspreidden zich door de industrie zodra lange contexts de cache tot bindende constraint maakten.
Dat gebeurt snel. Voor een model uit de 7B-klasse met 32 layers en 8 key-value heads met dimension 128 is de cache in fp16 128 KB per token:
| context tokens | één gebruiker | 8 gebruikers | 64 gebruikers |
|---|---|---|---|
| 4.000 | 0,49 GB | 3,91 GB | 31,2 GB |
| 32.000 | 3,91 GB | 31,25 GB | 250,0 GB |
| 128.000 | 15,62 GB | 125,00 GB | 1.000,0 GB |
| 1.000.000 | 122,07 GB | 976,56 GB | 7.812,5 GB |
De eigen weights van dat model zijn 13,0 GB in fp16, het getal in de tabel aan het einde van dit hoofdstuk. Dus bij een context van 128.000 tokens is de cache van één gebruiker groter dan het model. Dit is de rekenkunde die Hoofdstuk 16 in geld omzet, en het is waarom een lang gesprek niet alleen traag is — het bezet een vast stuk van een machine zolang de request leeft.
Batching: het getal dat omhooggaat en het getal dat omlaaggaat
Link naar de sectie: Batching: het getal dat omhooggaat en het getal dat omlaaggaatDecode is memory-bound: de weights worden door de bus gesleept om één token te produceren, en de rekeneenheden staan idle. Stop dus meer werk in dezelfde stap. Run meerdere requests tegelijk, en de weights, één keer gelezen, bedienen ze allemaal. Gemeten op hetzelfde model, waarbij elke request een 64-token cache vasthoudt en één token decode:
| batch | latency per stap | throughput | latency 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 |
Lees de twee rechterkolommen naast elkaar, want ze zijn het hele punt. Van één request naar zestien gaan vermenigvuldigt throughput met 6,0 en vermenigvuldigt de wachttijd voor elke individuele request met 2,67. De batch maakte de server beter en elke gebruiker slechter.
Dat is geen bug die je kunt weg-tunen; het is de trade-off zelf, met aan elke kant een naam. Latency is wat iemand die op een antwoord wacht ervaart. Throughput is waardoor de factuur wordt gedeeld. Geen enkele instelling verbetert beide.
Let ook op waar het stopt. Van 16 naar 32 stijgt throughput met 9 % terwijl latency bijna verdubbelt: de stap is niet langer memory-bound maar compute-bound, en voorbij die knie koopt de batch niets. Elke deployment heeft zo'n knie; de locatie ervan moet je op de jouwe meten, maar het bestaan ervan niet.
Static batching verspilt het grootste deel van wat het wint
Link naar de sectie: Static batching verspilt het grootste deel van wat het wintDe naïeve manier om te batchen is requests verzamelen, ze samen runnen en teruggeven wanneer ze allemaal klaar zijn. Maar ze zijn niet tegelijk klaar: sommige antwoorden zijn twintig tokens en andere vijfhonderd. Een vaste batch runt tot het langste lid klaar is, en elke voltooide request blijft tot dan zijn slot bezetten en padding bijdragen.
Neem 64 requests met een realistische scheefheid in outputlengtes — mediaan 18 tokens, langste 231, 1.874 in totaal — en simuleer beide policies met de gemeten per-step-kost voor acht slots:
| policy | wall clock | throughput | gemiddelde latency per request | verspilde slot-steps |
|---|---|---|---|---|
| static batches van 8 | 176,9 s | 10,6 tok/s | 83,2 s | 3.214 |
| continuous, 8 slots | 109,0 s | 17,2 tok/s | 8,1 s | 0 |
Throughput verbetert met 1,6x. Gemiddelde latency verbetert met meer dan tien keer, omdat bij static batching een request die in vier stappen klaar was nog steeds wacht op een buur van 231 tokens voordat iemand er iets van hoort.
Continuous batching3 is de oplossing, en die is zo simpel als hij klinkt: de batch is geen groep maar een set slots, en een slot dat vrijkomt laat bij de eerstvolgende stap de volgende queued request toe. De scheduler werkt op de granulariteit van één token in plaats van één request. Elke serving-stack in productie doet dit nu.
Het heeft een tweede helft, en dat is de cache. Slots die komen en gaan laten cache memory gefragmenteerd achter, en voor elk slot zijn maximale mogelijke context reserveren verspilt het grootste deel van de reservering. PagedAttention4 leent het antwoord van operating systems: sla de cache op in blokken van vaste grootte met een block table per sequence, zodat de cache van een sequence fysiek verspreid kan liggen terwijl hij logisch contiguous blijft — waardoor twee sequences met een gedeelde prefix ook de blocks kunnen delen die die prefix bevatten. Daarop is vLLM gebouwd, en daarom is een serving engine een memory allocator met een transformer eraan vast.
Quantization, en het eerste dat misgaat
Link naar de sectie: Quantization, en het eerste dat misgaatDe andere helft van de rekening zijn de weights zelf. Een half miljard parameters op vier bytes per stuk is 1,98 GB; op twee bytes, 0,99 GB; op één byte, 0,49 GB. Minder bits per weight maakt het model kleiner op disk, kleiner in memory, en — omdat decode bandwidth-bound is — maakt elke stap sneller, omdat er minder bytes te verplaatsen zijn.
Het eenvoudigste schema is symmetrische absolute-maximum quantization, en het past in drie regels:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedKies een scale zodat de grootste weight op de grootste integer mapped, deel, rond af, sla de integers en de scale op. Reconstrueer door terug te vermenigvuldigen. Er is niets slims aan, en het werkt — totdat het niet werkt.
Gemeten op de echte weights van het model: alle 168 projection matrices, 357,8 miljoen parameters, relatieve fout :
| schema | gemiddelde relatieve fout | slechtste matrix |
|---|---|---|
| INT8, één scale voor de hele matrix | 0,0400 | 0,1487 |
| INT8, één scale per outputrij | 0,0100 | 0,0149 |
| INT4, één scale voor de hele matrix | 0,6026 | 0,9931 |
| INT4, één scale per outputrij | 0,1790 | 0,2589 |
| INT4, één scale per groep van 128 | 0,1323 | 0,1992 |
| NF4, één scale per block van 64 | 0,0952 | 0,1205 |
| INT3, één scale per groep van 128 | 0,3044 | 0,4123 |
| INT2, één scale per groep van 128 | 0,7790 | 0,8076 |
De vierde rij is de instorting. Een relatieve fout van 0,99 op de slechtste matrix betekent dat de reconstructie in wezen niets van het origineel behoudt — de matrix is vervangen door noise van ongeveer de juiste grootte. De oorzaak is zichtbaar in hetzelfde experiment op één matrix:
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 %)Eén weight op zesduizend ligt voorbij zes standaarddeviaties, en de grootste ligt 24 verder. Met één scale voor de hele matrix bepaalt die ene weight de stapgrootte voor alle 4,3 miljoen weights. Bij 8 bits zijn er 256 stappen en landt de typische weight nog steeds op een betekenisvolle. Bij 4 bits zijn er 16, de buitenste gereserveerd voor een waarde die bijna niets heeft, en de gewone weights — dus allemaal — ronden af naar twee of drie verschillende niveaus.
Alles na die rij is dezelfde reparatie op verschillende granulariteiten: geef de scale een kleiner gebied. Per outputrij deelt de fout door 3,4; per groep van 128 opeenvolgende weights deelt hem opnieuw. De kost is bookkeeping — een 16-bit scale per groep van 128 is bits per weight in plaats van 4 — en het wint het grootste deel van het gat terug.
NF4 benadert het van de andere kant.5 De niveaus hoeven niet gelijkmatig verdeeld te zijn. Weights binnen een block zijn ongeveer normaal verdeeld, dus kies de zestien niveaus als de kwantielen van een normale verdeling: dicht bij nul waar de weights werkelijk zijn, dun in de tails waar ze niet zijn. Zelfde vier bits, zelfde block scaling, met een kleiner block — 4,25 bits per weight tegenover 4,125 bij group-128 — en de gemeten fout daalt van 0,1323 naar 0,0952, 28 % lager. Een deel daarvan is het fijnere block en de rest is de niveaus plaatsen waar de massa zit; de twee scheiden zou een derde rij vereisen.
De outlier features
Link naar de sectie: De outlier featuresHet floating-pointvak van Hoofdstuk 2 eindigde met een belofte: dat dit hoofdstuk weights zou quantizen naar 8 en 4 bits en een handvol outlier features zou vinden die weigeren samengedrukt te worden. Hier zijn ze, en ze verklaren waarom "gewoon de getallen afronden" nooit zou werken op activations.
De weights hierboven gedroegen zich slecht. De activations spelen in een andere klasse. Neem een gewone prompt van 84 tokens, vang de residual stream bij elke layer, en meet de grootste magnitude die elk van de 896 dimensions bereikt:
| layer | grootste |h| | grootste |h| van mediane dimension | verhouding | dimensions boven 6x de mediaan |
|---|---|---|---|---|
| 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 |
Dimension 62 bereikt 1.579,6 terwijl de mediane dimension nooit boven 1,6 komt. Het is geen toeval van één token of één layer: dezelfde dimension is er bij layer 4 en nog steeds bij layer 20, met bijna dezelfde waarde. Dit zijn de outlier features,6 en ze zijn systematisch — een eigenschap van het getrainde model, niet van de input.
Het histogram van die 896 maxima per dimension bij layer 16 maakt de vorm onmiskenbaar:
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 | # 1Negenhonderd dimensions in een keurige stapel onder 8, helemaal niets gedurende drie octaven, en dan één dimension alleen aan het verre uiteinde. Quantize die tensor nu naar INT8 en tel wat er gebeurt:
| schema | relatieve fout | gebruikte verschillende integerniveaus, hele tensor |
|---|---|---|
| één scale voor de hele tensor | 0,1083 | 14 van 256 |
| één scale per token (per rij) | 0,0433 | 158 |
| hele tensor, 1 outlier dimension behouden in fp32 | 0,0442 | 48 |
| hele tensor, 4 outlier dimensions behouden in fp32 | 0,0279 | 57 |
| hele tensor, 16 outlier dimensions behouden in fp32 | 0,0085 | 102 |
Veertien niveaus van de 256. De scale werd ingesteld door 1.579,6, dus elke stap is 12,44 breed, en de typische activation — mediane magnitude 0,26, negenennegentigste percentiel 2,51 — heeft nergens om te landen. Per dimension is het nog scherper:
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 levelsEén niveau. De hele dimension, elke token, gequantized naar hetzelfde getal. Acht bits werden toegewezen en ruwweg nul werden gebruikt, en het model dat die activations leest krijgt een constante aangereikt.
Die meting is de rechtvaardiging voor elke techniek die mensen daadwerkelijk gebruiken:
Houd de outliers erbuiten. LLM.int8()6 splitst de matrix multiply op: de dimensions met extreme magnitudes worden in 16 bits berekend, al het andere in INT8, en de helften worden opgeteld. De tabel hierboven is het bewijs — vier dimensions verwijderen verlaagt de fout met bijna een factor vier. SmoothQuant7 verplaatst in plaats daarvan de moeilijkheid: deel de activations door een per-channel factor en vermenigvuldig de bijbehorende weight column ermee, waardoor het product onveranderd blijft en de outlier uit de tensor wordt verplaatst die hem niet kan absorberen naar de tensor die dat wel kan.
Kies de afronding, rond niet zomaar af. Niets hierboven vraagt waar de matrix voor is. GPTQ8 quantize kolom voor kolom en past na elke kolom de resterende full-precision columns aan om te compenseren voor de fout die al is gemaakt — waarbij de fout van de output van de layer op echte inputs wordt geminimaliseerd, niet die van zijn weights. AWQ9 merkt op dat een kleine fractie weight channels veel belangrijker is dan de rest, vindt ze uit activation-statistieken, en schaalt ze op vóór quantization zodat ze op fijnere niveaus landen. Beide hebben een calibratieset nodig; geen van beide heeft gradients nodig.
Details tonen
GGUF, en wat een bestandsformaat hiermee te maken heeft.
GGUF is geen quantization-methode; het is de container die llama.cpp gebruikt, en de verwarring in gguf vs gptq-vergelijkingen komt doordat de twee als hetzelfde soort ding worden behandeld. GGUF bevat tensors, tokenizer, architectuurmetadata en chat template in één memory-mappable bestand, en draagt daarbinnen een familie van block schema's — namen als Q4_K_M coderen bits per weight, block size, en of sommige tensors op hogere precision worden gehouden.
Het engineeringverschil dat ertoe doet: GPTQ en AWQ produceren weights die geoptimaliseerd zijn voor een GPU kernel, terwijl GGUF-schema's goedkoop worden decoded op een CPU met het bestand gemapped in plaats van geladen. Daarom bestaat hetzelfde nominale "4-bit 7B-model" in beide werelden met verschillende groottes en verschillende kwaliteit, en daarom is de eerlijke vergelijking nooit het format — het is de meting hieronder, uitgevoerd op je eigen taak.
Wat quantization werkelijk kost, gemeten
Link naar de sectie: Wat quantization werkelijk kost, gemetenBijna elk artikel over quantization stopt bij de vorige sectie: het legt de methode uit, citeert een compressieverhouding, en beweert dat kwaliteit "grotendeels behouden" blijft. Hoofdstuk 4 ging over jezelf niet voor de gek houden, dus laten we het uitzoeken.
Hetzelfde model, weights in-place gequantized met elk schema, daarna drie metingen: perplexity op 2.048 tokens held-out Engelse proza — hier de conceptversie van deze cursus, en daarom vervangt de repository die door een vast public-domain boek en print hij een tabel met dezelfde vorm maar andere getallen — een batterij van 16 korte feitelijke vragen met bekende antwoorden onder greedy decoding, en de fractie tokens waarop het gequantizeerde model overeenkomt met het full-precision model bij identieke context.
| schema | gemiddelde weight-fout | perplexity | vragenbatterij | eens met fp32 |
|---|---|---|---|---|
| fp32 (referentie) | 0,0000 | 23,08 | 13/16 | 100,0 % |
| INT8 per tensor | 0,0400 | 23,58 | 13/16 | — |
| INT8 per rij | 0,0100 | 22,96 | 13/16 | 98,6 % |
| INT4 per tensor | 0,6026 | 365.416.000 | 0/16 | — |
| INT4 per rij | 0,1790 | 46,18 | 6/16 | 58,3 % |
| INT4 group 128 | 0,1323 | 31,08 | 10/16 | 71,5 % |
| NF4 block 64 | 0,0952 | 24,55 | 11/16 | 84,7 % |
| INT3 group 128 | 0,3044 | 213,09 | 0/16 | 5,6 % |
| INT2 group 128 | 0,7790 | 26.325.436 | 0/16 | 0,0 % |
Vier dingen in die tabel zijn het waard om gewoon te zeggen.
INT8 goed gedaan is gratis. Per-row INT8 scoort 22,96 tegenover 23,08 van de referentie — een gat van één deel op tweehonderd, wat noise is en gelezen moet worden als "identiek". Welke kant de noise op wijst is niet stabiel: op het public-domain corpus van de repository komen dezelfde twee schema's uit op 22,24 tegenover 22,18: de helft van die afstand, en de andere kant op. Het is het eens met het full-precision model op 142 van 144 gegenereerde tokens. Een kwart van de memory tegenover de fp32-referentie, de helft tegenover de fp16 die je daadwerkelijk zou deployen, en geen detecteerbare kost. INT8 onzorgvuldig gedaan is ook bijna gratis: één scale per matrix kost 0,5 perplexitypunten en geen antwoorden in de batterij. Acht bits is vergevingsgezind genoeg dat granulariteit nauwelijks uitmaakt, en dat is precies waarom mensen van INT8 naar INT4 generaliseren en pijn krijgen.
INT4 met één scale per tensor vernietigt het model. Perplexity 365 miljoen: niet gedegradeerd, weggevaagd. Granulariteit is daarna het hele spel — per-tensor 365.416.000, per-row 46,18, per-group-of-128 31,08, NF4 24,55. Dezelfde vier bits per weight, een factor vijftien miljoen tussen slechtst en best.
Perplexity is een grof instrument en de batterij een nog grover. Tussen NF4 en group-128 INT4 is het perplexityverschil 6,5 punten en verschilt de batterij met één vraag — en het betrouwbaarheidsinterval van Hoofdstuk 4 zegt dat één vraag op zestien helemaal niets onderscheidt. Er is een scherpere demonstratie dan het interval: run dezelfde batterij met de standaard repetition penalty van het model uitgeschakeld, wat greedy decoding werkelijk betekent, en die twee rijen wisselen van plek. Eén vraag op zestien is geen klein effect, het is geen effect. De waarschuwing uit Hoofdstuk 8 geldt ook: perplexity is alleen vergelijkbaar tussen modellen die een tokenizer delen, dus een getal uit andermans write-up kan niet met het jouwe worden vergeleken.
De agreement-kolom is de scherpste van de drie, en bijna gratis: run het full-precision model greedily, en vraag dan het gequantizeerde model op elke positie wat het zou hebben gekozen met dezelfde prefix. Het heeft 144 onafhankelijke observaties in plaats van 16, heeft geen ground truth nodig, en degradeert soepel waar de batterij in sprongen degradeert. Het is ook exact de grootheid die de volgende sectie nodig heeft.
Dit is de belofte die Hoofdstuk 1 over dit hoofdstuk maakte, precies op schema: de wiskunde zegt dat een 4-bit model mogelijk is, en de engineering beslist of het bruikbaar is.
Speculative decoding
Link naar de sectie: Speculative decodingHoofdstuk 12 kondigde dit aan en liet de rekening hier achter.
Het idee komt rechtstreeks uit de prefill/decode-splitsing. Een voorgestelde sequence van tokens verifiëren kost één forward pass over posities — een matrix-matrixproduct, nauwelijks duurder dan de pass over één. Dus:
Een klein, goedkoop model genereert candidate tokens autoregressief.
Het grote model runt één forward pass over alle candidates tegelijk, en produceert wat het op elke positie zou hebben gezegd.
Behoud de langste prefix waarop de twee overeenkomen, plus de token die het grote model gratis levert bij de eerste mismatch. Gooi de rest weg en begin opnieuw.
De output distribution is onveranderd. Met greedy decoding is dat duidelijk — een token wordt alleen geaccepteerd als de target hem zou hebben geproduceerd. Met sampling vereist het een aangepaste acceptatieregel, en Leviathan et al. bewijzen dat de resulterende distribution exact die van de target is.10 Dit is de tweede exacte optimalisatie in dit hoofdstuk.
Alles hangt daarom af van de acceptance rate , die meetbaar is — het is de agreement-kolom hierboven, en daarom werd die daar berekend. Elk gequantizeerd model als draft gebruiken voor de full-precision target, over 144 generated positions:
| draft model | acceptance | langste geaccepteerde run | verwachte tokens per target pass, |
|---|---|---|---|
| fp32 (de target zelf) | 100,0 % | 48 | 5,00 |
| INT8 per rij | 98,6 % | 48 | 4,86 |
| NF4 block 64 | 84,7 % | 20 | 3,69 |
| INT4 group 128 | 71,5 % | 13 | 2,85 |
| INT4 per rij | 58,3 % | 7 | 2,24 |
| INT3 group 128 | 5,6 % | 2 | 1,06 |
| INT2 group 128 | 0,0 % | 0 | 1,00 |
Het verwachte aantal tokens dat per verification pass wordt geaccepteerd, bij draftlengte , is
en de netto speedup deelt dat door de eigen kost van de draft, een fractie van de target per token:
| acceptance | , | , | , | , |
|---|---|---|---|---|
| 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 |
De vette entry is degene om te onthouden: speculative decoding kan generation trager maken. Bij 30 % acceptance met een draft die een vijfde van de target kost, betaal je voor vijf forward passes en behoud je 1,4 tokens. De laatste kolom is de andere valkuil — een langere draft helpt alleen wanneer acceptance hoog is, omdat de tail van een -token guess bijna nooit wordt bereikt. Bij 90 % acceptance is 3,40x waard en bij 30 % is hij 0,79x waard: dezelfde configuratie, winst of verlies afhankelijk van een getal dat op je traffic wordt gemeten.
Distillation, en wat een soft label draagt
Link naar de sectie: Distillation, en wat een soft label draagtQuantization krimpt een model door dezelfde functie in minder bits op te slaan. Distillation krimpt het door een kleiner model te trainen om een groter model te imiteren11 — een idee dat bijna tien jaar ouder is dan deep learning.12
Het subtiele deel is waarvan de student leert. Niet het juiste antwoord: daarop had hij direct getraind kunnen worden. Wat de teacher toevoegt is de hele distribution. Vraag het model wat volgt op een phrase en kijk voorbij de argmax:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360Het hard label zegt jug en niets anders. Het soft label zegt jug, en ook dat cup bijna net zo goed was, bowl plausibel, en large — een bijvoeglijk naamwoord, een volledig andere grammaticale voortzetting — nog steeds live. Dat is het oorspronkelijke argument: dit is een 7, maar hij lijkt behoorlijk op een 1, en die gelijkenis is informatie die het hard label weggooit.
Daarom gebruikt distillation ook een temperature. De logits delen door vóór de softmax vlakt de distribution af en verhoogt het relatieve gewicht van de runners-up: bij deze phrase daalt de verhouding tussen de top token en de derde van 2,24 bij naar 1,50 bij — de wortel van de eerste, en dat is wat het delen van de logits door twee met een verhouding doet. Zelfde ordering, meer attention van de loss op de near misses. De gradient van de student draagt de onzekerheid van de teacher en niet alleen zijn oordeel.
Wat past in 8, 16 en 24 GB
Link naar de sectie: Wat past in 8, 16 en 24 GBAlles in dit hoofdstuk is nu één som:
waar de totale resident tokens over alle concurrent requests is. Toegepast: de 7B- en 70B-rijen gaan uit van 8 key-value heads met dimension 128, de 13B-rij van volledige multi-head attention met 40 heads, en zo werden die generaties modellen gebouwd — en dat is te zien.
8 GB
| model | precision | weights | vrij na overhead | context tokens die passen |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | past niet | — |
| 7B | int8 | 6,5 GB | past niet | — |
| 7B | int4 (g128) | 3,4 GB | 3,1 GB | 25.710 |
| 13B | int4 (g128) | 6,2 GB | 0,3 GB | 337 |
| 70B | int4 (g128) | 33,6 GB | past niet | — |
16 GB
| model | precision | weights | vrij na overhead | context tokens die passen |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | 1,5 GB | 11.972 |
| 7B | int8 | 6,5 GB | 8,0 GB | 65.378 |
| 7B | int4 (g128) | 3,4 GB | 11,1 GB | 91.246 |
| 13B | int8 | 12,1 GB | 2,4 GB | 3.136 |
| 13B | int4 (g128) | 6,2 GB | 8,3 GB | 10.822 |
24 GB
| model | precision | weights | vrij na overhead | context tokens die passen |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | 9,5 GB | 77.508 |
| 7B | int8 | 6,5 GB | 16,0 GB | 130.914 |
| 7B | int4 (g128) | 3,4 GB | 19,1 GB | 156.782 |
| 13B | int8 | 12,1 GB | 10,4 GB | 13.622 |
| 13B | int4 (g128) | 6,2 GB | 16,3 GB | 21.308 |
| 70B | int4 (g128) | 33,6 GB | past niet | — |
Kijk naar de 13B-rij in de 8 GB-tabel. De weights passen — 6,2 GB van 8 — dus volgens de gebruikelijke manier van praten "draait een 13B-model op een 8 GB-kaart". Het heeft 337 tokens context, wat geen gesprek is maar amper een prompt. "Past het" is de verkeerde vraag. De juiste is "met hoeveel context, en voor hoeveel gebruikers tegelijk".
Kijk ook naar de twee int8-rijen van 16 GB. De 7B krijgt 65.378 tokens en de 13B krijgt 3.136 — een twintigvoudig verschil door 5,6 GB extra weights, omdat de 13B hier multi-head attention heeft en zijn cache 800 KB per token kost tegenover 128 KB bij de 7B. Twee modellen van vergelijkbare grootte, één onbruikbaar voor long context, om een reden die in de headline van geen enkele model card voorkomt.
Waar dit hierna naartoe gaat
Link naar de sectie: Waar dit hierna naartoe gaatDertien hoofdstukken geleden was dit een perceptron met twee weights en een bias. Nu is het een transformer die is ontworpen, getraind, aligned, geleerd om compute te besteden aan moeilijke vragen, en served tegen een gemeten kost per token — zonder dat er nog een doos in zit die ongeopend is.
Dat eindigt hier, en het eindigt bewust.
Hoofdstuk 14 begint met het model ergens anders. Niet in je process, niet in je memory, niet in een variable die je kunt printen: op een machine die je niet beheert, achter een API key, een port en een rekening. Alles wat hier gemeten is gebeurt nog steeds — de prefill draait nog steeds vóór de eerste token, de cache groeit nog steeds met het gesprek, de batch waarin je zit is nog steeds van iemand anders en beslist nog steeds je latency — maar vanaf nu observeer je het via een stream van Server-Sent Events, een finish_reason, en een HTTP 429 met een Retry-After header. De vragen veranderen met het standpunt: niet hoe wordt deze gradient berekend maar waarom is mijn factuur verdrievoudigd. De taal verandert ook, en Hoofdstuk 14 legt die regel uit in plaats van hem aan te kondigen — tot hier hield de code weights, gradients, logits en tokenizer-bytes vast; vanaf daar houdt hij een connection, een retry, een cancellation en accumulated state vast. De dertien hoofdstukken achter je worden door de overgang niet weggegooid. Ze zijn de beschrijving van wat er aan de andere kant van de port draait.
Bronnen en methode
Link naar de sectie: Bronnen en methodeTwee weglatingen zijn bewust. FlashAttention (Dao et al., arXiv:2205.14135) is geen andere attention — het berekent dezelfde functie door de operatie te tilen zodat de score matrix nooit naar memory wordt geschreven, en daarom is de 67 MB in de tweede tabel van dit hoofdstuk in de praktijk kleiner dan de rekenkunde suggereert. En de kernels zelf zijn gedelegeerd: college 10 van Stanford's CS336 behandelt inference systems in de diepte die dit niet probeert, en de llama.cpp repository en de GGUF-specificatie zijn de primaire bronnen voor de CPU-kant.
Referenties
Link naar de sectie: Referenties-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). De paper is grotendeels een memory-bandwidth-argument, en leest ook zo. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Bevat het uptraining-recept dat een bestaande multi-head checkpoint converteert, en daarom verspreidde GQA zich zo snel. ↩
-
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. Introduceert iteration-level scheduling — continuous batching — en selective batching. ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. De paper waarop vLLM is gebouwd; §3 is de operating-systems-analogie volledig uitgewerkt. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 wordt gedefinieerd in §3; de zestien level values die in de meting hierboven worden gebruikt zijn degene die deze paper afleidt. ↩
-
Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). De outlier-feature-analyse in §4 is de bron van het hierboven gemeten fenomeen, inclusief de bevinding dat outliers systematisch op schaal ontstaan. ↩ ↩2
-
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). ↩
-
Frantar, E., Ashkboos, S., Hoefler, T. and 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. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). Theorem 1 is het bewijs dat de output distribution onveranderd is; Chen et al. (arXiv:2302.01318) publiceerden hetzelfde idee onafhankelijk. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). De temperature en het "dark knowledge"-argument. ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, negen jaar eerder, voor ensembles in plaats van transformers. ↩