Rendere l’inference economica: KV cache, batching e quantizzazione
Stesso model, stessa risposta in 8,8 s e 78,9 s. Poi INT4 misurato in tre modi, non dato per scontato.
In questa pagina
Lo stesso model, sulla stessa macchina, risponde alla stessa domanda con gli stessi 48 token. I due output sono identici token per token — verificato, non presunto.
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)È cambiato un solo argomento: use_cache=False. Nulla del model, del prompt, del sampling o dell’aritmetica è diverso, e la seconda esecuzione non è più accurata per tutta quella fatica. È nove volte più lenta per niente.
Questa è la forma di questo capitolo. Tutto ciò che contiene — la cache, il batch, i pesi quantizzati — è un tentativo di smettere di pagare per lavoro che non cambia la risposta, o di scoprire quanto costa una risposta più economica. Il Capitolo 10 ha stabilito il listino prezzi del training. Questo è il listino prezzi del lato che paghi per sempre: un model distribuito spende circa FLOP per ogni token che emette, su ogni richiesta, per il resto della sua vita.
Dove è finito il tempo della seconda esecuzione
Link alla sezione: Dove è finito il tempo della seconda esecuzionePer generare un token, un transformer decoder-only prende l’intera sequenza fin qui, la fa passare attraverso ogni layer e legge la distribuzione di probabilità dall’ultima posizione. Poi aggiunge il token scelto e lo fa di nuovo. Questa descrizione è corretta, ed è ciò che fa l’esecuzione lenta.
È anche enormemente dispendiosa, e il motivo è la maschera causale del Capitolo 9. I vettori key e value della posizione 7 sono calcolati dall’input della posizione 7 e dalle posizioni prima di essa. Quando arriva la posizione 8, la posizione 7 non può vederla — è questo che significa causale — quindi key e value della posizione 7 sono esattamente gli stessi numeri di prima. L’esecuzione lenta li ricalcola comunque, a ogni passo.
Quindi salvali. Quello store è la key-value cache, la singola ottimizzazione più importante nel serving dei language model:
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)Guarda che cosa viene passato al model dentro il loop: nxt, un token. Non la sequenza. La query del nuovo token fa attention su ogni key in cache, e le key in cache non sarebbero mai cambiate. Questa non è un’approssimazione — il controllo di output identico sopra è il punto. La cache non scambia qualità con velocità; elimina aritmetica ridondante.
Per vedere chiaramente lo scaling, togli di mezzo il transformer e misura una singola attention head con , un passo di generation calcolato in entrambi i modi:
| token nel context | ricalcola tutto | con una cache | rapporto | matrice dei punteggi |
|---|---|---|---|---|
| 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 |
La colonna di destra è la causa. Ricalcolare costruisce l’intera matrice di attention a ogni passo — il dal riquadro sulla notazione asintotica del Capitolo 9, pagato una volta per token. Con la cache costruisci invece una riga : a 4.096 token, 67 MB di score contro 16 KB.
Contare i multiply-accumulate invece dei millisecondi toglie la macchina dall’argomento. Per generare token da un avvio a freddo:
| token generati | con una cache | ricalcolando | rapporto |
|---|---|---|---|
| 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 |
Per step la versione con cache è lineare nel context e quella senza cache è quadratica; sommando su una generation, contro , con il rapporto che cresce senza limite. La differenza di nove volte dell’apertura è stata misurata su 48 token — sotto la prima riga di quella tabella.
La cache cambia anche ciò che deve stare in memoria. Su una GPU laptop da 8 GB che genera 256 token in fp16, prendendo il picco dell’allocator e sottraendo i pesi residenti:
| memoria di lavoro di picco | |
|---|---|
| con una cache | 21,8 MB |
| ricalcolando | 181,7 MB |
8,3 volte più memoria, spesa per produrre gli stessi token più lentamente. Questa è la promessa fatta nel Capitolo 5, che arriva da una direzione inattesa: lì, l’autodiff in reverse-mode doveva mantenere vivo ogni intermedio per il backward pass, e le attivazioni dominavano la memoria di training. In inference non c’è backward pass e niente da conservare per esso — quindi ciò che domina la memoria è invece la cache, ed è una scelta deliberata più che un costo inevitabile.
Prefill e decode sono due macchine diverse
Link alla sezione: Prefill e decode sono due macchine diverseGuarda di nuovo l’esecuzione veloce: il suo primo token si è comportato diversamente dagli altri quarantasette.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepIl prompt è costato 25,6 ms per token e ogni token generato 166 ms. Stesso model, stesso hardware, stessi pesi, una differenza di sei volte per token — e va nella direzione che la maggior parte delle persone non si aspetta. Il prompt è la parte economica. La generation si divide in due fasi con fisica davvero diversa:
Prefill
Link alla sezione: PrefillUn forward pass sull’intero prompt. Ogni token viene elaborato in parallelo, quindi ogni matrice di pesi viene caricata dalla memoria una volta e moltiplicata contro una matrice di centinaia di vettori token — un prodotto matrice-matrice, con molta aritmetica per byte spostato, che è ciò per cui una GPU è costruita. Il prefill è compute-bound, e il suo costo è circa lineare nella lunghezza del prompt.
Un forward pass per token, batch di uno e sequenza di uno. Ogni matrice di pesi viene ancora caricata per intero dalla memoria, e moltiplicata contro un singolo vettore — un prodotto matrice-vettore, con quasi nessuna aritmetica per byte spostato. Il decode è memory-bandwidth-bound, e il suo costo per token dipende a malapena dalla lunghezza del context.
Entrambe le metà sono misurabili. Prefill, un passaggio su token:
| prompt token | secondi | 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, un token contro una cache di :
| token in cache | ms per un token |
|---|---|
| 16 | 110,05 |
| 64 | 97,57 |
| 256 | 108,53 |
| 1024 | 103,86 |
Leggi due volte la seconda tabella. Passare da 16 token di context a 1.024 — sessantaquattro volte più storia su cui fare attention — ha cambiato il costo di uno step di nulla di misurabile. L’attention contro la cache è lavoro reale, ma è schiacciata dal costo fisso di trascinare mezzo miliardo di pesi attraverso il bus di memoria per produrre un vettore. Quel costo fisso è il motivo di tutto ciò che c’è nella prossima sezione.
Queste due fasi sono l’origine dei due numeri che ogni sistema di serving riporta. Il time to first token è essenzialmente prefill, e cresce con il prompt, ed è per questo che una conversazione lunga sembra lenta all’avvio. I token per secondo sono , ed è circa costante, ed è per questo che poi la risposta scorre in modo uniforme. Una chat che parte lenta e poi streamma fluidamente non è un trucco di rendering. Sono queste due tabelle.
La cache è anche il conto
Link alla sezione: La cache è anche il contoLa cache scambia aritmetica con memoria, e la memoria che vuole non è poca. Per ogni token nel context, ogni layer conserva un vettore key e un vettore value per ogni key-value head:
Il 2 è per key e value; tutto il resto è l’architettura. Per il model misurato in tutto questo capitolo — 24 layer, 14 query head, 2 key-value head, dimensione head 64 — in fp16 sono byte per token.
Le formule in questo campo hanno l’abitudine di sbagliare di un fattore due, quindi controllala contro l’allocator invece di crederci:
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/tokenEsatta, e resta esatta in ogni shape provata:
| batch | context | cache misurata | prevista | memoria di lavoro di picco |
|---|---|---|---|---|
| 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 |
Le ultime tre righe meritano una seconda occhiata. Trentadue utenti con 2.048 token ciascuno, sessantaquattro con 1.024, centoventotto con 512 — la cache è 768 MB in ogni caso, perché tutti e tre contengono 65.536 token. La cache dipende solo dal numero totale di token residenti, non da come sono distribuiti tra gli utenti. Questo fatto è il fondamento della sezione sul batching.
Da dove arrivano MQA e GQA
Link alla sezione: Da dove arrivano MQA e GQAIl Capitolo 9 ha introdotto multi-query e grouped-query attention e ha rimandato il motivo a questo capitolo. Il motivo è quella formula, e in particolare il al suo interno.
La standard multi-head attention dà a ogni query head le proprie key head e value head. Il model qui ha 14 query head; con una multi-head attention completa, la sua cache sarebbe byte per token — 84 KB invece di 12 KB, esattamente sette volte di più, il rapporto tra query head e key-value head.
La multi-query attention1 porta questo al limite: tutte le query head condividono una singola key-value head. La grouped-query attention2 è il compromesso che ha vinto — una manciata di key-value head, ciascuna condivisa da un gruppo di query head — perché la perdita di qualità della MQA era reale e quella della GQA no. Nessuna delle due compra aritmetica. Esistono per dividere quella formula per un intero, e si sono diffuse nel settore nel momento in cui i context lunghi hanno reso la cache il vincolo determinante.
Cosa che succede rapidamente. Per un model di classe 7B con 32 layer e 8 key-value head di dimensione 128, la cache è 128 KB per token in fp16:
| context token | un utente | 8 utenti | 64 utenti |
|---|---|---|---|
| 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 |
I pesi dello stesso model sono 13,0 GB in fp16, la cifra nella tabella alla fine di questo capitolo. Quindi, con un context da 128.000 token, la cache di un utente è più grande del model. Questa è l’aritmetica che il Capitolo 16 trasforma in denaro, ed è il motivo per cui una conversazione lunga non è semplicemente lenta — occupa una fetta fissa di una macchina finché la richiesta è viva.
Batching: il numero che sale e quello che scende
Link alla sezione: Batching: il numero che sale e quello che scendeIl decode è memory-bound: i pesi vengono trascinati attraverso il bus per produrre un token, e le unità aritmetiche restano inattive. Quindi metti più lavoro nello stesso step. Esegui più richieste insieme, e i pesi, letti una volta, servono tutte. Misurato sullo stesso model, con ogni richiesta che contiene una cache da 64 token e decodifica un token:
| batch | latenza per step | throughput | latenza 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 |
Leggi le due colonne di destra una contro l’altra, perché sono tutto il punto. Passare da una richiesta a sedici moltiplica il throughput per 6,0 e moltiplica l’attesa di ogni singola richiesta per 2,67. Il batch ha reso il server migliore e ogni utente peggiore.
Non è un bug da tarare via; è il trade-off stesso, e ha un nome su ciascun lato. La latenza è ciò che sperimenta una persona in attesa di una risposta. Il throughput è ciò per cui si divide la fattura. Nessuna impostazione migliora entrambi.
Nota anche dove si ferma. Da 16 a 32, il throughput guadagna il 9% mentre la latenza quasi raddoppia: lo step ha smesso di essere memory-bound ed è diventato compute-bound, e oltre quel ginocchio il batch non compra nulla. Ogni deployment ha un ginocchio del genere; la sua posizione va misurata sul tuo, ma la sua esistenza no.
Lo static batching spreca gran parte di ciò che guadagna
Link alla sezione: Lo static batching spreca gran parte di ciò che guadagnaIl modo ingenuo di fare batch è raccogliere richieste, eseguirle insieme e restituire quando sono tutte finite. Ma non finiscono insieme: alcune risposte sono di venti token e altre di cinquecento. Un batch fisso gira finché finisce il suo membro più lungo, e ogni richiesta già conclusa continua a occupare il suo slot, contribuendo padding, fino ad allora.
Prendi 64 richieste con una skew realistica di lunghezze di output — mediana 18 token, massimo 231, 1.874 in totale — e simula entrambe le policy al costo per-step misurato per otto slot:
| policy | wall clock | throughput | latenza media per richiesta | slot-step sprecati |
|---|---|---|---|---|
| batch statici da 8 | 176,9 s | 10,6 tok/s | 83,2 s | 3.214 |
| continuo, 8 slot | 109,0 s | 17,2 tok/s | 8,1 s | 0 |
Il throughput migliora di 1,6x. La latenza media migliora di più di dieci volte, perché con lo static batching una richiesta finita in quattro step aspetta comunque un vicino da 231 token prima che qualcuno ne sappia qualcosa.
Il continuous batching3 è la correzione, ed è semplice come sembra: il batch non è un gruppo ma un insieme di slot, e uno slot che si libera ammette la prossima richiesta in coda allo step immediatamente successivo. Lo scheduler lavora alla granularità di un token invece che di una richiesta. Ogni stack di serving in produzione ormai fa così.
Ha una seconda metà, che è la cache. Slot che entrano ed escono lasciano la memoria della cache frammentata, e riservare a ogni slot il suo context massimo possibile spreca gran parte della prenotazione. PagedAttention4 prende in prestito la risposta dai sistemi operativi: memorizzare la cache in blocchi di dimensione fissa con una block table per sequenza, così la cache di una sequenza può essere fisicamente sparsa pur restando logicamente contigua — il che permette anche a due sequenze con un prefisso condiviso di condividere i blocchi che lo contengono. È ciò su cui è costruito vLLM, ed è il motivo per cui un serving engine è un memory allocator con un transformer attaccato.
Quantizzazione, e la prima cosa che va storta
Link alla sezione: Quantizzazione, e la prima cosa che va stortaL’altra metà del conto sono i pesi stessi. Mezzo miliardo di parametri a quattro byte ciascuno sono 1,98 GB; a due byte, 0,99 GB; a un byte, 0,49 GB. Meno bit per peso riducono il model su disco, lo riducono in memoria e — poiché il decode è bandwidth-bound — rendono ogni step più veloce, dato che ci sono meno byte da spostare.
Lo schema più semplice è la quantizzazione simmetrica absolute-maximum, e sta in tre righe:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedScegli una scala così che il peso più grande mappi sull’intero più grande, dividi, arrotonda, salva gli interi e la scala. Ricostruisci moltiplicando indietro. Non ha nulla di brillante, e funziona — finché non funziona più.
Misurato sui pesi reali del model: tutte le 168 matrici di proiezione, 357,8 milioni di parametri, errore relativo :
| schema | errore relativo medio | matrice peggiore |
|---|---|---|
| INT8, una scala per l’intera matrice | 0,0400 | 0,1487 |
| INT8, una scala per riga di output | 0,0100 | 0,0149 |
| INT4, una scala per l’intera matrice | 0,6026 | 0,9931 |
| INT4, una scala per riga di output | 0,1790 | 0,2589 |
| INT4, una scala per gruppo di 128 | 0,1323 | 0,1992 |
| NF4, una scala per blocco di 64 | 0,0952 | 0,1205 |
| INT3, una scala per gruppo di 128 | 0,3044 | 0,4123 |
| INT2, una scala per gruppo di 128 | 0,7790 | 0,8076 |
La quarta riga è il collasso. Un errore relativo di 0,99 sulla matrice peggiore significa che la ricostruzione non conserva praticamente nulla dell’originale — la matrice è stata sostituita da rumore di magnitudine più o meno giusta. La causa è visibile nello stesso esperimento su una singola matrice:
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 %)Un peso su seimila sta oltre sei deviazioni standard, e il più grande è a 24. Con una sola scala per l’intera matrice, quel singolo peso imposta la dimensione dello step per tutti e 4,3 milioni gli altri. A 8 bit ci sono 256 step e il peso tipico atterra ancora su uno significativo. A 4 bit ce ne sono 16, l’estremo è riservato a un valore che quasi nessuno ha, e i pesi ordinari — cioè tutti — vengono arrotondati a due o tre livelli distinti.
Tutto ciò che viene dopo quella riga è la stessa riparazione a granularità diverse: dare alla scala un territorio più piccolo. Per riga di output divide l’errore per 3,4; per gruppo di 128 pesi consecutivi lo divide di nuovo. Il costo è bookkeeping — una scala a 16 bit per gruppo di 128 è bit per peso invece di 4 — e compra indietro gran parte del divario.
NF4 la affronta dall’altro lato.5 I livelli non devono essere equidistanti. I pesi dentro un blocco sono approssimativamente distribuiti normalmente, quindi scegli i sedici livelli come i quantili di una distribuzione normale: densi vicino allo zero dove i pesi stanno davvero, radi nelle code dove non stanno. Stessi quattro bit, stesso block scaling, su un blocco più piccolo — 4,25 bit per peso contro i 4,125 del group-128 — e l’errore misurato scende da 0,1323 a 0,0952, il 28% in meno. Parte di questo è il blocco più fine e il resto è mettere i livelli dove sta la massa, e separare le due cose richiederebbe una terza riga.
Le feature outlier
Link alla sezione: Le feature outlierIl riquadro floating-point del Capitolo 2 si chiudeva con una promessa: che questo capitolo avrebbe quantizzato i pesi a 8 e 4 bit e trovato una manciata di feature outlier che rifiutano di farsi comprimere. Eccole, e spiegano perché «basta arrotondare i numeri» non avrebbe mai funzionato sulle attivazioni.
I pesi sopra si comportavano male. Le attivazioni sono in un’altra categoria. Prendi un prompt ordinario di 84 token, cattura il residual stream a ogni layer e misura la magnitudine massima raggiunta da ciascuna delle 896 dimensioni:
| layer | massimo |h| | massimo |h| della dimensione mediana | rapporto | dimensioni sopra 6x la mediana |
|---|---|---|---|---|
| 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 |
La dimensione 62 raggiunge 1.579,6 mentre la dimensione mediana non supera mai 1,6. Non è un caso di un token o di un layer: la stessa dimensione è presente al layer 4 ed è ancora presente al layer 20, con quasi lo stesso valore. Queste sono le feature outlier,6 e sono sistematiche — una proprietà del model addestrato, non dell’input.
L’istogramma di quei 896 massimi per-dimensione al layer 16 rende la forma inequivocabile:
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 | # 1Novecento dimensioni in un mucchio ordinato sotto 8, niente per tre ottave, poi una dimensione da sola all’estremità lontana. Ora quantizza quel tensor a INT8 e conta cosa succede:
| schema | errore relativo | livelli interi distinti usati, tensor intero |
|---|---|---|
| una scala per l’intero tensor | 0,1083 | 14 su 256 |
| una scala per token (per riga) | 0,0433 | 158 |
| tensor intero, 1 dimensione outlier mantenuta in fp32 | 0,0442 | 48 |
| tensor intero, 4 dimensioni outlier mantenute in fp32 | 0,0279 | 57 |
| tensor intero, 16 dimensioni outlier mantenute in fp32 | 0,0085 | 102 |
Quattordici livelli su 256. La scala è stata impostata da 1.579,6, quindi ogni step è largo 12,44, e l’attivazione tipica — magnitudine mediana 0,26, novantanovesimo percentile 2,51 — non ha dove atterrare. Per dimensione è più netto:
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 levelsUn livello. L’intera dimensione, ogni token, quantizzata allo stesso numero. Sono stati allocati otto bit e ne sono stati usati circa zero, e al model che legge quelle attivazioni viene passato una costante.
Quella misura è la giustificazione di ogni tecnica che le persone usano davvero:
Tieni fuori gli outlier. LLM.int8()6 decompone la moltiplicazione di matrici: le dimensioni con magnitudini estreme sono calcolate a 16 bit, tutto il resto in INT8, e le metà vengono sommate. La tabella sopra è la ricevuta — rimuovere quattro dimensioni taglia l’errore di quasi quattro volte. SmoothQuant7 invece sposta la difficoltà: divide le attivazioni per un fattore per-channel e moltiplica per esso la colonna dei pesi corrispondente, lasciando invariato il prodotto e spostando l’outlier fuori dal tensor che non può assorbirlo dentro quello che può.
Scegli l’arrotondamento, non limitarti ad arrotondare. Nulla di quanto sopra chiede a che cosa serva la matrice. GPTQ8 quantizza colonna per colonna e, dopo ciascuna, regola le colonne full-precision rimanenti per compensare l’errore già commesso — minimizzando l’errore dell’output del layer su input reali invece che dei suoi pesi. AWQ9 osserva che una piccola frazione di canali di peso conta molto più del resto, li trova dalle statistiche di attivazione e li scala verso l’alto prima di quantizzare così che atterrino su livelli più fini. Entrambi richiedono un calibration set; nessuno dei due richiede gradienti.
Mostra dettagli
GGUF, e che cosa c’entra un formato file con tutto questo.
GGUF non è un metodo di quantizzazione; è il contenitore usato da llama.cpp, e la confusione nei confronti gguf vs gptq nasce dal trattare le due cose come se fossero dello stesso tipo. GGUF contiene tensor, tokenizer, metadati dell’architettura e chat template in un unico file memory-mappable, e porta dentro di sé una famiglia di schemi a blocchi — nomi come Q4_K_M codificano bit per peso, dimensione del blocco e se alcuni tensor vengono mantenuti a precisione più alta.
La differenza ingegneristica che conta: GPTQ e AWQ producono pesi ottimizzati per un kernel GPU, mentre gli schemi di GGUF vengono decodificati a basso costo su una CPU con il file mappato invece che caricato. Ecco perché lo stesso «model 7B a 4 bit» nominale esiste in entrambi i mondi con dimensioni diverse e qualità diversa, e perché il confronto onesto non è mai il formato — è la misura sotto, eseguita sul tuo task.
Quanto costa davvero la quantizzazione, misurato
Link alla sezione: Quanto costa davvero la quantizzazione, misuratoQuasi ogni articolo sulla quantizzazione si ferma alla sezione precedente: spiega il metodo, cita un rapporto di compressione e afferma che la qualità è «largamente preservata». Il Capitolo 4 parlava di non ingannarti da solo, quindi scopriamolo.
Stesso model, pesi quantizzati in place con ciascuno schema, poi tre misure: perplexity su 2.048 token di prosa inglese held-out — qui, la bozza di questo corso, motivo per cui il repository sostituisce un libro fisso di pubblico dominio e stampa una tabella della stessa forma con numeri diversi — una batteria di 16 brevi domande fattuali con risposte note sotto greedy decoding, e la frazione di token su cui il model quantizzato concorda con quello full-precision dato un context identico.
| schema | errore medio dei pesi | perplexity | batteria di domande | concorda con fp32 |
|---|---|---|---|---|
| fp32 (riferimento) | 0,0000 | 23,08 | 13/16 | 100,0% |
| INT8 per tensor | 0,0400 | 23,58 | 13/16 | — |
| INT8 per riga | 0,0100 | 22,96 | 13/16 | 98,6% |
| INT4 per tensor | 0,6026 | 365.416.000 | 0/16 | — |
| INT4 per riga | 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% |
Quattro cose in quella tabella meritano di essere dette chiaramente.
INT8 fatto bene è gratis. INT8 per-riga segna 22,96 contro 23,08 del riferimento — un divario di una parte su duecento, che è rumore e va letto come «identico». La direzione in cui punta il rumore non è stabile: sul corpus pubblico del repository gli stessi due schemi risultano 22,24 contro 22,18, metà di quella distanza e nell’altra direzione. Concorda con il model full-precision su 142 dei 144 token generati. Un quarto della memoria rispetto al riferimento fp32, metà rispetto all’fp16 che davvero distribuiresti, e nessun costo rilevabile. INT8 fatto con superficialità è quasi gratis anche lui: una scala per matrice costa 0,5 punti di perplexity e nessuna risposta della batteria. Otto bit sono abbastanza permissivi da rendere la granularità appena rilevante, che è esattamente il motivo per cui le persone generalizzano da INT8 a INT4 e si fanno male.
INT4 con una scala per tensor distrugge il model. Perplexity 365 milioni: non degradato, annientato. La granularità diventa allora tutto il gioco — per-tensor 365.416.000, per-riga 46,18, per-gruppo-di-128 31,08, NF4 24,55. Stessi quattro bit per peso, un fattore di quindici milioni tra peggiore e migliore.
La perplexity è uno strumento grossolano e la batteria uno ancora più grossolano. Tra NF4 e INT4 group-128 il divario di perplexity è 6,5 punti e la batteria differisce di una domanda — e l’intervallo di confidenza del Capitolo 4 dice che una domanda su sedici non distingue assolutamente nulla. C’è una dimostrazione più netta dell’intervallo: esegui la stessa batteria con la repetition penalty di default del model disattivata, che è ciò che greedy decoding significa davvero, e quelle due righe si scambiano di posto. Una domanda su sedici non è un effetto piccolo, è nessun effetto. Vale anche l’avvertimento del Capitolo 8: la perplexity è confrontabile solo tra model che condividono un tokenizer, quindi un numero da un articolo altrui non può essere confrontato con il tuo.
La colonna di concordanza è la più precisa delle tre, e quasi gratuita: esegui il model full-precision in greedy, poi chiedi a quello quantizzato, a ogni posizione, che cosa avrebbe scelto dato lo stesso prefisso. Ha 144 osservazioni indipendenti invece di 16, non richiede ground truth e degrada in modo continuo dove la batteria degrada a salti. È anche esattamente la quantità di cui ha bisogno la prossima sezione.
Questa è la promessa che il Capitolo 1 faceva su questo capitolo, arrivata puntuale: la matematica dice che un model a 4 bit è possibile, e l’ingegneria decide se è utilizzabile.
Speculative decoding
Link alla sezione: Speculative decodingIl Capitolo 12 lo ha annunciato e ha lasciato qui il conto.
L’idea viene direttamente dalla divisione prefill/decode. Verificare una sequenza proposta di token costa un forward pass su posizioni — un prodotto matrice-matrice, appena più costoso del passaggio su una. Quindi:
Un model piccolo ed economico genera token candidati in modo autoregressivo.
Il model grande esegue un forward pass su tutti i candidati insieme, producendo ciò che avrebbe detto a ogni posizione.
Tieni il prefisso più lungo su cui i due concordano, più il token che il model grande fornisce gratis al primo disaccordo. Scarta il resto e ricomincia.
La distribuzione di output è invariata. Con greedy decoding è ovvio — un token viene accettato solo se il target lo avrebbe prodotto. Con il sampling richiede una regola di accettazione modificata, e Leviathan et al. dimostrano che la distribuzione risultante è esattamente quella del target.10 Questa è la seconda ottimizzazione esatta di questo capitolo.
Tutto quindi dipende dall’acceptance rate , che è misurabile — è la colonna di concordanza sopra, motivo per cui è stata calcolata lì. Usando ogni model quantizzato come draft per il target full-precision, su 144 posizioni generate:
| draft model | acceptance | run accettata più lunga | token attesi per target pass, |
|---|---|---|---|
| fp32 (il target stesso) | 100,0% | 48 | 5,00 |
| INT8 per riga | 98,6% | 48 | 4,86 |
| NF4 block 64 | 84,7% | 20 | 3,69 |
| INT4 group 128 | 71,5% | 13 | 2,85 |
| INT4 per riga | 58,3% | 7 | 2,24 |
| INT3 group 128 | 5,6% | 2 | 1,06 |
| INT2 group 128 | 0,0% | 0 | 1,00 |
I token attesi accettati per verification pass, con lunghezza draft , sono
e lo speedup netto lo divide per il costo proprio del draft, una frazione del target per 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 |
La voce in grassetto è quella da ricordare: lo speculative decoding può rendere la generation più lenta. Con acceptance al 30% e un draft che costa un quinto del target, paghi cinque forward pass e tieni 1,4 token. L’ultima colonna è l’altra trappola — un draft più lungo aiuta solo quando l’acceptance è alta, perché la coda di un’ipotesi da token viene raggiunta quasi mai. Al 90% di acceptance vale 3,40x e al 30% vale 0,79x: la stessa configurazione, vittoria o perdita a seconda di un numero misurato sul tuo traffico.
Distillation, e che cosa porta una soft label
Link alla sezione: Distillation, e che cosa porta una soft labelLa quantizzazione riduce un model memorizzando la stessa funzione in meno bit. La distillation lo riduce addestrando un model più piccolo a imitare uno più grande11 — un’idea che precede il deep learning di quasi un decennio.12
La parte sottile è da cosa impara lo student. Non dalla risposta corretta: avrebbe potuto essere addestrato direttamente su quella. Ciò che il teacher aggiunge è l’intera distribuzione. Chiedi al model che cosa segue una frase e guarda oltre l’argmax:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360La hard label dice jug e nient’altro. La soft label dice jug, e anche che cup era quasi altrettanto buona, bowl plausibile, e large — un aggettivo, una continuazione grammaticale completamente diversa — ancora viva. Questo è l’argomento originale: questo è un 7, ma assomiglia parecchio a un 1, e la somiglianza è informazione che la hard label butta via.
È anche il motivo per cui la distillation usa una temperatura. Dividere i logits per prima della softmax appiattisce la distribuzione e aumenta il peso relativo dei runner-up: su questa frase, il rapporto tra il top token e il terzo scende da 2,24 a a 1,50 a — la radice quadrata del primo, che è ciò che dividere i logits per due fa a un rapporto. Stesso ordinamento, più attention della loss sui quasi-errori. Il gradiente dello student porta l’incertezza del teacher e non solo il suo verdetto.
Che cosa entra in 8, 16 e 24 GB
Link alla sezione: Che cosa entra in 8, 16 e 24 GBTutto in questo capitolo ora è una somma:
dove sono i token totali residenti su tutte le richieste concorrenti. Applicandola: le righe 7B e 70B assumono 8 key-value head di dimensione 128, la riga 13B full multi-head attention con 40 head, che è come erano costruite quelle generazioni di model — e si vede.
8 GB
| model | precisione | pesi | libero dopo overhead | context token che entrano |
|---|---|---|---|---|
| 7B | fp16 | 13,0 GB | non entra | — |
| 7B | int8 | 6,5 GB | non entra | — |
| 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 | non entra | — |
16 GB
| model | precisione | pesi | libero dopo overhead | context token che entrano |
|---|---|---|---|---|
| 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 | precisione | pesi | libero dopo overhead | context token che entrano |
|---|---|---|---|---|
| 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 | non entra | — |
Guarda la riga 13B nella tabella da 8 GB. I pesi entrano — 6,2 GB su 8 — quindi nel modo usuale di parlarne, un model 13B «gira su una scheda da 8 GB». Ha 337 token di context, che non è una conversazione ma a malapena un prompt. «Ci sta» è la domanda sbagliata. Quella giusta è «con quanto context, e per quanti utenti alla volta».
Guarda anche le due righe int8 da 16 GB. Il 7B ottiene 65.378 token e il 13B ne ottiene 3.136 — una differenza di venti volte da 5,6 GB di pesi extra, perché il 13B qui ha multi-head attention e la sua cache costa 800 KB per token contro i 128 KB del 7B. Due model di dimensioni simili, uno inutilizzabile per context lunghi, per un motivo che non appare nel titolo di nessuna model card.
Dove si va dopo
Link alla sezione: Dove si va dopoTredici capitoli fa questo era un perceptron con due pesi e un bias. Ora è un transformer che è stato progettato, addestrato, allineato, istruito a spendere compute sulle domande difficili e servito a un costo misurato per token — senza lasciare al suo interno una sola scatola chiusa.
Finisce qui, e finisce apposta.
Il Capitolo 14 inizia con il model da qualche altra parte. Non nel tuo processo, non nella tua memoria, non in una variabile che puoi stampare: su una macchina che non amministri, dietro una chiave API, una porta e una fattura. Tutto ciò che è stato misurato qui sta ancora accadendo — il prefill gira ancora prima del primo token, la cache cresce ancora con la conversazione, il batch in cui sei appartiene ancora a qualcun altro e decide ancora la tua latenza — ma da ora in poi lo osservi attraverso uno stream di Server-Sent Events, un finish_reason e un HTTP 429 con un header Retry-After. Le domande cambiano con il punto di osservazione: non come viene calcolato questo gradiente ma perché la mia fattura è triplicata. Cambia anche il linguaggio, e il Capitolo 14 spiega questa regola invece di annunciarla — fin qui il codice conteneva pesi, gradienti, logits e byte del tokenizer; da lì in poi contiene una connessione, un retry, una cancellazione e stato accumulato. I tredici capitoli alle tue spalle non vengono scartati attraversando la soglia. Sono la descrizione di ciò che gira dall’altra parte della porta.
Fonti e metodo
Link alla sezione: Fonti e metodoDue omissioni sono deliberate. FlashAttention (Dao et al., arXiv:2205.14135) non è una attention diversa — calcola la stessa funzione tileando l’operazione così che la matrice degli score non venga mai scritta in memoria, ed è per questo che i 67 MB nella seconda tabella di questo capitolo sono in pratica più piccoli di quanto suggerisca l’aritmetica. E i kernel stessi sono delegati: la lezione 10 del CS336 di Stanford copre i sistemi di inference con una profondità che questo testo non tenta, e il repository llama.cpp e la specifica GGUF sono le fonti primarie per il lato CPU.
Riferimenti
Link alla sezione: Riferimenti-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). L’articolo è in larga parte un argomento di memory bandwidth, e si legge come tale. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Include la ricetta di uptraining che converte un checkpoint multi-head esistente, motivo per cui GQA si è diffusa così in fretta. ↩
-
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. Introduce lo scheduling a livello di iterazione — continuous batching — e il selective batching. ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. L’articolo su cui è costruito vLLM; il §3 è l’analogia con i sistemi operativi per intero. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 è definito nel §3; i sedici valori dei livelli usati nella misura sopra sono quelli derivati da questo articolo. ↩
-
Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). L’analisi delle feature outlier nel §4 è la fonte del fenomeno misurato sopra, incluso il risultato che gli outlier emergono sistematicamente su scala. ↩ ↩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). Il Teorema 1 è la prova che la distribuzione di output è invariata; Chen et al. (arXiv:2302.01318) hanno pubblicato la stessa idea indipendentemente. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). La temperatura e l’argomento della «dark knowledge». ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, nove anni prima, per ensemble invece che per transformers. ↩