Ugrás a tartalomra
13/3013/30. fejezet

Olcsóbb inferencia: KV cache, batching és kvantálás

Ugyanaz a modell ugyanarra a kérdésre 8,8 és 78,9 mp alatt, byte-ra azonos kimenettel. Aztán INT4, háromféleképp mérve.

Ezen az oldalon

Ugyanaz a modell, ugyanazon a gépen, ugyanarra a kérdésre válaszolva ugyanazzal a 48 token mennyiséggel. A két kimenet tokenről tokenre azonos — ellenőrizve, nem feltételezve.

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)

Egyetlen argumentum változott: use_cache=False. Semmi sem más a modellben, a promptban, a samplingban vagy az aritmetikában, és a második futás a fáradságért cserébe nem pontosabb. Kilencszer lassabb a semmiért.

Erről szól ez a fejezet. Minden benne — a cache, a batch, a kvantált súlyok — arra tett kísérlet, hogy ne fizessünk olyan munkáért, amely nem változtatja meg a választ, vagy hogy kiderítsük, mennyibe kerül egy olcsóbb válasz. A 10. fejezet megadta a betanítás árlistáját. Ez annak az oldalnak az árlistája, amelyért örökké fizetsz: egy telepített modell nagyjából 2N2N FLOP-ot költ minden kibocsátott token esetén, minden kérésnél, élete végéig.

Egy token generálásához egy decoder-only transformer veszi az eddigi teljes szekvenciát, végigfuttatja minden rétegen, majd az utolsó pozícióból kiolvassa a valószínűségi eloszlást. Ezután hozzáfűzi a kiválasztott token elemet, és újra megteszi ugyanezt. Ez a leírás helyes, és pontosan ezt csinálja a lassú futás.

Csakhogy ez óriási pazarlás is, ennek oka pedig a 9. fejezet causal maskja. A 7. pozíció key és value vektorai a 7. pozíció bemenetéből és az előtte lévő pozíciókból számolódnak. Amikor megérkezik a 8. pozíció, a 7. pozíció nem láthatja — ezt jelenti a causal —, ezért a 7. pozíció key és value értékei pontosan ugyanazok a számok, mint korábban. A lassú futás mégis újraszámolja őket, minden lépésben.

Tehát tárold el őket. Ez a tároló a key-value cache, a nyelvi modellek servingjének egyetlen legfontosabb optimalizációja:

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)

Nézd meg, mi kerül a modellbe a cikluson belül: nxt, egyetlen token. Nem a szekvencia. Az új token query-je minden cache-elt key ellen végez attentiont, a cache-elt key értékek pedig eleve nem változhattak volna. Ez nem közelítés — a fenti azonos kimenetet ellenőrző próba épp erről szól. A cache nem minőséget cserél sebességre; redundáns aritmetikát töröl.

A skálázódás tiszta megmutatásához vedd ki a transformer részt, és mérj meg egyetlen attention headet d=64d = 64 mellett, a generálás egy lépését mindkét módon számolva:

token a contextbenminden újraszámolásacache használatávalarányscore mátrix
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

A jobb oldali oszlop az ok. Az újraszámolás minden lépésben felépíti a teljes n×nn \times n attention mátrixot — a 9. fejezet aszimptotikus jelölésről szóló dobozából ismert O(n2)O(n^2) költséget, minden token után egyszer. Cache használatával ehelyett egy 1×n1 \times n sort építesz: 4 096 token esetén 67 MB score áll szemben 16 KB-tal.

Ha milliszekundumok helyett multiply-accumulate műveleteket számolunk, a gép kikerül az érvelésből. TT token generálásához cold startból:

generált tokencache használatávalújraszámolvaarány
1282,6 M192,0 M73x
51223,1 M7,36 G318x
2048293,7 M392,6 G1,336x

Lépésenként a cache-elt változat lineáris a context méretével, a cache nélküli pedig négyzetes; egy teljes generáláson összegezve O(T2)O(T^2) áll szemben O(T3)O(T^3) értékkel, az arány pedig korlátlanul nő. A nyitó példában mért kilencszeres különbség 48 token alatt jött ki — jóval az első táblasor alatt.

A cache azt is megváltoztatja, minek kell memóriában lennie. Egy 8 GB-os laptop GPU-n 256 token fp16 generálásánál, az allocator csúcsértékéből kivonva a rezidens súlyokat:

csúcs munkamemória
cache használatával21,8 MB
újraszámolva181,7 MB

8,3-szor több memória, ugyanazoknak a token elemeknek a lassabb előállítására. Ez az 5. fejezetben tett ígéret, váratlan irányból megérkezve: ott a reverse-mode autodiffnek életben kellett tartania minden köztes értéket a backward passhoz, és az aktivációk uralták a betanítás memóriáját. Inference során nincs backward pass, és nincs mit megőrizni hozzá — így a memóriát ehelyett a cache uralja, ez pedig tudatos választás, nem elkerülhetetlen költség.

A prefill és a decode két különböző gép

Link a szakaszhoz: A prefill és a decode két különböző gép

Nézd meg újra a gyors futást: az első token másképp viselkedett, mint a másik negyvenhét.

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

A prompt költsége tokenenként 25,6 ms volt, minden generált token pedig 166 ms-ba került. Ugyanaz a modell, ugyanaz a hardver, ugyanazok a súlyok, hatszoros különbség tokenenként — ráadásul abba az irányba, amit a legtöbben nem várnak. A prompt az olcsó rész. A generálás két olyan fázisra válik szét, amelyek fizikája valóban eltér:

Egy forward pass a teljes prompton. Minden token párhuzamosan feldolgozódik, így minden súlymátrix egyszer töltődik be memóriából, és több száz token vektor mátrixával szorzódik — mátrix-mátrix szorzat, sok aritmetikával minden mozgatott byte után, pontosan az, amire egy GPU készült. A prefill compute-bound, költsége pedig nagyjából lineáris a prompt hosszával.

Egy forward pass tokenenként, egyes batch és egyes szekvencia mellett. Minden súlymátrix továbbra is teljes egészében betöltődik memóriából, és egy egyetlen vektorral szorzódik — mátrix-vektor szorzat, szinte semmi aritmetikával minden mozgatott byte után. A decode memory-bandwidth-bound, és tokenenkénti költsége alig függ a context hosszától.

Mindkét fél mérhető. Prefill, egy pass PP tokenen:

prompt tokenmásodpercms tokenenként
160,351521,97
320,525416,42
641,049116,39
1281,655212,93
2563,096512,10

Decode, egy token CC méretű cache ellen:

cache-elt tokenms egy tokenre
16110,05
6497,57
256108,53
1024103,86

Olvasd el kétszer a második táblát. Ha 16 token contextből 1 024-re lépünk — hatvannégyszer több történet, amin attentiont kell végezni —, egy lépés költsége mérhetően nem változott. A cache elleni attention valódi munka, de eltörpül amellett a fix költség mellett, hogy félmilliárd súlyt át kell húzni a memória buszon egyetlen vektor előállításához. Ez a fix költség az oka mindennek a következő szakaszban.

Ez a két fázis az eredete annak a két számnak, amelyet minden serving rendszer jelent. A time to first token lényegében prefill, és a prompttal együtt nő, ezért érződik lassú indulásúnak egy hosszú beszélgetés. A tokens per second értéke 1/decode step1/\text{decode step}, és nagyjából állandó, ezért folyik utána egyenletesen a válasz. Egy lassan induló, majd simán streamelő chat nem renderelési trükk. Ez a két tábla.

A cache aritmetikát cserél memóriára, és a kívánt memória nem kicsi. A context minden token eleme után minden réteg egy key vektort és egy value vektort tart minden key-value headhez:

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}

A 2 a key és value miatt van; minden más az architektúra. A fejezetben végig mért modellnél — 24 réteg, 14 query head, 2 key-value head, 64-es head dimenzió — fp16-ban ez 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 byte tokenenként.

Ezen a területen a képletek hajlamosak kettes faktorral tévedni, ezért ellenőrizd az allocatoron, ne csak hidd el:

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

Pontos, és pontos marad minden kipróbált shape esetén:

batchcontextmért cacheelőrejelzettcsúcs munkamemória
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

Az utolsó három sort érdemes újra megnézni. Harminckét felhasználó fejenként 2 048 tokennel, hatvannégy 1 024-gyel, százhuszonnyolc 512-vel — a cache mindhárom esetben 768 MB, mert mindhárom összesen 65 536 token elemet tart. A cache csak a rezidens token elemek teljes számától függ, nem attól, hogyan oszlanak meg a felhasználók között. Ez a tény a batching szakasz alapja.

A 9. fejezet bevezette a multi-query és grouped-query attentiont, az okot pedig erre a fejezetre halasztotta. Az ok ez a képlet, különösen a benne lévő HkvH_{kv}.

A standard multi-head attention minden query headnek saját key és value headeket ad. Az itt szereplő modellnek 14 query headje van; teljes multi-head attention mellett a cache mérete 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 byte lenne tokenenként — 84 KB 12 KB helyett, pontosan hétszer több, a query headek és key-value headek aránya.

A multi-query attention1 ezt a végletekig viszi: minden query head egyetlen key-value headet oszt meg. A grouped-query attention2 a győztes kompromisszum — néhány key-value head, mindegyiket query headek egy csoportja osztja meg —, mert az MQA minőségvesztése valós volt, a GQA-é pedig nem az. Egyik sem vesz aritmetikát. Azért léteznek, hogy ezt a képletet elosszák egy egész számmal, és abban a pillanatban elterjedtek az iparágban, amikor a hosszú contextek miatt a cache lett a kötő korlát.

Ami gyorsan megtörténik. Egy 7B-osztályú, 32 rétegű, 8 darab 128 dimenziós key-value headdel rendelkező modellnél a cache fp16-ban 128 KB tokenenként:

context tokenegy felhasználó8 felhasználó64 felhasználó
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

Ennek a modellnek a saját súlyai fp16-ban 13,0 GB-ot foglalnak, ahogy a fejezet végi táblázatban szerepel. Így 128 000 tokenes contextnél egy felhasználó cache-e nagyobb, mint a modell. Ezt az aritmetikát fordítja pénzre a 16. fejezet, és ezért nem pusztán lassú egy hosszú beszélgetés — a kérés teljes élettartama alatt lefoglal egy fix szeletet egy gépből.

Batching: a szám, amely nő, és a szám, amely csökken

Link a szakaszhoz: Batching: a szám, amely nő, és a szám, amely csökken

A decode memory-bound: a súlyok áthúzódnak a buszon egy token előállításához, az aritmetikai egységek pedig tétlenek. Tegyél tehát több munkát ugyanabba a lépésbe. Futtass több kérést egyszerre, és az egyszer beolvasott súlyok mindegyiket kiszolgálják. Ugyanazon a modellen mérve, minden kérés 64 tokenes cache-t tartva és egy token elemet dekódolva:

batchkésleltetés lépésenkéntthroughputkésleltetés B=1-hez képest
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

Olvasd egymással szembe a két jobb oldali oszlopot, mert ez a lényeg. Egy kérésről tizenhatra lépve a throughput 6,0-szorosára nő, miközben bármely egyedi kérés várakozása 2,67-szeresére nő. A batch jobbá tette a szervert, és rosszabbá minden felhasználót.

Ez nem olyan hiba, amit ki lehet hangolni; ez maga a csere, és mindkét oldalnak neve van. A latency az, amit a válaszra váró ember tapasztal. A throughput az, amivel a számlát elosztják. Nincs olyan beállítás, amely mindkettőt javítja.

Figyeld meg azt is, hol áll meg. 16-ról 32-re a throughput 9%-ot nyer, miközben a latency majdnem megduplázódik: a lépés már nem memory-bound, hanem compute-bound lett, és ezen a térden túl a batch semmit sem vesz. Minden deploymentnek van ilyen térde; a helyét a sajátodon kell megmérni, de a létezését nem.

A statikus batching elpazarolja annak nagy részét, amit megnyer

Link a szakaszhoz: A statikus batching elpazarolja annak nagy részét, amit megnyer

A naiv batching úgy működik, hogy összegyűjt BB kérést, együtt futtatja őket, majd akkor tér vissza, amikor mind kész. De nem együtt készülnek el: egyes válaszok húsz token hosszúak, mások ötszáz. Egy fix batch addig fut, amíg a leghosszabb tagja be nem fejeződik, és minden kész kérés addig is elfoglalja a helyét, paddinget termelve.

Vegyünk 64 kérést realisztikusan ferde kimeneti hosszakkal — medián 18 token, leghosszabb 231, összesen 1 874 —, és szimuláljuk mindkét policy-t a nyolc slotra mért lépésenkénti költséggel:

policyfalióra-időthroughputátlagos latency kérésenkéntelpazarolt slot-lépések
statikus 8-as batchek176,9 s10,6 tok/s83,2 s3,214
continuous, 8 slot109,0 s17,2 tok/s8,1 s0

A throughput 1,6x javul. Az átlagos latency több mint tízszeresen javul, mert statikus batching alatt egy négy lépésben végzett kérés még mindig megvár egy 231 tokenes szomszédot, mielőtt bárki hallana róla.

A continuous batching3 a megoldás, és pontosan olyan egyszerű, amilyennek hangzik: a batch nem csoport, hanem slotok halmaza, és egy felszabaduló slot a legközelebbi lépésben beengedi a következő sorban álló kérést. Az ütemező egy token, nem pedig egy kérés granularitásán dolgozik. Minden production serving stack ma már ezt csinálja.

Van egy második fele is: a cache. A be- és kilépő slotok fragmentálják a cache memóriát, és ha minden slotnak lefoglalod a lehető legnagyobb contextet, a foglalás nagy része kárba vész. A PagedAttention4 az operációs rendszerektől kölcsönzi a választ: a cache fix méretű blokkokban tárolódik, szekvenciánként blokktáblával, így egy szekvencia cache-e fizikailag szétszóródhat, miközben logikailag összefüggő marad — ez pedig azt is lehetővé teszi, hogy két közös prefixű szekvencia megossza az azt tartó blokkokat. Erre épül a vLLM, és ezért egy serving engine valójában egy memóriakezelő, amelyre rácsatoltak egy transformert.

Kvantálás, és az első dolog, ami elromlik

Link a szakaszhoz: Kvantálás, és az első dolog, ami elromlik

A számla másik fele maguk a súlyok. Félmilliárd paraméter négy byte-tal számolva 1,98 GB; két byte-tal 0,99 GB; egy byte-tal 0,49 GB. Kevesebb bit súlyonként kisebb modellt jelent lemezen, kisebbet memóriában, és — mivel a decode bandwidth-bound — minden lépést gyorsabbá tesz, mert kevesebb byte-ot kell mozgatni.

A legegyszerűbb séma a szimmetrikus abszolút maximum kvantálás, és három sorban elfér:

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álassz egy skálát úgy, hogy a legnagyobb súly a legnagyobb egész számra képeződjön le, ossz, kerekíts, tárold az egész számokat és a skálát. Rekonstruálj visszaszorzással. Nincs benne semmi okos, és működik — egészen addig, amíg nem.

A modell valódi súlyain mérve: mind a 168 projekciós mátrix, 357,8 millió paraméter, relatív hiba WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

sémaátlagos relatív hibalegrosszabb mátrix
INT8, egy skála a teljes mátrixra0,04000,1487
INT8, egy skála kimeneti soronként0,01000,0149
INT4, egy skála a teljes mátrixra0,60260,9931
INT4, egy skála kimeneti soronként0,17900,2589
INT4, egy skála 128-as csoportonként0,13230,1992
NF4, egy skála 64-es blokkonként0,09520,1205
INT3, egy skála 128-as csoportonként0,30440,4123
INT2, egy skála 128-as csoportonként0,77900,8076

A negyedik sor az összeomlás. A 0,99-es relatív hiba a legrosszabb mátrixon azt jelenti, hogy a rekonstrukció lényegében semmit sem őriz meg az eredetiből — a mátrixot nagyjából megfelelő nagyságrendű zaj váltotta fel. Az ok ugyanebben a kísérletben látható egyetlen mátrixon:

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

Hatezerből egy súly hat szórásnál is messzebb ül, a legnagyobb pedig 24-re. Egyetlen skálával a teljes mátrixra ez az egy súly állítja be a lépésközt mind a 4,3 millió súlyhoz. 8 biten 256 lépés van, és a tipikus súly még mindig értelmes lépcsőre esik. 4 biten 16 van, a legkülső pedig egy olyan értéknek van fenntartva, amilyen szinte sehol sincs, így a hétköznapi súlyok — vagyis gyakorlatilag mind — két-három különböző szintre kerekednek.

A sor után minden ugyanennek a javításnak más granularitása: adj kisebb területet a skálának. Kimeneti soronként 3,4-gyel osztja a hibát; 128 egymást követő súlyból álló csoportonként újra osztja. A költség könyvelés — egy 16 bites skála 128-as csoportonként 4+16/128=4.1254 + 16/128 = 4.125 bit súlyonként 4 helyett —, és visszaveszi a rés nagy részét.

Az NF4 a másik oldalról közelít.5 A szinteknek nem kell egyenlő távolságra lenniük. Egy blokkon belül a súlyok közel normális eloszlásúak, ezért válaszd a tizenhat szintet egy normális eloszlás kvantiliseiként: sűrűn nulla közelében, ahol a súlyok tényleg vannak, ritkán a széleken, ahol nincsenek. Ugyanaz a négy bit, ugyanaz a blokkskálázás, kisebb blokkon — 4,25 bit súlyonként a group-128 4,125 értékével szemben —, és a mért hiba 0,1323-ról 0,0952-re esik, 28%-kal alacsonyabbra. Ennek egy része a finomabb blokk, a többi az, hogy a szintek oda kerülnek, ahol a tömeg van; a kettő szétválasztásához kellene egy harmadik sor.

A 2. fejezet lebegőpontos doboza ígérettel zárult: ebben a fejezetben 8 és 4 bitre kvantáljuk a súlyokat, és találunk néhány outlier feature-t, amely nem hajlandó összepréselődni. Itt vannak, és megmagyarázzák, miért nem működhetett az aktivációkon a „csak kerekítsük a számokat” megközelítés.

A fenti súlyok rosszul viselkedtek. Az aktivációk egészen más ligában játszanak. Vegyünk egy hétköznapi, 84 token hosszú promptot, rögzítsük a residual streamet minden rétegnél, és mérjük meg, az 896 dimenzió mindegyike mekkora legnagyobb magnitúdót ér el:

réteglegnagyobb |h|medián dimenzió legnagyobb |h| értékearánydimenziók a medián 6x értéke felett
16,190,33918x2
41543,481,550996x34
81571,631,4981049x36
121575,031,5461019x34
161579,601,617977x32
201577,982,361668x24
24204,4410,76019x12

A 62. dimenzió eléri az 1 579,6-ot, miközben a medián dimenzió soha nem haladja meg az 1,6-ot. Nem egy token vagy egy réteg véletlenje: ugyanaz a dimenzió ott van a 4. rétegnél, és még mindig ott van a 20.-nál is, szinte ugyanazzal az értékkel. Ezek az outlier feature-ök,6 és rendszerszerűek — a betanított modell tulajdonságai, nem a bemeneté.

A 896 dimenziónkénti maximum hisztogramja a 16. rétegnél félreérthetetlenné teszi az alakot:

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

Kilencszáz dimenzió rendezett kupacban 8 alatt, három oktávon át semmi, majd egyetlen dimenzió magában a távoli végén. Most kvantáld ezt a tenzort INT8-ra, és számold meg, mi történik:

sémarelatív hibahasznált különböző egész szintek, teljes tenzor
egy skála a teljes tenzorra0,108314 a 256-ból
egy skála tokenenként (soronként)0,0433158
teljes tenzor, 1 outlier dimenzió fp32-ben tartva0,044248
teljes tenzor, 4 outlier dimenzió fp32-ben tartva0,027957
teljes tenzor, 16 outlier dimenzió fp32-ben tartva0,0085102

Tizennégy szint 256-ból. A skálát 1 579,6 állította be, ezért minden lépés 12,44 széles, és a tipikus aktivációnak — medián magnitúdó 0,26, kilencvenkilencedik percentilis 2,51 — nincs hová esnie. Dimenziónként még élesebb:

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

Egy szint. Az egész dimenzió, minden token, ugyanarra a számra kvantálva. Nyolc bit lett kiosztva, és nagyjából nulla lett felhasználva, a modell pedig, amely ezeket az aktivációkat olvassa, egy konstans értéket kap.

Ez a mérés igazolja az összes olyan technikát, amelyet az emberek ténylegesen használnak:

Tartsd távol az outliereket. Az LLM.int8()6 felbontja a mátrixszorzást: a szélsőséges magnitúdójú dimenziók 16 biten számolódnak, minden más INT8-ban, majd a felek összeadódnak. A fenti tábla a nyugta — négy dimenzió eltávolítása majdnem négyszeresére csökkenti a hibát. A SmoothQuant7 ehelyett áthelyezi a nehézséget: az aktivációkat elosztja egy csatornánkénti faktorral, a megfelelő súlyoszlopot pedig megszorozza vele; ez változatlanul hagyja a szorzatot, és az outliert abból a tenzorból, amely nem tudja elnyelni, átteszi abba, amelyik igen.

Válaszd meg a kerekítést, ne csak kerekíts. A fentiek közül semmi sem kérdezi meg, mire való a mátrix. A GPTQ8 oszlopról oszlopra kvantál, és mindegyik után igazítja a megmaradó teljes pontosságú oszlopokat, hogy kompenzálja a már elkövetett hibát — a réteg kimenetének hibáját minimalizálva valós bemeneteken, nem a súlyaiét. Az AWQ9 megfigyeli, hogy a súlycsatornák kis része sokkal többet számít, mint a többi, aktivációs statisztikákból megtalálja őket, és kvantálás előtt felskálázza, hogy finomabb szintekre essenek. Mindkettőhöz kalibrációs készlet kell; egyikhez sem kellenek gradientek.

Részletek megjelenítése

GGUF, és mi köze egy fájlformátumnak mindehhez.

A GGUF nem kvantálási módszer; ez az a konténer, amelyet a llama.cpp használ, és a gguf vs gptq összehasonlítások zavara abból jön, hogy a kettőt ugyanannak a dologtípusnak kezelik. A GGUF tenzorokat, tokenizer elemet, architektúra-metaadatokat és chat template-et tart egyetlen memory-mappable fájlban, és egy családnyi blokksémát hordoz magában — az olyan nevek, mint Q4_K_M, a bit/súly értéket, a blokkméretet és azt kódolják, hogy egyes tenzorok magasabb pontosságon maradnak-e.

A fontos mérnöki különbség: a GPTQ és az AWQ GPU kernelre optimalizált súlyokat állít elő, míg a GGUF sémái olcsón dekódolhatók CPU-n, mapped fájlból, betöltés helyett. Ezért létezik ugyanaz a névleges „4 bites 7B modell” mindkét világban eltérő méretekkel és eltérő minőséggel, és ezért az őszinte összehasonlítás soha nem a formátum — hanem az alábbi mérés, a saját feladatodon futtatva.

Mibe kerül valójában a kvantálás, mérve

Link a szakaszhoz: Mibe kerül valójában a kvantálás, mérve

Szinte minden kvantálásról szóló cikk az előző szakasznál megáll: elmagyarázza a módszert, idéz egy tömörítési arányt, és kijelenti, hogy a minőség „nagyrészt megmarad”. A 4. fejezet arról szólt, hogyan ne csapd be magad, ezért derítsük ki.

Ugyanaz a modell, a súlyok helyben kvantálva minden sémával, majd három mérés: perplexity 2 048 tokennyi félretett angol prózán — itt e kurzus vázlatán, ezért a repository egy fix public-domain könyvre cseréli, és ugyanolyan alakú, más számokat tartalmazó táblát nyomtat —, 16 rövid, ismert válaszú tényszerű kérdésből álló battery greedy decoding alatt, valamint annak hányada, hogy a kvantált modell azonos context mellett ugyanazt a token elemet választja-e, mint a teljes pontosságú.

sémaátlagos súlyhibaperplexitykérdés-batteryegyezés fp32-vel
fp32 (referencia)0,000023,0813/16100,0 %
INT8 tenzoronként0,040023,5813/16
INT8 soronként0,010022,9613/1698,6 %
INT4 tenzoronként0,6026365,416,0000/16
INT4 soronként0,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 %

Négy dolgot érdemes ebben a táblában kimondani egyenesen.

A jól megcsinált INT8 ingyen van. A soronkénti INT8 22,96-ot ér el a referencia 23,08 értékével szemben — kétszázadrésznyi rés, ami zaj, és „azonosként” olvasandó. Hogy a zaj melyik irányba mutat, nem stabil: a repository public-domain korpuszán ugyanez a két séma 22,24 és 22,18 értékre jön ki, fele ekkora távolsággal, a másik irányba mutatva. A 144 generált tokenből 142-nél egyezik a teljes pontosságú modellel. Az fp32 referenciához képest negyed memória, a ténylegesen telepítendő fp16-hoz képest fele, és kimutatható költség nélkül. A gondatlanul megcsinált INT8 is majdnem ingyen van: egy skála mátrixonként 0,5 perplexity pontba kerül, és nem vesz el battery-választ. A nyolc bit elég megbocsátó ahhoz, hogy a granularitás alig számítson, pontosan ezért általánosítanak az emberek INT8-ról INT4-re, és sérülnek meg.

Az egy skálás, tenzoronkénti INT4 elpusztítja a modellt. Perplexity 365 millió: nem romlott, megsemmisült. Ezután a granularitás a teljes játék — tenzoronként 365,416,000, soronként 46,18, 128-as csoportonként 31,08, NF4 24,55. Ugyanaz a négy bit súlyonként, tizenötmilliós faktor a legrosszabb és a legjobb között.

A perplexity durva műszer, a battery még durvább. Az NF4 és a group-128 INT4 között a perplexity rés 6,5 pont, a battery pedig egy kérdésben tér el — a 4. fejezet confidence intervalja szerint pedig tizenhatból egy kérdés semmit sem különböztet meg. Van az intervallumnál élesebb demonstráció: futtasd ugyanazt a battery-t úgy, hogy a modell alapértelmezett repetition penalty-je ki van kapcsolva, ami a greedy decoding tényleges jelentése, és ez a két sor helyet cserél. Tizenhatból egy kérdés nem kis hatás, hanem nincs hatás. A 8. fejezet figyelmeztetése is érvényes: a perplexity csak azonos tokenizert használó modellek között hasonlítható össze, ezért valaki más írásából származó szám nem vethető össze a tiéddel.

Az egyezési oszlop a három közül a legélesebb, és majdnem ingyen van: futtasd a teljes pontosságú modellt greedy módon, majd kérdezd meg a kvantáltat minden pozícióban, mit választott volna ugyanazzal a prefixszel. 16 helyett 144 független megfigyelése van, nem kell hozzá ground truth, és simán romlik ott, ahol a battery ugrásokban romlik. Pontosan ez az a mennyiség is, amelyre a következő szakasznak szüksége van.

Ez az ígéret, amelyet az 1. fejezet tett erről a fejezetről, menetrend szerint megérkezve: a matematika szerint lehetséges a 4 bites modell, a mérnöki munka dönti el, használható-e.

A 12. fejezet bejelentette ezt, és a számlát itt hagyta.

Az ötlet közvetlenül a prefill/decode szétválásából jön. Egy javasolt, γ\gamma tokenből álló szekvencia ellenőrzése egy forward pass γ\gamma pozíción — mátrix-mátrix szorzat, alig drágább, mint az egyen végzett pass. Tehát:

Egy kicsi, olcsó modell autoregresszíven generál γ\gamma jelölt token elemet.

A nagy modell egy forward passban fut le az összes γ\gamma jelöltön egyszerre, előállítva, mit mondott volna minden pozícióban.

Tartsd meg a leghosszabb prefixet, amelyben a kettő egyezik, plusz azt a token elemet, amelyet a nagy modell ingyen ad az első eltérésnél. Dobd el a többit, és kezdd újra.

A kimeneti eloszlás változatlan. Greedy decoding mellett ez nyilvánvaló — egy token csak akkor fogadódik el, ha a célmodell is azt állította volna elő. Sampling mellett módosított elfogadási szabály kell hozzá, Leviathan és társai pedig bizonyítják, hogy az eredő eloszlás pontosan a célmodellé.10 Ez a fejezet második exact optimalizációja.

Ezért minden az acceptance rate α\alpha értékén múlik, amely mérhető — ez a fenti egyezési oszlop, ezért számoltuk ki ott. Minden kvantált modellt draftként használva a teljes pontosságú célmodellhez, 144 generált pozíción:

draft modellacceptanceleghosszabb elfogadott futásvárható token cél-passonként, γ=4\gamma = 4
fp32 (maga a cél)100,0 %485,00
INT8 soronként98,6 %484,86
NF4 block 6484,7 %203,69
INT4 group 12871,5 %132,85
INT4 soronként58,3 %72,24
INT3 group 1285,6 %21,06
INT2 group 1280,0 %01,00

A verification passonként várhatóan elfogadott token szám draft hossz γ\gamma mellett:

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

és a nettó gyorsulás ezt osztja el a draft saját költségével, amely a cél tokenenkénti költségének cc hányada:

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

A félkövér bejegyzést kell megjegyezni: a speculative decoding lassabbá teheti a generálást. 30%-os acceptance mellett, amikor a draft a cél ötödébe kerül, öt forward passért fizetsz, és 1,4 token elemet tartasz meg. Az utolsó oszlop a másik csapda — a hosszabb draft csak magas acceptance mellett segít, mert egy γ\gamma tokenes tipp farka szinte soha nem érhető el. 90%-os acceptance mellett γ=8\gamma = 8 3,40x-et ér, 30%-nál 0,79x-et: ugyanaz a konfiguráció nyereség vagy veszteség, a saját forgalmadon mért számtól függően.

Distillation, és mit hordoz egy soft label

Link a szakaszhoz: Distillation, és mit hordoz egy soft label

A kvantálás úgy zsugorít egy modellt, hogy ugyanazt a függvényt kevesebb biten tárolja. A distillation úgy zsugorítja, hogy egy kisebb modellt tanít be egy nagyobb utánzására11 — ez az ötlet közel egy évtizeddel megelőzte a deep learninget.12

A finom rész az, hogy miből tanul a student. Nem a helyes válaszból: arra közvetlenül is betanítható lett volna. Amit a teacher hozzáad, az a teljes eloszlás. Kérdezd meg a modellt, mi követ egy kifejezést, és nézz túl az argmaxon:

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

A hard label azt mondja: jug, és semmi mást. A soft label azt mondja: jug, és azt is, hogy cup majdnem ugyanolyan jó volt, bowl plauzibilis, és large — egy melléknév, egy teljesen más nyelvtani folytatás — még mindig él. Ez az eredeti érv: ez egy 7-es, de elég sokban hasonlít egy 1-esre, és a hasonlóság olyan információ, amelyet a hard label kidob.

Ezért használ a distillation temperature-t is. Ha a logit értékeket TT értékkel osztjuk a softmax előtt, az ellapítja az eloszlást, és növeli a második helyezettek relatív súlyát: ennél a kifejezésnél a top token és a harmadik aránya T=1T = 1 mellett 2,24-ről T=2T = 2 mellett 1,50-re esik — az első négyzetgyökére, pontosan ezt teszi az aránnyal a logit értékek kettővel osztása. Ugyanaz a sorrend, több loss attention a közeli melléfogásokon. A student gradientje a teacher bizonytalanságát hordozza, nem csak az ítéletét.

Ebben a fejezetben minden most egyetlen összeg:

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

ahol TT az összes párhuzamos kérésben rezidens token teljes száma. Alkalmazva: a 7B és 70B sorok 8 darab 128 dimenziós key-value headet feltételeznek, a 13B sor teljes multi-head attentiont 40 headdel, mert az adott modellgenerációk így épültek — és ez látszik is.

8 GB

modelprecisionsúlyokoverhead után szabadelférő context token
7Bfp1613,0 GBnem fér el
7Bint86,5 GBnem fér el
7Bint4 (g128)3,4 GB3,1 GB25,710
13Bint4 (g128)6,2 GB0,3 GB337
70Bint4 (g128)33,6 GBnem fér el

16 GB

modelprecisionsúlyokoverhead után szabadelférő context token
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

modelprecisionsúlyokoverhead után szabadelférő context token
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 GBnem fér el

Nézd meg a 13B sort a 8 GB-os táblában. A súlyok elférnek — 6,2 GB a 8-ból —, tehát a szokásos beszédmód szerint egy 13B modell „fut 8 GB-os kártyán”. 337 tokennyi contextje van, ami nem beszélgetés, hanem alig egy prompt. A „belefér?” rossz kérdés. A helyes: „mekkora contexttel, és egyszerre hány felhasználónak?”

Nézd meg a két 16 GB-os int8 sort is. A 7B 65 378 tokenig jut, a 13B 3 136-ig — hússzoros különbség 5,6 GB extra súly miatt, mert a 13B itt multi-head attentiont használ, és cache-e tokenenként 800 KB-ba kerül a 7B 128 KB-jával szemben. Két hasonló méretű modell, az egyik használhatatlan hosszú contextre, olyan okból, amely egyetlen model card címében sem jelenik meg.

Tizenhárom fejezettel ezelőtt ez egy perceptron volt két súllyal és egy bias értékkel. Most már egy transformer, amelyet megterveztek, betanítottak, alignáltak, megtanítottak compute-ot költeni nehéz kérdésekre, és mért tokenenkénti költséggel serving alá tettek — úgy, hogy egyetlen doboz sem maradt benne bontatlanul.

Ez itt véget ér, és szándékosan ér véget.

A 14. fejezet úgy indul, hogy a modell valahol máshol van. Nem a folyamatodban, nem a memóriádban, nem egy változóban, amelyet ki tudsz printelni: egy olyan gépen, amelyet nem te adminisztrálsz, egy API key, egy port és egy számla mögött. Minden itt mért dolog továbbra is történik — a prefill továbbra is lefut az első token előtt, a cache továbbra is nő a beszélgetéssel, a batch, amelyben benne vagy, továbbra is valaki másé, és továbbra is eldönti a latency-det —, de innentől egy Server-Sent Events streamen, egy finish_reason elemen és egy Retry-After headerrel érkező HTTP 429-en keresztül figyeled. A kérdések a nézőponttal együtt változnak: nem az, hogy hogyan számolódik ez a gradient, hanem az, hogy miért triplázódott meg a számlám. A nyelv is változik, és a 14. fejezet ezt a szabályt magyarázza el, nem csak bejelenti — idáig a kód súlyokat, gradienteket, logit értékeket és tokenizer byte-okat tartott; onnantól kapcsolatot, retry-t, megszakítást és felhalmozott állapotot tart. A mögötted lévő tizenhárom fejezetet az átlépés nem dobja ki. Annak leírása, ami a port túloldalán fut.


Két kihagyás szándékos. A FlashAttention (Dao et al., arXiv:2205.14135) nem másfajta attention — ugyanazt a függvényt számolja ki úgy, hogy csempézi a műveletet, ezért a n×nn \times n score mátrix soha nem íródik ki memóriába, és ezért kisebb a gyakorlatban e fejezet második táblájának 67 MB-ja, mint amit az aritmetika sugall. Maguk a kernelek pedig delegálva vannak: a Stanford CS336 10. előadása olyan mélységben tárgyalja az inference rendszereket, amire ez nem vállalkozik, a llama.cpp repository és a GGUF specifikáció pedig a CPU-oldal elsődleges forrásai.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). A cikk nagyrészt memory-bandwidth érv, és annak is olvasható.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Tartalmazza az uptraining receptet, amely egy meglévő multi-head checkpointot alakít át, ezért terjedt el ilyen gyorsan a GQA.

  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. Bevezeti az iteration-level schedulinget — continuous batching — és a selective batchinget.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. A cikk, amelyre a vLLM épül; a 3. szakasz teljes egészében az operációs rendszerekkel vett analógia.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). Az NF4 a 3. szakaszban van definiálva; a fenti mérésben használt tizenhat szintérték az, amelyet ez a cikk vezet le.

  6. Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). A 4. szakasz outlier-feature elemzése a fent mért jelenség forrása, beleértve azt a megállapítást, hogy az outlierek skálán rendszerszerűen jelennek meg. 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). Az 1. tétel bizonyítja, hogy a kimeneti eloszlás változatlan; Chen et al. (arXiv:2302.01318) ugyanezt az ötletet függetlenül publikálta.

  11. Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). A temperature és a „dark knowledge” érv.

  12. Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, kilenc évvel korábban, ensemble-ökre, nem transformerekre.


Készítette

David Vicente Campos

A NeuraLIA Labs alapítója és a MyRealFood társalapítója

Mérnökinformatikus vagyok, a Leóni Egyetemen végeztem. Társalapítottam a MyRealFoodot, ahol CTO-ként felépítettem azt az alkalmazást, amelyet emberek milliói használtak arra, hogy egészségesebben táplálkozzanak, és megalapítottam a NeuraLIA Labst, ahol AI-termékeket fejlesztek. Itt arról írok, amit menet közben meg kellett értenem, úgy, ahogy szerettem volna, hogy valaki elmagyarázza nekem.

Továbbiak a szerzőről

Közzétette a NeuraLIA Labs.

Kapj új bejegyzéseket a postaládádba

AI-hírek, útmutatók és termékfrissítések — rövid email, amikor valami igazán hasznosat publikálunk.

Kurzusindex

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev11 perc olvasás

A Jev AI-modell döntésekre készült, nem prózára

A TypeSafe AI Jev modellje azért kap figyelmet, mert a szoftveres intelligenciát valószínűségi problémaként kezeli: válaszd ki a megfelelő ágat, rendelj hozzá bizalmi szintet, és ne fizess egy LLM-nek szövegírásért, amikor a kódnak döntésre van szüksége.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering11 perc olvasás

Kontextustervezés hosszú távú AI-ügynökökhöz

A hosszú ideig futó ügynökök nem csak azért vallanak kudarcot, mert kicsi az ablak. Akkor hibáznak, amikor a fájlok, eszközkimenetek és elavult előzmények kiszorítják azt a feladatot, amelyet az ügynöknek be kellett volna fejeznie.

Készen állsz, hogy a LIA válasszon helyetted?

Építs az összes AI-modellel egy helyen – kezdd el ma, ingyen.