Към съдържанието
13/30Глава 13 от 30

Как да направим inference евтин: KV cache, batching и квантизация

Същият model отговаря за 8,8 и 78,9 секунди с byte-идентичен изход. После INT4, измерен по три начина.

На тази страница

Същият model, на същата машина, отговаря на същия въпрос със същите 48 tokens. Двата изхода са идентични token по token — проверено, не предположено.

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)

Променен е един аргумент: use_cache=False. Нищо в model, prompt, sampling или аритметиката не е различно, а второто изпълнение не е по-точно за усилието си. То е девет пъти по-бавно за нищо.

Това е формата на тази глава. Всичко в нея — cache, batch, квантизираните тегла — е опит да спрем да плащаме за работа, която не променя отговора, или да разберем каква е цената на по-евтин отговор. Глава 10 установи ценовата листа за обучение. Това е ценовата листа за страната, за която плащате завинаги: внедрен model харчи приблизително 2N2N FLOPs за всеки token, който излъчва, при всяка заявка, до края на живота си.

Къде отиде времето на второто изпълнение

Връзка към раздела: Къде отиде времето на второто изпълнение

За да генерира token, decoder-only transformer взема цялата досегашна последователност, прекарва я през всеки слой и прочита вероятностното разпределение от последната позиция. След това добавя избрания token и прави същото отново. Това описание е вярно и точно това прави бавното изпълнение.

То е и изключително разточително, а причината е causal mask от Глава 9. Векторите key и value на позиция 7 се изчисляват от входа на позиция 7 и позициите преди нея. Когато пристигне позиция 8, позиция 7 не може да я вижда — това означава causal — така че key и value на позиция 7 са точно същите числа като преди. Бавното изпълнение въпреки това ги преизчислява, на всяка стъпка.

Затова ги съхранете. Това хранилище е key-value cache, най-важната оптимизация в serving на езикови модели:

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)

Вижте какво се подава към model вътре в цикъла: nxt, един token. Не последователността. Query на новия token attends against всеки кеширан key, а кешираните keys така или иначе нямаше да се променят. Това не е приближение — проверката за идентичен изход по-горе е смисълът. Cache не разменя качество срещу скорост; той изтрива излишна аритметика.

За да се види мащабирането чисто, махнете transformer и измерете една attention head с d=64d = 64, една стъпка на генериране, изчислена и по двата начина:

tokens in contextпреизчисляване на всичкос cacheсъотношениематрица на score
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

Дясната колона е причината. Преизчисляването изгражда пълната n×nn \times n attention матрица на всяка стъпка — O(n2)O(n^2) от карето с асимптотична нотация в Глава 9, платено веднъж за token. С cache вместо това изграждате ред 1×n1 \times n: при 4 096 tokens — 67 MB scores срещу 16 KB.

Броенето на multiply-accumulates вместо милисекунди премахва машината от аргумента. За генериране на TT tokens от студен старт:

генерирани tokensс cacheпреизчисляванесъотношение
1282,6 M192,0 M73x
51223,1 M7,36 G318x
2048293,7 M392,6 G1 336x

На стъпка кешираната версия е линейна спрямо context, а некешираната — квадратична; сумирано върху генериране, O(T2)O(T^2) срещу O(T3)O(T^3), като съотношението расте без ограничение. Деветкратната разлика в началото беше измерена върху 48 tokens — под първия ред на тази таблица.

Cache променя и това, което трябва да бъде в паметта. На лаптоп GPU с 8 GB, генериращ 256 tokens във fp16, като вземем пика на allocator и извадим resident weights:

пикова работна памет
с cache21,8 MB
преизчисляване181,7 MB

8,3 пъти повече памет, похарчена, за да произведе същите tokens по-бавно. Това е обещанието от Глава 5, пристигащо от неочаквана посока: там reverse-mode autodiff трябваше да държи всеки междинен резултат жив за backward pass, а активациите доминираха паметта при обучение. При inference няма backward pass и нищо за задържане за него — затова паметта се доминира от cache, и това е съзнателен избор, а не неизбежен разход.

Погледнете отново бързото изпълнение: първият му token се държеше различно от останалите четиридесет и седем.

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

Prompt струваше 25,6 ms на token, а всеки генериран token — 166 ms. Същият model, същият хардуер, същите тегла, шесткратна разлика на token — и посоката е обратна на това, което повечето хора очакват. Prompt е евтината част. Генерирането се разделя на две фази с наистина различна физика:

Един forward pass върху целия prompt. Всеки token се обработва паралелно, така че всяка матрица с тегла се зарежда от паметта веднъж и се умножава по матрица от стотици token вектори — matrix-matrix product, с много аритметика за всеки преместен byte, точно за което е създаден GPU. Prefill е compute-bound, а цената му е приблизително линейна спрямо дължината на prompt.

Един forward pass за token, batch от едно и последователност от едно. Всяка матрица с тегла все още се зарежда от паметта изцяло и се умножава по един вектор — matrix-vector product, почти без аритметика за всеки преместен byte. Decode е memory-bandwidth-bound, а цената му на token почти не зависи от дължината на context.

И двете половини са измерими. Prefill, един pass върху PP tokens:

prompt tokensсекундиms на token
160,351521,97
320,525416,42
641,049116,39
1281,655212,93
2563,096512,10

Decode, един token срещу cache от CC:

кеширани tokensms за един token
16110,05
6497,57
256108,53
1024103,86

Прочетете втората таблица два пъти. Преминаването от 16 tokens context към 1 024 — шестдесет и четири пъти повече история, върху която да се attend — не промени цената на стъпката с нищо измеримо. Attention срещу cache е реална работа, но е засенчена от фиксираната цена да прекарате половин милиард тегла през шината на паметта, за да произведете един вектор. Тази фиксирана цена е причината за всичко в следващия раздел.

Тези две фази са произходът на двете числа, които всяка serving система отчита. Time to first token е по същество prefill и расте с prompt, затова дълъг разговор се усеща бавен в началото. Tokens per second е 1/decode step1/\text{decode step} и е приблизително постоянен, затова отговорът после тече равномерно. Chat, който започва бавно и след това streams плавно, не е трик на визуализацията. Това са тези две таблици.

Cache разменя аритметика срещу памет, а паметта, която иска, не е малка. За всеки token в context всеки слой държи един key вектор и един value вектор за всяка 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 е за keys и values; всичко останало е архитектурата. За model, измерван през тази глава — 24 слоя, 14 query heads, 2 key-value heads, head dimension 64 — във fp16 това е 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 bytes на token.

Формулите в тази област имат навика да грешат с фактор две, така че проверете я срещу allocator, вместо да ѝ вярвате:

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

Точно, и остава точно при всяка изпробвана форма:

batchcontextизмерен cacheпредсказанпикова работна памет
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

Последните три реда заслужават втори поглед. Тридесет и двама потребители с по 2 048 tokens, шестдесет и четирима с по 1 024, сто двадесет и осем с по 512 — cache е 768 MB във всеки случай, защото и трите държат 65 536 tokens. Cache зависи само от общия брой resident tokens, не от това как са разпределени между потребителите. Този факт е основата на раздела за batching.

Глава 9 въведе multi-query и grouped-query attention и отложи причината за тази глава. Причината е тази формула, и по-конкретно HkvH_{kv} в нея.

Стандартният multi-head attention дава на всяка query head собствени key и value heads. Model тук има 14 query heads; с пълен multi-head attention неговият cache би бил 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 bytes на token — 84 KB вместо 12 KB, точно седем пъти повече, съотношението между query heads и key-value heads.

Multi-query attention1 довежда това до крайност: всички query heads споделят една key-value head. Grouped-query attention2 е компромисът, който спечели — няколко key-value heads, всяка споделена от група query heads — защото загубата на качество при MQA беше реална, а при GQA не е. Нито едното не купува аритметика. Те съществуват, за да разделят тази формула на цяло число, и се разпространиха из индустрията в момента, в който дългите contexts направиха cache ограничаващото условие.

Което става бързо. За model от 7B клас с 32 слоя и 8 key-value heads с размерност 128, cache е 128 KB на token във fp16:

context tokensедин потребител8 потребители64 потребители
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

Собствените тегла на този model са 13,0 GB във fp16, числото в таблицата в края на тази глава. Така че при context от 128 000 tokens cache на един потребител е по-голям от model. Това е аритметиката, която Глава 16 превръща в пари, и затова дълъг разговор не е просто бавен — той заема фиксиран дял от машина, докато заявката е жива.

Batching: числото, което расте, и числото, което пада

Връзка към раздела: Batching: числото, което расте, и числото, което пада

Decode е memory-bound: теглата се влачат през шината, за да произведат един token, а аритметичните блокове бездействат. Затова сложете повече работа в същата стъпка. Пуснете няколко заявки едновременно и теглата, прочетени веднъж, обслужват всички тях. Измерено на същия model, като всяка заявка държи 64-token cache и декодира един token:

batchлатентност на стъпкаthroughputлатентност спрямо 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

Прочетете двете десни колони една срещу друга, защото те са целият смисъл. Преминаването от една заявка към шестнадесет умножава throughput по 6,0 и умножава чакането за всяка отделна заявка по 2,67. Batch направи сървъра по-добър и всеки потребител по-зле.

Това не е bug, който да се настрои away; това е самият компромис, и има име от всяка страна. Латентност е това, което изпитва човек, чакащ отговор. Throughput е това, на което се дели фактурата. Няма настройка, която да подобрява и двете.

Обърнете внимание и къде спира. От 16 до 32 throughput печели 9 %, докато латентността почти се удвоява: стъпката е спряла да бъде memory-bound и е станала compute-bound, а след това коляно batch не купува нищо. Всяко deployment има такова коляно; местоположението му трябва да се измери на вашето, но съществуването му — не.

Static batching губи повечето от това, което печели

Връзка към раздела: Static batching губи повечето от това, което печели

Наивният начин за batching е да съберете BB заявки, да ги пуснете заедно и да върнете резултат, когато всички приключат. Но те не приключват заедно: някои отговори са двадесет tokens, а други — петстотин. Фиксиран batch работи, докато най-дългият му член приключи, и всяка завършена заявка продължава да заема слота си, добавяйки padding, дотогава.

Вземете 64 заявки с реалистично изкривяване на дължините на изхода — медиана 18 tokens, най-дълга 231, общо 1 874 — и симулирайте двете политики при измерената цена на стъпка за осем слота:

политикаwall clockthroughputсредна латентност на заявкаизгубени slot-steps
static batches of 8176,9 s10,6 tok/s83,2 s3 214
continuous, 8 slots109,0 s17,2 tok/s8,1 s0

Throughput се подобрява 1,6x. Средната латентност се подобрява повече от десет пъти, защото при static batching заявка, приключила за четири стъпки, все още чака 231-token съсед, преди някой да чуе за нея.

Continuous batching3 е поправката и е толкова проста, колкото звучи: batch не е група, а набор от слотове, и слот, който се освободи, допуска следващата чакаща заявка още на следващата стъпка. Scheduler работи с гранулярност един token, а не една заявка. Всяка serving stack в production вече прави това.

Има и втора половина — cache. Слотовете, които идват и си отиват, оставят cache паметта фрагментирана, а резервирането на максималния възможен context за всеки слот губи по-голямата част от резервацията. PagedAttention4 заема отговора от операционните системи: съхранявайте cache във фиксирани по размер блокове с block table за всяка последователност, така че cache на последователността да може да бъде физически разпръснат, докато остава логически непрекъснат — което също позволява две последователности със споделен prefix да споделят блоковете, които го държат. На това е изграден vLLM и затова serving engine е memory allocator с прикрепен transformer.

Квантизация и първото нещо, което се чупи

Връзка към раздела: Квантизация и първото нещо, което се чупи

Другата половина от сметката са самите тегла. Половин милиард параметъра по четири bytes са 1,98 GB; по два bytes — 0,99 GB; по един byte — 0,49 GB. По-малко bits на тегло свиват model на диска, свиват го в паметта и — понеже decode е bandwidth-bound — правят всяка стъпка по-бърза, защото има по-малко bytes за преместване.

Най-простата схема е symmetric absolute-maximum quantization и се побира в три реда:

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

Изберете scale, така че най-голямото тегло да се мапва към най-голямото цяло число, разделете, закръглете, съхранете целите числа и scale. Възстановете чрез умножаване обратно. В нея няма нищо хитро и работи — чак докато спре.

Измерено върху реалните тегла на model: всички 168 projection matrices, 357,8 милиона параметъра, относителна грешка WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

схемасредна относителна грешканай-лоша матрица
INT8, един scale за цялата матрица0,04000,1487
INT8, един scale на изходен ред0,01000,0149
INT4, един scale за цялата матрица0,60260,9931
INT4, един scale на изходен ред0,17900,2589
INT4, един scale на група от 1280,13230,1992
NF4, един scale на блок от 640,09520,1205
INT3, един scale на група от 1280,30440,4123
INT2, един scale на група от 1280,77900,8076

Четвъртият ред е сривът. Относителна грешка 0,99 на най-лошата матрица означава, че реконструкцията не запазва по същество нищо от оригинала — матрицата е заменена с шум с приблизително правилната големина. Причината се вижда в същия експеримент върху една матрица:

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

Едно тегло от шест хиляди е отвъд шест стандартни отклонения, а най-голямото е на 24. С един scale за цялата матрица това едно тегло задава размера на стъпката за всичките 4,3 милиона. При 8 bits има 256 стъпки и типичното тегло все още попада на смислена. При 4 bits има 16, най-външната е запазена за стойност, която почти нищо няма, а обикновените тегла — тоест всички — се закръглят до две или три различни нива.

Всичко след този ред е същата поправка с различна гранулярност: дайте на scale по-малка територия. Per output row дели грешката на 3,4; per group от 128 последователни тегла я дели отново. Цената е счетоводство — 16-bit scale на група от 128 е 4+16/128=4.1254 + 16/128 = 4.125 bits на тегло вместо 4 — и връща по-голямата част от разликата.

NF4 подхожда от другата страна.5 Нивата не трябва да са равномерно разположени. Теглата в block са приблизително нормално разпределени, затова изберете шестнадесетте нива като квантилите на нормално разпределение: гъсти близо до нулата, където теглата наистина са, редки в опашките, където не са. Същите четири bits, същото block scaling, при по-малък block — 4,25 bits на тегло срещу 4,125 за group-128 — и измерената грешка пада от 0,1323 до 0,0952, 28 % по-ниско. Част от това е по-финият block, останалото е поставянето на нивата там, където е масата, а разделянето на двете би изисквало трети ред.

Карето за floating-point в Глава 2 завърши с обещание: че тази глава ще квантизира тегла до 8 и 4 bits и ще намери шепа outlier features, отказващи да бъдат притиснати. Ето ги, и те обясняват защо „просто закръглете числата“ никога нямаше да работи върху активации.

Теглата по-горе се държаха лошо. Активациите са в друга лига. Вземете обикновен 84-token prompt, прихванете residual stream на всеки слой и измерете най-голямата големина, която всяко от 896-те измерения достига:

layerнай-голямо |h|най-голямо |h| на медианното измерениесъотношениеизмерения над 6x медианата
16,190,33918x2
41543,481,550996x34
81571,631,4981049x36
121575,031,5461019x34
161579,601,617977x32
201577,982,361668x24
24204,4410,76019x12

Измерение 62 достига 1 579,6, докато медианното измерение никога не надхвърля 1,6. Това не е случайност на един token или един слой: същото измерение е там на слой 4 и още е там на слой 20, с почти същата стойност. Това са outlier features,6 и те са систематични — свойство на обучения model, не на входа.

Хистограмата на тези 896 максимума по измерение на слой 16 прави формата несъмнена:

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

Деветстотин измерения в подредена купчина под 8, абсолютно нищо за три октави, после едно измерение само в далечния край. Сега квантизирайте този tensor до INT8 и пребройте какво става:

схемаотносителна грешкаизползвани различни integer нива, целият tensor
един scale за целия tensor0,108314 от 256
един scale на token (на ред)0,0433158
цял tensor, 1 outlier dimension запазено във fp320,044248
цял tensor, 4 outlier dimensions запазени във fp320,027957
цял tensor, 16 outlier dimensions запазени във fp320,0085102

Четиринадесет нива от 256. Scale беше зададен от 1 579,6, така че всяка стъпка е широка 12,44, а типичната активация — медианна големина 0,26, деветдесет и девети percentile 2,51 — няма къде да попадне. По измерение е още по-рязко:

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

Едно ниво. Цялото измерение, всеки token, квантизирано до едно и също число. Осем bits бяха разпределени и приблизително нула бяха използвани, а model, който чете тези активации, получава константа.

Това измерване е оправданието за всяка техника, която хората реално използват:

Дръжте outliers извън него. LLM.int8()6 разлага matrix multiply: измеренията с екстремни големини се изчисляват в 16 bits, всичко останало — в INT8, и половините се сумират. Таблицата по-горе е разписката — премахването на четири измерения намалява грешката почти четири пъти. SmoothQuant7 вместо това премества трудността: разделя активациите на per-channel фактор и умножава съответната колона от теглата по него, което оставя произведението непроменено и мести outlier от tensor, който не може да го поеме, в този, който може.

Изберете закръгляването, не просто закръгляйте. Нищо по-горе не пита за какво служи матрицата. GPTQ8 квантизира колона по колона и след всяка коригира останалите full-precision колони, за да компенсира вече направената грешка — минимизирайки грешката на изхода на слоя върху реални входове, а не грешката на теглата му. AWQ9 отбелязва, че малка част от weight channels имат много по-голямо значение от останалите, намира ги чрез статистики на активациите и ги scale-ва нагоре преди квантизация, така че да попаднат на по-фини нива. И двете изискват calibration set; нито едното не изисква gradients.

Покажи подробности

GGUF и какво общо има файлов формат с всичко това.

GGUF не е метод за квантизация; той е контейнерът, който llama.cpp използва, а объркването в сравненията gguf vs gptq идва от третирането на двете като един и същ вид нещо. GGUF държи tensors, tokenizer, architecture metadata и chat template в един memory-mappable файл и носи семейство block схеми вътре — имена като Q4_K_M кодират bits на тегло, block size и дали някои tensors се държат с по-висока precision.

Инженерната разлика, която има значение: GPTQ и AWQ произвеждат тегла, оптимизирани за GPU kernel, докато схемите на GGUF се декодират евтино на CPU с файла mapped, а не loaded. Затова същият номинален „4-bit 7B model“ съществува и в двата свята с различни размери и различно качество, и затова честното сравнение никога не е format — то е измерването по-долу, пуснато върху вашата собствена задача.

Какво всъщност струва квантизацията, измерено

Връзка към раздела: Какво всъщност струва квантизацията, измерено

Почти всяка статия за квантизация спира в предишния раздел: обяснява метода, цитира compression ratio и твърди, че качеството е „до голяма степен запазено“. Глава 4 беше за това да не се самозалъгвате, така че нека разберем.

Същият model, теглата квантизирани на място с всяка схема, после три измервания: perplexity върху 2 048 tokens задържана английска проза — тук, черновата на този курс, затова repository заменя с фиксирана public-domain книга и отпечатва таблица със същата форма и различни числа — набор от 16 кратки фактически въпроса с известни отговори при greedy decoding, и делът на tokens, при които квантизираният model се съгласява с full-precision model при идентичен context.

схемасредна грешка на теглатаperplexityнабор въпросисъгласие с fp32
fp32 (reference)0,000023,0813/16100,0 %
INT8 per tensor0,040023,5813/16
INT8 per row0,010022,9613/1698,6 %
INT4 per tensor0,6026365 416 0000/16
INT4 per row0,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 %

Четири неща в тази таблица си струва да бъдат казани ясно.

INT8, направен правилно, е безплатен. Per-row INT8 дава 22,96 срещу 23,08 на reference — разлика от една част на двеста, което е шум и трябва да се чете като „идентично“. Накъде сочи шумът не е стабилно: върху public-domain corpus в repository същите две схеми излизат 22,24 срещу 22,18: половината разстояние, и сочещо в другата посока. Той се съгласява с full-precision model върху 142 от 144 генерирани tokens. Четвърт от паметта спрямо fp32 reference, половина спрямо fp16, който реално бихте deploy-нали, и без откриваема цена. INT8, направен небрежно, също е почти безплатен: един scale на матрица струва 0,5 perplexity points и никакви отговори от набора. Осем bits са достатъчно forgiving, че гранулярността почти няма значение, което е точно причината хората да обобщават от INT8 към INT4 и да пострадат.

INT4 с един scale на tensor унищожава model. Perplexity 365 милиона: не влошен, а анихилиран. Гранулярността тогава е цялата игра — per-tensor 365 416 000, per-row 46,18, per-group-of-128 31,08, NF4 24,55. Същите четири bits на тегло, фактор петнадесет милиона между най-лошото и най-доброто.

Perplexity е груб инструмент, а наборът — още по-груб. Между NF4 и group-128 INT4 разликата в perplexity е 6,5 points, а наборът се различава с един въпрос — и confidence interval от Глава 4 казва, че един въпрос от шестнадесет не различава абсолютно нищо. Има по-остра демонстрация от интервала: пуснете същия набор с изключена стандартна repetition penalty на model, което всъщност означава greedy decoding, и тези два реда си разменят местата. Един въпрос от шестнадесет не е малък ефект, а никакъв ефект. Предупреждението от Глава 8 също важи: perplexity е сравнима само между модели, споделящи tokenizer, така че число от чужд write-up не може да се сравнява с вашето.

Колоната за съгласие е най-острата от трите, и почти безплатна: пуснете full-precision model greedily, после попитайте квантизирания на всяка позиция какво би избрал при същия prefix. Има 144 независими наблюдения вместо 16, не изисква ground truth и се влошава плавно там, където наборът се влошава на скокове. Тя е и точно величината, от която се нуждае следващият раздел.

Това е обещанието, което Глава 1 даде за тази глава, пристигащо по график: математиката казва, че 4-bit model е възможен, а инженерството решава дали е използваем.

Глава 12 обяви това и остави сметката тук.

Идеята идва директно от разделението prefill/decode. Проверяването на предложена последователност от γ\gamma tokens струва един forward pass върху γ\gamma позиции — matrix-matrix product, едва по-скъп от pass върху една. Така че:

Малък, евтин model генерира γ\gamma candidate tokens autoregressively.

Големият model пуска един forward pass върху всички γ\gamma candidates наведнъж, произвеждайки какво би казал на всяка позиция.

Запазете най-дългия prefix, по който двата са съгласни, плюс token, който големият model доставя безплатно при първото несъгласие. Изхвърлете останалото и започнете отново.

Изходното разпределение е непроменено. При greedy decoding това е очевидно — token се приема само ако target би го произвел. При sampling изисква модифицирано правило за приемане, а Leviathan et al. доказват, че полученото разпределение е точно target.10 Това е втората точна оптимизация в тази глава.

Следователно всичко зависи от acceptance rate α\alpha, който е измерим — това е колоната за съгласие по-горе, затова беше изчислена там. Използвайки всеки квантизиран model като draft за full-precision target, върху 144 генерирани позиции:

draft modelacceptanceнай-дълга приета поредицаочаквани tokens на target pass, γ=4\gamma = 4
fp32 (самият target)100,0 %485,00
INT8 per row98,6 %484,86
NF4 block 6484,7 %203,69
INT4 group 12871,5 %132,85
INT4 per row58,3 %72,24
INT3 group 1285,6 %21,06
INT2 group 1280,0 %01,00

Очакваните tokens, приети на verification pass, при draft length γ\gamma, са

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

а нетното ускорение дели това на собствената цена на draft, дял cc от target на token:

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

Удебеленият запис е този, който трябва да запомните: speculative decoding може да направи генерирането по-бавно. При 30 % acceptance с draft, струващ една пета от target, плащате за пет forward passes и запазвате 1,4 tokens. Последната колона е другият капан — по-дълъг draft помага само когато acceptance е висока, защото опашката на γ\gamma-token предположение почти никога не се достига. При 90 % acceptance γ=8\gamma = 8 струва 3,40x, а при 30 % струва 0,79x: същата конфигурация, печалба или загуба според число, измерено върху вашия трафик.

Квантизацията свива model, като съхранява същата функция в по-малко bits. Дистилацията го свива, като обучава по-малък model да имитира по-голям11 — идея, която предхожда deep learning с почти десетилетие.12

Фината част е от какво учи student. Не от правилния отговор: можеше да бъде обучен директно върху него. Това, което teacher добавя, е цялото разпределение. Попитайте model какво следва след фраза и погледнете отвъд argmax:

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

Hard label казва jug и нищо друго. Soft label казва jug, а също че cup е било почти толкова добро, bowl — правдоподобно, и large — прилагателно, напълно различно граматично продължение — все още живо. Това е оригиналният аргумент: това е 7, но прилича доста на 1, а приликата е информация, която hard label изхвърля.

Затова дистилацията използва и температура. Делението на logits на TT преди softmax изглажда разпределението и повишава относителната тежест на подгласниците: при тази фраза съотношението между top token и третия пада от 2,24 при T=1T = 1 до 1,50 при T=2T = 2 — квадратният корен на първото, което е ефектът от делене на logits на две върху съотношение. Същият ред, повече от attention на loss върху близките пропуски. Gradient на student носи несигурността на teacher, а не само присъдата му.

Всичко в тази глава вече е една сума:

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

където TT е общият брой resident tokens през всички едновременни заявки. Прилагане: редовете 7B и 70B приемат 8 key-value heads с размерност 128, редът 13B — пълен multi-head attention с 40 heads, както са били построени тези поколения model — и това личи.

8 GB

modelprecisionweightsсвободно след overheadcontext tokens, които се побират
7Bfp1613,0 GBне се побира
7Bint86,5 GBне се побира
7Bint4 (g128)3,4 GB3,1 GB25 710
13Bint4 (g128)6,2 GB0,3 GB337
70Bint4 (g128)33,6 GBне се побира

16 GB

modelprecisionweightsсвободно след overheadcontext tokens, които се побират
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

modelprecisionweightsсвободно след overheadcontext tokens, които се побират
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 GBне се побира

Погледнете реда 13B в таблицата за 8 GB. Теглата се побират — 6,2 GB от 8 — така че по обичайния начин на говорене 13B model „работи на 8 GB карта“. Той има 337 tokens context, което не е разговор, а едва prompt. „Побира ли се“ е грешният въпрос. Правилният е „с колко context и за колко потребители едновременно“.

Погледнете и двата int8 реда при 16 GB. 7B получава 65 378 tokens, а 13B — 3 136 — двадесеткратна разлика от 5,6 GB допълнителни тегла, защото 13B тук има multi-head attention и неговият cache струва 800 KB на token срещу 128 KB на 7B. Два model с подобен размер, единият неизползваем за дълъг context, по причина, която не се появява в заглавието на никоя model card.

Преди тринадесет глави това беше perceptron с две тегла и bias. Сега е transformer, който е проектиран, обучен, aligned, научен да харчи compute за трудни въпроси и served при измерена цена на token — без нито една неотворена кутия вътре.

Това приключва тук, и приключва нарочно.

Глава 14 започва с model някъде другаде. Не във вашия process, не във вашата памет, не в променлива, която можете да print-нете: на машина, която не администрирате, зад API key, port и bill. Всичко измерено тук все още се случва — prefill все още върви преди първия token, cache все още расте с разговора, batch, в който сте, все още принадлежи на някой друг и все още решава вашата латентност — но оттук нататък го наблюдавате през stream от Server-Sent Events, finish_reason и HTTP 429 с header Retry-After. Въпросите се променят с гледната точка: не как се изчислява този gradient, а защо фактурата ми стана тройна. Езикът също, и Глава 14 обяснява това правило, вместо да го обявява — дотук code държеше weights, gradients, logits и tokenizer bytes; оттам нататък държи connection, retry, cancellation и accumulated state. Тринадесетте глави зад вас не се изхвърлят при преминаването. Те са описанието на това, което работи от другата страна на port.


Два пропуска са умишлени. FlashAttention (Dao et al., arXiv:2205.14135) не е различен attention — той изчислява същата функция чрез tiling на операцията, така че n×nn \times n score matrix никога да не се записва в паметта, затова 67 MB във втората таблица на тази глава са по-малки на практика, отколкото аритметиката предполага. А самите kernels са делегирани: лекция 10 от Stanford CS336 покрива inference systems в дълбочина, която това не се опитва да постигне, а repository llama.cpp и спецификацията GGUF са основните източници за CPU страната.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Статията е до голяма степен аргумент за memory-bandwidth и се чете като такъв.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Включва uptraining рецептата, която преобразува съществуващ multi-head checkpoint, затова 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. Въвежда iteration-level scheduling — continuous batching — и selective batching.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. Статията, върху която е изграден vLLM; §3 е аналогията с операционните системи в пълен вид.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 е дефиниран в §3; шестнадесетте стойности на нивата, използвани в измерването по-горе, са тези, които тази статия извежда.

  6. Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). Анализът на outlier-feature в §4 е източникът на явлението, измерено по-горе, включително откритието, че outliers се появяват систематично при мащаб. 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 е доказателството, че изходното разпределение е непроменено; Chen et al. (arXiv:2302.01318) публикуват същата идея независимо.

  11. Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Температурата и аргументът за „dark knowledge“.

  12. Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Дистилация, девет години по-рано, за ensembles вместо transformers.


Създадено от

David Vicente Campos

Основател на NeuraLIA Labs и съосновател на MyRealFood

Компютърен инженер съм, завършил Университета в Леон. Съосновах MyRealFood, където като CTO създадох приложението, което милиони хора са използвали, за да се хранят по-здравословно, и основах NeuraLIA Labs, където изграждам AI продукти. Тук пиша за това, което трябваше да разбера по пътя, така, както ми се иска някой да ми го беше обяснил.

Още за автора

Публикувано от NeuraLIA Labs.

Получавайте нови публикации във входящата си поща

Новини за AI, ръководства и продуктови обновления — кратък имейл, когато публикуваме нещо, което си заслужава.

Индекс на курса

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev12 мин четене

AI моделът Jev е създаден за решения, не за проза

Jev на TypeSafe AI привлича внимание, защото разглежда софтуерната интелигентност като проблем на вероятностите: изберете правилния клон, добавете увереност и не плащайте на LLM да пише текст, когато кодът има нужда от решение.

Abstract legal research workspace with documents, search nodes and governance controls.
openai11 мин четене

Astra for Law на OpenAI е правна AI система, не нов модел

Правният старт на OpenAI е не толкова за нов базов модел, колкото за системата около него: домейн извличане, надеждни инструменти, права, бенчмаркове и пътища за преглед.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering12 мин четене

Инженеринг на контекста за AI агенти с дълъг хоризонт

Дълго работещите агенти не се провалят само защото прозорецът е малък. Те се провалят, когато файлове, изходи от инструменти и остаряла история изтласкат задачата, която агентът е трябвало да завърши.

Готови ли сте LIA да избира вместо вас?

Създавайте с всички AI модели на едно място — започнете безплатно още днес.