Inference को सस्ता बनाना: KV cache, Batching और Quantization
वही model, वही सवाल: 8.8 सेकंड बनाम 78.9, byte-identical 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 में कुछ भी अलग नहीं है, और दूसरी run अपनी मेहनत के बदले ज़्यादा accurate भी नहीं है। वह बिना वजह नौ गुना धीमी है।
यही इस अध्याय का आकार है। इसमें सब कुछ — cache, batch, quantized weights — या तो उस काम की कीमत चुकाना बंद करने की कोशिश है जो जवाब नहीं बदलता, या यह पता लगाने की कि सस्ता जवाब असल में कितना महँगा पड़ता है। अध्याय 10 ने training की price list तय की थी। यह उस हिस्से की price list है जिसके लिए आप हमेशा भुगतान करते हैं: deployed model अपनी ज़िंदगी भर, हर request पर, हर निकाले गए token के लिए लगभग FLOPs खर्च करता है।
दूसरी run का समय कहाँ गया
सेक्शन का लिंक: दूसरी run का समय कहाँ गयाएक token generate करने के लिए decoder-only transformer अब तक की पूरी sequence लेता है, उसे हर layer से चलाता है, और आख़िरी position से probability distribution पढ़ता है। फिर वह चुना गया token जोड़ता है और यह फिर से करता है। यह वर्णन सही है, और slow run यही करती है।
यह बेहद wasteful भी है, और वजह अध्याय 9 का causal mask है। Position 7 के key और value vectors position 7 के input और उससे पहले की positions से compute होते हैं। जब position 8 आती है, position 7 उसे नहीं देख सकती — causal का मतलब यही है — इसलिए position 7 के key और value ठीक वही numbers हैं जो पहले थे। Slow run फिर भी हर step पर उन्हें दुबारा compute करती है।
तो उन्हें store कर लें। वही store key-value cache है, language model serving में सबसे निर्णायक optimisation:
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)देखिए loop के अंदर model को क्या fed किया जाता है: nxt, एक token। Sequence नहीं। नए token की query हर cached key पर attend करती है, और cached keys बदलने वाली थीं ही नहीं। यह approximation नहीं है — ऊपर identical-output check का point यही है। cache quality के बदले speed नहीं लेता; वह redundant arithmetic हटा देता है।
Scaling साफ़ देखने के लिए transformer को हटाकर के साथ एक single attention head time करें, generation का एक step दोनों तरीकों से compute करके:
| context में tokens | सब कुछ recompute | 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 |
दाईं ओर वाला column कारण है। Recomputing हर step पर पूरी attention matrix बनाता है — अध्याय 9 के asymptotic-notation box का , हर token पर एक बार चुकाया गया। cache के साथ आप इसके बजाय row बनाते हैं: 4,096 tokens पर, 67 MB scores बनाम 16 KB।
Milliseconds के बजाय multiply-accumulates गिनने से machine argument से हट जाती है। Cold start से token generate करने के लिए:
| generated tokens | cache के साथ | recomputing | 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 |
हर step पर cached version context में linear है और uncached quadratic; पूरी generation पर जोड़ें तो बनाम , और ratio बिना सीमा के बढ़ता है। Opening में नौ गुना अंतर 48 tokens पर measure किया गया था — उस table की पहली row से भी कम।
cache यह भी बदल देता है कि memory में क्या होना चाहिए। 8 GB laptop GPU पर fp16 में 256 tokens generate करते हुए, allocator का peak लेकर resident weights घटाने पर:
| peak working memory | |
|---|---|
| cache के साथ | 21.8 MB |
| recomputing | 181.7 MB |
8.3 गुना ज़्यादा memory, वही tokens और धीरे produce करने पर खर्च हुई। यह अध्याय 5 में किया गया वादा है, अप्रत्याशित दिशा से आता हुआ: वहाँ reverse-mode autodiff को backward pass के लिए हर intermediate alive रखना पड़ता था, और activations training memory पर हावी थे। inference में backward pass नहीं है और उसके लिए retain करने को कुछ नहीं — इसलिए memory पर जो हावी है वह cache है, और यह unavoidable cost नहीं बल्कि deliberate choice है।
Prefill और decode दो अलग machines हैं
सेक्शन का लिंक: Prefill और decode दो अलग machines हैंFast run को फिर देखें: उसका पहला token बाकी सैंतालीस जैसा behave नहीं करता था।
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 दो phases में split होती है जिनकी physics सचमुच अलग है:
Prefill
सेक्शन का लिंक: Prefillपूरे prompt पर एक forward pass। हर token parallel में process होता है, इसलिए हर weight matrix memory से एक बार load होती है और सैकड़ों token vectors की matrix के खिलाफ multiply होती है — matrix-matrix product, moved byte के हिसाब से बहुत arithmetic, जिसके लिए GPU बना है। Prefill compute-bound है, और इसकी लागत prompt length में लगभग linear है।
Decode
सेक्शन का लिंक: Decodeहर token पर एक forward pass, batch of one और sequence of one। हर weight matrix फिर भी पूरी memory से load होती है, और single vector के खिलाफ multiply होती है — matrix-vector product, moved byte के हिसाब से लगभग कोई arithmetic नहीं। Decode memory-bandwidth-bound है, और प्रति token इसकी लागत context की length पर मुश्किल से निर्भर करती है।
दोनों हिस्से measurable हैं। Prefill, tokens पर एक pass:
| 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, के cache के खिलाफ एक token:
| cached tokens | एक token के लिए ms |
|---|---|
| 16 | 110.05 |
| 64 | 97.57 |
| 256 | 108.53 |
| 1024 | 103.86 |
दूसरी table को दो बार पढ़ें। context के 16 tokens से 1,024 तक जाना — attend करने के लिए चौंसठ गुना ज़्यादा history — ने एक step की लागत को measurably बदला ही नहीं। cache के खिलाफ attention वास्तविक काम है, लेकिन वह आधा billion weights memory bus से खींचकर एक vector produce करने की fixed cost के सामने दब जाता है। वही fixed cost अगले section की हर चीज़ की वजह है।
ये दो phases वे दो numbers पैदा करते हैं जो हर serving system report करता है। Time to first token मूलतः prefill है, और prompt के साथ बढ़ता है, इसलिए लंबी conversation शुरू होने में धीमी लगती है। Tokens per second है, और लगभग constant है, इसलिए reply फिर बराबर बहती है। जो chat धीमी शुरू होती है और फिर smoothly stream करती है, वह rendering trick नहीं है। यह ये दो tables हैं।
cache भी bill है
सेक्शन का लिंक: cache भी bill हैcache arithmetic को memory से trade करता है, और उसे जितनी memory चाहिए वह छोटी नहीं है। context के हर token के लिए, हर layer हर key-value head पर एक key vector और एक value vector रखती है:
2 keys और values के लिए है; बाकी सब architecture है। इस अध्याय में measured model के लिए — 24 layers, 14 query heads, 2 key-value heads, head dimension 64 — fp16 में यह bytes per token है।
इस field के formulae अक्सर factor of two से गलत हो जाते हैं, इसलिए इसे मानने के बजाय allocator के खिलाफ check करें:
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/tokenExact, और tried हर shape पर exact रहता है:
| 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 |
आख़िरी तीन rows को फिर देखें। बत्तीस users जिनमें 2,048 tokens each, चौंसठ में 1,024, एक सौ अट्ठाईस में 512 — cache हर case में 768 MB है, क्योंकि तीनों में 65,536 tokens resident हैं। cache केवल resident tokens की कुल संख्या पर निर्भर करता है, इस पर नहीं कि वे users में कैसे distributed हैं। यही fact batching section की नींव है।
MQA और GQA कहाँ से आते हैं
सेक्शन का लिंक: MQA और GQA कहाँ से आते हैंअध्याय 9 ने multi-query और grouped-query attention introduce किए और कारण इस अध्याय तक टाल दिया। कारण वही formula है, और खासकर उसमें ।
Standard multi-head attention हर query head को अपने key और value heads देता है। यहाँ model में 14 query heads हैं; full multi-head attention के साथ इसका cache bytes per token होता — 12 KB की जगह 84 KB, ठीक सात गुना ज़्यादा, query heads और key-value heads का ratio।
Multi-query attention1 इसे limit तक ले जाता है: सभी query heads एक single key-value head share करते हैं। Grouped-query attention2 वह compromise है जो जीता — कुछ key-value heads, जिनमें हर एक query heads के एक group द्वारा shared है — क्योंकि MQA की quality loss real थी और GQA की नहीं। इनमें से कोई arithmetic नहीं खरीदता। वे उस formula को integer से divide करने के लिए मौजूद हैं, और long contexts ने cache को binding constraint बनाया तो वे industry में तुरंत फैल गए।
और ऐसा तेज़ी से होता है। 32 layers और dimension 128 के 8 key-value heads वाले 7B-class model के लिए, fp16 में cache 128 KB per token है:
| context tokens | एक 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 |
उस model के अपने weights fp16 में 13.0 GB हैं, इस अध्याय के अंत वाली table का figure। इसलिए 128,000-token context पर, एक user का cache model से बड़ा है। यही arithmetic अध्याय 16 पैसे में बदलता है, और यही वजह है कि लंबी conversation सिर्फ धीमी नहीं होती — वह request alive रहने तक machine का fixed slice घेरती है।
Batching: जो number ऊपर जाता है और जो नीचे जाता है
सेक्शन का लिंक: Batching: जो number ऊपर जाता है और जो नीचे जाता हैDecode memory-bound है: weights bus से खींचे जाते हैं ताकि एक token produce हो, और arithmetic units idle रहते हैं। तो उसी step में और काम डालें। कई requests एक साथ run करें, और weights, एक बार पढ़े जाने पर, उन सबकी सेवा करें। उसी model पर measured, हर request में 64-token cache है और एक token decode हो रहा है:
| batch | प्रति step latency | 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 |
दाईं ओर के दो columns को एक-दूसरे के खिलाफ पढ़ें, क्योंकि पूरा point वही है। एक request से सोलह तक जाने पर throughput 6.0 से multiply होता है और किसी individual request का इंतज़ार 2.67 से multiply होता है। batch ने server को बेहतर और हर user को बदतर बनाया।
यह tune करके हटाने वाली bug नहीं है; यही trade है, और हर side पर इसका नाम है। Latency वह है जो reply का इंतज़ार कर रहा व्यक्ति अनुभव करता है। Throughput वह है जिससे invoice divide होता है। कोई setting दोनों को improve नहीं करती।
यह भी देखें कि यह कहाँ रुकता है। 16 से 32 तक throughput 9 % बढ़ता है जबकि latency लगभग double होती है: step memory-bound रहना बंद कर compute-bound हो गया है, और उस knee के बाद batch कुछ नहीं खरीदता। हर deployment में ऐसा knee होता है; उसकी location आपके setup पर measure करनी होगी, पर उसका existence नहीं।
Static batching अपनी जीत का ज़्यादातर हिस्सा waste करता है
सेक्शन का लिंक: Static batching अपनी जीत का ज़्यादातर हिस्सा waste करता हैBatch करने का naive तरीका है requests collect करना, उन्हें साथ run करना, और सबके done होने पर return करना। लेकिन वे साथ finish नहीं करते: कुछ replies बीस tokens के होते हैं और कुछ पाँच सौ के। fixed batch अपने longest member के finish होने तक run करता है, और हर finished request तब तक अपना slot घेरकर padding contribute करती रहती है।
64 requests लें जिनमें output lengths का realistic skew है — median 18 tokens, longest 231, total 1,874 — और eight slots के measured per-step cost पर दोनों policies simulate करें:
| policy | wall clock | throughput | प्रति request mean latency | wasted slot-steps |
|---|---|---|---|---|
| 8 के static batches | 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 improve होता है। Mean latency दस गुना से ज़्यादा improve होती है, क्योंकि static batching में चार steps में finish हुई request भी तब तक इंतज़ार करती है जब तक 231-token neighbour खत्म न हो जाए।
Continuous batching3 fix है, और जितना सुनाई देता है उतना ही simple: batch कोई group नहीं बल्कि slots का set है, और जो slot free होता है वह अगले ही step पर अगली queued request admit करता है। scheduler एक request नहीं बल्कि एक token की granularity पर काम करता है। Production में हर serving stack अब यही करता है।
इसका दूसरा आधा cache है। जो slots आते-जाते हैं वे cache memory fragment कर देते हैं, और हर slot का maximum possible context reserve करना reservation का ज़्यादातर हिस्सा waste करता है। PagedAttention4 operating systems से जवाब उधार लेता है: cache को fixed-size blocks में store करें, हर sequence के लिए block table के साथ, ताकि sequence का cache physically scattered हो सके पर logically contiguous रहे — और इससे shared prefix वाली दो sequences उसे रखने वाले blocks share भी कर सकें। vLLM इसी पर बना है, और इसलिए serving engine असल में transformer attached memory allocator है।
Quantization, और पहली चीज़ जो गलत होती है
सेक्शन का लिंक: Quantization, और पहली चीज़ जो गलत होती हैBill का दूसरा आधा खुद weights हैं। आधा billion parameters चार bytes each पर 1.98 GB; दो bytes पर 0.99 GB; एक byte पर 0.49 GB। हर weight पर fewer bits model को disk पर छोटा करते हैं, memory में छोटा करते हैं, और — क्योंकि decode bandwidth-bound है — हर step को तेज़ बनाते हैं, क्योंकि move करने को fewer bytes हैं।
सबसे simple scheme symmetric absolute-maximum quantization है, और यह तीन lines में fit होती है:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedScale चुनें ताकि सबसे बड़ा weight सबसे बड़े integer पर map हो, divide करें, round करें, integers और scale store करें। वापस reconstruct करने के लिए multiply back करें। इसमें clever कुछ नहीं है, और यह काम करता है — जब तक नहीं करता।
model के real weights पर measured: सभी 168 projection matrices, 357.8 million parameters, relative error :
| scheme | mean relative error | worst matrix |
|---|---|---|
| INT8, पूरी matrix के लिए एक scale | 0.0400 | 0.1487 |
| INT8, प्रति output row एक scale | 0.0100 | 0.0149 |
| INT4, पूरी matrix के लिए एक scale | 0.6026 | 0.9931 |
| INT4, प्रति output row एक scale | 0.1790 | 0.2589 |
| INT4, 128 के हर group पर एक scale | 0.1323 | 0.1992 |
| NF4, 64 के हर block पर एक scale | 0.0952 | 0.1205 |
| INT3, 128 के हर group पर एक scale | 0.3044 | 0.4123 |
| INT2, 128 के हर group पर एक scale | 0.7790 | 0.8076 |
चौथी row collapse है। worst matrix पर 0.99 का relative error मतलब reconstruction में original का लगभग कुछ नहीं बचा — matrix सही magnitude के आसपास के noise से replace हो गई है। कारण एक single matrix पर उसी experiment में दिखता है:
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 बाहर है। पूरी matrix के लिए single scale के साथ, वही एक weight उनमें से सभी 4.3 million के लिए step size set करता है। 8 bits पर 256 steps हैं और typical weight फिर भी meaningful step पर land करता है। 4 bits पर 16 हैं, outermost ऐसे value के लिए reserve है जो लगभग किसी के पास नहीं, और ordinary weights — यानी लगभग सभी — दो या तीन distinct levels पर round होते हैं।
उस row के बाद सब एक ही repair है, अलग granularities पर: scale को छोटा territory दें। Per output row error को 3.4 से divide करता है; 128 consecutive weights का per group फिर divide करता है। लागत bookkeeping है — 128 के हर group पर 16-bit scale bits per weight है, 4 की जगह — और यह gap का ज़्यादातर हिस्सा वापस खरीद लेता है।
NF4 दूसरी तरफ़ से आता है।5 Levels equally spaced होने ज़रूरी नहीं। Block के भीतर weights लगभग normally distributed हैं, इसलिए sixteen levels को normal distribution के quantiles की तरह चुनें: zero के पास dense जहाँ weights सच में हैं, tails में sparse जहाँ वे नहीं हैं। वही four bits, वही block scaling, छोटे block पर — group-128 के 4.125 के मुकाबले 4.25 bits per weight — और measured error 0.1323 से 0.0952 हो जाता है, 28 % कम। इसका कुछ हिस्सा finer block है और बाकी mass जहाँ है वहाँ levels रखना; दोनों को अलग करने के लिए तीसरी row चाहिए होगी।
Outlier features
सेक्शन का लिंक: Outlier featuresअध्याय 2 के floating-point box ने एक वादा करके खत्म किया था: यह अध्याय weights को 8 और 4 bits में quantize करेगा और कुछ outlier features पाएगा जो squeeze होने से इंकार करेंगे। वे यहाँ हैं, और वे बताते हैं कि activations पर “बस numbers round कर दो” कभी काम करने वाला नहीं था।
ऊपर के weights badly behaved थे। activations बिल्कुल अलग league में हैं। एक ordinary 84-token prompt लें, हर layer पर residual stream capture करें, और measure करें कि 896 dimensions में से हर एक कितनी largest magnitude तक पहुँचती है:
| layer | largest |h| | median dimension's largest |h| | ratio | 6x median से ऊपर dimensions |
|---|---|---|---|---|
| 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 का fluke नहीं है: वही dimension layer 4 पर मौजूद है और layer 20 पर भी है, लगभग उसी value के साथ। यही outlier features हैं,6 और ये systematic हैं — trained model की property, input की नहीं।
Layer 16 पर उन 896 per-dimension maxima का histogram shape को unmistakable बना देता है:
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आठ से नीचे साफ़ pile में नौ सौ dimensions, तीन octaves तक कुछ नहीं, फिर far end पर अकेला एक dimension। अब उस tensor को INT8 में quantize करें और देखें क्या होता है:
| scheme | relative error | पूरे tensor में इस्तेमाल हुए distinct integer levels |
|---|---|---|
| पूरे tensor के लिए एक scale | 0.1083 | 256 में से 14 |
| प्रति token एक scale (per row) | 0.0433 | 158 |
| पूरा tensor, 1 outlier dimension fp32 में रखा | 0.0442 | 48 |
| पूरा tensor, 4 outlier dimensions fp32 में रखे | 0.0279 | 57 |
| पूरा tensor, 16 outlier dimensions fp32 में रखे | 0.0085 | 102 |
256 में से चौदह levels। Scale 1,579.6 से set हुआ था, इसलिए हर step 12.44 wide है, और typical activation — median magnitude 0.26, ninety-ninth percentile 2.51 — के land करने की जगह ही नहीं है। Per dimension यह और stark है:
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, एक ही number में quantized। Eight bits allocate किए गए और लगभग zero इस्तेमाल हुए, और model को वे activations पढ़ते समय एक constant दिया जाता है।
यही measurement उन सभी techniques का justification है जिन्हें लोग सच में use करते हैं:
Outliers को इससे बाहर रखें। LLM.int8()6 matrix multiply को decompose करता है: extreme magnitudes वाले dimensions 16 bits में compute होते हैं, बाकी सब INT8 में, और halves sum होते हैं। ऊपर की table receipt है — चार dimensions हटाने से error लगभग चार गुना कटता है। SmoothQuant7 इसके बजाय difficulty migrate करता है: activations को per-channel factor से divide करें और matching weight column को उससे multiply करें, जिससे product unchanged रहता है और outlier उस tensor से बाहर चला जाता है जो उसे absorb नहीं कर सकता, उस tensor में जो कर सकता है।
Rounding चुनें, सिर्फ round न करें। ऊपर कुछ भी यह नहीं पूछता कि matrix किस लिए है। GPTQ8 column by column quantize करता है और हर column के बाद बाकी full-precision columns को already committed error compensate करने के लिए adjust करता है — real inputs पर layer के output का error minimise करता है, weights का नहीं। AWQ9 note करता है कि weight channels का एक छोटा fraction बाकी से कहीं ज़्यादा matter करता है, उन्हें activation statistics से find करता है, और quantizing से पहले scale up करता है ताकि वे finer levels पर land करें। दोनों को calibration set चाहिए; किसी को gradients नहीं चाहिए।
विवरण दिखाएँ
GGUF, और file format का इस सब से क्या लेना-देना है।
GGUF quantization method नहीं है; यह वह container है जिसे llama.cpp use करता है, और gguf vs gptq comparisons में confusion दोनों को एक ही तरह की चीज़ मानने से आती है। GGUF tensors, tokenizer, architecture metadata और chat template को एक memory-mappable file में रखता है, और अपने अंदर block schemes की एक family carry करता है — Q4_K_M जैसे names bits per weight, block size, और क्या कुछ tensors higher precision पर रखे गए हैं, encode करते हैं।
जो engineering difference matter करता है: GPTQ और AWQ GPU kernel के लिए optimised weights produce करते हैं, जबकि GGUF के schemes CPU पर file mapped रहते हुए, loaded होने के बजाय, cheaply decode होते हैं। इसलिए वही nominal “4-bit 7B model” दोनों worlds में अलग sizes और अलग quality पर मौजूद है, और इसलिए honest comparison कभी format नहीं होता — वह नीचे की measurement है, आपके अपने task पर run की गई।
Quantization की असली लागत, measured
सेक्शन का लिंक: Quantization की असली लागत, measuredQuantization पर लगभग हर article पिछले section पर रुक जाता है: method समझाता है, compression ratio quote करता है, और assert करता है कि quality “largely preserved” है। अध्याय 4 खुद को fool न करने के बारे में था, तो पता लगाते हैं।
वही model, हर scheme के साथ weights in place quantized, फिर तीन measurements: held-out English prose के 2,048 tokens पर perplexity — यहाँ इस course का draft, इसलिए repository एक fixed public-domain book substitute करती है और समान shape की table अलग numbers के साथ print करती है — greedy decoding के तहत known answers वाले 16 short factual questions की battery, और identical context given होने पर quantized model कितने fraction tokens पर full-precision one से agree करता है।
| scheme | mean weight error | perplexity | question battery | fp32 से agrees |
|---|---|---|---|---|
| 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 % |
उस table में चार बातें साफ़-साफ़ कहने लायक हैं।
INT8 ठीक से किया जाए तो free है। Per-row INT8 reference के 23.08 के मुकाबले 22.96 score करता है — दो सौ में एक part का gap, जो noise है और “identical” पढ़ा जाना चाहिए। Noise किस दिशा में point करता है, stable नहीं है: repository के public-domain corpus पर वही दो schemes 22.18 के मुकाबले 22.24 आते हैं: आधी distance, और दूसरी दिशा में। यह 144 generated tokens में से 142 पर full-precision model से agree करता है। fp32 reference के मुकाबले memory का एक quarter, fp16 के मुकाबले आधी जिसे आप वास्तव में deploy करेंगे, और कोई detectable cost नहीं। INT8 carelessly किया जाए तो भी लगभग free है: प्रति matrix एक scale 0.5 perplexity points और battery answers में कोई cost नहीं। Eight bits इतने forgiving हैं कि granularity मुश्किल से matter करती है, और यही वजह है कि लोग INT8 से INT4 तक generalise करते हैं और चोट खाते हैं।
प्रति tensor एक scale वाला INT4 model को नष्ट कर देता है। Perplexity 365 million: degraded नहीं, annihilated। फिर granularity ही पूरा game है — per-tensor 365,416,000, per-row 46.18, per-group-of-128 31.08, NF4 24.55। प्रति weight वही four bits, worst और best के बीच पंद्रह million का factor।
Perplexity coarse instrument है और battery उससे भी coarser। NF4 और group-128 INT4 के बीच perplexity gap 6.5 points है और battery में एक question का फर्क — और अध्याय 4 का confidence interval कहता है कि सोलह में एक question बिल्कुल कुछ distinguish नहीं करता। Interval से भी sharp demonstration है: वही battery model की stock repetition penalty switched off के साथ run करें, जो greedy decoding का असली मतलब है, और वे दो rows जगह बदल लेते हैं। सोलह में एक question छोटा effect नहीं, no effect है। अध्याय 8 की warning भी लागू होती है: perplexity केवल उन models के बीच comparable है जो tokenizer share करते हैं, इसलिए किसी और की write-up का number आपके number से compare नहीं किया जा सकता।
Agreement column तीनों में सबसे sharp है, और लगभग free: full-precision model greedily run करें, फिर quantized one से हर position पर पूछें कि same prefix given होने पर वह क्या चुनता। इसमें 16 की जगह 144 independent observations हैं, ground truth नहीं चाहिए, और battery जहाँ jumps में degrade होती है वहाँ यह smoothly degrade होता है। यह ठीक वही quantity भी है जिसकी अगले section को ज़रूरत है।
यह अध्याय 1 का इस अध्याय के बारे में किया गया वादा है, समय पर आता हुआ: mathematics कहती है 4-bit model possible है, और engineering तय करती है कि वह usable है या नहीं।
Speculative decoding
सेक्शन का लिंक: Speculative decodingअध्याय 12 ने इसे announce किया था और bill यहाँ छोड़ दिया था।
Idea सीधे prefill/decode split से आता है। tokens की proposed sequence verify करने की लागत positions पर एक forward pass है — matrix-matrix product, एक पर pass से मुश्किल से ज़्यादा expensive। तो:
एक छोटा, सस्ता model autoregressively candidate tokens generate करता है।
Verify
सेक्शन का लिंक: Verifyबड़ा model सभी candidates पर एक साथ एक forward pass run करता है, यह produce करते हुए कि वह हर position पर क्या कहता।
Accept
सेक्शन का लिंक: Acceptसबसे लंबा prefix रखें जिस पर दोनों agree करते हैं, साथ में वह token जो बड़ा model first disagreement पर free में supply करता है। बाकी discard करें और फिर शुरू करें।
Output distribution unchanged है। Greedy decoding के साथ यह obvious है — token तभी accepted है अगर target उसे produce करता। Sampling के साथ modified acceptance rule चाहिए, और Leviathan et al. prove करते हैं कि resulting distribution ठीक target का है।10 यह इस अध्याय की दूसरी exact optimisation है।
इसलिए सब कुछ acceptance rate पर टिका है, जो measurable है — यह ऊपर वाला agreement column है, इसलिए वहाँ compute किया गया। हर quantized model को full-precision target के draft के रूप में use करते हुए, 144 generated positions पर:
| draft model | acceptance | longest accepted run | target pass per expected tokens, |
|---|---|---|---|
| fp32 (target खुद) | 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 |
Draft length पर, प्रति verification pass accepted expected tokens है
और net speedup इसे draft की अपनी cost से divide करता है, target per token का fraction :
| 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 |
Bold entry याद रखने वाली है: speculative decoding generation को धीमा कर सकता है। 30 % acceptance पर, target के पाँचवें हिस्से की cost वाले draft के साथ, आप पाँच forward passes के पैसे देते हैं और 1.4 tokens रखते हैं। आख़िरी column दूसरा trap है — लंबा draft तभी मदद करता है जब acceptance high हो, क्योंकि -token guess की tail तक लगभग कभी पहुँचा नहीं जाता। 90 % acceptance पर 3.40x के लायक है और 30 % पर 0.79x: वही configuration, आपके traffic पर measured number के हिसाब से win या loss।
Distillation, और soft label क्या carry करता है
सेक्शन का लिंक: Distillation, और soft label क्या carry करता हैQuantization model को उसी function को fewer bits में store करके shrink करता है। Distillation उसे छोटे model को बड़े model की नकल करना train करके shrink करता है11 — ऐसा idea जो deep learning से लगभग एक decade पुराना है।12
Subtle हिस्सा यह है कि student किससे सीखता है। Correct answer से नहीं: उस पर directly train किया जा सकता था। 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 लगभग उतना ही अच्छा था, bowl plausible था, और large — एक adjective, पूरी तरह अलग grammatical continuation — अभी भी live था। यही original argument है: यह 7 है, लेकिन 1 जैसा काफी दिखता है, और resemblance information है जिसे hard label फेंक देता है।
यही वजह है कि distillation temperature use करता है। softmax से पहले logits को से divide करना distribution को flatten करता है और runners-up का relative weight बढ़ाता है: इस phrase पर, top token और third के बीच ratio पर 2.24 से पर 1.50 तक गिरता है — पहले का square root, जो logits को दो से divide करने पर ratio के साथ होता है। वही ordering, loss का ज़्यादा attention near misses पर। Student का gradient teacher की uncertainty carry करता है, सिर्फ verdict नहीं।
8, 16 और 24 GB में क्या fit होता है
सेक्शन का लिंक: 8, 16 और 24 GB में क्या fit होता हैइस अध्याय की हर चीज़ अब एक sum है:
जहाँ सभी concurrent requests में resident total tokens है। इसे apply करें: 7B और 70B rows dimension 128 के 8 key-value heads assume करती हैं, 13B row 40 heads के साथ full multi-head attention, जैसे model की वे generations बनी थीं — और यह दिखता है।
8 GB
| model | precision | weights | overhead के बाद free | fit होने वाले context tokens |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | fit नहीं होता | — |
| 7B | int8 | 6.5 GB | 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 | fit नहीं होता | — |
16 GB
| model | precision | weights | overhead के बाद free | fit होने वाले context tokens |
|---|---|---|---|---|
| 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 | overhead के बाद free | fit होने वाले context tokens |
|---|---|---|---|---|
| 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 | fit नहीं होता | — |
8 GB table में 13B row देखें। weights fit होते हैं — 8 में से 6.2 GB — इसलिए usual बात करने के तरीके से 13B model “8 GB card पर चलता है”। उसके पास 337 tokens का context है, जो conversation नहीं बल्कि मुश्किल से prompt है। “क्या यह fit होता है” गलत सवाल है। सही सवाल है “कितने context के साथ, और एक साथ कितने users के लिए”।
16 GB की दो int8 rows भी देखें। 7B को 65,378 tokens मिलते हैं और 13B को 3,136 — extra weights के 5.6 GB से twenty-fold difference, क्योंकि यहाँ 13B में multi-head attention है और उसके cache की लागत 7B के 128 KB के मुकाबले 800 KB per token है। Similar size के दो models, एक long context के लिए unusable, ऐसी वजह से जो किसी model card की headline में नहीं दिखती।
यह आगे कहाँ जाता है
सेक्शन का लिंक: यह आगे कहाँ जाता हैतेरह अध्याय पहले यह दो weights और एक bias वाला perceptron था। अब यह transformer है जिसे design, train, align किया गया है, hard questions पर compute खर्च करना सिखाया गया है, और प्रति token measured cost पर serve किया गया है — और इसमें कोई box unopened नहीं बचा।
यह यहीं खत्म होता है, और जानबूझकर खत्म होता है।
अध्याय 14 model के कहीं और होने से शुरू होता है। आपके process में नहीं, आपकी memory में नहीं, किसी variable में नहीं जिसे आप print कर सकें: ऐसी machine पर जिसे आप administer नहीं करते, API key, port और bill के पीछे। यहाँ measure की गई हर चीज़ अभी भी हो रही है — पहला token आने से पहले prefill अब भी run होता है, cache अब भी conversation के साथ बढ़ता है, जिस batch में आप हैं वह अब भी किसी और का है और अब भी आपकी latency decide करता है — लेकिन अब से आप उसे Server-Sent Events की stream, एक finish_reason, और Retry-After header वाले HTTP 429 के ज़रिए observe करते हैं। Vantage point के साथ questions बदलते हैं: यह gradient कैसे compute होता है नहीं, बल्कि मेरा invoice तीन गुना क्यों हो गया। भाषा भी बदलती है, और अध्याय 14 उस rule को announce करने के बजाय explain करता है — यहाँ तक code weights, gradients, logits और tokenizer bytes hold करता था; वहाँ से आगे वह connection, retry, cancellation और accumulated state hold करता है। आपके पीछे के तेरह अध्याय crossing से discard नहीं होते। वे port के दूसरी तरफ़ चल रही चीज़ का description हैं।
Sources and method
सेक्शन का लिंक: Sources and methodदो omissions deliberate हैं। FlashAttention (Dao et al., arXiv:2205.14135) कोई अलग attention नहीं है — यह operation को tile करके same function compute करता है ताकि score matrix कभी memory में लिखी ही न जाए, इसलिए इस अध्याय की दूसरी table में 67 MB practice में arithmetic के suggest करने से छोटा है। और kernels खुद delegated हैं: Stanford की CS336 का lecture 10 inference systems को उस depth में cover करता है जहाँ यह जाने की कोशिश नहीं करता, और llama.cpp repository और GGUF specification CPU side के primary sources हैं।
संदर्भ
सेक्शन का लिंक: संदर्भ-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Paper largely memory-bandwidth argument है, और वैसा ही पढ़ता है। ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). इसमें uptraining recipe शामिल है जो existing multi-head checkpoint convert करती है, इसलिए 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. Iteration-level scheduling — continuous batching — और selective batching introduce करता है। ↩
-
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 analogy है। ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 §3 में defined है; ऊपर की measurement में इस्तेमाल sixteen level values वही हैं जो यह paper derive करता है। ↩
-
Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). §4 में outlier-feature analysis ऊपर measured phenomenon का source है, जिसमें यह finding भी शामिल है कि outliers scale पर systematically emerge होते हैं। ↩ ↩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 unchanged है; Chen et al. (arXiv:2302.01318) ने same idea independently publish किया। ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Temperature और “dark knowledge” argument। ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, नौ साल पहले, transformers के बजाय ensembles के लिए। ↩