Як здешевити inference: KV cache, batching і квантування
Той самий model відповідає за 8,8 с і 78,9 с із byte-ідентичним output. Далі — INT4, виміряний трьома способами.
На цій сторінці
Той самий model, на тій самій машині, відповідає на те саме питання тими самими 48 token. Два outputs ідентичні token за token — це перевірено, а не припущено.
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)Змінився один argument: use_cache=False. Нічого в model, prompt, sampling чи arithmetic не відрізняється, і другий запуск не стає точнішим за ці зусилля. Він просто в дев’ять разів повільніший без жодної користі.
Саме так виглядає цей розділ. Усе в ньому — cache, batch, quantized weights — є спробою перестати платити за роботу, яка не змінює відповідь, або з’ясувати, скільки коштує дешевша відповідь. Розділ 10 встановив прайс для training. Це прайс для тієї сторони, за яку ви платите завжди: deployed model витрачає приблизно FLOPs за кожен token, який він видає, у кожному request, до кінця свого життя.
Куди пішов час другого запуску
Посилання на розділ: Куди пішов час другого запускуЩоб згенерувати token, decoder-only transformer бере всю sequence на цей момент, проганяє її через кожен layer і зчитує probability distribution з останньої позиції. Потім він додає вибраний token і робить це знову. Цей опис правильний, і саме це робить повільний запуск.
Але це ще й надзвичайно марнотратно, і причина — causal mask із Розділу 9. Key і value vectors позиції 7 обчислюються з input позиції 7 і позицій перед нею. Коли приходить позиція 8, позиція 7 не може її бачити — саме це означає causal — тож key і value позиції 7 є точно тими самими числами, що й раніше. Повільний запуск усе одно переобчислює їх на кожному кроці.
Тож збережіть їх. Це сховище — key-value cache, найважливіша optimization у serving мовних models:
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 усередині loop: nxt, один token. Не sequence. Query нового token attends до кожного cached key, а cached keys і так ніколи не мали змінитися. Це не approximation — перевірка ідентичності output вище і є суттю. Cache не міняє якість на швидкість; він видаляє надлишкову arithmetic.
Щоб чисто побачити scaling, приберіть transformer і виміряйте одну attention head з , один крок generation, обчислений обома способами:
| tokens in context | переобчислити все | з cache | ratio | score matrix |
|---|---|---|---|---|
| 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 |
Правий стовпець — причина. Переобчислення будує повну attention matrix на кожному кроці — ту саму з блоку asymptotic notation у Розділі 9, сплачену один раз за token. З cache ви натомість будуєте рядок : при 4,096 tokens це 67 MB scores проти 16 KB.
Підрахунок multiply-accumulates замість milliseconds прибирає машину з аргументу. Щоб згенерувати tokens з cold start:
| tokens generated | з cache | переобчислення | ratio |
|---|---|---|---|
| 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 |
На кожному кроці cached version є linear щодо context, а uncached — quadratic; у сумі за generation це проти , причому ratio росте без межі. Дев’ятикратну різницю на початку було виміряно на 48 tokens — ще до першого рядка тієї таблиці.
Cache також змінює те, що має бути в memory. На 8 GB laptop GPU, який генерує 256 tokens у fp16, якщо взяти peak allocator і відняти resident weights:
| peak working memory | |
|---|---|
| з cache | 21.8 MB |
| переобчислення | 181.7 MB |
У 8.3 раза більше memory, витраченої на те, щоб створити ті самі tokens повільніше. Це обіцянка з Розділу 5, яка приходить із несподіваного боку: там reverse-mode autodiff мав тримати кожен intermediate живим для backward pass, і activations домінували training memory. В inference немає backward pass і нічого не треба зберігати для нього — тож memory натомість домінує cache, і це свідомий вибір, а не неминуча вартість.
Prefill і decode — це дві різні машини
Посилання на розділ: Prefill і decode — це дві різні машиниЩе раз подивіться на швидкий запуск: його перший token поводився не так, як інші сорок сім.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepPrompt коштував 25.6 ms за token, а кожен generated token — 166 ms. Той самий model, те саме hardware, ті самі weights, шестикратна різниця за token — і вона спрямована не туди, куди очікує більшість. Prompt — це дешева частина. Generation ділиться на дві фази з по-справжньому різною фізикою:
Prefill
Посилання на розділ: PrefillОдин forward pass через увесь prompt. Кожен token обробляється паралельно, тож кожна weight matrix завантажується з memory один раз і множиться на matrix із сотень token vectors — matrix-matrix product, із великою кількістю arithmetic на кожен переміщений byte, саме те, для чого створений GPU. Prefill є compute-bound, а його вартість приблизно linear від довжини prompt.
Один forward pass на token, batch of one і sequence of one. Кожна weight matrix усе ще повністю завантажується з memory і множиться на один vector — matrix-vector product, майже без arithmetic на переміщений byte. Decode є memory-bandwidth-bound, а його вартість за token майже не залежить від довжини context.
Обидві половини можна виміряти. Prefill, один pass через tokens:
| prompt tokens | seconds | 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, один token проти cache з :
| cached tokens | ms for one token |
|---|---|
| 16 | 110.05 |
| 64 | 97.57 |
| 256 | 108.53 |
| 1024 | 103.86 |
Прочитайте другу таблицю двічі. Перехід від 16 tokens context до 1,024 — у шістдесят чотири рази більше history для attention — не змінив вартість кроку на вимірювану величину. Attention до cache — реальна робота, але її затьмарює фіксована вартість протягування пів мільярда weights через memory bus, щоб створити один vector. Саме ця фіксована вартість є причиною всього в наступному розділі.
Ці дві фази — джерело двох чисел, які показує кожна serving system. Time to first token — це по суті prefill, і він росте разом із prompt, тому довга розмова повільно стартує. Tokens per second — це , і він приблизно сталий, тому відповідь далі тече рівномірно. Chat, який повільно починається, а потім плавно streaming, — це не rendering trick. Це дві наведені таблиці.
Cache — це також рахунок
Посилання на розділ: Cache — це також рахунокCache міняє arithmetic на memory, і memory, якої він хоче, не мала. Для кожного token у context кожен layer тримає один key vector і один value vector на кожну key-value head:
2 — це keys і values; усе інше — architecture. Для model, вимірюваного в цьому розділі, — 24 layers, 14 query heads, 2 key-value heads, head dimension 64 — у fp16 це bytes на token.
Formulae в цій сфері мають звичку помилятися вдвічі, тож перевірте це на allocator, а не просто вірте:
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Точно, і залишається точним для кожної перевіреної shape:
| batch | context | measured cache | predicted | peak working memory |
|---|---|---|---|---|
| 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 |
Останні три рядки заслуговують на другий погляд. Тридцять два users із 2,048 tokens кожен, шістдесят чотири з 1,024, сто двадцять вісім з 512 — cache у кожному випадку 768 MB, бо всі три варіанти тримають 65,536 tokens. Cache залежить лише від загальної кількості resident tokens, а не від того, як вони розподілені між users. Цей факт — фундамент розділу про batching.
Звідки беруться MQA і GQA
Посилання на розділ: Звідки беруться MQA і GQAРозділ 9 представив multi-query і grouped-query attention, відклавши причину до цього розділу. Причина — ця formula, а саме у ній.
Standard multi-head attention дає кожній query head власні key і value heads. Тут model має 14 query heads; із full multi-head attention його cache був би bytes на token — 84 KB замість 12 KB, рівно в сім разів більше, за ratio query heads до key-value heads.
Multi-query attention1 доводить це до межі: усі query heads ділять одну key-value head. Grouped-query attention2 — компроміс, який переміг: кілька key-value heads, кожна з яких спільна для групи query heads, — бо втрата якості MQA була реальною, а в GQA її немає. Жоден із них не купує arithmetic. Вони існують, щоб поділити цю formula на integer, і поширилися індустрією тієї миті, коли long contexts зробили cache binding constraint.
А він робить це швидко. Для 7B-class model із 32 layers і 8 key-value heads dimension 128 cache становить 128 KB на token у fp16:
| context tokens | one user | 8 users | 64 users |
|---|---|---|---|
| 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 |
Власні weights цього model — 13.0 GB у fp16, цифра в таблиці наприкінці розділу. Тож при context у 128,000 tokens cache одного user більший за model. Це arithmetic, яку Розділ 16 перетворює на гроші, і тому довга розмова не просто повільна — вона займає фіксований шматок машини весь час, поки request живий.
Batching: число, яке зростає, і число, яке падає
Посилання на розділ: Batching: число, яке зростає, і число, яке падаєDecode є memory-bound: weights протягуються через bus, щоб створити один token, а arithmetic units простоюють. Тож покладіть більше роботи в той самий крок. Запустіть кілька requests одночасно, і weights, прочитані один раз, обслужать їх усі. Виміряно на тому самому model, кожен request тримає 64-token cache і decode один token:
| batch | latency per step | throughput | latency 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 |
Читайте два праві стовпці один проти одного, бо в них уся суть. Перехід від одного request до шістнадцяти множить throughput на 6.0 і множить очікування будь-якого окремого request на 2.67. Batch зробив server кращим, а кожного user — гіршим.
Це не bug, який можна налаштуванням прибрати; це сама tradeoff, і він має назву з кожного боку. Latency — це те, що відчуває людина, яка чекає відповіді. Throughput — це те, на що ділиться invoice. Жоден setting не покращує обидва.
Зверніть увагу й на те, де це зупиняється. Від 16 до 32 throughput зростає на 9 %, а latency майже подвоюється: крок перестав бути memory-bound і став compute-bound, а після цього knee batch уже нічого не купує. Кожен deployment має такий knee; його місце треба виміряти на вашому, але його існування — ні.
Static batching марнує більшість того, що виграє
Посилання на розділ: Static batching марнує більшість того, що виграєНаївний спосіб batch — зібрати requests, запустити їх разом і повернути результат, коли всі завершаться. Але вони не завершуються разом: деякі replies мають двадцять tokens, а деякі — п’ятсот. Fixed batch працює, доки не завершиться найдовший member, і кожен завершений request до того часу продовжує займати свій slot, додаючи padding.
Візьміть 64 requests із реалістично скошеними output lengths — median 18 tokens, longest 231, 1,874 загалом — і змоделюйте обидві policies за виміряною per-step cost для восьми slots:
| policy | wall clock | throughput | mean latency per request | wasted slot-steps |
|---|---|---|---|---|
| static batches of 8 | 176.9 s | 10.6 tok/s | 83.2 s | 3,214 |
| continuous, 8 slots | 109.0 s | 17.2 tok/s | 8.1 s | 0 |
Throughput поліпшується в 1.6x. Mean latency поліпшується більше ніж у десять разів, бо за static batching request, який завершився за чотири steps, усе одно чекає на 231-token сусіда, перш ніж хтось почує результат.
Continuous batching3 — це виправлення, і воно настільки просте, як звучить: batch — це не група, а набір slots, і slot, який звільнився, приймає наступний request із queue вже на наступному step. Scheduler працює з granularity одного token, а не одного request. Кожен production serving stack тепер робить саме це.
У нього є друга половина — cache. Slots, які приходять і йдуть, залишають cache memory fragmented, а резервування для кожного slot його максимально можливого context марнує більшість reservation. PagedAttention4 позичає відповідь в operating systems: зберігати cache у fixed-size blocks із block table для кожної sequence, щоб cache sequence міг бути фізично розкиданим, але логічно contiguous — і це також дозволяє двом sequences зі спільним prefix ділити blocks, які його тримають. На цьому побудований vLLM, і тому serving engine — це memory allocator із прикріпленим transformer.
Quantization і перше, що ламається
Посилання на розділ: Quantization і перше, що ламаєтьсяДруга половина рахунку — це самі weights. Пів мільярда parameters по чотири bytes кожен — 1.98 GB; по два bytes — 0.99 GB; по одному byte — 0.49 GB. Менше bits на weight зменшує model на disk, зменшує його в memory і — бо decode bandwidth-bound — робить кожен step швидшим, адже треба переміщувати менше bytes.
Найпростіша scheme — symmetric absolute-maximum quantization, і вона вміщується в три рядки:
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 так, щоб найбільша weight відповідала найбільшому integer, поділіть, округліть, збережіть integers і scale. Відновіть множенням назад. У цьому немає нічого хитрого, і це працює — аж доки не перестає.
Виміряно на реальних weights model: усі 168 projection matrices, 357.8 million parameters, relative error :
| scheme | mean relative error | worst matrix |
|---|---|---|
| INT8, one scale for the whole matrix | 0.0400 | 0.1487 |
| INT8, one scale per output row | 0.0100 | 0.0149 |
| INT4, one scale for the whole matrix | 0.6026 | 0.9931 |
| INT4, one scale per output row | 0.1790 | 0.2589 |
| INT4, one scale per group of 128 | 0.1323 | 0.1992 |
| NF4, one scale per block of 64 | 0.0952 | 0.1205 |
| INT3, one scale per group of 128 | 0.3044 | 0.4123 |
| INT2, one scale per group of 128 | 0.7790 | 0.8076 |
Четвертий рядок — це collapse. Relative error 0.99 на worst matrix означає, що reconstruction по суті не зберігає нічого від original — matrix замінено noise приблизно правильної magnitude. Причину видно в тому самому experiment на одній matrix:
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 %)Одна weight із шести тисяч лежить далі ніж на шість standard deviations, а найбільша — на 24. З одним scale для всієї matrix ця одна weight задає step size для всіх 4.3 million інших. При 8 bits є 256 steps, і типова weight усе ще потрапляє на meaningful one. При 4 bits їх 16, крайній зарезервовано для value, якого майже ні в кого немає, а ordinary weights — тобто всі вони — округлюються до двох або трьох distinct levels.
Усе після цього рядка — той самий ремонт на різних granularities: дати scale меншу територію. Per output row ділить error на 3.4; per group of 128 consecutive weights ділить його ще раз. Вартість — bookkeeping: 16-bit scale на group of 128 — це bits на weight замість 4 — і він повертає більшу частину розриву.
NF4 заходить із іншого боку.5 Levels не мусять бути equally spaced. Weights у block приблизно normally distributed, тож виберіть sixteen levels як quantiles normal distribution: щільно біля zero, де weights справді є, рідко в tails, де їх немає. Ті самі four bits, той самий block scaling, на меншому block — 4.25 bits на weight проти 4.125 у group-128 — і measured error падає з 0.1323 до 0.0952, на 28 % нижче. Частина цього — finer block, решта — розміщення levels там, де mass, а щоб розділити ці дві причини, потрібен був би третій рядок.
Outlier features
Посилання на розділ: Outlier featuresБлок про floating-point у Розділі 2 завершився обіцянкою: цей розділ quantize weights до 8 і 4 bits і знайде жменьку outlier features, які відмовляються стискатися. Ось вони, і вони пояснюють, чому «просто округлити числа» ніколи не мало спрацювати для activations.
Weights вище поводилися погано. Activations — в іншій лізі. Візьміть звичайний 84-token prompt, зафіксуйте residual stream на кожному layer і виміряйте найбільшу magnitude, якої досягає кожен із 896 dimensions:
| layer | largest |h| | median dimension's largest |h| | ratio | dimensions above 6x the median |
|---|---|---|---|---|
| 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 |
Dimension 62 досягає 1,579.6, тоді як median dimension ніколи не перевищує 1.6. Це не випадковість одного token чи одного layer: та сама dimension є на layer 4 і все ще є на layer 20, майже з тим самим value. Це outlier features,6 і вони systematic — властивість trained model, а не input.
Histogram цих 896 per-dimension maxima на layer 16 робить форму безпомилковою:
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Дев’ятсот dimensions акуратною купою нижче 8, взагалі нічого на три octaves, а потім одна dimension самотньо в далекому кінці. Тепер quantize цей tensor до INT8 і порахуйте, що станеться:
| scheme | relative error | distinct integer levels used, whole tensor |
|---|---|---|
| one scale for the whole tensor | 0.1083 | 14 of 256 |
| one scale per token (per row) | 0.0433 | 158 |
| whole tensor, 1 outlier dimension kept in fp32 | 0.0442 | 48 |
| whole tensor, 4 outlier dimensions kept in fp32 | 0.0279 | 57 |
| whole tensor, 16 outlier dimensions kept in fp32 | 0.0085 | 102 |
Чотирнадцять levels із 256. Scale задавався 1,579.6, тож кожен step має ширину 12.44, а типовій activation — median magnitude 0.26, ninety-ninth percentile 2.51 — нікуди потрапити. Per dimension це ще різкіше:
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Один level. Уся dimension, кожен token, quantized до того самого number. Було виділено eight bits, а використано приблизно нуль, і model, який читає ці activations, отримує constant.
Це вимірювання — виправдання кожної technique, яку люди реально використовують:
Тримайте outliers поза цим. LLM.int8()6 decomposes matrix multiply: dimensions з extreme magnitudes обчислюються в 16 bits, усе інше — в INT8, а halves сумуються. Таблиця вище — receipt: removing four dimensions зменшує error майже в чотири рази. SmoothQuant7 натомість переносить складність: поділіть activations на per-channel factor і помножте відповідний weight column на нього, що залишає product незмінним і переносить outlier з tensor, який не може його поглинути, у той, який може.
Вибирайте rounding, а не просто round. Нічого вище не питає, для чого matrix. GPTQ8 quantizes column by column і після кожного column коригує remaining full-precision columns, щоб компенсувати вже committed error — minimising error output layer на реальних inputs, а не його weights. AWQ9 зауважує, що мала частка weight channels має набагато більше значення за решту, знаходить їх із activation statistics і масштабує вгору перед quantizing, щоб вони потрапили на finer levels. Обом потрібен calibration set; жодному не потрібні gradients.
Показати подробиці
GGUF, і до чого тут file format.
GGUF — не quantization method; це container, який використовує llama.cpp, а плутанина в comparisons gguf vs gptq виникає через те, що їх трактують як речі одного типу. GGUF тримає tensors, tokenizer, architecture metadata і chat template в одному memory-mappable file, а всередині несе family block schemes — назви на кшталт Q4_K_M кодують bits per weight, block size і те, чи деякі tensors лишаються з higher precision.
Важлива engineering-різниця: GPTQ і AWQ створюють weights, optimized для GPU kernel, тоді як schemes GGUF дешево decoded на CPU з mapped, а не loaded file. Саме тому той самий nominal «4-bit 7B model» існує в обох світах із різними sizes і різною quality, і тому чесне comparison — ніколи не format, а measurement нижче, запущене на вашій власній task.
Скільки quantization насправді коштує: виміряно
Посилання на розділ: Скільки quantization насправді коштує: виміряноМайже кожна стаття про quantization зупиняється на попередньому розділі: пояснює method, цитує compression ratio і стверджує, що quality «здебільшого збережена». Розділ 4 був про те, як не обдурити себе, тож з’ясуймо.
Той самий model, weights quantized in place кожною scheme, потім три measurements: perplexity на 2,048 tokens відкладеної англійської prose — тут, чернетці цього course, саме тому repository підставляє fixed public-domain book і друкує таблицю тієї самої форми з іншими numbers — battery з 16 short factual questions із known answers під greedy decoding, і fraction of tokens, на яких quantized model погоджується з full-precision model за identical context.
| scheme | mean weight error | perplexity | question battery | agrees with fp32 |
|---|---|---|---|---|
| fp32 (reference) | 0.0000 | 23.08 | 13/16 | 100.0 % |
| INT8 per tensor | 0.0400 | 23.58 | 13/16 | — |
| INT8 per row | 0.0100 | 22.96 | 13/16 | 98.6 % |
| INT4 per tensor | 0.6026 | 365,416,000 | 0/16 | — |
| INT4 per row | 0.1790 | 46.18 | 6/16 | 58.3 % |
| INT4 group 128 | 0.1323 | 31.08 | 10/16 | 71.5 % |
| NF4 block 64 | 0.0952 | 24.55 | 11/16 | 84.7 % |
| INT3 group 128 | 0.3044 | 213.09 | 0/16 | 5.6 % |
| INT2 group 128 | 0.7790 | 26,325,436 | 0/16 | 0.0 % |
Чотири речі в цій таблиці варто сказати прямо.
Правильно зроблений INT8 безкоштовний. Per-row INT8 дає 22.96 проти 23.08 у reference — розрив одна частина на двісті, тобто noise, який слід читати як «identical». У який бік дивиться noise — нестабільно: на public-domain corpus у repository ті самі дві schemes дають 22.24 проти 22.18: половина цієї відстані, і в інший бік. Він погоджується з full-precision model на 142 із 144 generated tokens. Чверть memory проти fp32 reference, половина проти fp16, який ви реально deploy, і жодної detectable cost. INT8, зроблений недбало, теж майже безкоштовний: один scale на matrix коштує 0.5 perplexity points і жодної відповіді в battery. Eight bits достатньо forgiving, щоб granularity майже не мала значення, саме тому люди узагальнюють з INT8 на INT4 і травмуються.
INT4 з одним scale на tensor знищує model. Perplexity 365 million: не degraded, а annihilated. Далі granularity — уся гра: per-tensor 365,416,000, per-row 46.18, per-group-of-128 31.08, NF4 24.55. Ті самі four bits на weight, factor у п’ятнадцять мільйонів між worst і best.
Perplexity — грубий інструмент, а battery ще грубіший. Між NF4 і group-128 INT4 gap perplexity — 6.5 points, а battery різниться на одне question — і confidence interval із Розділу 4 каже, що одне question із sixteen не розрізняє взагалі нічого. Є різкіша демонстрація, ніж interval: запустіть ту саму battery з вимкненим stock repetition penalty model, що власне й означає greedy decoding, і ці два рядки поміняються місцями. Одне question із sixteen — це не малий effect, це no effect. Попередження Розділу 8 теж застосовне: perplexity можна порівнювати лише між models зі спільним tokenizer, тож число з чужого write-up не можна порівнювати з вашим.
Стовпець agreement — найгостріший із трьох, і майже безкоштовний: запустіть full-precision model greedily, а потім на кожній position запитайте quantized model, що він вибрав би за того самого prefix. Він має 144 independent observations замість 16, не потребує ground truth і деградує плавно там, де battery деградує стрибками. Це також рівно та quantity, яка потрібна наступному розділу.
Це обіцянка, яку Розділ 1 дав щодо цього розділу, вчасно виконана: mathematics каже, що 4-bit model можливий, а engineering вирішує, чи придатний він до використання.
Speculative decoding
Посилання на розділ: Speculative decodingРозділ 12 оголосив це й залишив рахунок тут.
Ідея прямо випливає з поділу prefill/decode. Verification proposed sequence з tokens коштує один forward pass через positions — matrix-matrix product, ледь дорожчий за pass через одну. Тож:
Малий дешевий model generates candidate tokens autoregressively.
Великий model runs один forward pass через усі candidates одразу, producing те, що він сказав би на кожній position.
Залиште longest prefix, на якому обидва погоджуються, плюс token, який великий model дає безкоштовно на першій disagreement. Решту відкиньте й почніть знову.
Output distribution не змінюється. З greedy decoding це очевидно — token accepted лише якщо target його й produced би. З sampling потрібне modified acceptance rule, і Leviathan et al. доводять, що resulting distribution точно дорівнює target.10 Це друга exact optimisation у цьому розділі.
Отже, все залежить від acceptance rate , яку можна виміряти — це agreement column вище, саме тому його там обчислено. Використовуючи кожен quantized model як draft для full-precision target, на 144 generated positions:
| draft model | acceptance | longest accepted run | expected tokens per target pass, |
|---|---|---|---|
| fp32 (the target itself) | 100.0 % | 48 | 5.00 |
| INT8 per row | 98.6 % | 48 | 4.86 |
| NF4 block 64 | 84.7 % | 20 | 3.69 |
| INT4 group 128 | 71.5 % | 13 | 2.85 |
| INT4 per row | 58.3 % | 7 | 2.24 |
| INT3 group 128 | 5.6 % | 2 | 1.06 |
| INT2 group 128 | 0.0 % | 0 | 1.00 |
Expected tokens accepted per verification pass, при draft length , дорівнює
а net speedup ділить це на власну cost draft, fraction від target на token:
| acceptance | , | , | , | , |
|---|---|---|---|---|
| 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 |
Жирний запис — той, який слід запам’ятати: speculative decoding може зробити generation повільнішою. При 30 % acceptance з draft, що коштує п’яту частину target, ви платите за five forward passes і зберігаєте 1.4 tokens. Останній стовпець — інша пастка: longer draft допомагає лише за high acceptance, бо tail -token guess майже ніколи не досягається. При 90 % acceptance вартий 3.40x, а при 30 % — 0.79x: та сама configuration, win або loss залежно від number, виміряного на вашому traffic.
Distillation і те, що несе soft label
Посилання на розділ: Distillation і те, що несе soft labelQuantization стискає model, зберігаючи ту саму function у меншій кількості bits. Distillation стискає його, training менший model імітувати більший11 — ідея, що майже на decade старша за deep learning.12
Тонкість у тому, з чого student learns. Не з правильної answer: на ній його можна було train directly. Те, що додає teacher, — це whole distribution. Запитайте model, що йде після phrase, і подивіться далі за argmax:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360Hard label каже jug і нічого більше. Soft label каже jug, а також що cup був майже так само good, bowl plausible, а large — adjective, зовсім інше grammatical continuation — усе ще live. Це original argument: це 7, але воно дуже схоже на 1, і ця resemblance — інформація, яку hard label викидає.
Саме тому distillation використовує temperature. Ділення logit на перед softmax вирівнює distribution і піднімає relative weight runners-up: у цій phrase ratio між top token і third падає з 2.24 при до 1.50 при — square root першого, саме це робить ділення logit на два з ratio. Той самий ordering, більше attention loss на near misses. Gradient student несе uncertainty teacher, а не лише його verdict.
Що вміщується в 8, 16 і 24 GB
Посилання на розділ: Що вміщується в 8, 16 і 24 GBУсе в цьому розділі тепер одна сума:
де — це total resident tokens across all concurrent requests. Застосуймо: рядки 7B і 70B припускають 8 key-value heads dimension 128, рядок 13B — full multi-head attention із 40 heads, саме так були побудовані ті generations of model — і це видно.
8 GB
| model | precision | weights | free after overhead | context tokens that fit |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | does not fit | — |
| 7B | int8 | 6.5 GB | does not fit | — |
| 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 | does not fit | — |
16 GB
| model | precision | weights | free after overhead | context tokens that fit |
|---|---|---|---|---|
| 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 | weights | free after overhead | context tokens that fit |
|---|---|---|---|---|
| 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 | does not fit | — |
Подивіться на рядок 13B у таблиці 8 GB. Weights вміщуються — 6.2 GB із 8 — тож у звичній манері говорити 13B model «runs on an 8 GB card». Він має 337 tokens context, тобто це не розмова, а ледве prompt. «Чи вміщується?» — неправильне питання. Правильне: «з яким context і для скількох users одночасно».
Подивіться також на два рядки int8 у 16 GB. 7B отримує 65,378 tokens, а 13B — 3,136: двадцятикратна різниця від 5.6 GB extra weights, бо 13B тут має multi-head attention, і його cache коштує 800 KB на token проти 128 KB у 7B. Два models подібного size, один непридатний для long context, з причини, яка не з’являється в headline жодної model card.
Куди це веде далі
Посилання на розділ: Куди це веде даліТринадцять розділів тому це був perceptron із двома weights і bias. Тепер це transformer, який було designed, trained, aligned, навчено витрачати compute на складні questions і served за виміряною cost per token — без жодного unopened box усередині.
На цьому все закінчується, і закінчується навмисно.
Розділ 14 починається з model десь в іншому місці. Не у вашому process, не у вашій memory, не в variable, яку можна print: на machine, яку ви не administer, за API key, port і bill. Усе, що вимірювалося тут, усе ще відбувається — prefill усе ще runs перед першим token, cache усе ще grows разом із conversation, batch, у якому ви перебуваєте, усе ще належить комусь іншому й усе ще decides вашу latency — але відтепер ви observes це через stream Server-Sent Events, finish_reason і HTTP 429 з header Retry-After. Questions змінюються разом із vantage point: не як обчислюється цей gradient, а чому мій invoice потроївся. Language теж змінюється, і Розділ 14 пояснює це правило, а не оголошує його — до цього моменту code тримав weights, gradients, logits і tokenizer bytes; далі він тримає connection, retry, cancellation і accumulated state. Тринадцять розділів позаду вас не відкидаються під час переходу. Вони — опис того, що працює по той бік port.
Джерела й метод
Посилання на розділ: Джерела й методДва omissions навмисні. FlashAttention (Dao et al., arXiv:2205.14135) — це не інший attention: він обчислює ту саму function, tiling operation так, щоб score matrix ніколи не записувалася в memory, тому 67 MB у другій таблиці цього розділу на practice менші, ніж підказує arithmetic. А самі kernels delegated: lecture 10 of Stanford's CS336 covers inference systems із depth, на яку цей текст не претендує, а repository llama.cpp і GGUF specification — primary sources для CPU side.
Примітки
Посилання на розділ: Примітки-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Paper значною мірою є аргументом про memory-bandwidth, і саме так читається. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Містить uptraining recipe, який converts existing multi-head checkpoint, тому GQA поширився так швидко. ↩
-
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. Introduces iteration-level scheduling — continuous batching — and selective batching. ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. Paper, на якому побудований vLLM; §3 повністю розкриває аналогію з operating systems. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 defined у §3; sixteen level values, використані у measurement вище, — ті, які derives ця paper. ↩
-
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 — джерело phenomenon, виміряного вище, включно з finding, що outliers systematically emerge at scale. ↩ ↩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 — proof, що output distribution не змінюється; Chen et al. (arXiv:2302.01318) published ту саму ідею independently. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Temperature і аргумент про «dark knowledge». ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, на дев’ять років раніше, для ensembles, а не transformers. ↩