Tanie inference: KV cache, batching i quantization
Ten sam model, to samo pytanie: 8,8 s i 78,9 s, wynik identyczny bajt w bajt. Potem INT4 mierzone na trzy sposoby.
Na tej stronie
Ten sam model, na tej samej maszynie, odpowiada na to samo pytanie tymi samymi 48 token. Dwa wyniki są identyczne token po token — sprawdzone, nie założone.
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)Zmienił się jeden argument: use_cache=False. Nic w modelu, prompt, sampling ani arytmetyce nie jest inne, a drugi przebieg nie jest za ten wysiłek dokładniejszy. Jest dziewięć razy wolniejszy bez żadnego powodu.
Taki jest kształt tego rozdziału. Wszystko tutaj — cache, batch, quantized weights — jest próbą przestania płacić za pracę, która nie zmienia odpowiedzi, albo sprawdzenia, ile kosztuje tańsza odpowiedź. Rozdział 10 ustalił cennik training. To jest cennik strony, za którą płacisz wiecznie: wdrożony model wydaje mniej więcej FLOPs za każdy token, który emituje, przy każdym request, przez resztę swojego życia.
Gdzie poszedł czas drugiego przebiegu
Link do sekcji: Gdzie poszedł czas drugiego przebieguAby wygenerować token, decoder-only transformer bierze całą dotychczasową sekwencję, przepuszcza ją przez każdą warstwę i odczytuje rozkład prawdopodobieństwa z ostatniej pozycji. Potem dopisuje wybrany token i robi to znowu. Ten opis jest poprawny — i dokładnie to robi wolny przebieg.
Jest też ogromnie marnotrawny, a powodem jest maska przyczynowa z Rozdziału 9. Wektory key i value pozycji 7 są obliczane z wejścia pozycji 7 oraz pozycji przed nią. Kiedy pojawia się pozycja 8, pozycja 7 nie może jej zobaczyć — to właśnie znaczy „przyczynowa” — więc key i value pozycji 7 są dokładnie tymi samymi liczbami co wcześniej. Wolny przebieg mimo to przelicza je na każdym kroku.
Więc je zapisz. Ten zapis to key-value cache, najważniejsza pojedyncza optymalizacja w serving modeli językowych:
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)Zobacz, co trafia do modelu w pętli: nxt, jeden token. Nie sekwencja. Query nowego token attend względem każdego zapisanego key, a zapisane key i tak nigdy nie miały się zmienić. To nie jest przybliżenie — o to chodziło w sprawdzeniu identycznego wyniku powyżej. Cache nie wymienia jakości na szybkość; usuwa zbędną arytmetykę.
Aby czysto zobaczyć skalowanie, usuń transformer i zmierz jedną głowicę attention z , jeden krok generation liczony na oba sposoby:
| token w kontekście | przeliczanie wszystkiego | z cache | stosunek | macierz wyników |
|---|---|---|---|---|
| 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 |
Prawa kolumna jest przyczyną. Przeliczanie buduje pełną macierz attention na każdym kroku — z ramki o notacji asymptotycznej w Rozdziale 9, płacone raz za token. Z cache budujesz zamiast tego wiersz : przy 4,096 token, 67 MB wyników kontra 16 KB.
Liczenie multiply-accumulates zamiast milisekund usuwa maszynę z argumentu. Aby wygenerować token od zimnego startu:
| wygenerowane token | z cache | przeliczanie | stosunek |
|---|---|---|---|
| 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 |
Na krok wersja z cache jest liniowa względem kontekstu, a bez cache kwadratowa; zsumowane przez generation, kontra , ze stosunkiem rosnącym bez ograniczeń. Dziewięciokrotna różnica z otwarcia została zmierzona na 48 token — krócej niż pierwszy wiersz tej tabeli.
Cache zmienia też to, co musi być w pamięci. Na laptopowym GPU 8 GB generującym 256 token w fp16, biorąc szczyt alokatora i odejmując rezydentne wagi:
| szczytowa pamięć robocza | |
|---|---|
| z cache | 21.8 MB |
| przeliczanie | 181.7 MB |
8,3 raza więcej pamięci, wydane po to, by wyprodukować te same token wolniej. To obietnica z Rozdziału 5, przychodząca z nieoczekiwanej strony: tam reverse-mode autodiff musiał trzymać każdy stan pośredni przy życiu dla backward pass, a activations dominowały pamięć training. Przy inference nie ma backward pass i nie ma czego dla niego przechowywać — więc pamięć dominuje zamiast tego cache, i jest to świadomy wybór, nie nieunikniony koszt.
Prefill i decode to dwie różne maszyny
Link do sekcji: Prefill i decode to dwie różne maszynySpójrz jeszcze raz na szybki przebieg: jego pierwszy token zachował się inaczej niż pozostałe czterdzieści siedem.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepPrompt kosztował 25.6 ms na token, a każdy wygenerowany token kosztował 166 ms. Ten sam model, ten sam sprzęt, te same wagi, sześciokrotna różnica na token — i w kierunku, którego większość osób się nie spodziewa. Prompt jest częścią tanią. Generation dzieli się na dwie fazy o naprawdę odmiennej fizyce:
Prefill
Link do sekcji: PrefillJeden forward pass przez cały prompt. Każdy token jest przetwarzany równolegle, więc każda macierz wag jest wczytywana z pamięci raz i mnożona przez macierz setek wektorów token — iloczyn macierz-macierz, z dużą ilością arytmetyki na każdy przeniesiony bajt, czyli dokładnie tym, do czego zbudowano GPU. Prefill jest compute-bound, a jego koszt jest z grubsza liniowy względem długości prompt.
Decode
Link do sekcji: DecodeJeden forward pass na token, batch jeden i sekwencja jeden. Każda macierz wag nadal jest wczytywana z pamięci w całości i mnożona przez pojedynczy wektor — iloczyn macierz-wektor, z prawie zerową arytmetyką na każdy przeniesiony bajt. Decode jest memory-bandwidth-bound, a jego koszt na token ledwie zależy od długości kontekstu.
Obie połówki da się zmierzyć. Prefill, jedno przejście przez token:
| prompt tokens | sekundy | ms na 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, jeden token względem cache :
| token w cache | ms dla jednego token |
|---|---|
| 16 | 110.05 |
| 64 | 97.57 |
| 256 | 108.53 |
| 1024 | 103.86 |
Przeczytaj drugą tabelę dwa razy. Przejście z 16 token kontekstu do 1,024 — sześćdziesiąt cztery razy więcej historii do attend — zmieniło koszt kroku o nic mierzalnego. Attention względem cache to realna praca, ale przyćmiewa ją stały koszt przeciągnięcia pół miliarda wag przez magistralę pamięci, aby wyprodukować jeden wektor. Ten stały koszt jest powodem wszystkiego w następnej sekcji.
Te dwie fazy są źródłem dwóch liczb raportowanych przez każdy system serving. Time to first token to zasadniczo prefill i rośnie z prompt, dlatego długa rozmowa wolno startuje. Tokens per second to i jest mniej więcej stałe, dlatego odpowiedź potem płynie równo. Chat, który zaczyna powoli, a potem płynnie streamuje, nie jest sztuczką renderowania. To są te dwie tabele.
Cache też jest rachunkiem
Link do sekcji: Cache też jest rachunkiemCache wymienia arytmetykę na pamięć, a pamięć, której chce, nie jest mała. Dla każdego token w kontekście każda warstwa trzyma jeden wektor key i jeden wektor value na key-value head:
2 oznacza keys i values; cała reszta to architektura. Dla modelu mierzonego w tym rozdziale — 24 warstwy, 14 query heads, 2 key-value heads, wymiar head 64 — w fp16 daje to bajtów na token.
Formuły w tej dziedzinie mają zwyczaj mylić się o czynnik dwa, więc sprawdź to względem alokatora zamiast wierzyć:
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/tokenDokładnie, i pozostaje dokładnie dla każdego sprawdzonego kształtu:
| batch | context | zmierzony cache | przewidywany | szczytowa pamięć robocza |
|---|---|---|---|---|
| 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 |
Ostatnie trzy wiersze zasługują na drugie spojrzenie. Trzydziestu dwóch użytkowników po 2,048 token każdy, sześćdziesięciu czterech po 1,024, stu dwudziestu ośmiu po 512 — cache ma 768 MB w każdym przypadku, bo wszystkie trzy trzymają 65,536 token. Cache zależy tylko od łącznej liczby rezydentnych token, a nie od tego, jak są rozdzielone między użytkowników. Ten fakt jest fundamentem sekcji o batching.
Skąd biorą się MQA i GQA
Link do sekcji: Skąd biorą się MQA i GQARozdział 9 wprowadził multi-query i grouped-query attention oraz odłożył powód do tego rozdziału. Powodem jest ta formuła, a konkretnie w niej.
Standardowe multi-head attention daje każdej query head własne key i value heads. Ten model ma 14 query heads; przy pełnym multi-head attention jego cache wynosiłby bajtów na token — 84 KB zamiast 12 KB, dokładnie siedem razy więcej, czyli stosunek query heads do key-value heads.
Multi-query attention1 doprowadza to do granicy: wszystkie query heads współdzielą jedną key-value head. Grouped-query attention2 to kompromis, który wygrał — garść key-value heads, każda współdzielona przez grupę query heads — bo utrata jakości MQA była realna, a GQA nie jest. Żadne z nich nie kupuje arytmetyki. Istnieją po to, by podzielić tę formułę przez liczbę całkowitą, i rozeszły się po branży w chwili, gdy długie konteksty uczyniły cache ograniczeniem wiążącym.
A robią to szybko. Dla modelu klasy 7B z 32 warstwami i 8 key-value heads o wymiarze 128 cache ma 128 KB na token w fp16:
| context tokens | jeden użytkownik | 8 użytkowników | 64 użytkowników |
|---|---|---|---|
| 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 |
Własne wagi tego modelu mają 13.0 GB w fp16, jak w tabeli na końcu rozdziału. Więc przy context 128,000 token cache jednego użytkownika jest większy niż model. To arytmetyka, którą Rozdział 16 zamienia w pieniądze, i dlatego długa rozmowa nie jest tylko wolna — zajmuje stały kawałek maszyny tak długo, jak request żyje.
Batching: liczba, która rośnie, i liczba, która maleje
Link do sekcji: Batching: liczba, która rośnie, i liczba, która malejeDecode jest memory-bound: wagi są przeciągane przez magistralę, by wyprodukować jeden token, a jednostki arytmetyczne bezczynnie czekają. Dołóż więc więcej pracy do tego samego kroku. Uruchom kilka request naraz, a wagi, odczytane raz, obsłużą je wszystkie. Zmierzone na tym samym modelu, każdy request trzyma 64-token cache i dekoduje jeden token:
| batch | latency na krok | 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 |
Czytaj dwie prawe kolumny względem siebie, bo to cały sens. Przejście z jednego request do szesnastu mnoży throughput przez 6,0 i mnoży czekanie pojedynczego request przez 2,67. Batch uczynił serwer lepszym, a każdego użytkownika gorszym.
To nie błąd do dostrojenia; to sama wymiana, i po każdej stronie ma nazwę. Latency to to, czego doświadcza osoba czekająca na odpowiedź. Throughput to to, przez co dzieli się fakturę. Żadne ustawienie nie poprawia obu.
Zauważ też, gdzie to się zatrzymuje. Od 16 do 32 throughput zyskuje 9 %, podczas gdy latency prawie się podwaja: krok przestał być memory-bound i stał się compute-bound, a za tym kolanem batch niczego nie kupuje. Każde deployment ma takie kolano; jego położenie trzeba zmierzyć u siebie, ale jego istnienie nie.
Static batching marnuje większość tego, co wygrywa
Link do sekcji: Static batching marnuje większość tego, co wygrywaNaiwny sposób na batch polega na zebraniu request, uruchomieniu ich razem i zwróceniu wyników, gdy wszystkie skończą. Ale nie kończą razem: niektóre odpowiedzi mają dwadzieścia token, inne pięćset. Stały batch działa, aż skończy jego najdłuższy członek, a każdy zakończony request nadal zajmuje swój slot, dokładając padding, aż do tego momentu.
Weź 64 request z realistycznie skośnym rozkładem długości wyjścia — mediana 18 token, najdłuższy 231, łącznie 1,874 — i zasymuluj obie polityki przy zmierzonym koszcie kroku dla ośmiu slotów:
| polityka | wall clock | throughput | średnie latency na request | zmarnowane slot-kroki |
|---|---|---|---|---|
| statyczne batch po 8 | 176.9 s | 10.6 tok/s | 83.2 s | 3,214 |
| continuous, 8 slotów | 109.0 s | 17.2 tok/s | 8.1 s | 0 |
Throughput poprawia się 1,6x. Średnie latency poprawia się ponad dziesięciokrotnie, bo przy static batching request, który skończył w czterech krokach, nadal czeka na sąsiada z 231 token, zanim ktokolwiek o nim usłyszy.
Continuous batching3 jest poprawką i jest tak proste, jak brzmi: batch nie jest grupą, lecz zbiorem slotów, a slot, który się zwalnia, przyjmuje następny request z kolejki już w kolejnym kroku. Scheduler działa z granulacją jednego token, nie jednego request. Każdy stos serving w produkcji robi to teraz.
Ma też drugą połowę: cache. Sloty, które przychodzą i odchodzą, zostawiają pofragmentowaną pamięć cache, a rezerwowanie dla każdego slotu maksymalnie możliwego context marnuje większość rezerwacji. PagedAttention4 pożycza odpowiedź z systemów operacyjnych: przechowuj cache w blokach stałego rozmiaru z tabelą bloków na sekwencję, tak aby cache sekwencji mógł być fizycznie rozproszony, pozostając logicznie ciągły — co pozwala też dwóm sekwencjom ze wspólnym prefiksem współdzielić bloki, które go trzymają. Na tym zbudowano vLLM i dlatego silnik serving jest alokatorem pamięci z dołączonym transformer.
Quantization i pierwsza rzecz, która idzie źle
Link do sekcji: Quantization i pierwsza rzecz, która idzie źleDruga połowa rachunku to same wagi. Pół miliarda parametrów po cztery bajty każdy to 1.98 GB; po dwa bajty, 0.99 GB; po jeden bajt, 0.49 GB. Mniej bitów na wagę zmniejsza model na dysku, zmniejsza go w pamięci i — ponieważ decode jest bandwidth-bound — sprawia, że każdy krok jest szybszy, bo trzeba przenieść mniej bajtów.
Najprostszy schemat to symetryczna quantization względem maksimum bezwzględnego i mieści się w trzech liniach:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedWybierz skalę tak, by największa waga mapowała się na największą liczbę całkowitą, podziel, zaokrąglij, zapisz liczby całkowite i skalę. Odtwórz przez przemnożenie z powrotem. Nie ma w tym nic sprytnego i działa — aż przestaje.
Zmierzone na rzeczywistych wagach modelu: wszystkie 168 macierzy projekcji, 357.8 miliona parametrów, błąd względny :
| schemat | średni błąd względny | najgorsza macierz |
|---|---|---|
| INT8, jedna skala dla całej macierzy | 0.0400 | 0.1487 |
| INT8, jedna skala na wiersz wyjściowy | 0.0100 | 0.0149 |
| INT4, jedna skala dla całej macierzy | 0.6026 | 0.9931 |
| INT4, jedna skala na wiersz wyjściowy | 0.1790 | 0.2589 |
| INT4, jedna skala na grupę 128 | 0.1323 | 0.1992 |
| NF4, jedna skala na blok 64 | 0.0952 | 0.1205 |
| INT3, jedna skala na grupę 128 | 0.3044 | 0.4123 |
| INT2, jedna skala na grupę 128 | 0.7790 | 0.8076 |
Czwarty wiersz to załamanie. Błąd względny 0.99 na najgorszej macierzy oznacza, że rekonstrukcja nie zachowuje właściwie nic z oryginału — macierz została zastąpiona szumem o mniej więcej właściwej wielkości. Przyczyna jest widoczna w tym samym eksperymencie na pojedynczej macierzy:
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 %)Jedna waga na sześć tysięcy leży dalej niż sześć odchyleń standardowych, a największa jest 24 odchylenia dalej. Przy jednej skali dla całej macierzy ta jedna waga ustala rozmiar kroku dla wszystkich 4,3 miliona pozostałych. Przy 8 bitach jest 256 kroków i typowa waga nadal ląduje na sensownym. Przy 4 bitach jest ich 16, skrajny zarezerwowany jest dla wartości, której prawie nic nie ma, a zwykłe wagi — czyli wszystkie — zaokrąglają się do dwóch lub trzech różnych poziomów.
Wszystko po tym wierszu to ta sama naprawa na różnych granularnościach: daj skali mniejsze terytorium. Skala na wiersz wyjściowy dzieli błąd przez 3,4; skala na grupę 128 kolejnych wag dzieli go znowu. Kosztem jest księgowość — 16-bitowa skala na grupę 128 to bitu na wagę zamiast 4 — a odzyskuje większość luki.
NF4 podchodzi do tego z drugiej strony.5 Poziomy nie muszą być równo rozmieszczone. Wagi w bloku są w przybliżeniu normalnie rozłożone, więc wybierz szesnaście poziomów jako kwantyle rozkładu normalnego: gęsto przy zerze, gdzie wagi faktycznie są, rzadko w ogonach, gdzie ich nie ma. Te same cztery bity, ta sama skala blokowa, przy mniejszym bloku — 4.25 bitu na wagę kontra 4.125 dla group-128 — a zmierzony błąd spada z 0.1323 do 0.0952, o 28 % mniej. Część tego to drobniejszy blok, reszta to ustawienie poziomów tam, gdzie jest masa, a rozdzielenie tych dwóch wymagałoby trzeciego wiersza.
Cechy odstające
Link do sekcji: Cechy odstająceRamka o floating-point z Rozdziału 2 skończyła się obietnicą: że ten rozdział skwantyzuje wagi do 8 i 4 bitów i znajdzie garść cech odstających, które nie dadzą się ścisnąć. Oto one — i wyjaśniają, dlaczego „po prostu zaokrąglij liczby” nigdy nie miało działać na activations.
Wagi powyżej zachowywały się źle. Activations są w innej lidze. Weź zwykły 84-token prompt, przechwyć residual stream na każdej warstwie i zmierz największą wartość bezwzględną, jaką osiąga każdy z 896 wymiarów:
| warstwa | największe |h| | największe |h| wymiaru medianowego | stosunek | wymiary powyżej 6x mediany |
|---|---|---|---|---|
| 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 |
Wymiar 62 osiąga 1,579.6, podczas gdy wymiar medianowy nigdy nie przekracza 1.6. To nie wybryk jednego token ani jednej warstwy: ten sam wymiar jest obecny w warstwie 4 i nadal w warstwie 20, z prawie tą samą wartością. To są outlier features,6 i są systematyczne — właściwość wytrenowanego modelu, nie wejścia.
Histogram tych 896 maksimów per wymiar w warstwie 16 czyni kształt oczywistym:
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 | # 1Dziewięćset wymiarów w schludnej kupce poniżej 8, kompletnie nic przez trzy oktawy, a potem jeden samotny wymiar na dalekim końcu. Teraz skwantyzuj ten tensor do INT8 i policz, co się dzieje:
| schemat | błąd względny | użyte różne poziomy całkowite, cały tensor |
|---|---|---|
| jedna skala dla całego tensora | 0.1083 | 14 z 256 |
| jedna skala na token (na wiersz) | 0.0433 | 158 |
| cały tensor, 1 wymiar odstający zachowany w fp32 | 0.0442 | 48 |
| cały tensor, 4 wymiary odstające zachowane w fp32 | 0.0279 | 57 |
| cały tensor, 16 wymiarów odstających zachowanych w fp32 | 0.0085 | 102 |
Czternaście poziomów z 256. Skalę ustawiło 1,579.6, więc każdy krok ma szerokość 12.44, a typowa activation — mediana wartości bezwzględnej 0.26, dziewięćdziesiąty dziewiąty percentyl 2.51 — nie ma gdzie wylądować. Per wymiar jest to jeszcze ostrzejsze:
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 levelsJeden poziom. Cały wymiar, każdy token, skwantyzowany do tej samej liczby. Przydzielono osiem bitów i użyto mniej więcej zera, a model czytający te activations dostaje stałą.
Ten pomiar uzasadnia każdą technikę, której ludzie naprawdę używają:
Trzymaj outliers poza tym. LLM.int8()6 rozkłada mnożenie macierzy: wymiary o ekstremalnych wartościach liczone są w 16 bitach, wszystko inne w INT8, a połówki są sumowane. Powyższa tabela jest pokwitowaniem — usunięcie czterech wymiarów tnie błąd prawie czterokrotnie. SmoothQuant7 zamiast tego przenosi trudność: podziel activations przez współczynnik per channel i pomnóż odpowiadającą mu kolumnę wag przez ten współczynnik, co pozostawia iloczyn bez zmian i przenosi outlier z tensora, który nie może go wchłonąć, do tego, który może.
Wybierz rounding, nie po prostu zaokrąglaj. Nic powyżej nie pyta, do czego macierz służy. GPTQ8 kwantyzuje kolumna po kolumnie i po każdej dostosowuje pozostałe kolumny full-precision, by skompensować już popełniony błąd — minimalizując błąd wyjścia warstwy na rzeczywistych wejściach, a nie jej wag. AWQ9 zauważa, że mała część kanałów wag ma znacznie większe znaczenie niż reszta, znajduje je ze statystyk activation i skaluje w górę przed quantizing, aby trafiły na drobniejsze poziomy. Oba potrzebują zestawu kalibracyjnego; żadne nie potrzebuje gradientów.
Pokaż szczegóły
GGUF i co format pliku ma wspólnego z tym wszystkim.
GGUF nie jest metodą quantization; to kontener używany przez llama.cpp, a zamieszanie w porównaniach gguf vs gptq bierze się z traktowania tych dwóch rzeczy jak tego samego rodzaju. GGUF trzyma tensory, tokenizer, metadane architektury i chat template w jednym pliku mapowalnym do pamięci, oraz przenosi w środku rodzinę schematów blokowych — nazwy takie jak Q4_K_M kodują bity na wagę, rozmiar bloku i to, czy niektóre tensory są trzymane w wyższej precyzji.
Istotna różnica inżynieryjna: GPTQ i AWQ produkują wagi zoptymalizowane pod kernel GPU, podczas gdy schematy GGUF są tanio dekodowane na CPU z plikiem zmapowanym, a nie wczytanym. Dlatego ten sam nominalny „4-bitowy model 7B” istnieje w obu światach z różnymi rozmiarami i różną jakością, i dlatego uczciwe porównanie nigdy nie dotyczy formatu — tylko poniższego pomiaru, uruchomionego na twoim własnym zadaniu.
Ile quantization naprawdę kosztuje, zmierzone
Link do sekcji: Ile quantization naprawdę kosztuje, zmierzonePrawie każdy artykuł o quantization zatrzymuje się na poprzedniej sekcji: wyjaśnia metodę, cytuje współczynnik kompresji i stwierdza, że jakość jest „w dużej mierze zachowana”. Rozdział 4 był o tym, by nie oszukiwać samego siebie, więc sprawdźmy.
Ten sam model, wagi kwantyzowane in place każdym schematem, potem trzy pomiary: perplexity na 2,048 token zatrzymanej angielskiej prozy — tutaj szkicu tego kursu, dlatego repozytorium podstawia stałą książkę z domeny publicznej i drukuje tabelę o tym samym kształcie z innymi liczbami — zestaw 16 krótkich pytań faktograficznych ze znanymi odpowiedziami przy greedy decoding oraz odsetek token, przy których model quantized zgadza się z full-precision przy identycznym context.
| schemat | średni błąd wag | perplexity | zestaw pytań | zgodność z fp32 |
|---|---|---|---|---|
| fp32 (referencja) | 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 % |
Cztery rzeczy w tej tabeli warto powiedzieć wprost.
INT8 zrobione poprawnie jest darmowe. Per-row INT8 daje 22.96 wobec referencyjnego 23.08 — różnica jednej części na dwieście, czyli szum, który należy czytać jako „identyczne”. Kierunek szumu nie jest stabilny: na korpusie domeny publicznej z repozytorium te same dwa schematy wychodzą 22.24 wobec 22.18: połowa tej odległości i w drugą stronę. Zgadza się z modelem full-precision na 142 ze 144 wygenerowanych token. Jedna czwarta pamięci wobec referencji fp32, połowa wobec fp16, które faktycznie byś wdrożył, i brak wykrywalnego kosztu. INT8 zrobione niedbale też jest prawie darmowe: jedna skala na macierz kosztuje 0.5 punktu perplexity i żadnej odpowiedzi z baterii. Osiem bitów wybacza na tyle, że granularność prawie nie ma znaczenia — dokładnie dlatego ludzie uogólniają z INT8 na INT4 i dostają po łapach.
INT4 z jedną skalą na tensor niszczy model. Perplexity 365 milionów: nie pogorszone, unicestwione. Granularność staje się wtedy całą grą — per-tensor 365,416,000, per-row 46.18, per-group-of-128 31.08, NF4 24.55. Te same cztery bity na wagę, czynnik piętnastu milionów między najgorszym a najlepszym.
Perplexity jest narzędziem zgrubnym, a bateria jeszcze bardziej zgrubnym. Między NF4 a group-128 INT4 luka perplexity to 6.5 punktu, a bateria różni się o jedno pytanie — a przedział ufności z Rozdziału 4 mówi, że jedno pytanie z szesnastu nie odróżnia absolutnie niczego. Jest ostrzejsza demonstracja niż przedział: uruchom tę samą baterię z wyłączoną fabryczną karą za powtórzenia modelu, czyli tym, co naprawdę oznacza greedy decoding, a te dwa wiersze zamienią się miejscami. Jedno pytanie z szesnastu to nie mały efekt, tylko brak efektu. Ostrzeżenie z Rozdziału 8 też obowiązuje: perplexity jest porównywalne tylko między modelami współdzielącymi tokenizer, więc liczby z czyjegoś opisu nie da się porównać z twoją.
Kolumna zgodności jest najostrzejsza z trzech i prawie darmowa: uruchom model full-precision zachłannie, potem zapytaj quantized, na każdej pozycji, co wybrałby przy tym samym prefiksie. Ma 144 niezależne obserwacje zamiast 16, nie potrzebuje ground truth i degraduje się płynnie tam, gdzie bateria degraduje skokami. Jest też dokładnie tą wielkością, której potrzebuje następna sekcja.
To obietnica z Rozdziału 1 dotycząca tego rozdziału, przychodząca zgodnie z planem: matematyka mówi, że 4-bitowy model jest możliwy, a inżynieria decyduje, czy jest używalny.
Speculative decoding
Link do sekcji: Speculative decodingRozdział 12 zapowiedział to i zostawił rachunek tutaj.
Pomysł wynika prosto z podziału prefill/decode. Weryfikacja zaproponowanej sekwencji token kosztuje jeden forward pass przez pozycji — iloczyn macierz-macierz, ledwie droższy niż przejście przez jedną. Więc:
Mały, tani model generuje autoregresyjnie kandydatów token.
Verify
Link do sekcji: VerifyDuży model wykonuje jeden forward pass przez wszystkich kandydatów naraz, produkując to, co powiedziałby na każdej pozycji.
Accept
Link do sekcji: AcceptZachowaj najdłuższy prefiks, na którym oba się zgadzają, plus token, który duży model dostarcza za darmo przy pierwszej niezgodzie. Odrzuć resztę i zacznij znowu.
Rozkład wyjściowy pozostaje bez zmian. Przy greedy decoding to oczywiste — token jest akceptowany tylko wtedy, gdy target by go wyprodukował. Przy sampling wymaga to zmodyfikowanej reguły akceptacji, a Leviathan i współautorzy dowodzą, że wynikowy rozkład jest dokładnie rozkładem target.10 To druga dokładna optymalizacja w tym rozdziale.
Wszystko zależy więc od acceptance rate , które jest mierzalne — to kolumna zgodności powyżej, dlatego została tam policzona. Używając każdego quantized model jako draft dla full-precision target, na 144 wygenerowanych pozycjach:
| draft model | acceptance | najdłuższy zaakceptowany przebieg | oczekiwane token na target pass, |
|---|---|---|---|
| fp32 (sam 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 |
Oczekiwana liczba zaakceptowanych token na verification pass, przy długości draft , wynosi
a netto speedup dzieli to przez własny koszt draft, ułamek target na 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 |
Pogrubiony wpis to ten do zapamiętania: speculative decoding może spowolnić generation. Przy 30 % acceptance z draft kosztującym jedną piątą target płacisz za pięć forward pass i zachowujesz 1.4 token. Ostatnia kolumna to druga pułapka — dłuższy draft pomaga tylko przy wysokim acceptance, bo ogon -token zgadywania prawie nigdy nie jest osiągany. Przy 90 % acceptance jest warte 3.40x, a przy 30 % jest warte 0.79x: ta sama konfiguracja, zysk albo strata zależnie od liczby zmierzonej na twoim ruchu.
Distillation i co niesie soft label
Link do sekcji: Distillation i co niesie soft labelQuantization zmniejsza model przez zapisanie tej samej funkcji w mniejszej liczbie bitów. Distillation zmniejsza go przez trenowanie mniejszego modelu, by naśladował większy11 — pomysł starszy od deep learning o prawie dekadę.12
Subtelna część dotyczy tego, z czego uczy się student. Nie z poprawnej odpowiedzi: na niej można by go wytrenować bezpośrednio. To, co dodaje teacher, to cały rozkład. Zapytaj model, co następuje po frazie, i spójrz poza argmax:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360Hard label mówi jug i nic więcej. Soft label mówi jug, a także że cup było prawie równie dobre, bowl prawdopodobne, a large — przymiotnik, zupełnie inna kontynuacja gramatyczna — nadal żywe. To oryginalny argument: to jest 7, ale wygląda całkiem podobnie do 1, a podobieństwo jest informacją, którą hard label wyrzuca.
To także powód, dla którego distillation używa temperatury. Podzielenie logits przez przed softmax spłaszcza rozkład i podnosi względną wagę runner-up: w tej frazie stosunek między top token a trzecim spada z 2.24 przy do 1.50 przy — pierwiastka kwadratowego z pierwszego, czyli tego, co podzielenie logits przez dwa robi ze stosunkiem. Ta sama kolejność, więcej attention funkcji straty na bliskich pomyłkach. Gradient studenta niesie niepewność teacher, a nie tylko jego werdykt.
Co mieści się w 8, 16 i 24 GB
Link do sekcji: Co mieści się w 8, 16 i 24 GBWszystko w tym rozdziale jest teraz jedną sumą:
gdzie to łączna liczba rezydentnych token we wszystkich współbieżnych request. Zastosowanie: wiersze 7B i 70B zakładają 8 key-value heads o wymiarze 128, wiersz 13B pełne multi-head attention z 40 heads, bo tak zbudowano te generacje modeli — i to widać.
8 GB
| model | precyzja | wagi | wolne po narzucie | context tokens, które się mieszczą |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | nie mieści się | — |
| 7B | int8 | 6.5 GB | nie mieści się | — |
| 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 | nie mieści się | — |
16 GB
| model | precyzja | wagi | wolne po narzucie | context tokens, które się mieszczą |
|---|---|---|---|---|
| 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 | precyzja | wagi | wolne po narzucie | context tokens, które się mieszczą |
|---|---|---|---|---|
| 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 | nie mieści się | — |
Spójrz na wiersz 13B w tabeli 8 GB. Wagi się mieszczą — 6.2 GB z 8 — więc w zwykłym sposobie mówienia model 13B „działa na karcie 8 GB”. Ma 337 token kontekstu, czyli nie rozmowę, lecz ledwie prompt. „Czy się mieści” to złe pytanie. Właściwe brzmi: „z jakim context i dla ilu użytkowników naraz”.
Spójrz też na dwa wiersze int8 w tabeli 16 GB. 7B dostaje 65,378 token, a 13B dostaje 3,136 — dwudziestokrotna różnica z 5.6 GB dodatkowych wag, bo ten 13B ma multi-head attention, a jego cache kosztuje 800 KB na token wobec 128 KB dla 7B. Dwa modele podobnego rozmiaru, jeden bezużyteczny dla long context, z powodu, którego nie ma w nagłówku żadnej model card.
Dokąd to prowadzi dalej
Link do sekcji: Dokąd to prowadzi dalejTrzynaście rozdziałów temu był to perceptron z dwiema wagami i bias. Teraz jest to transformer, który został zaprojektowany, wytrenowany, aligned, nauczony wydawać compute na trudne pytania i serwowany przy zmierzonym koszcie na token — bez żadnego nieotwartego pudełka w środku.
To kończy się tutaj, i kończy się celowo.
Rozdział 14 zaczyna się z modelem gdzieś indziej. Nie w twoim procesie, nie w twojej pamięci, nie w zmiennej, którą możesz wydrukować: na maszynie, której nie administrujesz, za kluczem API, portem i rachunkiem. Wszystko zmierzone tutaj nadal się dzieje — prefill nadal działa przed pierwszym token, cache nadal rośnie z rozmową, batch, w którym jesteś, nadal należy do kogoś innego i nadal decyduje o twoim latency — ale od teraz obserwujesz to przez strumień Server-Sent Events, finish_reason i HTTP 429 z nagłówkiem Retry-After. Pytania zmieniają się wraz z punktem widzenia: nie jak liczony jest ten gradient, lecz dlaczego moja faktura potroiła się. Język też się zmienia, a Rozdział 14 wyjaśnia tę regułę zamiast ją ogłaszać — do tego miejsca kod trzymał wagi, gradienty, logits i bajty tokenizer; odtąd trzyma połączenie, retry, cancellation i zgromadzony stan. Trzynaście rozdziałów za tobą nie zostaje wyrzuconych przy przekroczeniu granicy. Są opisem tego, co działa po drugiej stronie portu.
Źródła i metoda
Link do sekcji: Źródła i metodaDwa pominięcia są celowe. FlashAttention (Dao et al., arXiv:2205.14135) nie jest innym attention — liczy tę samą funkcję przez kafelkowanie operacji tak, aby macierz wyników nigdy nie została zapisana do pamięci, dlatego 67 MB w drugiej tabeli tego rozdziału jest w praktyce mniejsze, niż sugeruje arytmetyka. A same kernels są delegowane: wykład 10 Stanford CS336 omawia systemy inference na głębokości, której ten tekst nie próbuje osiągnąć, a repozytorium llama.cpp i specyfikacja GGUF są głównymi źródłami dla strony CPU.
Przypisy
Link do sekcji: Przypisy-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Artykuł jest w dużej mierze argumentem o bandwidth pamięci i tak się go czyta. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Zawiera przepis uptraining konwertujący istniejący checkpoint multi-head, dlatego GQA rozeszło się tak szybko. ↩
-
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. Wprowadza scheduling na poziomie iteracji — continuous batching — oraz selective batching. ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. Artykuł, na którym zbudowano vLLM; §3 to pełna analogia do systemów operacyjnych. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 zdefiniowano w §3; szesnaście wartości poziomów użytych w powyższym pomiarze to te wyprowadzone w tym artykule. ↩
-
Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). Analiza outlier-feature w §4 jest źródłem zjawiska zmierzonego powyżej, w tym ustalenia, że outliers pojawiają się systematycznie wraz ze skalą. ↩ ↩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 LLLM 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). Twierdzenie 1 dowodzi, że rozkład wyjściowy pozostaje bez zmian; Chen et al. (arXiv:2302.01318) opublikowali ten sam pomysł niezależnie. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Temperatura i argument „dark knowledge”. ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, dziewięć lat wcześniej, dla ensembles zamiast transformers. ↩