Siirry sisältöön
13/30Luku 13/30

Halpaa inferenceä: KV cache, batching ja kvantisointi

Sama malli vastaa 8,8 ja 78,9 sekunnissa, byte-tarkasti samoin. Sitten INT4, mitattuna kolmella tavalla.

Tällä sivulla

Sama malli, samalla koneella, vastaamassa samaan kysymykseen samoilla 48 tokenilla. Kaksi tulostetta ovat identtisiä token tokenilta — tarkistettu, ei oletettu.

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)

Yksi argumentti muuttui: use_cache=False. Mikään mallissa, promptissa, samplingissa tai aritmetiikassa ei ole eri tavalla, eikä toinen ajo ole vaivansa vastineeksi tarkempi. Se on yhdeksän kertaa hitaampi ilman syytä.

Tämä on tämän luvun muoto. Kaikki siinä — cache, batch, kvantisoidut painot — on yritys lakata maksamasta työstä, joka ei muuta vastausta, tai selvittää, mitä halvempi vastaus maksaa. Luku 10 määritti hinnaston koulutukselle. Tämä on hinnasto puolelle, josta maksat ikuisesti: käyttöönotettu malli käyttää karkeasti 2N2N FLOPia jokaista tuottamaansa tokenia kohti, jokaisessa pyynnössä, koko loppuelämänsä ajan.

Tokenin generoimiseksi decoder-only transformer ottaa koko tähänastisen sekvenssin, ajaa sen jokaisen kerroksen läpi ja lukee todennäköisyysjakauman viimeisestä positiosta. Sitten se lisää valitun tokenin ja tekee saman uudelleen. Kuvaus on oikea, ja juuri niin hidas ajo toimii.

Se on myös valtavan tuhlaavaa, ja syy on luvun 9 causal mask. Position 7 key- ja value-vektorit lasketaan position 7 syötteestä ja sitä edeltävistä positioista. Kun positio 8 saapuu, positio 7 ei voi nähdä sitä — sitä causal tarkoittaa — joten position 7 key ja value ovat täsmälleen samat luvut kuin ennen. Hidas ajo laskee ne silti uudelleen joka askeleella.

Siispä tallenna ne. Tämä tallennus on key-value cache, kielenmallien servingin yksittäinen merkittävin optimointi:

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)

Katso, mitä mallille syötetään silmukan sisällä: nxt, yksi token. Ei sekvenssi. Uuden tokenin query attends jokaista cached keytä vasten, eivätkä cached keyt olisi koskaan muuttuneet. Tämä ei ole approksimaatio — yllä oleva identtisen tuloksen tarkistus on koko pointti. Cache ei vaihda laatua nopeuteen; se poistaa turhan aritmetiikan.

Jotta skaalaus näkyy puhtaasti, riisu transformer pois ja mittaa yhden attention headin aika arvolla d=64d = 64, yksi generointiaskele laskettuna molemmilla tavoilla:

tokenia kontekstissalaske kaikki uudelleencachellasuhdescore-matriisi
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

Oikeanpuoleinen sarake on syy. Uudelleenlaskenta rakentaa täyden n×nn \times n attention-matriisin joka askeleella — luvun 9 asymptotic-notation-laatikon O(n2)O(n^2), maksettuna kerran per token. Cachella rakennat sen sijaan 1×n1 \times n-rivin: 4,096 tokenilla 67 MB scoreja vastaan 16 KB.

Kun lasketaan multiply-accumulate-operaatioita millisekuntien sijaan, kone poistuu argumentista. Kun kylmästä alusta generoidaan TT tokenia:

generoidut tokenitcachellauudelleenlaskentasuhde
1282.6 M192.0 M73x
51223.1 M7.36 G318x
2048293.7 M392.6 G1,336x

Askelta kohti cached-versio on lineaarinen kontekstin suhteen ja uncached-versio neliöllinen; generoinnin yli summattuna O(T2)O(T^2) vastaan O(T3)O(T^3), ja suhde kasvaa rajatta. Aloituksen yhdeksänkertainen ero mitattiin 48 tokenilla — alle tuon taulukon ensimmäisen rivin.

Cache muuttaa myös sitä, minkä täytyy olla muistissa. 8 GB:n kannettavan GPU:lla generoitaessa 256 tokenia fp16:lla, kun allokaattorin huipusta vähennetään muistissa olevat painot:

työmuistin huippu
cachella21.8 MB
uudelleenlaskenta181.7 MB

8.3 kertaa enemmän muistia, käytettynä samojen tokenien tuottamiseen hitaammin. Tämä on luvussa 5 annettu lupaus odottamattomasta suunnasta: siellä reverse-mode autodiff joutui pitämään jokaisen välituloksen elossa backward passia varten, ja aktivoinnit hallitsivat koulutusmuistia. Inferencessä ei ole backward passia eikä mitään säilytettävää sitä varten — joten muistia hallitsee sen sijaan cache, ja se on tarkoituksellinen valinta eikä väistämätön kustannus.

Prefill ja decode ovat kaksi eri konetta

Linkki osioon: Prefill ja decode ovat kaksi eri konetta

Katso nopeaa ajoa uudelleen: sen ensimmäinen token käyttäytyi eri tavalla kuin muut neljäkymmentäseitsemän.

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

Prompt maksoi 25.6 ms per token ja jokainen generoitu token 166 ms. Sama malli, sama laitteisto, samat painot, kuusinkertainen ero per token — ja suunta on päinvastainen kuin useimmat odottavat. Prompt on halpa osa. Generointi jakautuu kahteen vaiheeseen, joilla on aidosti eri fysiikka:

Yksi forward pass koko promptin yli. Jokainen token käsitellään rinnakkain, joten kukin painomatriisi ladataan muistista kerran ja kerrotaan satojen token-vektorien matriisia vasten — matriisi-matriisi-tulo, jossa on paljon aritmetiikkaa siirrettyä tavua kohti, juuri sitä varten GPU on rakennettu. Prefill on compute-bound, ja sen kustannus on karkeasti lineaarinen promptin pituuden suhteen.

Yksi forward pass per token, batch yksi ja sekvenssi yksi. Jokainen painomatriisi ladataan silti muistista kokonaan ja kerrotaan yhtä vektoria vasten — matriisi-vektori-tulo, jossa on lähes ei lainkaan aritmetiikkaa siirrettyä tavua kohti. Decode on memory-bandwidth-bound, ja sen kustannus per token riippuu tuskin lainkaan kontekstin pituudesta.

Molemmat puolet ovat mitattavissa. Prefill, yksi läpivienti PP tokenin yli:

prompt-tokenitsekuntiams per token
160.351521.97
320.525416.42
641.049116.39
1281.655212.93
2563.096512.10

Decode, yksi token CC tokenin cachea vasten:

cached tokenitms yhdelle tokenille
16110.05
6497.57
256108.53
1024103.86

Lue toinen taulukko kahdesti. Siirtyminen 16 kontekstin tokenista 1,024:ään — kuusikymmentänelinkertaisesti enemmän historiaa attendattavaksi — ei muuttanut yhden askeleen kustannusta mitattavasti. Attention cachea vasten on todellista työtä, mutta se jää pieneksi verrattuna puolen miljardin painon raahaamiseen muistiväylän läpi yhden vektorin tuottamiseksi. Tämä kiinteä kustannus on syy kaikkeen seuraavassa osiossa.

Nämä kaksi vaihetta ovat alkuperä kahdelle luvulle, jotka jokainen serving-järjestelmä raportoi. Time to first token on käytännössä prefill, ja se kasvaa promptin mukana, minkä vuoksi pitkä keskustelu tuntuu hitaalta käynnistyä. Tokens per second on 1/decode step1/\text{decode step}, ja se on suunnilleen vakio, minkä vuoksi vastaus virtaa sen jälkeen tasaisesti. Chat, joka alkaa hitaasti ja sitten striimaa sulavasti, ei ole renderöintitemppu. Se on nämä kaksi taulukkoa.

Cache vaihtaa aritmetiikan muistiin, eikä sen haluama muisti ole pieni. Jokaiselle kontekstin tokenille jokainen kerros pitää yhden key-vektorin ja yhden value-vektorin per key-value head:

bytes per token=2×L×Hkv×dhead×bytes per element\text{bytes per token} = 2 \times L \times H_{kv} \times d_{\text{head}} \times \text{bytes per element}

2 tulee keyistä ja valueista; kaikki muu on arkkitehtuuria. Tässä luvussa kauttaaltaan mitatulle mallille — 24 kerrosta, 14 query headia, 2 key-value headia, head-dimensio 64 — fp16:lla tämä on 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 tavua per token.

Tämän alan kaavoilla on tapana olla pielessä kertoimella kaksi, joten tarkista se allokaattoria vasten äläkä vain usko:

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

Täsmälleen, ja se pysyy täsmällisenä jokaisessa kokeillussa muodossa:

batchkontekstimitattu cacheennustetyömuistin huippu
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

Viimeiset kolme riviä ansaitsevat toisen katseen. Kolmekymmentäkaksi käyttäjää, kullakin 2,048 tokenia; kuusikymmentäneljä, kullakin 1,024; satakaksikymmentäkahdeksan, kullakin 512 — cache on 768 MB joka tapauksessa, koska kaikissa kolmessa on 65,536 tokenia. Cache riippuu vain residenttien tokenien kokonaismäärästä, ei siitä, miten ne jakautuvat käyttäjien kesken. Tämä fakta on batching-osion perusta.

Luku 9 esitteli multi-query ja grouped-query attentionin ja lykkäsi syyn tähän lukuun. Syy on tuo kaava, ja erityisesti siinä oleva HkvH_{kv}.

Tavallinen multi-head attention antaa jokaiselle query headille omat key- ja value-headit. Tässä mallissa on 14 query headia; täydellä multi-head attentionilla sen cache olisi 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 tavua per token — 84 KB 12 KB:n sijaan, täsmälleen seitsemän kertaa enemmän, query headien ja key-value headien suhde.

Multi-query attention1 vie tämän äärirajalle: kaikki query headit jakavat yhden key-value headin. Grouped-query attention2 on voittanut kompromissi — kourallinen key-value headeja, joista kukin on query headien ryhmän jakama — koska MQA:n laatuhäviö oli todellinen ja GQA:n ei. Kumpikaan ei osta yhtään aritmetiikkaa. Ne ovat olemassa jakaakseen tuon kaavan kokonaisluvulla, ja ne levisivät alalle sillä hetkellä, kun pitkät kontekstit tekivät cachesta sitovan rajoitteen.

Ja niin tapahtuu nopeasti. 7B-luokan mallilla, jossa on 32 kerrosta ja 8 key-value headia dimensiolla 128, cache on 128 KB per token fp16:lla:

kontekstin tokenityksi käyttäjä8 käyttäjää64 käyttäjää
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

Tuon mallin omat painot ovat fp16:lla 13.0 GB, luku tämän luvun lopun taulukossa. Siis 128,000 tokenin contextissa yhden käyttäjän cache on suurempi kuin malli. Tämä on aritmetiikka, jonka luku 16 muuttaa rahaksi, ja siksi pitkä keskustelu ei ole vain hidas — se varaa kiinteän siivun koneesta niin kauan kuin pyyntö on elossa.

Batching: luku joka kasvaa ja luku joka pienenee

Linkki osioon: Batching: luku joka kasvaa ja luku joka pienenee

Decode on memory-bound: painot raahataan väylän läpi yhden tokenin tuottamiseksi, ja aritmetiikkayksiköt odottavat. Laita siis enemmän työtä samaan askeleeseen. Aja useita pyyntöjä kerralla, ja kerran luetut painot palvelevat niitä kaikkia. Mitattuna samalla mallilla, jokaisella pyynnöllä 64-tokeninen cache ja decodataan yksi token:

batchlatenssi per askelthroughputlatenssi vs B=1
10.1286 s7.78 tok/s1.00x
20.1839 s10.88 tok/s1.43x
40.1909 s20.95 tok/s1.49x
80.2781 s28.76 tok/s2.16x
160.3430 s46.64 tok/s2.67x
320.6302 s50.78 tok/s4.90x

Lue kaksi oikeanpuoleista saraketta toisiaan vasten, koska ne ovat koko pointti. Siirtyminen yhdestä pyynnöstä kuuteentoista moninkertaistaa throughputin 6.0:lla ja moninkertaistaa yksittäisen pyynnön odotuksen 2.67:llä. Batch teki palvelimesta paremman ja jokaisesta käyttäjästä huonomman.

Se ei ole bugi, joka viritetään pois; se on itse vaihtokauppa, ja sillä on nimi kummallakin puolella. Latenssi on se, mitä vastausta odottava ihminen kokee. Throughput on se, millä lasku jaetaan. Mikään asetus ei paranna molempia.

Huomaa myös, missä se pysähtyy. Välillä 16–32 throughput kasvaa 9 %, kun latenssi lähes kaksinkertaistuu: askel ei ole enää memory-bound vaan compute-bound, eikä batch osta mitään tuon polven jälkeen. Jokaisessa deploymentissa on tällainen polvi; sen sijainti täytyy mitata omallasi, mutta sen olemassaoloa ei.

Staattinen batching hukkaa suurimman osan siitä, minkä se voittaa

Linkki osioon: Staattinen batching hukkaa suurimman osan siitä, minkä se voittaa

Naiivi tapa batchata on kerätä BB pyyntöä, ajaa ne yhdessä ja palauttaa, kun kaikki ovat valmiita. Mutta ne eivät valmistu yhdessä: jotkut vastaukset ovat kaksikymmentä tokenia ja jotkut viisisataa. Kiinteä batch jatkuu, kunnes sen pisin jäsen valmistuu, ja jokainen valmis pyyntö jatkaa paikkansa varaamista, paddingia tuottaen, siihen asti.

Ota 64 pyyntöä realistisesti vinoutuneilla tulospituuksilla — mediaani 18 tokenia, pisin 231, yhteensä 1,874 — ja simuloi molemmat käytännöt mitatulla askelkustannuksella kahdeksalle paikalle:

käytäntöseinäkellothroughputkeskimääräinen latenssi per pyyntöhukatut paikka-askeleet
staattiset 8:n batchit176.9 s10.6 tok/s83.2 s3,214
jatkuva, 8 paikkaa109.0 s17.2 tok/s8.1 s0

Throughput paranee 1.6x. Keskimääräinen latenssi paranee yli kymmenkertaisesti, koska staattisessa batchingissa neljässä askeleessa valmistunut pyyntö odottaa silti 231-tokenista naapuria ennen kuin kukaan kuulee siitä.

Continuous batching3 on korjaus, ja se on juuri niin yksinkertainen kuin kuulostaa: batch ei ole ryhmä vaan joukko paikkoja, ja vapautuva paikka ottaa seuraavan jonossa olevan pyynnön heti seuraavalla askeleella. Scheduler toimii yhden tokenin eikä yhden pyynnön tarkkuudella. Jokainen tuotannon serving-stack tekee tätä nyt.

Sillä on toinenkin puoli, joka on cache. Tulevat ja menevät paikat jättävät cache-muistin pirstaleiseksi, ja jokaisen paikan suurimman mahdollisen kontekstin varaaminen hukkaa suurimman osan varauksesta. PagedAttention4 lainaa vastauksen käyttöjärjestelmiltä: tallenna cache kiinteänkokoisiin lohkoihin ja pidä lohkotaulukko per sekvenssi, jotta sekvenssin cache voi olla fyysisesti hajallaan mutta loogisesti yhtenäinen — mikä antaa myös kahden jaetun prefiksin sekvenssin jakaa sitä pitävät lohkot. Tälle vLLM on rakennettu, ja siksi serving engine on muistiallokointijärjestelmä, johon on kiinnitetty transformer.

Kvantisointi ja ensimmäinen asia joka menee pieleen

Linkki osioon: Kvantisointi ja ensimmäinen asia joka menee pieleen

Laskun toinen puoli ovat itse painot. Puoli miljardia parametria neljällä tavulla kukin on 1.98 GB; kahdella tavulla 0.99 GB; yhdellä tavulla 0.49 GB. Vähemmän bittejä per paino kutistaa mallia levyllä, kutistaa sitä muistissa ja — koska decode on bandwidth-bound — tekee jokaisesta askeleesta nopeamman, koska siirrettäviä tavuja on vähemmän.

Yksinkertaisin menetelmä on symmetrinen absolute-maximum-kvantisointi, ja se mahtuu kolmeen riviin:

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

Valitse skaala niin, että suurin paino osuu suurimpaan kokonaislukuun, jaa, pyöristä, tallenna kokonaisluvut ja skaala. Rekonstruoi kertomalla takaisin. Siinä ei ole mitään nokkelaa, ja se toimii — kunnes ei toimi.

Mitattuna mallin todellisilla painoilla: kaikki 168 projektiomatriisia, 357.8 miljoonaa parametria, suhteellinen virhe WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

menetelmäkeskimääräinen suhteellinen virhehuonoin matriisi
INT8, yksi skaala koko matriisille0.04000.1487
INT8, yksi skaala per tulosrivi0.01000.0149
INT4, yksi skaala koko matriisille0.60260.9931
INT4, yksi skaala per tulosrivi0.17900.2589
INT4, yksi skaala per 128:n ryhmä0.13230.1992
NF4, yksi skaala per 64:n lohko0.09520.1205
INT3, yksi skaala per 128:n ryhmä0.30440.4123
INT2, yksi skaala per 128:n ryhmä0.77900.8076

Neljäs rivi on romahdus. Suhteellinen virhe 0.99 huonoimmassa matriisissa tarkoittaa, että rekonstruktio ei säilytä alkuperäisestä käytännössä mitään — matriisi on korvattu suunnilleen oikeansuuruisella kohinalla. Syy näkyy saman kokeen yhdessä matriisissa:

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

Yksi paino kuudestatuhannesta on yli kuuden keskihajonnan päässä, ja suurin on 24:n päässä. Kun koko matriisille on yksi skaala, tuo yksi paino määrittää askelkoon kaikille 4.3 miljoonalle. 8 bitillä askelia on 256 ja tyypillinen paino osuu silti merkitykselliselle tasolle. 4 bitillä niitä on 16, uloin varattu arvolle, jota lähes millään ei ole, ja tavalliset painot — eli kaikki — pyöristyvät kahdelle tai kolmelle erilliselle tasolle.

Kaikki tuon rivin jälkeen on sama korjaus eri rakeisuuksilla: anna skaalalle pienempi alue. Per tulosrivi jakaa virheen 3.4:llä; per 128 peräkkäisen painon ryhmä jakaa sen uudelleen. Kustannus on kirjanpitoa — 16-bittinen skaala per 128:n ryhmä on 4+16/128=4.1254 + 16/128 = 4.125 bittiä per paino 4:n sijaan — ja se ostaa suurimman osan kuilusta takaisin.

NF4 lähestyy asiaa toiselta puolelta.5 Tasojen ei tarvitse olla tasavälisiä. Lohkon sisäiset painot ovat likimäärin normaalijakautuneita, joten valitse kuusitoista tasoa normaalijakauman kvantiileiksi: tiheästi nollan lähelle, missä painot oikeasti ovat, harvasti häntiin, missä ne eivät ole. Samat neljä bittiä, sama lohkoskaalaus, pienemmässä lohkossa — 4.25 bittiä per paino verrattuna group-128:n 4.125:een — ja mitattu virhe laskee 0.1323:sta 0.0952:een, 28 % alemmas. Osa siitä on hienompi lohko ja loppu tasojen sijoittaminen sinne, missä massa on, ja näiden erottaminen vaatisi kolmannen rivin.

Luvun 2 floating-point-laatikko päättyi lupaukseen: tässä luvussa painot kvantisoitaisiin 8 ja 4 bittiin ja löydettäisiin kourallinen outlier features, jotka kieltäytyvät puristumasta. Tässä ne ovat, ja ne selittävät, miksi ”pyöristä vain numerot” ei koskaan toimisi aktivoinneille.

Yllä olevat painot käyttäytyivät huonosti. Aktivoinnit ovat aivan eri liigassa. Ota tavallinen 84-tokeninen prompt, tallenna residual stream jokaisessa kerroksessa ja mittaa suurin magnitudi, jonka kukin 896 dimensiosta saavuttaa:

kerrossuurin |h|mediaanidimension suurin |h|suhdedimensiot yli 6x mediaanin
16.190.33918x2
41543.481.550996x34
81571.631.4981049x36
121575.031.5461019x34
161579.601.617977x32
201577.982.361668x24
24204.4410.76019x12

Dimensio 62 saavuttaa 1,579.6, kun mediaanidimensio ei koskaan ylitä 1.6:ta. Se ei ole yhden tokenin tai yhden kerroksen sattuma: sama dimensio on paikalla kerroksessa 4 ja yhä kerroksessa 20, lähes samalla arvolla. Nämä ovat outlier features,6 ja ne ovat systemaattisia — koulutetun mallin ominaisuus, eivät syötteen.

Näiden 896 dimensionkohtaisten maksimien histogrammi kerroksessa 16 tekee muodosta erehtymättömän:

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

Yhdeksänsataa dimensiota siistissä kasassa alle 8:n, ei mitään kolmeen oktaaviin, sitten yksi dimensio yksin kaukaisessa päässä. Kvantisoi nyt tuo tensori INT8:aan ja laske, mitä tapahtuu:

menetelmäsuhteellinen virhekäytetyt erilliset kokonaislukutasot, koko tensori
yksi skaala koko tensorille0.108314 / 256
yksi skaala per token (per rivi)0.0433158
koko tensori, 1 outlier-dimensio fp32:ssa0.044248
koko tensori, 4 outlier-dimensiota fp32:ssa0.027957
koko tensori, 16 outlier-dimensiota fp32:ssa0.0085102

Neljätoista tasoa 256:sta. Skaalan asetti 1,579.6, joten jokainen askel on 12.44 leveä, eikä tyypillisellä aktivoinnilla — mediaanimagnitudi 0.26, yhdeksäskymmenesyhdeksäs persentiili 2.51 — ole paikkaa johon laskeutua. Dimension tasolla se on vielä jyrkempää:

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

Yksi taso. Koko dimensio, jokainen token, kvantisoituna samaan numeroon. Kahdeksan bittiä allokoitiin ja suunnilleen nollaa käytettiin, ja aktivointeja lukevalle mallille annetaan vakio.

Tämä mittaus on perustelu jokaiselle tekniikalle, jota ihmiset oikeasti käyttävät:

Pidä outlierit poissa siitä. LLM.int8()6 hajottaa matriisikertolaskun: äärimmäisen magnitudin dimensiot lasketaan 16 bitillä, kaikki muu INT8:lla, ja puolikkaat summataan. Yllä oleva taulukko on kuitti — neljän dimension poistaminen leikkaa virheen lähes neljäsosaan. SmoothQuant7 siirtää sen sijaan vaikeuden: jaa aktivoinnit kanavakohtaisella kertoimella ja kerro vastaava painosarake sillä, mikä jättää tulon muuttumattomaksi ja siirtää outlierin tensorista, joka ei voi sitä imeä, siihen joka voi.

Valitse pyöristys, älä vain pyöristä. Mikään yllä ei kysy, mitä varten matriisi on. GPTQ8 kvantisoi sarake sarakkeelta ja jokaisen jälkeen säätää jäljellä olevia full-precision-sarakkeita kompensoidakseen jo tehdyn virheen — minimoiden kerroksen ulostulon virheen todellisilla syötteillä, ei sen painojen. AWQ9 huomaa, että pieni osuus painokanavista merkitsee paljon enemmän kuin loput, löytää ne aktivointitilastoista ja skaalaa ne ylös ennen kvantisointia, jotta ne osuvat hienommille tasoille. Molemmat tarvitsevat kalibrointijoukon; kumpikaan ei tarvitse gradientteja.

Näytä lisätiedot

GGUF, ja mitä tekemistä tiedostomuodolla on tämän kaiken kanssa.

GGUF ei ole kvantisointimenetelmä; se on kontti, jota llama.cpp käyttää, ja sekaannus gguf vs gptq-vertailuissa tulee siitä, että näitä käsitellään samanlaisina asioina. GGUF pitää tensorit, tokenizerin, arkkitehtuurimetadatan ja chat-templaten yhdessä memory-mappable-tiedostossa, ja sen sisällä kulkee perhe lohkomalleja — nimet kuten Q4_K_M koodaavat bitit per paino, lohkokoon ja sen, pidetäänkö jotkin tensorit suuremmalla tarkkuudella.

Merkityksellinen engineering-ero: GPTQ ja AWQ tuottavat painoja, jotka on optimoitu GPU-kernelille, kun taas GGUF:n menetelmät dekoodataan halvalla CPU:lla tiedoston ollessa mapattuna eikä ladattuna. Siksi sama nimellinen ”4-bit 7B model” on olemassa molemmissa maailmoissa eri kokoisena ja eri laatuisena, ja siksi rehellinen vertailu ei koskaan ole formaatti — se on alla oleva mittaus, ajettuna omalla tehtävälläsi.

Mitä kvantisointi oikeasti maksaa, mitattuna

Linkki osioon: Mitä kvantisointi oikeasti maksaa, mitattuna

Lähes jokainen kvantisointia käsittelevä artikkeli pysähtyy edelliseen osioon: se selittää menetelmän, lainaa pakkaussuhteen ja väittää, että laatu ”säilyy pitkälti”. Luku 4 käsitteli sitä, ettei huijaa itseään, joten selvitetään.

Sama malli, painot kvantisoitu paikan päällä kullakin menetelmällä, sitten kolme mittausta: perplexity 2,048 tokenilla sivussa pidettyä englanninkielistä proosaa — tässä tämän kurssin luonnos, minkä vuoksi repository korvaa sen kiinteällä public-domain-kirjalla ja tulostaa samanmuotoisen taulukon eri numeroilla — 16 lyhyen faktakysymyksen sarja, joilla on tunnetut vastaukset greedy decodingilla, ja niiden tokenien osuus, joissa kvantisoitu malli on samaa mieltä full-precision-mallin kanssa identtisessä kontekstissa.

menetelmäkeskimääräinen painovirheperplexitykysymyssarjasamaa mieltä fp32:n kanssa
fp32 (referenssi)0.000023.0813/16100.0 %
INT8 per tensor0.040023.5813/16
INT8 per rivi0.010022.9613/1698.6 %
INT4 per tensor0.6026365,416,0000/16
INT4 per rivi0.179046.186/1658.3 %
INT4 ryhmä 1280.132331.0810/1671.5 %
NF4 lohko 640.095224.5511/1684.7 %
INT3 ryhmä 1280.3044213.090/165.6 %
INT2 ryhmä 1280.779026,325,4360/160.0 %

Neljä asiaa tuossa taulukossa kannattaa sanoa suoraan.

Oikein tehty INT8 on ilmainen. Rivikohtainen INT8 saa 22.96 referenssin 23.08:aa vastaan — yhden osan ero kahdestasadasta, mikä on kohinaa ja pitäisi lukea ”identtiseksi”. Se, mihin suuntaan kohina osoittaa, ei ole vakaa: repositoryn public-domain-korpuksella samat kaksi menetelmää päätyvät 22.24 vastaan 22.18: puolet tuosta etäisyydestä ja toiseen suuntaan. Se on samaa mieltä full-precision-mallin kanssa 142:ssa 144 generoidusta tokenista. Neljäsosa muistista fp32-referenssiin verrattuna, puolet fp16:een verrattuna, jonka oikeasti deployaisit, eikä havaittavaa kustannusta. Huolimattomasti tehty INT8 on sekin lähes ilmainen: yksi skaala per matriisi maksaa 0.5 perplexity-pistettä eikä yhtään vastausta kysymyssarjassa. Kahdeksan bittiä antaa tarpeeksi anteeksi, että rakeisuudella on tuskin väliä, ja juuri siksi ihmiset yleistävät INT8:sta INT4:ään ja satuttavat itsensä.

INT4 yhdellä skaalalla per tensor tuhoaa mallin. Perplexity 365 miljoonaa: ei heikentynyt, vaan annihiloitu. Rakeisuus on sen jälkeen koko peli — per-tensor 365,416,000, per-rivi 46.18, per-128-ryhmä 31.08, NF4 24.55. Samat neljä bittiä per paino, viisitoista miljoonaa kertaa ero huonoimman ja parhaan välillä.

Perplexity on karkea instrumentti ja kysymyssarja vielä karkeampi. NF4:n ja group-128 INT4:n välillä perplexity-ero on 6.5 pistettä ja kysymyssarja eroaa yhdellä kysymyksellä — ja luvun 4 luottamusväli sanoo, että yksi kysymys kuudestatoista ei erota yhtään mitään. Intervallia terävämpi demonstraatio on olemassa: aja sama kysymyssarja mallin vakiotoistopenalty pois kytkettynä, mikä on mitä greedy decoding oikeasti tarkoittaa, ja nuo kaksi riviä vaihtavat paikkaa. Yksi kysymys kuudestatoista ei ole pieni vaikutus, se ei ole vaikutus. Myös luvun 8 varoitus pätee: perplexity on vertailukelpoinen vain mallien välillä, joilla on sama tokenizer, joten jonkun toisen kirjoituksen lukua ei voi verrata omaasi.

Agreement-sarake on näistä kolmesta terävin, ja lähes ilmainen: aja full-precision-malli greedy-moodissa, ja kysy sitten kvantisoidulta mallilta jokaisessa positiossa, minkä se olisi valinnut samalla prefiksillä. Siinä on 144 riippumatonta havaintoa 16:n sijaan, se ei tarvitse ground truthia, ja se heikkenee sulavasti siinä missä kysymyssarja heikkenee hyppäyksissä. Se on myös täsmälleen suure, jota seuraava osio tarvitsee.

Tämä on lupaus, jonka luku 1 antoi tästä luvusta, saapuen aikataulussa: matematiikka sanoo, että 4-bittinen malli on mahdollinen, ja engineering päättää, onko se käyttökelpoinen.

Luku 12 ilmoitti tämän ja jätti laskun tänne.

Idea tulee suoraan prefill/decode-jaosta. γ\gamma tokenin ehdotetun sekvenssin verifiointi maksaa yhden forward passin γ\gamma position yli — matriisi-matriisi-tulon, hädin tuskin kalliimman kuin läpivienti yhden yli. Siis:

Pieni, halpa malli generoi γ\gamma candidate tokenia autoregressiivisesti.

Suuri malli ajaa yhden forward passin kaikkien γ\gamma candidatejen yli kerralla, tuottaen sen, mitä se olisi sanonut jokaisessa positiossa.

Pidä pisin prefiksi, josta mallit ovat samaa mieltä, sekä token, jonka suuri malli antaa ilmaiseksi ensimmäisessä erimielisyydessä. Hylkää loput ja aloita uudelleen.

Tulosteen jakauma ei muutu. Greedy decodingilla se on ilmeistä — token hyväksytään vain, jos target olisi tuottanut sen. Samplingilla se vaatii muokatun hyväksymissäännön, ja Leviathan et al. todistavat, että tuloksena oleva jakauma on täsmälleen targetin.10 Tämä on tämän luvun toinen täsmällinen optimointi.

Kaikki riippuu siis hyväksymisasteesta α\alpha, joka on mitattavissa — se on yllä oleva agreement-sarake, minkä vuoksi se laskettiin siellä. Kun jokaista kvantisoitua mallia käytetään draftina full-precision-targetille, 144 generoidun position yli:

draft modelhyväksyntäpisin hyväksytty putkiodotetut tokenit per target pass, γ=4\gamma = 4
fp32 (target itse)100.0 %485.00
INT8 per rivi98.6 %484.86
NF4 lohko 6484.7 %203.69
INT4 ryhmä 12871.5 %132.85
INT4 per rivi58.3 %72.24
INT3 ryhmä 1285.6 %21.06
INT2 ryhmä 1280.0 %01.00

Odotetut hyväksytyt tokenit per verifiointiläpivienti draft-pituudella γ\gamma on

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

ja nettinopeutus jakaa sen draftin omalla kustannuksella, joka on murto-osa cc targetista per token:

hyväksyntäc=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

Lihavoitu kohta on se, joka kannattaa muistaa: speculative decoding voi tehdä generoinnista hitaampaa. 30 % hyväksynnällä ja draftilla, joka maksaa viidenneksen targetista, maksat viidestä forward passista ja pidät 1.4 tokenia. Viimeinen sarake on toinen ansa — pidempi draft auttaa vain, kun hyväksyntä on korkea, koska γ\gamma-tokenisen arvauksen häntään ei juuri koskaan päästä. 90 % hyväksynnällä γ=8\gamma = 8 on 3.40x arvoinen ja 30 %:lla 0.79x: sama konfiguraatio, voitto tai tappio riippuen luvusta, joka mitataan omalla liikenteelläsi.

Distillation ja mitä soft label kantaa

Linkki osioon: Distillation ja mitä soft label kantaa

Kvantisointi kutistaa mallia tallentamalla saman funktion vähemmissä biteissä. Distillation kutistaa sen kouluttamalla pienemmän mallin matkimaan suurempaa11 — idea, joka edelsi deep learningiä lähes vuosikymmenellä.12

Hienovarainen osa on mistä student oppii. Ei oikeasta vastauksesta: se olisi voitu kouluttaa suoraan sillä. Teacher lisää koko jakauman. Kysy mallilta, mitä fraasin jälkeen tulee, ja katso argmaxin ohi:

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

Hard label sanoo jug eikä mitään muuta. Soft label sanoo jug, ja myös että cup oli lähes yhtä hyvä, bowl mahdollinen, ja large — adjektiivi, täysin erilainen kieliopillinen jatko — yhä elossa. Se on alkuperäinen argumentti: tämä on 7, mutta se näyttää melko paljon 1:ltä, ja samankaltaisuus on informaatiota, jonka hard label heittää pois.

Siksi distillation käyttää myös lämpötilaa. logitien jakaminen arvolla TT ennen softmaxia litistää jakaumaa ja nostaa kakkossijojen suhteellista painoa: tässä fraasissa suhde top tokenin ja kolmannen välillä putoaa 2.24:stä arvolla T=1T = 1 arvoon 1.50 arvolla T=2T = 2 — ensimmäisen neliöjuuri, mitä logitien jakaminen kahdella tekee suhteelle. Sama järjestys, enemmän lossin attentionia läheltä menneisiin. Studentin gradientti kantaa teacherin epävarmuutta eikä vain sen tuomiota.

Kaikki tässä luvussa on nyt yksi summa:

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

missä TT on residenttien tokenien kokonaismäärä kaikissa samanaikaisissa pyynnöissä. Sovellettuna: 7B- ja 70B-rivit olettavat 8 key-value headia dimensiolla 128, 13B-rivi täyden multi-head attentionin 40 headilla, kuten nuo mallisukupolvet rakennettiin — ja se näkyy.

8 GB

modelprecisionpainotvapaana overheadin jälkeenmahtuvat kontekstin tokenit
7Bfp1613.0 GBei mahdu
7Bint86.5 GBei mahdu
7Bint4 (g128)3.4 GB3.1 GB25,710
13Bint4 (g128)6.2 GB0.3 GB337
70Bint4 (g128)33.6 GBei mahdu

16 GB

modelprecisionpainotvapaana overheadin jälkeenmahtuvat kontekstin tokenit
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

modelprecisionpainotvapaana overheadin jälkeenmahtuvat kontekstin tokenit
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 GBei mahdu

Katso 13B-riviä 8 GB:n taulukossa. Painot mahtuvat — 6.2 GB kahdeksasta — joten tavallisen puhetavan mukaan 13B-malli ”runs on an 8 GB card”. Sillä on 337 tokenia kontekstia, mikä ei ole keskustelu vaan hädin tuskin prompt. ”Mahtuuko se” on väärä kysymys. Oikea on ”kuinka suurella kontekstilla ja kuinka monelle käyttäjälle kerralla”.

Katso myös kahta 16 GB:n int8-riviä. 7B saa 65,378 tokenia ja 13B saa 3,136 — kaksikymmenkertainen ero 5.6 GB:sta lisäpainoja, koska tässä 13B:ssä on multi-head attention ja sen cache maksaa 800 KB per token verrattuna 7B:n 128 KB:hen. Kaksi samankokoista mallia, toinen käyttökelvoton pitkälle kontekstille syystä, joka ei näy minkään model cardin otsikossa.

Kolmetoista lukua sitten tämä oli perceptron kahdella painolla ja biasilla. Nyt se on transformer, joka on suunniteltu, koulutettu, aligned, opetettu käyttämään computea vaikeisiin kysymyksiin ja served mitatulla kustannuksella per token — eikä siihen jäänyt yhtään avaamatonta laatikkoa.

Se päättyy tähän, ja se päättyy tarkoituksella.

Luku 14 alkaa siitä, että malli on jossain muualla. Ei prosessissasi, ei muistissasi, ei muuttujassa jonka voit tulostaa: koneella jota et administratoi, API-avaimen, portin ja laskun takana. Kaikki täällä mitattu tapahtuu yhä — prefill ajetaan yhä ennen ensimmäistä tokenia, cache kasvaa yhä keskustelun mukana, batch jossa olet kuuluu yhä jollekin toiselle ja päättää yhä latenssisi — mutta tästä eteenpäin havainnoit sitä Server-Sent Events -virran, finish_reason:n ja HTTP 429:n kautta, jossa on Retry-After-header. Kysymykset muuttuvat näkökulman mukana: ei miten tämä gradientti lasketaan vaan miksi laskuni kolminkertaistui. Niin muuttuu kielikin, ja luku 14 selittää tuon säännön eikä vain ilmoita sitä — tähän asti koodi piti sisällään painot, gradientit, logits ja tokenizer-tavut; tästä eteenpäin se pitää yhteyden, uudelleenyrityksen, peruutuksen ja kertyneen tilan. Takana olevia kolmeatoista lukua ei heitetä pois rajan ylityksessä. Ne ovat kuvaus siitä, mikä pyörii portin toisella puolella.


Kaksi poisjättöä on tarkoituksellisia. FlashAttention (Dao et al., arXiv:2205.14135) ei ole eri attention — se laskee saman funktion pilkkomalla operaation niin, ettei n×nn \times n score-matriisia koskaan kirjoiteta muistiin, minkä vuoksi tämän luvun toisen taulukon 67 MB on käytännössä pienempi kuin aritmetiikka antaa ymmärtää. Ja itse kernelit on delegoitu: Stanfordin CS336:n luento 10 käsittelee inference-järjestelmiä siinä syvyydessä, johon tämä ei pyri, ja llama.cpp-repository sekä GGUF-spesifikaatio ovat CPU-puolen ensisijaiset lähteet.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Paperi on pitkälti memory-bandwidth-argumentti, ja sellaisena se myös lukee.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Sisältää uptraining-reseptin, joka muuntaa olemassa olevan multi-head-checkpointin, minkä vuoksi GQA levisi niin nopeasti.

  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. Esittelee iteration-level schedulingin — continuous batchingin — ja selective batchingin.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. Paperi, jolle vLLM on rakennettu; §3 on käyttöjärjestelmäanalogia kokonaisuudessaan.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 määritellään §3:ssa; yllä olevassa mittauksessa käytetyt kuudentoista tason arvot ovat ne, jotka tämä paperi johtaa.

  6. Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). §4:n outlier-feature-analyysi on yllä mitatun ilmiön lähde, mukaan lukien havainto, että outlierit syntyvät systemaattisesti skaalassa. 2

  7. Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J. and Han, S. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438 (2022).

  8. Frantar, E., Ashkboos, S., Hoefler, T. and Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022).

  9. Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023).

  10. Leviathan, Y., Kalman, M. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). Theorem 1 on todistus siitä, että tulosteen jakauma ei muutu; Chen et al. (arXiv:2302.01318) julkaisivat saman idean itsenäisesti.

  11. Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Lämpötila ja ”dark knowledge” -argumentti.

  12. Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, yhdeksän vuotta aiemmin, ensembleille eikä transformereille.


Tekijä

David Vicente Campos

NeuraLIA Labsin perustaja ja MyRealFoodin toinen perustaja

Olen valmistunut tietotekniikan insinööriksi Leónin yliopistosta. Olin mukana perustamassa MyRealFoodia, jossa teknologiajohtajana rakensin sovelluksen, jota miljoonat ihmiset ovat käyttäneet syödäkseen paremmin, ja perustin NeuraLIA Labsin, jossa rakennan tekoälytuotteita. Täällä kirjoitan siitä, mitä minun on pitänyt ymmärtää matkan varrella, niin kuin olisin toivonut jonkun selittävän asiat minulle.

Lisää kirjoittajasta

Julkaisija: NeuraLIA Labs.

Uudet julkaisut suoraan sähköpostiisi

AI-uutisia, oppaita ja tuoteuutisia — lyhyt sähköposti, kun julkaisemme jotain aikasi arvoista.

Kurssin hakemisto

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev9 min lukuaikaa

Jev AI -malli on rakennettu päätöksiä, ei proosaa varten

TypeSafe AI:n Jev herättää huomiota, koska se käsittelee ohjelmistojen älykkyyttä todennäköisyysongelmana: valitse oikea haara, liitä mukaan varmuus ja vältä maksamasta LLM:lle tekstin kirjoittamisesta, kun koodi tarvitsee päätöksen.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering9 min lukuaikaa

Kontekstisuunnittelu pitkän aikavälin AI-agenteille

Pitkäkestoiset agentit eivät epäonnistu vain siksi, että ikkuna on pieni. Ne epäonnistuvat, kun tiedostot, työkalujen tulosteet ja vanhentunut historia syrjäyttävät tehtävän, joka agentin piti saada valmiiksi.

Valmis antamaan LIA:n valita puolestasi?

Rakenna kaikilla tekoälymalleilla yhdessä paikassa — aloita ilmaiseksi jo tänään.