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.
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 FLOPs for hver token, den udsender, på hver request, resten af dens levetid.
Hvor tiden i den anden kørsel blev af
Link til afsnittet: Hvor tiden i den anden kørsel blev afFor 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:
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 , hvor ét generation-trin beregnes på begge måder:
| tokens i context | genberegn alt | med cache | forhold | 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 |
Kolonnen til højre er årsagen. Genberegning bygger hele attention-matricen ved hvert trin — fra boksen om asymptotisk notation i Kapitel 9, betalt én gang per token. Med cache bygger du en 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 tokens fra en cold start:
| genererede tokens | med cache | genberegning | forhold |
|---|---|---|---|
| 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 trin er den cached version lineær i context, og den uncached er kvadratisk; summeret over en generation, mod , 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 cache | 21,8 MB |
| genberegning | 181,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 maskinerSe igen på den hurtige kørsel: dens første token opførte sig anderledes end de andre syvogfyrre.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepPrompt 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:
Prefill
Link til afsnittet: PrefillÉ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 tokens:
| prompt tokens | sekunder | 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 mod en cache på :
| cached tokens | ms for én token |
|---|---|
| 16 | 110,05 |
| 64 | 97,57 |
| 256 | 108,53 |
| 1024 | 103,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 , 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 er også regningen
Link til afsnittet: Cache er også regningenCache 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:
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 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:
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/tokenPræcist, og det forbliver præcist på tværs af hver afprøvet shape:
| batch | context | målt cache | forudsagt | peak arbejdshukommelse |
|---|---|---|---|---|
| 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 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.
Hvor MQA og GQA kommer fra
Link til afsnittet: Hvor MQA og GQA kommer fraKapitel 9 introducerede multi-query og grouped-query attention og udskød forklaringen til dette kapitel. Årsagen er den formel, og specifikt 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 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 bruger | 8 brugere | 64 brugere |
|---|---|---|---|
| 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 |
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 nedDecode 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:
| batch | latency per trin | 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 |
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 vinderDen naive måde at batche på er at samle 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:
| politik | vægur | throughput | gennemsnitlig latency per request | spildte slot-trin |
|---|---|---|---|---|
| statiske batches på 8 | 176,9 s | 10,6 tok/s | 83,2 s | 3.214 |
| kontinuerlig, 8 slots | 109,0 s | 17,2 tok/s | 8,1 s | 0 |
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 galtDen 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:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedVæ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 :
| skema | gennemsnitlig relativ fejl | værste matrix |
|---|---|---|
| INT8, én scale for hele matricen | 0,0400 | 0,1487 |
| INT8, én scale per outputrække | 0,0100 | 0,0149 |
| INT4, én scale for hele matricen | 0,6026 | 0,9931 |
| INT4, én scale per outputrække | 0,1790 | 0,2589 |
| INT4, én scale per gruppe på 128 | 0,1323 | 0,1992 |
| NF4, én scale per blok på 64 | 0,0952 | 0,1205 |
| INT3, én scale per gruppe på 128 | 0,3044 | 0,4123 |
| INT2, én scale per gruppe på 128 | 0,7790 | 0,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:
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 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.
Outlier features
Link til afsnittet: Outlier featuresKapitel 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:
| lag | største |h| | mediandimensionens største |h| | forhold | dimensioner over 6x medianen |
|---|---|---|---|---|
| 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 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:
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 | # 1Ni 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:
| skema | relativ fejl | forskellige heltalsniveauer brugt, hele tensoren |
|---|---|---|
| én scale for hele tensoren | 0,1083 | 14 af 256 |
| én scale per token (per række) | 0,0433 | 158 |
| hel tensor, 1 outlier-dimension holdt i fp32 | 0,0442 | 48 |
| hel tensor, 4 outlier-dimensioner holdt i fp32 | 0,0279 | 57 |
| hel tensor, 16 outlier-dimensioner holdt i fp32 | 0,0085 | 102 |
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:
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.
Hvad kvantisering faktisk koster, målt
Link til afsnittet: Hvad kvantisering faktisk koster, måltNæ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.
| skema | gennemsnitlig vægtfejl | perplexity | spørgebatteri | enig med fp32 |
|---|---|---|---|---|
| fp32 (reference) | 0,0000 | 23,08 | 13/16 | 100,0 % |
| INT8 per tensor | 0,0400 | 23,58 | 13/16 | — |
| INT8 per række | 0,0100 | 22,96 | 13/16 | 98,6 % |
| INT4 per tensor | 0,6026 | 365.416.000 | 0/16 | — |
| INT4 per række | 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 % |
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.
Speculative decoding
Link til afsnittet: Speculative decodingKapitel 12 annoncerede dette og efterlod regningen her.
Ideen kommer direkte fra prefill/decode-opdelingen. At verificere en foreslået sekvens på tokens koster ét forward pass over positioner — et matrix-matrix-produkt, knap dyrere end passet over én. Så:
En lille, billig model genererer kandidat-tokens autoregressivt.
Den store model kører ét forward pass over alle 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 , 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 model | acceptance | længste accepterede run | forventede tokens per target pass, |
|---|---|---|---|
| fp32 (target selv) | 100,0 % | 48 | 5,00 |
| INT8 per række | 98,6 % | 48 | 4,86 |
| NF4 block 64 | 84,7 % | 20 | 3,69 |
| INT4 group 128 | 71,5 % | 13 | 2,85 |
| INT4 per række | 58,3 % | 7 | 2,24 |
| INT3 group 128 | 5,6 % | 2 | 1,06 |
| INT2 group 128 | 0,0 % | 0 | 1,00 |
De forventede tokens accepteret per verification pass, ved draft-længde , er
og den netto speedup dividerer det med draftens egen omkostning, en brøkdel af 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 |
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 -token gæt næsten aldrig nås. Ved 90 % acceptance er 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ærerKvantisering 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:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360Den 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 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 til 1,50 ved — 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.
Hvad der passer i 8, 16 og 24 GB
Link til afsnittet: Hvad der passer i 8, 16 og 24 GBAlt i dette kapitel er nu én sum:
hvor 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
| model | precision | vægte | ledigt efter overhead | context tokens der passer |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | passer ikke | — |
| 7B | int8 | 6,5 GB | passer ikke | — |
| 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 | passer ikke | — |
16 GB
| model | precision | vægte | ledigt efter overhead | context tokens der passer |
|---|---|---|---|---|
| 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 | vægte | ledigt efter overhead | context tokens der passer |
|---|---|---|---|---|
| 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 | passer 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.
Hvor det fortsætter
Link til afsnittet: Hvor det fortsætterFor 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.
Kilder og metode
Link til afsnittet: Kilder og metodeTo udeladelser er bevidste. FlashAttention (Dao et al., arXiv:2205.14135) er ikke en anden attention — den beregner samme funktion ved at tile operationen, så 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.
Referencer
Link til afsnittet: Referencer-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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
-
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 er beviset for, at outputfordelingen er uændret; Chen et al. (arXiv:2302.01318) publicerede samme idé uafhængigt. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Temperaturen og "dark knowledge"-argumentet. ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, ni år tidligere, for ensembles snarere end transformers. ↩