Spring til indhold
13/30Kapitel 13 af 30

Gør inference billig: KV cache, batching og kvantisering

Samme model svarer på 8,8 og 78,9 sekunder med byte-identisk output. INT4 målt på tre måder — ikke bare påstået.

På denne side

Den samme model, på den samme maskine, svarer på det samme spørgsmål med de samme 48 tokens. De to outputs er identiske token for token — kontrolleret, ikke antaget.

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)

Ét argument ændrede sig: use_cache=False. Intet ved modellen, prompt, sampling eller aritmetikken er anderledes, og den anden kørsel er ikke mere præcis for besværet. Den er ni gange langsommere uden grund.

Det er formen på dette kapitel. Alt i det — cache, batch, de kvantiserede vægte — er et forsøg på at holde op med at betale for arbejde, der ikke ændrer svaret, eller på at finde ud af, hvad et billigere svar koster. Kapitel 10 fastlagde prislisten for træning. Dette er prislisten for den side, du betaler for for altid: en deployed model bruger omtrent 2N2N FLOPs for hver token, den udsender, på hver request, resten af dens levetid.

For at generere en token tager en decoder-only transformer hele sekvensen indtil nu, kører den gennem hvert lag og læser sandsynlighedsfordelingen fra den sidste position. Derefter tilføjer den den valgte token og gør det igen. Den beskrivelse er korrekt, og det er det, den langsomme kørsel gør.

Den er også enormt spildsom, og årsagen er causal mask fra Kapitel 9. Position 7's key- og value-vektorer beregnes ud fra position 7's input og positionerne før den. Når position 8 ankommer, kan position 7 ikke se den — det er det, causal betyder — så position 7's key og value er præcis de samme tal som før. Den langsomme kørsel beregner dem alligevel igen ved hvert trin.

Så gem dem. Det lager er key-value cache, den enkeltstående mest betydningsfulde optimering i serving af sprogmodeller:

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)

Se på, hvad der sendes ind i modellen inde i løkken: nxt, én token. Ikke sekvensen. Den nye tokens query attends mod hver cached key, og de cached keys ville aldrig ændre sig. Dette er ikke en approximation — den identiske-output-kontrol ovenfor er pointen. Cache bytter ikke kvalitet for hastighed; den sletter redundant aritmetik.

For at se skaleringen rent kan vi fjerne transformer og time et enkelt attention head med d=64d = 64, hvor ét generation-trin beregnes på begge måder:

tokens i contextgenberegn altmed cacheforholdscore-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

Kolonnen til højre er årsagen. Genberegning bygger hele n×nn \times n attention-matricen ved hvert trin — O(n2)O(n^2) fra boksen om asymptotisk notation i Kapitel 9, betalt én gang per token. Med cache bygger du en 1×n1 \times n række i stedet: ved 4.096 tokens, 67 MB scores mod 16 KB.

At tælle multiply-accumulates i stedet for millisekunder fjerner maskinen fra argumentet. For at generere TT tokens fra en cold start:

genererede tokensmed cachegenberegningforhold
1282,6 M192,0 M73x
51223,1 M7,36 G318x
2048293,7 M392,6 G1.336x

Per trin er den cached version lineær i context, og den uncached er kvadratisk; summeret over en generation, O(T2)O(T^2) mod O(T3)O(T^3), hvor forholdet vokser uden grænse. Den ni-dobbelte forskel i åbningen blev målt over 48 tokens — kortere end tabellens første række.

Cache ændrer også, hvad der skal være i hukommelsen. På en laptop-GPU med 8 GB, der genererer 256 tokens i fp16, når vi tager allocatorens peak og trækker de residente vægte fra:

peak arbejdshukommelse
med cache21,8 MB
genberegning181,7 MB

8,3 gange mere hukommelse, brugt på at producere de samme tokens langsommere. Det er løftet fra Kapitel 5, ankommet fra en uventet retning: dér måtte reverse-mode autodiff holde hver intermediate i live til backward pass, og activations dominerede træningshukommelsen. Ved inference er der intet backward pass og intet at bevare til det — så det, der i stedet dominerer hukommelsen, er cache, og den er et bevidst valg snarere end en uundgåelig omkostning.

Prefill og decode er to forskellige maskiner

Link til afsnittet: Prefill og decode er to forskellige maskiner

Se igen på den hurtige kørsel: dens første token opførte sig anderledes end de andre syvogfyrre.

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

Prompt kostede 25,6 ms per token, og hver genereret token kostede 166 ms. Samme model, samme hardware, samme vægte, en seksdobbel forskel per token — og den går den vej, de fleste ikke forventer. Prompt er den billige del. Generation deles i to faser med reelt forskellig fysik:

Ét forward pass over hele prompt. Hver token behandles parallelt, så hver vægtmatrix læses fra hukommelsen én gang og multipliceres med en matrix af hundredvis af token-vektorer — et matrix-matrix-produkt, med masser af aritmetik per flyttet byte, hvilket er det, en GPU er bygget til. Prefill er compute-bound, og omkostningen er omtrent lineær i prompt-længden.

Ét forward pass per token, batch på én og sekvens på én. Hver vægtmatrix læses stadig fuldt ud fra hukommelsen og multipliceres med en enkelt vektor — et matrix-vektor-produkt, med næsten ingen aritmetik per flyttet byte. Decode er memory-bandwidth-bound, og omkostningen per token afhænger næsten ikke af context-længden.

Begge halvdele kan måles. Prefill, ét pass over PP tokens:

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

Decode, én token mod en cache på CC:

cached tokensms for én token
16110,05
6497,57
256108,53
1024103,86

Læs den anden tabel to gange. At gå fra 16 tokens context til 1.024 — fireogtres gange mere historik at attend over — ændrede omkostningen ved et trin med intet målbart. Attention mod cache er reelt arbejde, men det drukner i den faste omkostning ved at trække en halv milliard vægte gennem hukommelsesbussen for at producere én vektor. Den faste omkostning er grunden til alt i næste afsnit.

Disse to faser er oprindelsen til de to tal, ethvert serving-system rapporterer. Time to first token er i praksis prefill, og den vokser med prompt, hvilket er grunden til, at en lang samtale føles langsom at starte. Tokens per second er 1/decode step1/\text{decode step}, og den er omtrent konstant, hvilket er grunden til, at svaret derefter flyder jævnt. En chat, der starter langsomt og derefter streamer glat, er ikke et rendering-trick. Det er disse to tabeller.

Cache bytter aritmetik for hukommelse, og den hukommelse, den vil have, er ikke lille. For hver token i context holder hvert lag én key-vektor og én value-vektor per 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}

2-tallet er for keys og values; alt andet er arkitekturen. For modellen målt gennem dette kapitel — 24 lag, 14 query heads, 2 key-value heads, head-dimension 64 — er det i fp16 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 bytes per token.

Formler på dette felt har en vane med at ramme ved siden af med en faktor to, så tjek det mod allocatoren i stedet for at tro på det:

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

Præcist, og det forbliver præcist på tværs af hver afprøvet shape:

batchcontextmålt cacheforudsagtpeak arbejdshukommelse
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

De sidste tre rækker fortjener et ekstra blik. Toogtredive brugere med 2.048 tokens hver, fireogtres med 1.024, hundrede og otteogtyve med 512 — cache er 768 MB i alle tilfælde, fordi alle tre holder 65.536 tokens. Cache afhænger kun af det samlede antal residente tokens, ikke af hvordan de er fordelt mellem brugere. Det faktum er fundamentet for batching-afsnittet.

Kapitel 9 introducerede multi-query og grouped-query attention og udskød forklaringen til dette kapitel. Årsagen er den formel, og specifikt HkvH_{kv} i den.

Standard multi-head attention giver hvert query head sine egne key- og value-heads. Modellen her har 14 query heads; med fuld multi-head attention ville dens cache være 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 bytes per token — 84 KB i stedet for 12 KB, præcis syv gange mere, forholdet mellem query heads og key-value heads.

Multi-query attention1 tager dette til grænsen: alle query heads deler ét enkelt key-value head. Grouped-query attention2 er kompromiset, der vandt — en håndfuld key-value heads, hver delt af en gruppe query heads — fordi MQA's kvalitetstab var reelt, og GQA's ikke er det. Ingen af dem køber aritmetik. De findes for at dividere den formel med et heltal, og de spredte sig i branchen i samme øjeblik, lange contexts gjorde cache til den bindende begrænsning.

Det gør den hurtigt. For en 7B-klasse model med 32 lag og 8 key-value heads med dimension 128 er cache 128 KB per token i fp16:

context tokensén bruger8 brugere64 brugere
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

Den models egne vægte er 13,0 GB i fp16, tallet i tabellen sidst i dette kapitel. Så ved en context på 128.000 tokens er én brugers cache større end modellen. Det er den aritmetik, Kapitel 16 omsætter til penge, og det er derfor, en lang samtale ikke bare er langsom — den optager en fast del af en maskine, så længe requesten lever.

Batching: tallet der går op, og tallet der går ned

Link til afsnittet: Batching: tallet der går op, og tallet der går ned

Decode er memory-bound: vægtene trækkes gennem bussen for at producere én token, og de aritmetiske enheder står stille. Så læg mere arbejde i samme trin. Kør flere requests på én gang, og vægtene, læst én gang, betjener dem alle. Målt på samme model, hvor hver request holder en cache på 64 tokens og decoder én token:

batchlatency per trinthroughputlatency 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

Læs de to kolonner til højre op mod hinanden, for de er hele pointen. At gå fra én request til seksten multiplicerer throughput med 6,0 og multiplicerer ventetiden for enhver individuel request med 2,67. Batch gjorde serveren bedre og hver bruger værre.

Det er ikke en bug, der kan tunes væk; det er selve byttet, og det har et navn på hver side. Latency er det, en person der venter på et svar, oplever. Throughput er det, fakturaen divideres med. Ingen indstilling forbedrer begge dele.

Bemærk også, hvor det stopper. Fra 16 til 32 stiger throughput 9 %, mens latency næsten fordobles: trinnet er holdt op med at være memory-bound og er blevet compute-bound, og efter det knæk køber batch intet. Enhver deployment har sådan et knæk; hvor det ligger, skal måles på din, men dets eksistens gør ikke.

Statisk batching spilder det meste af det, den vinder

Link til afsnittet: Statisk batching spilder det meste af det, den vinder

Den naive måde at batche på er at samle BB requests, køre dem sammen og returnere, når alle er færdige. Men de bliver ikke færdige samtidig: nogle svar er tyve tokens, andre fem hundrede. En fast batch kører, indtil dens længste medlem er færdigt, og hver færdig request bliver ved med at optage sin plads og bidrage med padding indtil da.

Tag 64 requests med en realistisk skævhed i outputlængder — median 18 tokens, længste 231, 1.874 i alt — og simuler begge politikker ved den målte per-trin-omkostning for otte pladser:

politikvægurthroughputgennemsnitlig latency per requestspildte slot-trin
statiske batches på 8176,9 s10,6 tok/s83,2 s3.214
kontinuerlig, 8 slots109,0 s17,2 tok/s8,1 s0

Throughput forbedres med 1,6x. Gennemsnitlig latency forbedres med mere end ti gange, fordi en request, der blev færdig på fire trin under statisk batching, stadig venter på en nabo med 231 tokens, før nogen hører om den.

Continuous batching3 er løsningen, og den er så enkel, som den lyder: batch er ikke en gruppe, men et sæt slots, og et slot der frigøres, optager den næste request i køen på det allernæste trin. Scheduleren arbejder på granulariteten af én token i stedet for én request. Hver serving-stack i produktion gør dette nu.

Den har en anden halvdel, som er cache. Slots, der kommer og går, efterlader cache-hukommelse fragmenteret, og at reservere hvert slots maksimalt mulige context spilder det meste af reservationen. PagedAttention4 låner svaret fra operativsystemer: gem cache i blokke af fast størrelse med en bloktabel per sekvens, så en sekvens' cache kan være fysisk spredt, mens den forbliver logisk sammenhængende — hvilket også lader to sekvenser med et fælles prefix dele blokkene, der holder det. Det er det, vLLM er bygget på, og derfor er en serving engine en memory allocator med en transformer sat på.

Kvantisering, og det første der går galt

Link til afsnittet: Kvantisering, og det første der går galt

Den anden halvdel af regningen er vægtene selv. En halv milliard parametre ved fire bytes hver er 1,98 GB; ved to bytes, 0,99 GB; ved én byte, 0,49 GB. Færre bits per vægt gør modellen mindre på disk, gør den mindre i hukommelsen og — fordi decode er bandwidth-bound — gør hvert trin hurtigere, da der er færre bytes at flytte.

Det enkleste skema er symmetrisk absolute-maximum-kvantisering, og det passer på tre linjer:

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

Vælg en scale, så den største vægt mappes til det største heltal, divider, afrund, gem heltallene og scale. Rekonstruér ved at multiplicere tilbage. Der er intet smart ved det, og det virker — lige indtil det ikke gør.

Målt på modellens reelle vægte: alle 168 projektionsmatricer, 357,8 millioner parametre, relativ fejl WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

skemagennemsnitlig relativ fejlværste matrix
INT8, én scale for hele matricen0,04000,1487
INT8, én scale per outputrække0,01000,0149
INT4, én scale for hele matricen0,60260,9931
INT4, én scale per outputrække0,17900,2589
INT4, én scale per gruppe på 1280,13230,1992
NF4, én scale per blok på 640,09520,1205
INT3, én scale per gruppe på 1280,30440,4123
INT2, én scale per gruppe på 1280,77900,8076

Den fjerde række er kollapset. En relativ fejl på 0,99 på den værste matrix betyder, at rekonstruktionen stort set ikke bevarer noget af originalen — matricen er blevet erstattet af støj med omtrent den rigtige størrelsesorden. Årsagen er synlig i samme eksperiment på en enkelt matrix:

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

Én vægt ud af seks tusind ligger mere end seks standardafvigelser ude, og den største er 24 ude. Med en enkelt scale for hele matricen sætter den ene vægt trinstørrelsen for alle 4,3 millioner af dem. Ved 8 bits er der 256 trin, og den typiske vægt lander stadig på et meningsfuldt trin. Ved 4 bits er der 16, de yderste reserveret til en værdi, næsten intet har, og de almindelige vægte — hvilket er dem alle — afrundes til to eller tre forskellige niveauer.

Alt efter den række er samme reparation ved forskellige granulariteter: giv scale et mindre område. Per outputrække dividerer fejlen med 3,4; per gruppe på 128 fortløbende vægte dividerer den igen. Omkostningen er bogføring — en 16-bit scale per gruppe på 128 er 4+16/128=4.1254 + 16/128 = 4.125 bits per vægt i stedet for 4 — og det køber det meste af afstanden tilbage.

NF4 angriber det fra den anden side.5 Niveauerne behøver ikke være jævnt fordelt. Vægte inden for en blok er omtrent normalfordelte, så vælg de seksten niveauer som kvantilerne af en normalfordeling: tæt omkring nul, hvor vægtene faktisk er, spredt i halerne, hvor de ikke er. Samme fire bits, samme blok-scaling, ved en mindre blok — 4,25 bits per vægt mod group-128's 4,125 — og den målte fejl falder fra 0,1323 til 0,0952, 28 % lavere. En del af det er den finere blok, og resten er at placere niveauerne dér, hvor massen er; at adskille de to ville kræve en tredje række.

Kapitel 2's floating-point-boks sluttede med et løfte: at dette kapitel ville kvantisere vægte til 8 og 4 bits og finde en håndfuld outlier features, der nægtede at blive presset sammen. Her er de, og de forklarer, hvorfor "bare afrund tallene" aldrig ville virke på activations.

Vægtene ovenfor opførte sig dårligt. Activations er i en anden liga. Tag en almindelig prompt på 84 tokens, fang residual stream ved hvert lag, og mål den største magnitude, hver af de 896 dimensioner når:

lagstørste |h|mediandimensionens største |h|forholddimensioner over 6x medianen
16,190,33918x2
41543,481,550996x34
81571,631,4981049x36
121575,031,5461019x34
161579,601,617977x32
201577,982,361668x24
24204,4410,76019x12

Dimension 62 når 1.579,6, mens mediandimensionen aldrig overstiger 1,6. Det er ikke et tilfælde fra én token eller ét lag: den samme dimension er der ved lag 4 og stadig ved lag 20, med næsten samme værdi. Det er outlier features,6 og de er systematiske — en egenskab ved den trænede model, ikke ved inputtet.

Histogrammet over de 896 maksimummer per dimension ved lag 16 gør formen umiskendelig:

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

Ni hundrede dimensioner i en pæn bunke under 8, slet ingenting i tre oktaver, og så én dimension alene helt ude i enden. Kvantiser nu den tensor til INT8, og tæl hvad der sker:

skemarelativ fejlforskellige heltalsniveauer brugt, hele tensoren
én scale for hele tensoren0,108314 af 256
én scale per token (per række)0,0433158
hel tensor, 1 outlier-dimension holdt i fp320,044248
hel tensor, 4 outlier-dimensioner holdt i fp320,027957
hel tensor, 16 outlier-dimensioner holdt i fp320,0085102

Fjorten niveauer ud af 256. Scale blev sat af 1.579,6, så hvert trin er 12,44 bredt, og den typiske activation — medianmagnitude 0,26, nioghalvfemsende percentil 2,51 — har ingen steder at lande. Per dimension er det skarpere:

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

Ét niveau. Hele dimensionen, hver token, kvantiseret til det samme tal. Otte bits blev afsat, og omtrent nul blev brugt, og modellen, der læser disse activations, får udleveret en konstant.

Den måling er begrundelsen for hver teknik, folk faktisk bruger:

Hold outliers udenfor. LLM.int8()6 dekomponerer matrixmultiplikationen: dimensionerne med ekstreme magnitudes beregnes i 16 bits, alt andet i INT8, og halvdelene summeres. Tabellen ovenfor er kvitteringen — at fjerne fire dimensioner reducerer fejlen med en faktor på næsten fire. SmoothQuant7 flytter i stedet vanskeligheden: divider activations med en per-channel faktor og multiplicér den matchende vægtkolonne med den, hvilket lader produktet være uændret og flytter outlier fra den tensor, der ikke kan absorbere den, til den der kan.

Vælg afrundingen, afrund ikke bare. Intet ovenfor spørger, hvad matricen er til for. GPTQ8 kvantiserer kolonne for kolonne og justerer efter hver de resterende full-precision kolonner for at kompensere for den fejl, der allerede er begået — minimerer fejlen i lagets output på reelle inputs snarere end i dets vægte. AWQ9 bemærker, at en lille brøkdel af vægtkanaler betyder langt mere end resten, finder dem fra activation-statistikker og scaler dem op før kvantisering, så de lander på finere niveauer. Begge kræver et kalibreringssæt; ingen af dem kræver gradients.

Vis detaljer

GGUF, og hvad et filformat har med noget af dette at gøre.

GGUF er ikke en kvantiseringsmetode; det er den container, llama.cpp bruger, og forvirringen i gguf vs gptq-sammenligninger kommer af at behandle de to som samme slags ting. GGUF holder tensors, tokenizer, arkitekturmetadata og chat template i én memory-mappable fil og bærer en familie af blokskemaer indeni — navne som Q4_K_M koder bits per vægt, blokstørrelse, og om nogle tensors holdes ved højere precision.

Den engineering-forskel, der betyder noget: GPTQ og AWQ producerer vægte optimeret til en GPU-kernel, mens GGUF's skemaer dekodes billigt på en CPU med filen mapped snarere end indlæst. Det er derfor, den samme nominelle "4-bit 7B model" findes i begge verdener med forskellige størrelser og forskellig kvalitet, og derfor er den ærlige sammenligning aldrig formatet — det er målingen nedenfor, kørt på din egen opgave.

Næsten hver artikel om kvantisering stopper ved forrige afsnit: den forklarer metoden, citerer et kompressionsforhold og hævder, at kvaliteten er "largely preserved". Kapitel 4 handlede om ikke at narre sig selv, så lad os finde ud af det.

Samme model, vægte kvantiseret in place med hvert skema, derefter tre målinger: perplexity på 2.048 tokens holdt-ude engelsk prosa — her, udkastet til dette kursus, hvilket er grunden til, at repositoriet erstatter det med en fast public-domain bog og printer en tabel med samme form og andre tal — et batteri af 16 korte faktuelle spørgsmål med kendte svar under greedy decoding, og fraktionen af tokens hvor den kvantiserede model er enig med full-precision modellen givet identisk context.

skemagennemsnitlig vægtfejlperplexityspørgebatterienig med fp32
fp32 (reference)0,000023,0813/16100,0 %
INT8 per tensor0,040023,5813/16
INT8 per række0,010022,9613/1698,6 %
INT4 per tensor0,6026365.416.0000/16
INT4 per række0,179046,186/1658,3 %
INT4 group 1280,132331,0810/1671,5 %
NF4 block 640,095224,5511/1684,7 %
INT3 group 1280,3044213,090/165,6 %
INT2 group 1280,779026.325.4360/160,0 %

Fire ting i den tabel er værd at sige direkte.

INT8 gjort ordentligt er gratis. Per-række INT8 scorer 22,96 mod referencens 23,08 — en forskel på én del ud af to hundrede, hvilket er støj og bør læses som "identisk". Hvilken vej støjen peger, er ikke stabil: på repositoriets public-domain corpus kommer de samme to skemaer ud 22,24 mod 22,18: halvdelen af afstanden, og den anden vej. Den er enig med full-precision modellen på 142 af 144 genererede tokens. En fjerdedel af hukommelsen mod fp32-referencen, halvdelen mod den fp16, du faktisk ville deploye, og ingen detekterbar omkostning. INT8 gjort skødesløst er næsten også gratis: én scale per matrix koster 0,5 perplexity-point og ingen batterisvar. Otte bits er tilgivende nok til, at granularitet knap betyder noget, hvilket er præcis derfor, folk generaliserer fra INT8 til INT4 og kommer til skade.

INT4 med én scale per tensor ødelægger modellen. Perplexity 365 millioner: ikke degraderet, udslettet. Granularitet er derefter hele spillet — per-tensor 365.416.000, per-række 46,18, per-gruppe-på-128 31,08, NF4 24,55. Samme fire bits per vægt, en faktor på femten millioner mellem værst og bedst.

Perplexity er et groft instrument, og batteriet er endnu grovere. Mellem NF4 og group-128 INT4 er perplexity-afstanden 6,5 point, og batteriet afviger med ét spørgsmål — og Kapitel 4's konfidensinterval siger, at ét spørgsmål ud af seksten ikke skelner noget som helst. Der findes en skarpere demonstration end intervallet: kør samme batteri med modellens standard repetition penalty slået fra, hvilket er det, greedy decoding faktisk betyder, og de to rækker bytter plads. Ét spørgsmål ud af seksten er ikke en lille effekt, det er ingen effekt. Kapitel 8's advarsel gælder også: perplexity kan kun sammenlignes mellem modeller, der deler tokenizer, så et tal fra en andens gennemgang kan ikke sammenlignes med dit.

Enighedskolonnen er den skarpeste af de tre, og næsten gratis: kør full-precision modellen greedily, og spørg derefter den kvantiserede, ved hver position, hvad den ville have valgt givet samme prefix. Den har 144 uafhængige observationer i stedet for 16, kræver ingen ground truth og degraderer glat, hvor batteriet degraderer i hop. Det er også præcis den størrelse, næste afsnit har brug for.

Det er løftet, Kapitel 1 gav om dette kapitel, ankommet til tiden: matematikken siger, at en 4-bit model er mulig, og engineering afgør, om den er brugbar.

Kapitel 12 annoncerede dette og efterlod regningen her.

Ideen kommer direkte fra prefill/decode-opdelingen. At verificere en foreslået sekvens på γ\gamma tokens koster ét forward pass over γ\gamma positioner — et matrix-matrix-produkt, knap dyrere end passet over én. Så:

En lille, billig model genererer γ\gamma kandidat-tokens autoregressivt.

Den store model kører ét forward pass over alle γ\gamma kandidater på én gang og producerer, hvad den ville have sagt ved hver position.

Behold det længste prefix, hvor de to er enige, plus den token den store model leverer gratis ved den første uenighed. Kassér resten og start igen.

Outputfordelingen er uændret. Med greedy decoding er det indlysende — en token accepteres kun, hvis target ville have produceret den. Med sampling kræver det en modificeret acceptregel, og Leviathan et al. beviser, at den resulterende fordeling er præcis target'ens.10 Dette er den anden eksakte optimering i dette kapitel.

Alt afhænger derfor af acceptance rate α\alpha, som kan måles — det er enighedskolonnen ovenfor, og derfor blev den beregnet der. Ved at bruge hver kvantiseret model som draft for full-precision target, over 144 genererede positioner:

draft modelacceptancelængste accepterede runforventede tokens per target pass, γ=4\gamma = 4
fp32 (target selv)100,0 %485,00
INT8 per række98,6 %484,86
NF4 block 6484,7 %203,69
INT4 group 12871,5 %132,85
INT4 per række58,3 %72,24
INT3 group 1285,6 %21,06
INT2 group 1280,0 %01,00

De forventede tokens accepteret per verification pass, ved draft-længde γ\gamma, er

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

og den netto speedup dividerer det med draftens egen omkostning, en brøkdel cc af target per token:

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

Den fede post er den, du skal huske: speculative decoding kan gøre generation langsommere. Ved 30 % acceptance med en draft, der koster en femtedel af target, betaler du for fem forward passes og beholder 1,4 tokens. Den sidste kolonne er den anden fælde — en længere draft hjælper kun, når acceptance er høj, fordi halen af et γ\gamma-token gæt næsten aldrig nås. Ved 90 % acceptance er γ=8\gamma = 8 3,40x værd, og ved 30 % er den 0,79x værd: samme konfiguration, en gevinst eller et tab afhængigt af et tal målt på din trafik.

Distillation, og hvad en soft label bærer

Link til afsnittet: Distillation, og hvad en soft label bærer

Kvantisering krymper en model ved at gemme samme funktion i færre bits. Distillation krymper den ved at træne en mindre model til at imitere en større11 — en idé, der er næsten et årti ældre end deep learning.12

Den subtile del er hvad studenten lærer af. Ikke det korrekte svar: den kunne være blevet trænet direkte på det. Det læreren tilføjer, er hele fordelingen. Spørg modellen, hvad der følger efter en frase, og se forbi argmax:

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

Den hårde label siger jug og intet andet. Den soft label siger jug, og også at cup var næsten lige så godt, bowl plausibelt, og large — et adjektiv, en helt anden grammatisk fortsættelse — stadig levende. Det er det oprindelige argument: dette er et 7-tal, men det ligner ret meget et 1-tal, og ligheden er information, som den hårde label smider væk.

Det er også derfor, distillation bruger en temperatur. At dividere logits med TT før softmax flader fordelingen ud og hæver den relative vægt af runner-ups: på denne frase falder forholdet mellem den øverste token og den tredje fra 2,24 ved T=1T = 1 til 1,50 ved T=2T = 2 — kvadratroden af den første, hvilket er det, at dividere logits med to gør ved et forhold. Samme orden, mere af lossens attention på de nære fejl. Studentens gradient bærer lærerens usikkerhed og ikke kun dens dom.

Alt i dette kapitel er nu én sum:

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

hvor TT er det samlede antal residente tokens på tværs af alle concurrent requests. Anvendt: 7B- og 70B-rækkerne antager 8 key-value heads med dimension 128, 13B-rækken fuld multi-head attention med 40 heads, hvilket er sådan de generationer af model blev bygget — og det kan ses.

8 GB

modelprecisionvægteledigt efter overheadcontext tokens der passer
7Bfp1613,0 GBpasser ikke
7Bint86,5 GBpasser ikke
7Bint4 (g128)3,4 GB3,1 GB25.710
13Bint4 (g128)6,2 GB0,3 GB337
70Bint4 (g128)33,6 GBpasser ikke

16 GB

modelprecisionvægteledigt efter overheadcontext tokens der passer
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

modelprecisionvægteledigt efter overheadcontext tokens der passer
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 GBpasser ikke

Se på 13B-rækken i 8 GB-tabellen. Vægtene passer — 6,2 GB af 8 — så på den sædvanlige måde at tale på "kører en 13B model på et 8 GB-kort". Den har 337 tokens context, hvilket ikke er en samtale, men knap nok en prompt. "Passer den" er det forkerte spørgsmål. Det rigtige er "med hvor meget context, og for hvor mange brugere på én gang".

Se også på de to 16 GB int8-rækker. 7B får 65.378 tokens, og 13B får 3.136 — en tyvedobbelt forskel fra 5,6 GB ekstra vægte, fordi 13B her har multi-head attention, og dens cache koster 800 KB per token mod 7B's 128 KB. To modeller af lignende størrelse, én ubrugelig til long context, af en grund der ikke står i nogen model cards overskrift.

For tretten kapitler siden var dette en perceptron med to vægte og en bias. Det er nu en transformer, der er blevet designet, trænet, aligned, lært at bruge compute på svære spørgsmål og served til en målt omkostning per token — uden en eneste boks i den uåbnet.

Det slutter her, og det slutter med vilje.

Kapitel 14 begynder med modellen et andet sted. Ikke i din process, ikke i din hukommelse, ikke i en variabel du kan printe: på en maskine du ikke administrerer, bag en API key, en port og en regning. Alt målt her sker stadig — prefill kører stadig før den første token, cache vokser stadig med samtalen, den batch du er i, tilhører stadig en anden og afgør stadig din latency — men fra nu af observerer du det gennem en strøm af Server-Sent Events, en finish_reason og en HTTP 429 med en Retry-After header. Spørgsmålene ændrer sig med udsigtspunktet: ikke hvordan beregnes denne gradient, men hvorfor blev min faktura tredoblet. Det samme gør sproget, og Kapitel 14 forklarer den regel i stedet for at annoncere den — indtil hertil holdt koden vægte, gradients, logits og tokenizer-bytes; derfra holder den en connection, et retry, en cancellation og akkumuleret state. De tretten kapitler bag dig kasseres ikke ved overgangen. De er beskrivelsen af, hvad der kører på den anden side af porten.


To udeladelser er bevidste. FlashAttention (Dao et al., arXiv:2205.14135) er ikke en anden attention — den beregner samme funktion ved at tile operationen, så n×nn \times n score-matricen aldrig skrives til hukommelsen, hvilket er grunden til, at de 67 MB i dette kapitels anden tabel i praksis er mindre, end aritmetikken antyder. Og selve kernels er delegeret: lecture 10 i Stanfords CS336 dækker inference-systemer i den dybde, dette ikke forsøger, og llama.cpp-repositoriet og GGUF-specifikationen er de primære kilder til CPU-siden.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Artiklen er i høj grad et memory-bandwidth-argument, og den læses sådan.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Indeholder uptraining-opskriften, der konverterer et eksisterende multi-head checkpoint, hvilket er grunden til, at GQA spredte sig så hurtigt.

  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. Introducerer scheduling på iterationsniveau — continuous batching — og selektiv batching.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. Artiklen vLLM er bygget på; §3 er operativsystem-analogien i fuld længde.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 defineres i §3; de seksten niveauværdier brugt i målingen ovenfor er dem, denne artikel udleder.

  6. Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). Analysen af outlier features i §4 er kilden til fænomenet målt ovenfor, inklusive fundet af at outliers opstår systematisk ved skala. 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 er beviset for, at outputfordelingen er uændret; Chen et al. (arXiv:2302.01318) publicerede samme idé uafhængigt.

  11. Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Temperaturen og "dark knowledge"-argumentet.

  12. Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, ni år tidligere, for ensembles snarere end transformers.

Klar til at lade LIA vælge for dig?

Byg med alle AI-modeller ét sted — kom gratis i gang i dag.