Inference’ı Ucuzlatmak: KV cache, Batching ve Quantization
Aynı model aynı soruyu 8,8 ve 78,9 saniyede, byte-identical yanıtlıyor. Sonra INT4: iddia değil, üç ölçüm.
Bu sayfada
Aynı model, aynı makinede, aynı soruyu aynı 48 token ile yanıtlıyor. İki çıktı token token aynı — varsayılmadı, kontrol edildi.
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)Tek bir argüman değişti: use_cache=False. Model, prompt, sampling ya da aritmetik açısından hiçbir şey farklı değil; ikinci çalışma da çektiği zahmete karşılık daha doğru değil. Hiçbir kazanç olmadan dokuz kat daha yavaş.
Bu bölümün şekli bu. İçindeki her şey — cache, batch, quantized ağırlıklar — yanıtı değiştirmeyen iş için ödeme yapmayı durdurma ya da daha ucuz bir yanıtın neye mal olduğunu öğrenme girişimi. Chapter 10 training için fiyat listesini kurmuştu. Bu, sonsuza kadar ödeyeceğin tarafın fiyat listesi: deployed bir model, ömrünün geri kalanı boyunca her istekte, ürettiği her token için kabaca FLOPs harcar.
İkinci çalışmanın zamanı nereye gitti
Bölüme bağlantı: İkinci çalışmanın zamanı nereye gittiBir token üretmek için decoder-only transformer, o ana kadarki tüm sequence’ı alır, her layer’dan geçirir ve son position’dan probability distribution’ı okur. Sonra seçilen token’ı ekler ve aynı şeyi tekrar yapar. Bu açıklama doğru; yavaş çalışmanın yaptığı da budur.
Ama aynı zamanda muazzam derecede savurgandır; nedeni de Chapter 9’daki causal mask’tir. Position 7’nin key ve value vectors, position 7’nin input’undan ve ondan önceki position’lardan hesaplanır. Position 8 geldiğinde, position 7 onu göremez — causal tam da bu demektir — dolayısıyla position 7’nin key ve value değerleri öncekiyle birebir aynı sayılardır. Yavaş çalışma yine de her adımda bunları yeniden hesaplar.
Öyleyse sakla. Bu saklama, language model serving’deki tek başına en sonuç belirleyici optimizasyon olan key-value cache’tir:
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 içinde modele ne verildiğine bak: nxt, tek token. Sequence değil. Yeni token’ın query’si cached her key’e attention uygular; cached key’ler zaten asla değişmeyecekti. Bu bir approximation değildir — yukarıdaki identical-output kontrolünün amacı bu. Cache kaliteyi hızla takas etmez; gereksiz aritmetiği siler.
Ölçeklenmeyi temiz görmek için transformer’ı çıkarıp ile tek bir attention head’i zamanla; generation’ın tek adımı iki yolla hesaplanır:
| context içindeki token | her şeyi yeniden hesaplama | cache ile | oran | 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 |
Sağdaki sütun nedendir. Yeniden hesaplama her adımda tam attention matrix’i kurar — Chapter 9’daki asymptotic-notation kutusundaki , token başına bir kez ödenir. Cache ile bunun yerine bir satırı kurarsın: 4.096 token’da 67 MB score’a karşı 16 KB.
Milisaniye yerine multiply-accumulate saymak makineyi argümandan çıkarır. Cold start’tan token üretmek için:
| üretilen token | cache ile | yeniden hesaplama | oran |
|---|---|---|---|
| 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 |
Adım başına cached sürüm context’e göre lineer, uncached sürüm quadratic’tir; bir generation boyunca toplandığında , ’a karşıdır ve oran sınırsız büyür. Girişteki dokuz kat fark 48 token üzerinden ölçüldü — bu tablonun ilk satırından bile kısa.
Cache ayrıca bellekte neyin bulunması gerektiğini de değiştirir. 8 GB laptop GPU’da fp16 ile 256 token üretilirken, allocator peak alınıp resident weights çıkarıldığında:
| peak working memory | |
|---|---|
| cache ile | 21,8 MB |
| yeniden hesaplama | 181,7 MB |
8,3 kat daha fazla memory, aynı token’ları daha yavaş üretmek için harcandı. Bu, Chapter 5’te verilen sözün beklenmedik bir yönden gelişi: orada reverse-mode autodiff, backward pass için her intermediate’ı canlı tutmak zorundaydı ve training memory’sini activations domine ediyordu. Inference sırasında backward pass yoktur ve onun için tutulacak bir şey de yoktur — bu yüzden memory’yi onun yerine domine eden şey cache olur; bu, kaçınılmaz bir maliyet değil bilinçli bir tercihtir.
Prefill ve decode iki farklı makinedir
Bölüme bağlantı: Prefill ve decode iki farklı makinedirHızlı çalışmaya tekrar bak: ilk token, diğer kırk yediden farklı davrandı.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepPrompt token başına 25,6 ms’ye mal oldu; her generated token 166 ms’ye. Aynı model, aynı hardware, aynı weights; token başına altı kat fark — üstelik çoğu insanın beklemediği yönde. Prompt ucuz kısımdır. Generation, gerçekten farklı fiziğe sahip iki phase’e ayrılır:
Prefill
Bölüme bağlantı: PrefillTüm prompt üzerinde tek forward pass. Her token paralel işlenir, bu yüzden her weight matrix memory’den bir kez yüklenir ve yüzlerce token vector’ünden oluşan bir matrix’e karşı çarpılır — matrix-matrix product; taşınan byte başına çok aritmetik vardır, GPU da tam bunun için yapılmıştır. Prefill compute-bound’dır ve maliyeti prompt uzunluğuna kabaca lineer gider.
Decode
Bölüme bağlantı: DecodeToken başına tek forward pass, batch one ve sequence one. Her weight matrix hâlâ memory’den tam olarak yüklenir ve tek bir vector ile çarpılır — matrix-vector product; taşınan byte başına neredeyse hiç aritmetik yoktur. Decode memory-bandwidth-bound’dır ve token başına maliyeti context uzunluğuna çok az bağlıdır.
İki yarı da ölçülebilir. Prefill, token üzerinde tek pass:
| prompt token | saniye | token başına ms |
|---|---|---|
| 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’e karşı tek token:
| cached token | tek token için ms |
|---|---|
| 16 | 110,05 |
| 64 | 97,57 |
| 256 | 108,53 |
| 1024 | 103,86 |
İkinci tabloyu iki kez oku. 16 token context’ten 1.024’e çıkmak — attention uygulanacak history’nin altmış dört kat artması — bir adımın maliyetini ölçülebilir biçimde değiştirmedi. Cache’e karşı attention gerçek bir iştir, ama tek vector üretmek için yarım milyar weight’i memory bus üzerinden sürüklemenin sabit maliyetinin yanında sönük kalır. Bir sonraki bölümdeki her şeyin nedeni bu sabit maliyettir.
Bu iki phase, her serving system’in raporladığı iki sayının kökenidir. Time to first token esasen prefill’dir ve prompt ile büyür; uzun bir konuşmanın başlamakta yavaş hissettirmesi bu yüzdendir. Tokens per second ’tir ve kabaca sabittir; yanıtın sonra düzenli akmasının nedeni budur. Yavaş başlayıp sonra pürüzsüz stream eden bir chat, rendering hilesi değildir. Bu iki tablodur.
Cache aynı zamanda faturadır
Bölüme bağlantı: Cache aynı zamanda faturadırCache aritmetiği memory ile takas eder; istediği memory de küçük değildir. Context’teki her token için, her layer key-value head başına bir key vector ve bir value vector tutar:
2, keys ve values içindir; geri kalanı architecture’dır. Bu bölüm boyunca ölçülen model için — 24 layers, 14 query heads, 2 key-value heads, head dimension 64 — fp16’da bu, token başına byte eder.
Bu alandaki formüller iki kat şaşırmaya meraklıdır; bu yüzden inanmak yerine allocator’a karşı kontrol et:
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/tokenTam doğru; denenen her shape’te tam doğru kalıyor:
| 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 |
Son üç satır ikinci kez bakmayı hak ediyor. Her biri 2.048 token tutan otuz iki user, 1.024 tutan altmış dört user, 512 tutan yüz yirmi sekiz user — cache her durumda 768 MB, çünkü üçü de toplam 65.536 token tutuyor. Cache yalnızca resident toplam token sayısına bağlıdır; bunların user’lar arasında nasıl dağıldığına değil. Bu gerçek batching bölümünün temelidir.
MQA ve GQA nereden geliyor
Bölüme bağlantı: MQA ve GQA nereden geliyorChapter 9 multi-query ve grouped-query attention’ı tanıtmış, nedenini bu bölüme ertelemişti. Neden o formül ve özellikle içindeki ’tir.
Standard multi-head attention, her query head’e kendi key ve value heads’ini verir. Buradaki modelde 14 query heads var; full multi-head attention ile cache’i token başına byte olurdu — 12 KB yerine 84 KB, tam olarak yedi kat fazla; query heads’in key-value heads’e oranı.
Multi-query attention1 bunu sınıra götürür: tüm query heads tek bir key-value head’i paylaşır. Grouped-query attention2 kazanan uzlaşmadır — birkaç key-value head, her biri bir query heads grubunca paylaşılır — çünkü MQA’nın kalite kaybı gerçekti, GQA’nınki ise değil. İkisi de aritmetik satın almaz. Var olma sebepleri o formülü bir tam sayıya bölmektir; uzun context’ler cache’i bağlayıcı kısıt yaptığı anda sektöre yayılmaları da bu yüzden oldu.
Ve bunu hızla yapar. 32 layers ve dimension 128 olan 8 key-value heads’e sahip 7B sınıfı bir model için cache fp16’da token başına 128 KB’tır:
| context token | bir user | 8 user | 64 user |
|---|---|---|---|
| 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 |
Bu modelin kendi weights’i fp16’da 13,0 GB; bu bölümün sonundaki tablodaki rakam. Dolayısıyla 128.000 token context’te tek bir user’ın cache’i modelden büyüktür. Bu, Chapter 16’nın paraya çevirdiği aritmetiktir ve uzun bir konuşmanın yalnızca yavaş olmamasının nedeni budur — request canlı kaldığı sürece makinenin sabit bir dilimini işgal eder.
Batching: yukarı çıkan sayı ve aşağı inen sayı
Bölüme bağlantı: Batching: yukarı çıkan sayı ve aşağı inen sayıDecode memory-bound’dır: weights bus üzerinden sürüklenerek tek token üretir ve arithmetic units boşa bekler. Öyleyse aynı adıma daha fazla iş koy. Birkaç request’i aynı anda çalıştır; bir kez okunan weights hepsine hizmet etsin. Aynı modelde ölçüldü: her request 64 token cache tutuyor ve bir token decode ediyor:
| batch | adım başına 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 |
Sağdaki iki sütunu birbirine karşı oku; bütün mesele onlar. Bir request’ten on altıya çıkmak throughput’u 6,0 ile çarpar ve herhangi bir tekil request’in bekleme süresini 2,67 ile çarpar. Batch server’ı daha iyi, her user’ı daha kötü yaptı.
Bu ayarla yok edilecek bir bug değil; takasın kendisi ve iki tarafında birer adı var. Latency, yanıt bekleyen kişinin yaşadığı şeydir. Throughput, faturanın bölündüğü şeydir. Hiçbir ayar ikisini birden iyileştirmez.
Nerede durduğuna da dikkat et. 16’dan 32’ye throughput %9 kazanırken latency neredeyse ikiye katlanır: adım memory-bound olmaktan çıkıp compute-bound olmuştur; bu dizden sonra batch hiçbir şey satın almaz. Her deployment’ta böyle bir diz vardır; konumunu kendi sisteminde ölçmek gerekir, ama varlığı ölçüme bağlı değildir.
Static batching kazandığının çoğunu boşa harcar
Bölüme bağlantı: Static batching kazandığının çoğunu boşa harcarBatch yapmanın naif yolu request toplamak, birlikte çalıştırmak ve hepsi bittiğinde dönmektir. Ama birlikte bitmezler: bazı yanıtlar yirmi token, bazıları beş yüz token’dır. Fixed batch en uzun üyesi bitene kadar çalışır; bitmiş her request de o zamana kadar slot’unu işgal etmeyi ve padding katkısı yapmayı sürdürür.
Gerçekçi output length çarpıklığına sahip 64 request al — median 18 token, en uzun 231, toplam 1.874 — ve sekiz slot için ölçülen per-step cost ile iki policy’yi simüle et:
| policy | wall clock | throughput | request başına mean latency | wasted slot-steps |
|---|---|---|---|---|
| 8’li static batches | 176,9 s | 10,6 tok/s | 83,2 s | 3.214 |
| continuous, 8 slot | 109,0 s | 17,2 tok/s | 8,1 s | 0 |
Throughput 1,6x iyileşir. Mean latency on kattan fazla iyileşir; çünkü static batching altında dört adımda biten bir request bile, kimse onu duymadan önce 231 token’lık komşusunu bekler.
Continuous batching3 çözümüdür ve kulağa geldiği kadar basittir: batch bir grup değil, bir slot kümesidir; boşalan bir slot bir sonraki adımda kuyruktaki sonraki request’i kabul eder. Scheduler bir request değil, bir token granularity’sinde çalışır. Production’daki her serving stack artık bunu yapıyor.
İkinci yarısı da cache’tir. Gelip giden slot’lar cache memory’yi fragmented bırakır; her slot için olası maksimum context’i reserve etmek ise reservation’ın çoğunu boşa harcar. PagedAttention4 yanıtı operating systems’den ödünç alır: cache’i sequence başına bir block table ile fixed-size blocks içinde sakla; böylece bir sequence’ın cache’i fiziksel olarak dağınık ama mantıksal olarak contiguous kalabilir — bu ayrıca shared prefix’i olan iki sequence’ın onu tutan blocks’u paylaşmasını sağlar. vLLM bunun üzerine kuruludur; bir serving engine’in transformer takılmış bir memory allocator olmasının nedeni de budur.
Quantization ve ilk yanlış giden şey
Bölüme bağlantı: Quantization ve ilk yanlış giden şeyFaturanın diğer yarısı weights’in kendisidir. Dört byte’lık yarım milyar parameters 1,98 GB eder; iki byte’ta 0,99 GB; bir byte’ta 0,49 GB. Weight başına daha az bit, modeli disk üzerinde küçültür, memory’de küçültür ve — decode bandwidth-bound olduğu için — her adımı hızlandırır, çünkü taşınacak byte daha azdır.
En basit scheme symmetric absolute-maximum quantization’dır ve üç satıra sığar:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedEn büyük weight en büyük integer’a denk gelecek şekilde bir scale seç, böl, yuvarla, integer’ları ve scale’i sakla. Geri kurmak için tekrar çarp. Bunda zekice bir şey yoktur ve çalışır — çalışmadığı ana kadar.
Modelin gerçek weights’i üzerinde ölçüldü: 168 projection matrix’in tamamı, 357,8 milyon parameters, relative error :
| scheme | mean relative error | worst matrix |
|---|---|---|
| INT8, tüm matrix için tek scale | 0,0400 | 0,1487 |
| INT8, output row başına tek scale | 0,0100 | 0,0149 |
| INT4, tüm matrix için tek scale | 0,6026 | 0,9931 |
| INT4, output row başına tek scale | 0,1790 | 0,2589 |
| INT4, 128’lik grup başına tek scale | 0,1323 | 0,1992 |
| NF4, 64’lük block başına tek scale | 0,0952 | 0,1205 |
| INT3, 128’lik grup başına tek scale | 0,3044 | 0,4123 |
| INT2, 128’lik grup başına tek scale | 0,7790 | 0,8076 |
Çöküş dördüncü satırdır. En kötü matrix’te 0,99 relative error, reconstruction’ın original’dan özünde hiçbir şey tutmadığı anlamına gelir — matrix, yaklaşık doğru magnitude’da noise ile değiştirilmiştir. Nedeni aynı experiment’ın tek matrix üzerindeki halinde görünür:
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 %)Altı bin weight’ten biri altı standard deviation’ın ötesindedir; en büyüğü 24 uzaktadır. Tüm matrix için tek scale ile o tek weight, 4,3 milyon weight’in tamamı için step size’ı belirler. 8 bit’te 256 step vardır ve tipik weight yine anlamlı birine düşer. 4 bit’te 16 vardır; en dıştaki, neredeyse hiçbir weight’in sahip olmadığı bir değer için ayrılmıştır ve sıradan weights — yani hepsi — iki ya da üç distinct level’a yuvarlanır.
O satırdan sonraki her şey aynı onarımın farklı granularity’lerdeki halidir: scale’e daha küçük bir bölge ver. Output row başına yapmak error’ı 3,4’e böler; ardışık 128 weight’lik grup başına yapmak bir kez daha böler. Maliyeti bookkeeping’dir — 128’lik grup başına 16-bit scale, 4 yerine weight başına bit demektir — ve gap’in çoğunu geri satın alır.
NF4 buna diğer taraftan yaklaşır.5 Level’ların eşit aralıklı olması gerekmez. Bir block içindeki weights yaklaşık normal dağılır; o halde on altı level’ı normal distribution quantile’ları olarak seç: weights’in gerçekten bulunduğu zero yakınında yoğun, bulunmadığı tails’te seyrek. Aynı dört bit, aynı block scaling, daha küçük block — group-128’in 4,125’ine karşı weight başına 4,25 bit — ve ölçülen error 0,1323’ten 0,0952’ye düşer, %28 daha düşük. Bunun bir kısmı daha ince block’tan, kalanı level’ları kütlenin olduğu yere koymaktan gelir; ikisini ayırmak için üçüncü bir satır gerekir.
Outlier features
Bölüme bağlantı: Outlier featuresChapter 2’nin floating-point kutusu bir sözle bitmişti: bu bölüm weights’i 8 ve 4 bit’e quantize edecek ve sıkıştırılmayı reddeden bir avuç outlier features bulacaktı. İşte buradalar; activations üzerinde “sayıları yuvarla geç”in neden asla çalışmayacağını açıklıyorlar.
Yukarıdaki weights kötü huyluydu. Activations bambaşka bir ligde. Sıradan 84-token prompt al, her layer’da residual stream’i yakala ve 896 dimension’ın her birinin ulaştığı en büyük magnitude’u ölç:
| layer | en büyük |h| | median dimension’ın en büyük |h| | oran | median’ın 6x üstündeki 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, median dimension 1,6’yı hiç aşmazken 1.579,6’ya ulaşır. Bu tek token’ın ya da tek layer’ın rastlantısı değildir: aynı dimension layer 4’te oradadır ve layer 20’de neredeyse aynı değerle hâlâ oradadır. Bunlar outlier features’dır,6 ve sistematiktir — input’un değil, trained model’in bir özelliği.
Layer 16’da bu 896 per-dimension maxima’nın histogramı şekli tartışmasız yapar:
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 | # 1Sekiz’in altında düzenli bir yığın halinde dokuz yüz dimension, üç octave boyunca hiçbir şey yok, sonra en uçta tek başına bir dimension. Şimdi o tensor’ı INT8’e quantize et ve ne olduğunu say:
| scheme | relative error | tüm tensor’da kullanılan distinct integer levels |
|---|---|---|
| tüm tensor için tek scale | 0,1083 | 256’dan 14 |
| token başına tek scale (row başına) | 0,0433 | 158 |
| tüm tensor, 1 outlier dimension fp32 tutuldu | 0,0442 | 48 |
| tüm tensor, 4 outlier dimensions fp32 tutuldu | 0,0279 | 57 |
| tüm tensor, 16 outlier dimensions fp32 tutuldu | 0,0085 | 102 |
256 level’dan on dört. Scale 1.579,6 tarafından belirlendi, bu yüzden her step 12,44 genişliğinde; tipik activation — median magnitude 0,26, doksan dokuzuncu percentile 2,51 — inecek yer bulamıyor. Dimension başına daha da sert:
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 levelsTek level. Tüm dimension, her token, aynı sayıya quantize edildi. Sekiz bit ayrıldı ve kabaca sıfırı kullanıldı; bu activations’ı okuyan modele bir constant verildi.
Bu ölçüm, insanların gerçekten kullandığı her tekniğin gerekçesidir:
Outlier’ları bunun dışında tut. LLM.int8()6 matrix multiply’ı ayrıştırır: extreme magnitude’a sahip dimensions 16 bit’te hesaplanır, geri kalan her şey INT8’de; sonra yarılar toplanır. Yukarıdaki tablo makbuzdur — dört dimension’ı çıkarmak error’ı neredeyse dört kat azaltır. SmoothQuant7 ise zorluğu taşır: activations’ı per-channel factor ile böl ve eşleşen weight column’u onunla çarp; product değişmeden kalır ve outlier, onu absorbe edemeyen tensor’dan edebilen tensor’a taşınır.
Yuvarlamayı seç, sadece yuvarlama. Yukarıdakilerin hiçbiri matrix’in ne için olduğunu sormaz. GPTQ8 column by column quantize eder ve her column’dan sonra kalan full-precision columns’u, işlenmiş error’ı telafi edecek şekilde ayarlar — weights’in değil, layer’ın gerçek inputs üzerindeki output error’ını minimize eder. AWQ9, weight channels’ın küçük bir bölümünün diğerlerinden çok daha önemli olduğunu not eder, bunları activation statistics’ten bulur ve quantize etmeden önce büyütür; böylece daha ince level’lara düşerler. İkisi de calibration set ister; hiçbiri gradients istemez.
Ayrıntıları göster
GGUF ve file format’ın bununla ne ilgisi var.
GGUF bir quantization yöntemi değildir; llama.cpp’in kullandığı container’dır ve gguf vs gptq karşılaştırmalarındaki kafa karışıklığı ikisine aynı tür şey muamelesi yapılmasından gelir. GGUF tensors, tokenizer, architecture metadata ve chat template’i tek memory-mappable file içinde tutar; içinde de bir family block scheme taşır — Q4_K_M gibi adlar weight başına bit’i, block size’ı ve bazı tensors’ın daha yüksek precision’da tutulup tutulmadığını encode eder.
Önemli engineering farkı: GPTQ ve AWQ bir GPU kernel için optimize edilmiş weights üretirken, GGUF’un scheme’leri file yüklü değil mapped iken CPU üzerinde ucuzca decode edilir. Aynı nominal “4-bit 7B model”in iki dünyada farklı size ve farklı quality ile var olmasının nedeni budur; dürüst karşılaştırma da asla format değildir — aşağıdaki ölçümdür, kendi task’ında çalıştırılır.
Quantization gerçekte neye mal olur, ölçüldü
Bölüme bağlantı: Quantization gerçekte neye mal olur, ölçüldüQuantization hakkındaki neredeyse her article önceki bölümde durur: yöntemi açıklar, compression ratio alıntılar ve quality’nin “büyük ölçüde korunduğunu” iddia eder. Chapter 4 kendini kandırmamak hakkındaydı; o halde öğrenelim.
Aynı model, weights her scheme ile yerinde quantized; sonra üç ölçüm: held-out English prose’un 2.048 token’ı üzerinde perplexity — burada, bu course’un draft’ı; repository’nin fixed public-domain book koyup aynı şekilli ama farklı sayılı bir tablo basmasının nedeni bu — greedy decoding altında known answer’lı 16 kısa factual questions battery’si ve quantized model’in identical context verildiğinde full-precision model ile aynı token’ı seçtiği oran.
| scheme | mean weight error | perplexity | question battery | fp32 ile agree |
|---|---|---|---|---|
| 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 % |
Bu tabloda dört şey açıkça söylenmeye değer.
INT8 düzgün yapıldığında bedavadır. Per-row INT8, reference’ın 23,08’ine karşı 22,96 skor alır — iki yüzde bir parça kadar fark; bu noise’dur ve “identical” okunmalıdır. Noise’un hangi yöne baktığı stable değildir: repository’nin public-domain corpus’unda aynı iki scheme 22,18’e karşı 22,24 çıkar; bu mesafenin yarısı ve ters yöne. 144 generated token’dan 142’sinde full-precision model ile agree eder. fp32 reference’a karşı memory’nin dörtte biri, gerçekten deploy edeceğin fp16’ya karşı yarısı ve detectable cost yok. INT8 dikkatsizce yapıldığında da neredeyse bedavadır: matrix başına tek scale 0,5 perplexity point ve battery’de sıfır answer kaybettirir. Sekiz bit yeterince affedicidir; granularity neredeyse fark etmez. İnsanların INT8’den INT4’e genelleyip zarar görmesinin nedeni tam da budur.
Tensor başına tek scale ile INT4 modeli yok eder. Perplexity 365 milyon: bozulmuş değil, annihilated. Sonra tüm oyun granularity olur — per-tensor 365.416.000, per-row 46,18, per-group-of-128 31,08, NF4 24,55. Weight başına aynı dört bit, en kötü ile en iyi arasında on beş milyon kat fark.
Perplexity kaba bir alettir, battery daha da kabadır. NF4 ile group-128 INT4 arasında perplexity gap 6,5 point ve battery bir question farklı — Chapter 4’ün confidence interval’ı ise on altıda bir question’ın hiçbir şeyi ayırt etmediğini söyler. Interval’dan daha keskin bir demonstration var: aynı battery’yi modelin stock repetition penalty’si kapalıyken çalıştır; greedy decoding’in gerçekten anlamı budur, ve o iki satır yer değiştirir. On altıda bir question küçük etki değil, hiç etki değildir. Chapter 8’in uyarısı da geçerlidir: perplexity yalnızca tokenizer paylaşan modeller arasında karşılaştırılabilir; başka birinin write-up’ındaki sayı seninkiyle karşılaştırılamaz.
Agreement sütunu üçünün en keskinidir ve neredeyse bedava: full-precision model’i greedily çalıştır, sonra quantized model’e her position’da aynı prefix verildiğinde neyi seçeceğini sor. 16 yerine 144 independent observation vardır, ground truth istemez ve battery’nin sıçrayarak bozulduğu yerde pürüzsüz bozulur. Aynı zamanda bir sonraki bölümün ihtiyaç duyduğu miktarın ta kendisidir.
Bu, Chapter 1’in bu bölüm hakkında verdiği sözün vaktinde gelişi: mathematics 4-bit model’in mümkün olduğunu söyler; engineering kullanılabilir olup olmadığına karar verir.
Speculative decoding
Bölüme bağlantı: Speculative decodingChapter 12 bunu duyurmuş ve faturayı buraya bırakmıştı.
Fikir doğrudan prefill/decode ayrımından gelir. token’lık önerilmiş bir sequence’ı doğrulamak, position üzerinde tek forward pass’e mal olur — matrix-matrix product; tek position pass’inden ancak biraz daha pahalı. O halde:
Küçük, ucuz bir model autoregressively candidate token üretir.
Verify
Bölüme bağlantı: VerifyBüyük model tüm candidate üzerinde bir kerede tek forward pass çalıştırır ve her position’da ne söyleyeceğini üretir.
Accept
Bölüme bağlantı: Acceptİkisinin agree ettiği en uzun prefix’i, ayrıca ilk disagreement’ta büyük model’in bedava verdiği token’ı tut. Gerisini at ve yeniden başla.
Output distribution değişmez. Greedy decoding ile bu açıktır — bir token yalnızca target onu üretecekse kabul edilir. Sampling ile modified acceptance rule gerekir; Leviathan et al. resulting distribution’ın target’ınkiyle tam aynı olduğunu kanıtlar.10 Bu bölümdeki ikinci exact optimisation budur.
Bu yüzden her şey acceptance rate ’e bağlıdır; ölçülebilir — yukarıdaki agreement sütunudur, bu yüzden orada hesaplandı. Her quantized model’i full-precision target için draft olarak kullanıp 144 generated position üzerinde:
| draft model | acceptance | en uzun accepted run | target pass başına expected tokens, |
|---|---|---|---|
| fp32 (target’ın kendisi) | 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 için verification pass başına expected accepted tokens:
ve net speedup, bunu draft’ın kendi maliyetine böler; target’ın token başına maliyetinin 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 |
Hatırlanacak entry bold olan: speculative decoding generation’ı yavaşlatabilir. Target’ın beşte biri maliyetindeki draft ile %30 acceptance’ta, beş forward pass öder ve 1,4 token tutarsın. Son sütun diğer tuzaktır — daha uzun draft yalnızca acceptance yüksekken yardım eder; çünkü -token guess’in tail’ine neredeyse hiç ulaşılmaz. %90 acceptance’ta 3,40x eder; %30’da 0,79x eder: aynı configuration, traffic’in üzerinde ölçülen bir sayıya bağlı olarak kazanç ya da kayıp.
Distillation ve soft label’ın taşıdığı şey
Bölüme bağlantı: Distillation ve soft label’ın taşıdığı şeyQuantization aynı function’ı daha az bit’te saklayarak modeli küçültür. Distillation, daha küçük bir modeli daha büyük olanı taklit edecek şekilde train ederek küçültür11 — deep learning’den neredeyse on yıl eski bir fikir.12
İnce kısım student’ın neyden öğrendiğidir. Correct answer’dan değil: doğrudan onun üzerinde train edilebilirdi. Teacher’ın eklediği şey tüm distribution’dır. Model’e bir phrase’den sonra ne geldiğini sor ve argmax’in ötesine bak:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360Hard label jug der ve başka hiçbir şey söylemez. Soft label jug der; ayrıca cup’nin neredeyse aynı kadar iyi olduğunu, bowl’nin plausible olduğunu ve large’nin — bir adjective, tamamen farklı grammatical continuation — hâlâ canlı olduğunu söyler. Original argüman budur: bu bir 7, ama 1’e oldukça benziyor; benzerlik, hard label’ın çöpe attığı bilgidir.
Distillation’ın temperature kullanmasının nedeni de budur. softmax’ten önce logits’i ile bölmek distribution’ı düzleştirir ve runners-up’ın relative weight’ini yükseltir: bu phrase’de top token ile üçüncü arasındaki oran ’da 2,24’ten ’ta 1,50’ye düşer — ilkinin karekökü; logits’i ikiye bölmenin bir orana yaptığı şey de budur. Aynı ordering, loss’un attention’ının daha büyük kısmı near misses üzerinde. Student’ın gradient’i teacher’ın verdict’ini değil yalnızca, uncertainty’sini de taşır.
8, 16 ve 24 GB’a ne sığar
Bölüme bağlantı: 8, 16 ve 24 GB’a ne sığarBu bölümdeki her şey artık tek bir toplamdır:
Burada , tüm concurrent requests üzerindeki toplam resident token’dır. Uygulanışı: 7B ve 70B satırları dimension 128 olan 8 key-value heads varsayar; 13B satırı 40 heads ile full multi-head attention varsayar, çünkü o nesil modeller böyle inşa edilmişti — ve bu belli oluyor.
8 GB
| model | precision | weights | overhead sonrası free | sığan context token |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | sığmaz | — |
| 7B | int8 | 6,5 GB | sığmaz | — |
| 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 | sığmaz | — |
16 GB
| model | precision | weights | overhead sonrası free | sığan context token |
|---|---|---|---|---|
| 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 sonrası free | sığan context token |
|---|---|---|---|---|
| 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 | sığmaz | — |
8 GB tablosundaki 13B satırına bak. Weights sığıyor — 8’in 6,2 GB’ı — bu yüzden alışıldık konuşma biçimiyle 13B model “8 GB card üzerinde çalışır”. 337 token context’i var; bu bir conversation değil, ancak bir prompt. “Sığıyor mu” yanlış soru. Doğru soru “ne kadar context ile ve aynı anda kaç user için”dir.
İki 16 GB int8 satırına da bak. 7B 65.378 token alırken 13B 3.136 alır — 5,6 GB extra weights’ten doğan yirmi kat fark; çünkü buradaki 13B’de multi-head attention vardır ve cache’i token başına 800 KB’tır, 7B’nin 128 KB’ına karşı. Benzer size’da iki model; biri uzun context için kullanışsız, hem de hiçbir model card headline’ında görünmeyen bir nedenle.
Bundan sonra nereye gidiyor
Bölüme bağlantı: Bundan sonra nereye gidiyorOn üç bölüm önce bu, iki weight ve bir bias’a sahip bir perceptron’dı. Şimdi tasarlanmış, trained, aligned, zor sorular için compute harcamayı öğrenmiş ve token başına ölçülü maliyetle served edilen bir transformer — içinde açılmamış kutu kalmadan.
Bu burada biter ve bilerek biter.
Chapter 14 model başka bir yerdeyken başlar. Senin process’inde değil, memory’inde değil, print edebileceğin bir variable’da değil: administer etmediğin bir makinede, bir API key’in, bir port’un ve bir bill’in arkasında. Burada ölçülen her şey hâlâ oluyor — prefill hâlâ ilk token’dan önce çalışıyor, cache hâlâ conversation ile büyüyor, içinde bulunduğun batch hâlâ başka birine ait ve hâlâ latency’ni belirliyor — ama bundan sonra onu Server-Sent Events stream’i, bir finish_reason ve Retry-After header’lı HTTP 429 üzerinden gözlemlersin. Sorular vantage point ile değişir: bu gradient nasıl hesaplanıyor değil, faturam neden üçe katlandı. Dil de değişir; Chapter 14 bu kuralı duyurmak yerine açıklar — buraya kadar code weights, gradients, logits ve tokenizer bytes tutuyordu; oradan sonra connection, retry, cancellation ve accumulated state tutar. Arkandaki on üç bölüm bu geçişle atılmaz. Port’un öte tarafında çalışan şeyin description’ıdır.
Kaynaklar ve yöntem
Bölüme bağlantı: Kaynaklar ve yöntemİki omission bilinçli. FlashAttention (Dao et al., arXiv:2205.14135) farklı bir attention değildir — aynı function’ı operation’ı tile ederek hesaplar; böylece score matrix asla memory’ye yazılmaz. Bu yüzden bu bölümün ikinci tablosundaki 67 MB pratikte aritmetiğin düşündürdüğünden küçüktür. Kernels ise devredilmiştir: Stanford CS336’nın lecture 10’u inference systems’i burada amaçlanandan daha derin işler; CPU tarafı için primary sources llama.cpp repository ve GGUF specification’dır.
Referanslar
Bölüme bağlantı: Referanslar-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Paper büyük ölçüde bir memory-bandwidth argümanıdır ve öyle okunur. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Existing multi-head checkpoint’i dönüştüren uptraining recipe’yi içerir; GQA’nın bu kadar hızlı yayılmasının nedeni budur. ↩
-
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’i — continuous batching — ve selective batching’i tanıtır. ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. vLLM’in üzerine kurulduğu paper; §3 operating-systems benzetmesini tam olarak verir. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 §3’te tanımlanır; yukarıdaki ölçümde kullanılan on altı level value, bu paper’ın türettikleridir. ↩
-
Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). §4’teki outlier-feature analysis, yukarıda ölçülen phenomenon’ın kaynağıdır; outlier’ların scale ile sistematik olarak ortaya çıktığı bulgusu dahil. ↩ ↩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, output distribution’ın değişmediğinin kanıtıdır; Chen et al. (arXiv:2302.01318) aynı fikri independent olarak yayımladı. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Temperature ve “dark knowledge” argümanı. ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, dokuz yıl önce; transformers yerine ensembles için. ↩