Inference mai ieftin: KV cache, batching și quantization
Același model răspunde identic în 8,8 secunde și în 78,9. Apoi INT4, măsurat în trei moduri, nu doar afirmat.
Pe această pagină
Același model, pe aceeași mașină, răspunzând la aceeași întrebare cu aceiași 48 de token. Cele două rezultate sunt identice token cu token — verificat, nu presupus.
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)S-a schimbat un singur argument: use_cache=False. Nimic despre model, prompt, sampling sau aritmetică nu este diferit, iar a doua rulare nu este mai precisă pentru efortul ei. Este de nouă ori mai lentă degeaba.
Aceasta este forma capitolului. Tot ce conține — cache-ul, batch-ul, ponderile quantized — este o încercare de a nu mai plăti pentru muncă ce nu schimbă răspunsul sau de a afla cât costă un răspuns mai ieftin. Capitolul 10 a stabilit lista de prețuri pentru training. Aceasta este lista de prețuri pentru partea pe care o plătești pentru totdeauna: un model deployed cheltuiește aproximativ FLOPs pentru fiecare token pe care îl emite, la fiecare request, pentru tot restul vieții lui.
Unde s-a dus timpul celei de-a doua rulări
Link către secțiunea: Unde s-a dus timpul celei de-a doua rulăriPentru a genera un token, un decoder-only transformer ia întreaga secvență de până atunci, o trece prin fiecare layer și citește distribuția de probabilitate din ultima poziție. Apoi adaugă token-ul ales și o face din nou. Descrierea este corectă și exact asta face rularea lentă.
Este și enorm de risipitoare, iar motivul este causal mask din Capitolul 9. Vectorii key și value ai poziției 7 sunt calculați din inputul poziției 7 și pozițiile de dinainte. Când apare poziția 8, poziția 7 nu o poate vedea — asta înseamnă causal — deci key și value ale poziției 7 sunt exact aceleași numere ca înainte. Rularea lentă le recalculează oricum, la fiecare pas.
Așa că stochează-le. Acea stocare este key-value cache, cea mai importantă optimizare din serving-ul modelelor lingvistice:
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)Uită-te ce este dat modelului în interiorul buclei: nxt, un singur token. Nu secvența. Query-ul noului token attends against fiecare key din cache, iar key-urile din cache nu aveau cum să se schimbe. Aceasta nu este o aproximație — verificarea outputului identic de mai sus este ideea. Cache-ul nu schimbă calitatea pe viteză; șterge aritmetica redundantă.
Ca să vezi scalarea curat, scoate transformer-ul din ecuație și cronometrează un singur attention head cu , un pas de generation calculat în ambele feluri:
| token în context | recalculează tot | cu cache | raport | matricea de scoruri |
|---|---|---|---|---|
| 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 |
Coloana din dreapta este cauza. Recalcularea construiește matricea attention completă la fiecare pas — din caseta de notație asimptotică a Capitolului 9, plătită o dată per token. Cu cache-ul construiești în schimb un rând : la 4.096 de token, 67 MB de scoruri față de 16 KB.
Numărarea operațiilor multiply-accumulate în loc de milisecunde scoate mașina din argument. Pentru a genera token dintr-un start rece:
| token generați | cu cache | recalculând | raport |
|---|---|---|---|
| 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 pas, versiunea cu cache este liniară în context, iar cea fără cache este pătratică; însumate pe o generation, față de , cu raportul crescând fără limită. Diferența de nouă ori din deschidere a fost măsurată pe 48 de token — sub primul rând al acelui tabel.
Cache-ul schimbă și ce trebuie să fie în memorie. Pe un GPU de laptop de 8 GB care generează 256 de token în fp16, luând vârful allocatorului și scăzând ponderile rezidente:
| memorie de lucru de vârf | |
|---|---|
| cu cache | 21,8 MB |
| recalculând | 181,7 MB |
De 8,3 ori mai multă memorie, cheltuită pentru a produce aceiași token mai lent. Aceasta este promisiunea făcută în Capitolul 5, venind dintr-o direcție neașteptată: acolo, autodiff în reverse-mode trebuia să țină fiecare intermediar în viață pentru backward pass, iar activations dominau memoria de training. La inference nu există backward pass și nimic de păstrat pentru el — deci ce domină memoria este cache-ul, iar el este o alegere deliberată, nu un cost inevitabil.
Prefill și decode sunt două mașini diferite
Link către secțiunea: Prefill și decode sunt două mașini diferiteUită-te din nou la rularea rapidă: primul ei token s-a comportat diferit de ceilalți patruzeci și șapte.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepPrompt-ul a costat 25,6 ms per token, iar fiecare token generat a costat 166 ms. Același model, același hardware, aceleași ponderi, o diferență de șase ori per token — și merge în direcția la care majoritatea oamenilor nu se așteaptă. Prompt-ul este partea ieftină. Generation se împarte în două faze cu fizici cu adevărat diferite:
Un forward pass peste întregul prompt. Fiecare token este procesat în paralel, așa că fiecare matrice de ponderi este încărcată din memorie o singură dată și înmulțită cu o matrice de sute de vectori token — un produs matrice-matrice, cu multă aritmetică pentru fiecare byte mutat, exact pentru ce este construit un GPU. Prefill este compute-bound, iar costul lui este aproximativ liniar în lungimea prompt-ului.
Un forward pass per token, batch de unu și secvență de unu. Fiecare matrice de ponderi este tot încărcată integral din memorie și înmulțită cu un singur vector — un produs matrice-vector, cu aproape deloc aritmetică pentru fiecare byte mutat. Decode este memory-bandwidth-bound, iar costul său per token depinde foarte puțin de lungimea context-ului.
Ambele jumătăți sunt măsurabile. Prefill, o singură trecere peste token:
| token în prompt | secunde | 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, un token față de un cache de :
| token în cache | ms pentru un token |
|---|---|
| 16 | 110,05 |
| 64 | 97,57 |
| 256 | 108,53 |
| 1024 | 103,86 |
Citește al doilea tabel de două ori. Trecerea de la 16 token de context la 1.024 — de șaizeci și patru de ori mai mult istoric peste care să faci attention — nu a schimbat costul unui pas cu nimic măsurabil. Attention împotriva cache-ului este muncă reală, dar este eclipsată de costul fix al tragerii a jumătate de miliard de ponderi prin magistrala de memorie pentru a produce un vector. Acel cost fix este motivul pentru tot ce urmează în secțiunea următoare.
Aceste două faze sunt originea celor două numere raportate de orice sistem de serving. Time to first token este în esență prefill și crește cu prompt-ul, motiv pentru care o conversație lungă pare lentă la început. Tokens per second este și este aproximativ constant, motiv pentru care răspunsul curge apoi uniform. Un chat care pornește încet și apoi face streaming lin nu este un truc de randare. Sunt aceste două tabele.
Cache-ul este și factura
Link către secțiunea: Cache-ul este și facturaCache-ul schimbă aritmetica pe memorie, iar memoria pe care o vrea nu este puțină. Pentru fiecare token din context, fiecare layer păstrează un vector key și un vector value per key-value head:
2-ul este pentru keys și values; restul ține de arhitectură. Pentru modelul măsurat în tot acest capitol — 24 de layers, 14 query heads, 2 key-value heads, head dimension 64 — în fp16 asta înseamnă bytes per token.
Formulae din acest domeniu au obiceiul să fie greșite cu un factor de doi, așa că verific-o cu allocatorul în loc să o crezi:
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, și rămâne exactă în toate formele încercate:
| batch | context | cache măsurat | prezis | memorie de lucru de vârf |
|---|---|---|---|---|
| 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 |
Ultimele trei rânduri merită privite încă o dată. Treizeci și doi de useri cu 2.048 de token fiecare, șaizeci și patru cu 1.024, o sută douăzeci și opt cu 512 — cache-ul este 768 MB în fiecare caz, pentru că toate trei țin 65.536 de token. Cache-ul depinde doar de numărul total de token rezidenți, nu de cum sunt distribuiți între useri. Acest fapt este fundația secțiunii despre batching.
De unde vin MQA și GQA
Link către secțiunea: De unde vin MQA și GQACapitolul 9 a introdus multi-query și grouped-query attention și a amânat motivul pentru acest capitol. Motivul este acea formulă și, mai precis, din ea.
Standard multi-head attention oferă fiecărui query head propriile key și value heads. Modelul de aici are 14 query heads; cu full multi-head attention, cache-ul lui ar fi bytes per token — 84 KB în loc de 12 KB, exact de șapte ori mai mult, raportul dintre query heads și key-value heads.
Multi-query attention1 duce asta la limită: toate query heads împart un singur key-value head. Grouped-query attention2 este compromisul care a câștigat — câteva key-value heads, fiecare împărțit de un grup de query heads — pentru că pierderea de calitate a MQA era reală, iar cea a GQA nu este. Niciuna nu cumpără aritmetică. Există ca să împartă acea formulă la un întreg și s-au răspândit în industrie în clipa în care contextele lungi au făcut din cache constrângerea principală.
Ceea ce se întâmplă repede. Pentru un model din clasa 7B cu 32 de layers și 8 key-value heads de dimension 128, cache-ul este 128 KB per token în fp16:
| token în context | un user | 8 useri | 64 useri |
|---|---|---|---|
| 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 |
Ponderile proprii ale acelui model sunt 13,0 GB în fp16, cifra din tabelul de la finalul capitolului. Deci la un context de 128.000 de token, cache-ul unui singur user este mai mare decât modelul. Aceasta este aritmetica pe care Capitolul 16 o transformă în bani și motivul pentru care o conversație lungă nu este doar una lentă — ea ocupă o felie fixă dintr-o mașină cât timp request-ul este viu.
Batching: numărul care crește și numărul care scade
Link către secțiunea: Batching: numărul care crește și numărul care scadeDecode este memory-bound: ponderile sunt trase prin magistrală pentru a produce un token, iar unitățile aritmetice stau degeaba. Așa că pune mai multă muncă în același pas. Rulează mai multe request-uri deodată, iar ponderile, citite o singură dată, le servesc pe toate. Măsurat pe același model, fiecare request ținând un cache de 64 de token și făcând decode pentru un token:
| batch | latență per pas | throughput | latență 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 |
Citește cele două coloane din dreapta una în raport cu cealaltă, pentru că ele sunt toată ideea. Trecerea de la un request la șaisprezece multiplică throughput-ul cu 6,0 și multiplică așteptarea pentru fiecare request individual cu 2,67. Batch-ul a făcut serverul mai bun și fiecare user mai rău.
Nu este un bug care poate fi reglat până dispare; acesta este schimbul în sine și are câte un nume pe fiecare parte. Latența este ceea ce simte o persoană care așteaptă un răspuns. Throughput-ul este ceea ce împarte factura. Nicio setare nu le îmbunătățește pe amândouă.
Observă și unde se oprește. De la 16 la 32, throughput-ul câștigă 9 %, în timp ce latența aproape se dublează: pasul a încetat să mai fie memory-bound și a devenit compute-bound, iar după acel cot batch-ul nu mai cumpără nimic. Fiecare deployment are un astfel de cot; poziția lui trebuie măsurată pe al tău, dar existența lui nu.
Static batching irosește mare parte din ce câștigă
Link către secțiunea: Static batching irosește mare parte din ce câștigăModul naiv de a face batch este să colectezi request-uri, să le rulezi împreună și să returnezi când toate sunt gata. Dar nu se termină împreună: unele răspunsuri au douăzeci de token, altele cinci sute. Un batch fix rulează până când cel mai lung membru se termină, iar fiecare request terminat continuă să-și ocupe slotul, contribuind padding, până atunci.
Ia 64 de request-uri cu o asimetrie realistă a lungimilor de output — mediană 18 token, cel mai lung 231, 1.874 în total — și simulează ambele politici la costul per pas măsurat pentru opt sloturi:
| politică | timp total | throughput | latență medie per request | slot-pași irosiți |
|---|---|---|---|---|
| batch-uri statice de 8 | 176,9 s | 10,6 tok/s | 83,2 s | 3.214 |
| continuu, 8 sloturi | 109,0 s | 17,2 tok/s | 8,1 s | 0 |
Throughput-ul se îmbunătățește de 1,6x. Latența medie se îmbunătățește de peste zece ori, pentru că sub static batching un request care s-a terminat în patru pași încă așteaptă un vecin de 231 de token înainte să audă cineva de el.
Continuous batching3 este soluția și este la fel de simplă cum sună: batch-ul nu este un grup, ci un set de sloturi, iar un slot care se eliberează admite următorul request din coadă chiar la pasul următor. Schedulerul lucrează la granularitatea unui token, nu a unui request. Orice stack de serving în producție face asta acum.
Are și o a doua jumătate, care este cache-ul. Sloturile care vin și pleacă lasă memoria cache fragmentată, iar rezervarea pentru fiecare slot a context-ului maxim posibil irosește cea mai mare parte a rezervării. PagedAttention4 împrumută răspunsul de la sistemele de operare: stochează cache-ul în blocuri de dimensiune fixă, cu un tabel de blocuri per secvență, astfel încât cache-ul unei secvențe să poată fi împrăștiat fizic, rămânând logic contiguu — ceea ce permite și ca două secvențe cu prefix comun să împartă blocurile care îl conțin. Pe asta este construit vLLM și de aceea un engine de serving este un allocator de memorie cu un transformer atașat.
Quantization și primul lucru care merge prost
Link către secțiunea: Quantization și primul lucru care merge prostCealaltă jumătate a facturii sunt ponderile înseși. O jumătate de miliard de parametri la patru bytes fiecare înseamnă 1,98 GB; la doi bytes, 0,99 GB; la un byte, 0,49 GB. Mai puțini biți per pondere micșorează modelul pe disc, îl micșorează în memorie și — pentru că decode este bandwidth-bound — fac fiecare pas mai rapid, deoarece sunt mai puțini bytes de mutat.
Cea mai simplă schemă este quantization simetrică absolute-maximum și încape în trei linii:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedAlege o scară astfel încât cea mai mare pondere să se mapeze la cel mai mare întreg, împarte, rotunjește, stochează întregii și scara. Reconstruiește înmulțind înapoi. Nu este nimic isteț în asta și funcționează — până când nu mai funcționează.
Măsurat pe ponderile reale ale modelului: toate cele 168 de matrice de proiecție, 357,8 milioane de parametri, eroare relativă :
| schemă | eroare relativă medie | cea mai rea matrice |
|---|---|---|
| INT8, o scară pentru întreaga matrice | 0,0400 | 0,1487 |
| INT8, o scară per rând de output | 0,0100 | 0,0149 |
| INT4, o scară pentru întreaga matrice | 0,6026 | 0,9931 |
| INT4, o scară per rând de output | 0,1790 | 0,2589 |
| INT4, o scară per grup de 128 | 0,1323 | 0,1992 |
| NF4, o scară per bloc de 64 | 0,0952 | 0,1205 |
| INT3, o scară per grup de 128 | 0,3044 | 0,4123 |
| INT2, o scară per grup de 128 | 0,7790 | 0,8076 |
Al patrulea rând este prăbușirea. O eroare relativă de 0,99 pe cea mai rea matrice înseamnă că reconstrucția nu păstrează practic nimic din original — matricea a fost înlocuită cu zgomot de magnitudine aproximativ corectă. Cauza se vede în același experiment pe o singură matrice:
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 %)O pondere din șase mii se află dincolo de șase deviații standard, iar cea mai mare este la 24. Cu o singură scară pentru întreaga matrice, acea pondere stabilește mărimea pasului pentru toate cele 4,3 milioane de ponderi. La 8 biți există 256 de pași, iar ponderea tipică tot ajunge pe unul semnificativ. La 4 biți există 16, cel mai exterior este rezervat pentru o valoare pe care aproape nimic nu o are, iar ponderile obișnuite — adică toate — se rotunjesc la două sau trei niveluri distincte.
Tot ce urmează după acel rând este aceeași reparație la granularități diferite: dă scării un teritoriu mai mic. Per rând de output împarte eroarea la 3,4; per grup de 128 de ponderi consecutive o împarte din nou. Costul este contabilitate — o scară pe 16 biți per grup de 128 înseamnă biți per pondere în loc de 4 — și recuperează cea mai mare parte din diferență.
NF4 atacă problema din cealaltă parte.5 Nivelurile nu trebuie să fie spațiate egal. Ponderile dintr-un bloc sunt aproximativ distribuite normal, așa că alege cele șaisprezece niveluri ca cuantile ale unei distribuții normale: dense aproape de zero, unde sunt de fapt ponderile, rare în cozi, unde nu sunt. Aceiași patru biți, aceeași scalare pe bloc, la un bloc mai mic — 4,25 biți per pondere față de 4,125 pentru group-128 — iar eroarea măsurată scade de la 0,1323 la 0,0952, cu 28 % mai jos. O parte vine din blocul mai fin, restul din plasarea nivelurilor acolo unde este masa, iar separarea celor două ar cere un al treilea rând.
Outlier features
Link către secțiunea: Outlier featuresCaseta despre floating-point din Capitolul 2 s-a încheiat cu o promisiune: că acest capitol va face quantize ponderilor la 8 și 4 biți și va găsi o mână de outlier features care refuză să fie comprimate. Iată-le, și ele explică de ce „doar rotunjește numerele” nu avea cum să funcționeze pe activations.
Ponderile de mai sus s-au comportat prost. Activations sunt într-o cu totul altă ligă. Ia un prompt obișnuit de 84 de token, capturează residual stream la fiecare layer și măsoară cea mai mare magnitudine pe care o atinge fiecare dintre cele 896 de dimensiuni:
| layer | cel mai mare |h| | cel mai mare |h| al dimensiunii mediane | raport | dimensiuni peste 6x mediana |
|---|---|---|---|---|
| 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 |
Dimensiunea 62 ajunge la 1.579,6, în timp ce dimensiunea mediană nu trece niciodată de 1,6. Nu este o întâmplare a unui token sau a unui layer: aceeași dimensiune este acolo la layer 4 și încă acolo la layer 20, cu aproape aceeași valoare. Acestea sunt outlier features,6 și sunt sistematice — o proprietate a modelului trained, nu a inputului.
Histograma celor 896 de maxime per dimensiune la layer 16 face forma imposibil de confundat:
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 | # 1Nouă sute de dimensiuni într-o grămadă ordonată sub 8, nimic pentru trei octave, apoi o singură dimensiune singură la capătul îndepărtat. Acum fă quantize acelui tensor la INT8 și numără ce se întâmplă:
| schemă | eroare relativă | niveluri întregi distincte folosite, întregul tensor |
|---|---|---|
| o scară pentru întregul tensor | 0,1083 | 14 din 256 |
| o scară per token (per rând) | 0,0433 | 158 |
| întregul tensor, 1 dimensiune outlier păstrată în fp32 | 0,0442 | 48 |
| întregul tensor, 4 dimensiuni outlier păstrate în fp32 | 0,0279 | 57 |
| întregul tensor, 16 dimensiuni outlier păstrate în fp32 | 0,0085 | 102 |
Paisprezece niveluri din 256. Scara a fost setată de 1.579,6, deci fiecare pas are lățimea 12,44, iar activation-ul tipic — magnitudine mediană 0,26, percentila 99 la 2,51 — nu are unde să aterizeze. Per dimensiune este și mai dur:
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 levelsUn nivel. Întreaga dimensiune, fiecare token, quantized la același număr. Au fost alocați opt biți și s-a folosit aproximativ zero, iar modelul care citește acele activations primește o constantă.
Acea măsurătoare este justificarea pentru fiecare tehnică pe care oamenii chiar o folosesc:
Ține outliers în afara problemei. LLM.int8()6 descompune înmulțirea matricială: dimensiunile cu magnitudini extreme sunt calculate în 16 biți, restul în INT8, iar jumătățile sunt însumate. Tabelul de mai sus este chitanța — eliminarea a patru dimensiuni reduce eroarea de aproape patru ori. SmoothQuant7 mută în schimb dificultatea: împarte activations la un factor per-channel și înmulțește coloana de ponderi corespunzătoare cu el, ceea ce lasă produsul neschimbat și mută outlier-ul din tensorul care nu îl poate absorbi în cel care poate.
Alege rotunjirea, nu doar rotunji. Nimic de mai sus nu întreabă la ce servește matricea. GPTQ8 face quantize coloană cu coloană și, după fiecare, ajustează coloanele rămase în full-precision pentru a compensa eroarea deja comisă — minimizând eroarea outputului layer-ului pe inputuri reale, nu eroarea ponderilor lui. AWQ9 observă că o mică fracțiune de canale de ponderi contează mult mai mult decât restul, le găsește din statistici de activation și le scalează în sus înainte de quantizing ca să aterizeze pe niveluri mai fine. Ambele au nevoie de un set de calibrare; niciuna nu are nevoie de gradients.
Afișează detaliile
GGUF și ce legătură are un format de fișier cu toate acestea.
GGUF nu este o metodă de quantization; este containerul pe care îl folosește llama.cpp, iar confuzia din comparațiile gguf vs gptq vine din tratarea celor două ca fiind același tip de lucru. GGUF conține tensors, tokenizer, metadate de arhitectură și chat template într-un singur fișier memory-mappable și poartă în interior o familie de scheme pe blocuri — nume precum Q4_K_M encodează biți per pondere, dimensiunea blocului și dacă unii tensors sunt păstrați la precizie mai mare.
Diferența de inginerie care contează: GPTQ și AWQ produc ponderi optimizate pentru un GPU kernel, în timp ce schemele GGUF sunt decodate ieftin pe CPU cu fișierul mapat, nu încărcat. De aceea același „model 7B pe 4 biți” nominal există în ambele lumi la dimensiuni și calități diferite, și de aceea comparația onestă nu este niciodată formatul — este măsurătoarea de mai jos, rulată pe task-ul tău.
Ce costă de fapt quantization, măsurat
Link către secțiunea: Ce costă de fapt quantization, măsuratAproape fiecare articol despre quantization se oprește la secțiunea anterioară: explică metoda, citează un raport de compresie și afirmă că „calitatea este în mare parte păstrată”. Capitolul 4 a fost despre a nu te păcăli singur, așa că hai să aflăm.
Același model, ponderi quantized in place cu fiecare schemă, apoi trei măsurători: perplexity pe 2.048 de token de proză engleză held-out — aici, ciorna acestui curs, motiv pentru care repository-ul substituie o carte fixă din domeniul public și tipărește un tabel cu aceeași formă, dar numere diferite — o baterie de 16 întrebări factuale scurte cu răspunsuri cunoscute sub greedy decoding și fracțiunea de token pentru care modelul quantized este de acord cu cel full-precision dat același context.
| schemă | eroare medie a ponderilor | perplexity | baterie de întrebări | acord cu fp32 |
|---|---|---|---|---|
| fp32 (referință) | 0,0000 | 23,08 | 13/16 | 100,0 % |
| INT8 per tensor | 0,0400 | 23,58 | 13/16 | — |
| INT8 per rând | 0,0100 | 22,96 | 13/16 | 98,6 % |
| INT4 per tensor | 0,6026 | 365.416.000 | 0/16 | — |
| INT4 per rând | 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 % |
Patru lucruri din acel tabel merită spuse direct.
INT8 făcut corect este gratuit. INT8 per rând obține 22,96 față de 23,08 pentru referință — o diferență de o parte din două sute, adică zgomot și ar trebui citită ca „identic”. Direcția în care arată zgomotul nu este stabilă: pe corpusul public-domain al repository-ului, aceleași două scheme ies 22,24 față de 22,18: jumătate din acea distanță și în direcția opusă. Este de acord cu modelul full-precision pe 142 din 144 de token generați. Un sfert din memorie față de referința fp32, jumătate față de fp16-ul pe care l-ai deploya de fapt, și niciun cost detectabil. INT8 făcut neglijent este și el aproape gratuit: o scară per matrice costă 0,5 puncte de perplexity și niciun răspuns în baterie. Opt biți sunt destul de iertători încât granularitatea contează foarte puțin, exact motivul pentru care oamenii generalizează de la INT8 la INT4 și se lovesc.
INT4 cu o scară per tensor distruge modelul. Perplexity 365 de milioane: nu degradat, anihilat. Granularitatea devine apoi tot jocul — per-tensor 365.416.000, per-rând 46,18, per-grup-de-128 31,08, NF4 24,55. Aceiași patru biți per pondere, un factor de cincisprezece milioane între cel mai rău și cel mai bun.
Perplexity este un instrument grosier, iar bateria unul și mai grosier. Între NF4 și group-128 INT4 diferența de perplexity este 6,5 puncte, iar bateria diferă printr-o întrebare — iar intervalul de încredere din Capitolul 4 spune că o întrebare din șaisprezece nu distinge absolut nimic. Există o demonstrație mai tăioasă decât intervalul: rulează aceeași baterie cu repetition penalty-ul stock al modelului dezactivat, adică ceea ce înseamnă de fapt greedy decoding, iar cele două rânduri își schimbă locurile. O întrebare din șaisprezece nu este un efect mic, este niciun efect. Avertismentul din Capitolul 8 se aplică și el: perplexity este comparabilă doar între modele care împart un tokenizer, deci un număr din articolul altcuiva nu poate fi comparat cu al tău.
Coloana de acord este cea mai precisă dintre cele trei, și aproape gratuită: rulează modelul full-precision greedy, apoi întreabă modelul quantized, la fiecare poziție, ce ar fi ales dat același prefix. Are 144 de observații independente în loc de 16, nu are nevoie de ground truth și se degradează lin acolo unde bateria se degradează în salturi. Este și exact cantitatea de care are nevoie secțiunea următoare.
Aceasta este promisiunea făcută de Capitolul 1 despre acest capitol, sosind la timp: matematica spune că un model pe 4 biți este posibil, iar ingineria decide dacă este utilizabil.
Speculative decoding
Link către secțiunea: Speculative decodingCapitolul 12 a anunțat asta și a lăsat factura aici.
Ideea vine direct din împărțirea prefill/decode. Verificarea unei secvențe propuse de token costă un forward pass peste poziții — un produs matrice-matrice, abia mai scump decât trecerea peste una singură. Deci:
Un model mic și ieftin generează autoregresiv token candidați.
Modelul mare rulează un singur forward pass peste toți cei candidați deodată, producând ce ar fi spus la fiecare poziție.
Păstrează cel mai lung prefix asupra căruia cele două sunt de acord, plus token-ul pe care modelul mare îl furnizează gratuit la prima dezacordare. Aruncă restul și începe din nou.
Distribuția de output este neschimbată. Cu greedy decoding este evident — un token este acceptat doar dacă ținta l-ar fi produs. Cu sampling cere o regulă de acceptare modificată, iar Leviathan et al. demonstrează că distribuția rezultată este exact cea a țintei.10 Aceasta este a doua optimizare exactă din acest capitol.
Prin urmare, totul depinde de rata de acceptare , care este măsurabilă — este coloana de acord de mai sus, motiv pentru care a fost calculată acolo. Folosind fiecare model quantized ca draft pentru ținta full-precision, pe 144 de poziții generate:
| model draft | acceptare | cea mai lungă rulare acceptată | token așteptați per target pass, |
|---|---|---|---|
| fp32 (ținta însăși) | 100,0 % | 48 | 5,00 |
| INT8 per rând | 98,6 % | 48 | 4,86 |
| NF4 block 64 | 84,7 % | 20 | 3,69 |
| INT4 group 128 | 71,5 % | 13 | 2,85 |
| INT4 per rând | 58,3 % | 7 | 2,24 |
| INT3 group 128 | 5,6 % | 2 | 1,06 |
| INT2 group 128 | 0,0 % | 0 | 1,00 |
Numărul așteptat de token acceptați per verification pass, la draft length , este
iar accelerarea netă împarte asta la costul propriu al draft-ului, o fracțiune din țintă per token:
| acceptare | , | , | , | , |
|---|---|---|---|---|
| 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 |
Intrarea bold este cea de reținut: speculative decoding poate face generation mai lentă. La 30 % acceptare cu un draft care costă o cincime din țintă, plătești pentru cinci forward passes și păstrezi 1,4 token. Ultima coloană este cealaltă capcană — un draft mai lung ajută doar când acceptarea este mare, pentru că coada unei presupuneri de token este aproape niciodată atinsă. La 90 % acceptare, valorează 3,40x, iar la 30 % valorează 0,79x: aceeași configurație, câștig sau pierdere în funcție de un număr măsurat pe traficul tău.
Distillation și ce poartă o etichetă soft
Link către secțiunea: Distillation și ce poartă o etichetă softQuantization micșorează un model stocând aceeași funcție în mai puțini biți. Distillation îl micșorează antrenând un model mai mic să imite unul mai mare11 — o idee care precedă deep learning cu aproape un deceniu.12
Partea subtilă este din ce învață studentul. Nu răspunsul corect: ar fi putut fi antrenat direct pe acesta. Ce adaugă profesorul este întreaga distribuție. Întreabă modelul ce urmează după o frază și privește dincolo de argmax:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360Eticheta hard spune jug și nimic altceva. Eticheta soft spune jug, și mai spune că cup era aproape la fel de bun, bowl plauzibil, iar large — un adjectiv, o continuare gramaticală complet diferită — încă viu. Acesta este argumentul original: acesta este un 7, dar seamănă destul de mult cu un 1, iar asemănarea este informație pe care eticheta hard o aruncă.
Tot de aceea distillation folosește o temperatură. Împărțirea logits la înainte de softmax aplatizează distribuția și crește greutatea relativă a clasatelor următoare: pe această frază, raportul dintre top token și al treilea scade de la 2,24 la la 1,50 la — rădăcina pătrată a primului, exact ce face împărțirea logits la doi asupra unui raport. Aceeași ordine, mai mult din attention-ul loss-ului pe ratările apropiate. Gradient-ul studentului poartă incertitudinea profesorului, nu doar verdictul lui.
Ce încape în 8, 16 și 24 GB
Link către secțiunea: Ce încape în 8, 16 și 24 GBTotul în acest capitol este acum o singură sumă:
unde este numărul total de token rezidenți peste toate request-urile concurente. Aplicând-o: rândurile 7B și 70B presupun 8 key-value heads de dimension 128, rândul 13B full multi-head attention cu 40 de heads, așa cum au fost construite acele generații de model — și se vede.
8 GB
| model | precision | ponderi | liber după overhead | token de context care încap |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | nu încape | — |
| 7B | int8 | 6,5 GB | nu încape | — |
| 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 | nu încape | — |
16 GB
| model | precision | ponderi | liber după overhead | token de context care încap |
|---|---|---|---|---|
| 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 | ponderi | liber după overhead | token de context care încap |
|---|---|---|---|---|
| 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 | nu încape | — |
Uită-te la rândul 13B din tabelul de 8 GB. Ponderile încap — 6,2 GB din 8 — deci, în modul obișnuit de a vorbi, un model 13B „rulează pe o placă de 8 GB”. Are 337 de token de context, ceea ce nu este o conversație, ci abia un prompt. „Încape?” este întrebarea greșită. Întrebarea corectă este „cu cât context și pentru câți useri deodată”.
Uită-te și la cele două rânduri int8 din tabelul de 16 GB. 7B primește 65.378 de token, iar 13B primește 3.136 — o diferență de douăzeci de ori din 5,6 GB de ponderi în plus, pentru că 13B aici are multi-head attention și cache-ul lui costă 800 KB per token față de 128 KB pentru 7B. Două modele de dimensiune similară, unul inutilizabil pentru context lung, dintr-un motiv care nu apare în titlul niciunui model card.
Unde merge mai departe
Link către secțiunea: Unde merge mai departeAcum treisprezece capitole, acesta era un perceptron cu două ponderi și un bias. Acum este un transformer care a fost proiectat, trained, aligned, învățat să cheltuiască compute pe întrebări grele și served la un cost măsurat per token — fără nicio cutie rămasă nedeschisă în el.
Aici se termină, și se termină intenționat.
Capitolul 14 începe cu modelul altundeva. Nu în procesul tău, nu în memoria ta, nu într-o variabilă pe care o poți printa: pe o mașină pe care nu o administrezi, în spatele unei chei API, al unui port și al unei facturi. Tot ce s-a măsurat aici se întâmplă încă — prefill rulează încă înainte de primul token, cache-ul încă crește odată cu conversația, batch-ul în care ești încă aparține altcuiva și încă îți decide latența — dar de acum îl observi printr-un stream de Server-Sent Events, un finish_reason și un HTTP 429 cu un header Retry-After. Întrebările se schimbă odată cu punctul de observație: nu cum se calculează acest gradient, ci de ce mi s-a triplat factura. Se schimbă și limbajul, iar Capitolul 14 explică această regulă în loc să o anunțe — până aici codul ținea ponderi, gradients, logits și bytes de tokenizer; de acolo încolo ține o conexiune, un retry, o anulare și state acumulat. Cele treisprezece capitole din spate nu sunt aruncate la trecere. Ele sunt descrierea a ceea ce rulează de cealaltă parte a portului.
Surse și metodă
Link către secțiunea: Surse și metodăDouă omisiuni sunt deliberate. FlashAttention (Dao et al., arXiv:2205.14135) nu este o attention diferită — calculează aceeași funcție prin tiling-ul operației astfel încât matricea de scoruri să nu fie niciodată scrisă în memorie, motiv pentru care cei 67 MB din al doilea tabel al acestui capitol sunt mai mici în practică decât sugerează aritmetica. Iar kernels înșiși sunt delegați: cursul 10 din CS336 de la Stanford acoperă sistemele de inference în profunzimea pe care acesta nu o încearcă, iar repository-ul llama.cpp și specificația GGUF sunt sursele primare pentru partea de CPU.
Referințe
Link către secțiunea: Referințe-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Lucrarea este în mare parte un argument despre memory-bandwidth și se citește ca atare. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Include rețeta de uptraining care convertește un checkpoint multi-head existent, motiv pentru care GQA s-a răspândit atât de repede. ↩
-
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. Introduce scheduling la nivel de iterație — continuous batching — și selective batching. ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. Lucrarea pe care este construit vLLM; §3 este analogia completă cu sistemele de operare. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 este definit în §3; cele șaisprezece valori de nivel folosite în măsurătoarea de mai sus sunt cele derivate de această lucrare. ↩
-
Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). Analiza outlier-feature din §4 este sursa fenomenului măsurat mai sus, inclusiv constatarea că outliers apar sistematic la scară. ↩ ↩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). Teorema 1 este demonstrația că distribuția de output rămâne neschimbată; Chen et al. (arXiv:2302.01318) au publicat aceeași idee independent. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Temperatura și argumentul „dark knowledge”. ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, cu nouă ani mai devreme, pentru ensembles, nu pentru transformers. ↩