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.
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 FLOPia jokaista tuottamaansa tokenia kohti, jokaisessa pyynnössä, koko loppuelämänsä ajan.
Mihin toisen ajon aika meni
Linkki osioon: Mihin toisen ajon aika meniTokenin 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:
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 , yksi generointiaskele laskettuna molemmilla tavoilla:
| tokenia kontekstissa | laske kaikki uudelleen | cachella | suhde | score-matriisi |
|---|---|---|---|---|
| 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 |
Oikeanpuoleinen sarake on syy. Uudelleenlaskenta rakentaa täyden attention-matriisin joka askeleella — luvun 9 asymptotic-notation-laatikon , maksettuna kerran per token. Cachella rakennat sen sijaan -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 tokenia:
| generoidut tokenit | cachella | uudelleenlaskenta | suhde |
|---|---|---|---|
| 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 |
Askelta kohti cached-versio on lineaarinen kontekstin suhteen ja uncached-versio neliöllinen; generoinnin yli summattuna vastaan , 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 | |
|---|---|
| cachella | 21.8 MB |
| uudelleenlaskenta | 181.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 konettaKatso nopeaa ajoa uudelleen: sen ensimmäinen token käyttäytyi eri tavalla kuin muut neljäkymmentäseitsemän.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepPrompt 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:
Prefill
Linkki osioon: PrefillYksi 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.
Decode
Linkki osioon: DecodeYksi 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 tokenin yli:
| prompt-tokenit | sekuntia | 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, yksi token tokenin cachea vasten:
| cached tokenit | ms yhdelle tokenille |
|---|---|
| 16 | 110.05 |
| 64 | 97.57 |
| 256 | 108.53 |
| 1024 | 103.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 , 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 on myös lasku
Linkki osioon: Cache on myös laskuCache 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:
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 tavua per token.
Tämän alan kaavoilla on tapana olla pielessä kertoimella kaksi, joten tarkista se allokaattoria vasten äläkä vain usko:
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/tokenTäsmälleen, ja se pysyy täsmällisenä jokaisessa kokeillussa muodossa:
| batch | konteksti | mitattu cache | ennuste | työmuistin huippu |
|---|---|---|---|---|
| 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 |
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.
Mistä MQA ja GQA tulevat
Linkki osioon: Mistä MQA ja GQA tulevatLuku 9 esitteli multi-query ja grouped-query attentionin ja lykkäsi syyn tähän lukuun. Syy on tuo kaava, ja erityisesti siinä oleva .
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 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 tokenit | yksi käyttäjä | 8 käyttäjää | 64 käyttäjää |
|---|---|---|---|
| 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 |
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 pieneneeDecode 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:
| batch | latenssi per askel | throughput | latenssi 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 |
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 voittaaNaiivi tapa batchata on kerätä 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äkello | throughput | keskimääräinen latenssi per pyyntö | hukatut paikka-askeleet |
|---|---|---|---|---|
| staattiset 8:n batchit | 176.9 s | 10.6 tok/s | 83.2 s | 3,214 |
| jatkuva, 8 paikkaa | 109.0 s | 17.2 tok/s | 8.1 s | 0 |
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 pieleenLaskun 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:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedValitse 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 :
| menetelmä | keskimääräinen suhteellinen virhe | huonoin matriisi |
|---|---|---|
| INT8, yksi skaala koko matriisille | 0.0400 | 0.1487 |
| INT8, yksi skaala per tulosrivi | 0.0100 | 0.0149 |
| INT4, yksi skaala koko matriisille | 0.6026 | 0.9931 |
| INT4, yksi skaala per tulosrivi | 0.1790 | 0.2589 |
| INT4, yksi skaala per 128:n ryhmä | 0.1323 | 0.1992 |
| NF4, yksi skaala per 64:n lohko | 0.0952 | 0.1205 |
| INT3, yksi skaala per 128:n ryhmä | 0.3044 | 0.4123 |
| INT2, yksi skaala per 128:n ryhmä | 0.7790 | 0.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:
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 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.
Outlier features
Linkki osioon: Outlier featuresLuvun 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:
| kerros | suurin |h| | mediaanidimension suurin |h| | suhde | dimensiot yli 6x mediaanin |
|---|---|---|---|---|
| 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 |
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:
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 | # 1Yhdeksä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 virhe | käytetyt erilliset kokonaislukutasot, koko tensori |
|---|---|---|
| yksi skaala koko tensorille | 0.1083 | 14 / 256 |
| yksi skaala per token (per rivi) | 0.0433 | 158 |
| koko tensori, 1 outlier-dimensio fp32:ssa | 0.0442 | 48 |
| koko tensori, 4 outlier-dimensiota fp32:ssa | 0.0279 | 57 |
| koko tensori, 16 outlier-dimensiota fp32:ssa | 0.0085 | 102 |
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ää:
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 levelsYksi 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, mitattunaLä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 painovirhe | perplexity | kysymyssarja | samaa mieltä fp32:n kanssa |
|---|---|---|---|---|
| fp32 (referenssi) | 0.0000 | 23.08 | 13/16 | 100.0 % |
| INT8 per tensor | 0.0400 | 23.58 | 13/16 | — |
| INT8 per rivi | 0.0100 | 22.96 | 13/16 | 98.6 % |
| INT4 per tensor | 0.6026 | 365,416,000 | 0/16 | — |
| INT4 per rivi | 0.1790 | 46.18 | 6/16 | 58.3 % |
| INT4 ryhmä 128 | 0.1323 | 31.08 | 10/16 | 71.5 % |
| NF4 lohko 64 | 0.0952 | 24.55 | 11/16 | 84.7 % |
| INT3 ryhmä 128 | 0.3044 | 213.09 | 0/16 | 5.6 % |
| INT2 ryhmä 128 | 0.7790 | 26,325,436 | 0/16 | 0.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.
Speculative decoding
Linkki osioon: Speculative decodingLuku 12 ilmoitti tämän ja jätti laskun tänne.
Idea tulee suoraan prefill/decode-jaosta. tokenin ehdotetun sekvenssin verifiointi maksaa yhden forward passin position yli — matriisi-matriisi-tulon, hädin tuskin kalliimman kuin läpivienti yhden yli. Siis:
Draft
Linkki osioon: DraftPieni, halpa malli generoi candidate tokenia autoregressiivisesti.
Verify
Linkki osioon: VerifySuuri malli ajaa yhden forward passin kaikkien candidatejen yli kerralla, tuottaen sen, mitä se olisi sanonut jokaisessa positiossa.
Accept
Linkki osioon: AcceptPidä 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 , 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 model | hyväksyntä | pisin hyväksytty putki | odotetut tokenit per target pass, |
|---|---|---|---|
| fp32 (target itse) | 100.0 % | 48 | 5.00 |
| INT8 per rivi | 98.6 % | 48 | 4.86 |
| NF4 lohko 64 | 84.7 % | 20 | 3.69 |
| INT4 ryhmä 128 | 71.5 % | 13 | 2.85 |
| INT4 per rivi | 58.3 % | 7 | 2.24 |
| INT3 ryhmä 128 | 5.6 % | 2 | 1.06 |
| INT2 ryhmä 128 | 0.0 % | 0 | 1.00 |
Odotetut hyväksytyt tokenit per verifiointiläpivienti draft-pituudella on
ja nettinopeutus jakaa sen draftin omalla kustannuksella, joka on murto-osa targetista per token:
| hyväksyntä | , | , | , | , |
|---|---|---|---|---|
| 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 |
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 -tokenisen arvauksen häntään ei juuri koskaan päästä. 90 % hyväksynnällä 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 kantaaKvantisointi 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:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360Hard 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 ennen softmaxia litistää jakaumaa ja nostaa kakkossijojen suhteellista painoa: tässä fraasissa suhde top tokenin ja kolmannen välillä putoaa 2.24:stä arvolla arvoon 1.50 arvolla — 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.
Mikä mahtuu 8, 16 ja 24 GB:hen
Linkki osioon: Mikä mahtuu 8, 16 ja 24 GB:henKaikki tässä luvussa on nyt yksi summa:
missä 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
| model | precision | painot | vapaana overheadin jälkeen | mahtuvat kontekstin tokenit |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | ei mahdu | — |
| 7B | int8 | 6.5 GB | ei mahdu | — |
| 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 | ei mahdu | — |
16 GB
| model | precision | painot | vapaana overheadin jälkeen | mahtuvat kontekstin tokenit |
|---|---|---|---|---|
| 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 | painot | vapaana overheadin jälkeen | mahtuvat kontekstin tokenit |
|---|---|---|---|---|
| 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 | ei 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.
Mihin tämä johtaa seuraavaksi
Linkki osioon: Mihin tämä johtaa seuraavaksiKolmetoista 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.
Lähteet ja menetelmä
Linkki osioon: Lähteet ja menetelmäKaksi poisjättöä on tarkoituksellisia. FlashAttention (Dao et al., arXiv:2205.14135) ei ole eri attention — se laskee saman funktion pilkkomalla operaation niin, ettei 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.
Viitteet
Linkki osioon: Viitteet-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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
-
Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J. and Han, S. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438 (2022). ↩
-
Frantar, E., Ashkboos, S., Hoefler, T. and Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022). ↩
-
Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023). ↩
-
Leviathan, Y., Kalman, M. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). Theorem 1 on todistus siitä, että tulosteen jakauma ei muutu; Chen et al. (arXiv:2302.01318) julkaisivat saman idean itsenäisesti. ↩
-
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. ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, yhdeksän vuotta aiemmin, ensembleille eikä transformereille. ↩