Un modello piccolo scrive rapidamente una proposta; uno grande la controlla e ne conserva la parte utilizzabile. È l’intuizione dello speculative decoding. La domanda di questo episodio è precisa: possiamo ridurre il tempo di generazione mantenendo il comportamento probabilistico del modello grande?
La risposta richiede due verifiche diverse: una matematica, sulla distribuzione dei token prodotti, e una sperimentale, sui tempi. Per orientarsi sono utili gli episodi 13 sulla tokenizzazione, 15 sulla KV cache e 09 su latenza e throughput.
In breve
- Il modello draft propone una breve continuazione; il target valuta le posizioni della proposta insieme.
- La verifica conserva un prefisso della proposta. Dopo il primo rifiuto, il resto viene scartato.
- Nel campionamento esatto servono una regola di accettazione e una distribuzione di correzione precise.
- Conservare la distribuzione non implica ottenere la stessa frase con lo stesso seme casuale, né eliminare gli errori del target.
- Il beneficio dipende da accordo fra modelli, costo della proposta, verifica, memoria e carico.
Perché controllare può costare meno che generare
Nella generazione autoregressiva ordinaria il token successivo dipende da quelli già scelti. Per scrivere una continuazione nuova il target procede quindi per passi. Se invece riceve una sequenza candidata già disponibile, un Transformer causale può calcolare le distribuzioni delle sue posizioni con un’elaborazione parallela: ogni posizione continua a vedere soltanto il proprio passato.
Come spiega Hugging Face nell’introduzione all’assisted generation dell’11 maggio 2023, riutilizzare i pesi caricati per più posizioni può sfruttare meglio l’hardware. È interessante soprattutto quando il decode è limitato dai trasferimenti di memoria e rimane capacità di calcolo disponibile. Verificare quattro candidati, però, non ha per definizione il costo di generarne uno: quel rapporto va misurato.
Un giro completo: proposta, verifica, recupero
Consideriamo la versione classica a due modelli, su uno stesso vocabolario di token. Il draft genera k candidati in sequenza. Il target calcola le probabilità condizionate sui prefissi candidati, comprese quelle necessarie per un eventuale token aggiuntivo. I controlli si applicano da sinistra a destra.
Esempio ipotetico: il draft propone A, B, C, A. I primi due token superano il controllo, il terzo viene rifiutato. Si conservano A, B, si estrae una correzione nella terza posizione e si scarta l’ultimo A. Il calcolo relativo a quell’ultimo candidato assumeva un passato che non sarà più quello effettivo. Se tutti i quattro candidati vengono accettati, si può estrarre anche un quinto token dalla distribuzione target già calcolata.
Questo ciclo è descritto nell’algoritmo 1 di Leviathan, Kalman e Matias, ICML 2023. Il lavoro di Chen e colleghi, febbraio 2023, sviluppa indipendentemente uno schema affine di speculative sampling. “Verificare” significa applicare un algoritmo sui token: non chiedere al modello grande un parere sulla qualità della frase.
La regola che impedisce al piccolo di cambiare il grande
Chiamiamo p la distribuzione desiderata del target e q quella da cui il draft ha realmente campionato, a uno stesso prefisso. Nel rejection sampler classico un candidato x viene accettato con probabilità min(1, p(x)/q(x)). Per un token effettivamente proposto, q(x) è maggiore di zero.
Al rifiuto, la correzione viene campionata dalla distribuzione proporzionale a max(p(x) − q(x), 0), normalizzata sull’intero vocabolario. Il significato si vede meglio con un conto: i token sovraproposti vengono ridotti; la correzione reintegra la probabilità mancante. Se p e q coincidono, non ci sono rifiuti e non occorre normalizzare un residuo nullo.
Esempio svolto: tre token, nessuna rete neurale
Costruiamo distribuzioni ipotetiche sui token A, B e C. I numeri non provengono da un LLM. La colonna finale della tabella indica la probabilità congiunta che il draft proponga quel token e il controllo lo accetti.
| Token | Target p | Draft q | P(acc.) | Massa accettata |
|---|---|---|---|---|
| A | 0,50 | 0,70 | 5/7 ≈ 0,7143 | 0,50 |
| B | 0,30 | 0,20 | 1 | 0,20 |
| C | 0,20 | 0,10 | 1 | 0,10 |
La massa accettata totale è 0,80: nel 20% dei casi avviene un rifiuto. Il residuo positivo p − q è (0; 0,10; 0,10). Normalizzandolo otteniamo (0; 0,50; 0,50). La correzione restituisce quindi 0,10 di probabilità a B e 0,10 a C: il risultato finale è esattamente (0,50; 0,30; 0,20).
Questo esempio offre anche un controesempio utile. Se al rifiuto estraessimo semplicemente da p, otterremmo (0,60; 0,26; 0,14): A rimarrebbe troppo frequente. “Il grande corregge” è dunque una descrizione incompleta: come corregge determina il risultato.
Che cosa garantisce davvero l’esattezza
La garanzia degli algoritmi esatti riguarda la distribuzione della sequenza, applicando le regole corrette a ogni prefisso effettivo. Non promette la medesima traiettoria di numeri casuali. Nel greedy decoding si può invece verificare la coincidenza con le scelte massime del target, adottando una gestione coerente dei pareggi.
Temperatura, filtri top-k o top-p e vincoli devono essere riflessi nelle distribuzioni usate dal sampler: p rappresenta la politica target che vogliamo preservare e q la proposta effettiva. Inoltre le probabilità devono riferirsi agli stessi token e prefissi. Tokenizer differenti richiedono algoritmi di raccordo specifici; le formule qui illustrate non autorizzano a confrontare direttamente ID incompatibili.
La documentazione vLLM, consultata il 26 ottobre 2026, distingue esattezza teorica, test dell’algoritmo e stabilità numerica. Precisione floating point e forma dei batch possono introdurre variazioni. Una verifica rilassata non eredita automaticamente la garanzia del rejection sampler standard. E se il target produce informazioni false, conservarne la distribuzione conserva anche quel rischio.
Un budget di tempo: quando il conto torna
Per valutare la convenienza usiamo un budget ipotetico. Generare un token col solo target costa 40 ms. Il draft impiega 3 ms per candidato; verificare un blocco da quattro, includendo le distribuzioni necessarie, costa 50 ms; gestione e sincronizzazione aggiungono 2 ms. Un ciclo costa quindi 4 × 3 + 50 + 2 = 64 ms. Non sono misure hardware.
Supponiamo, soltanto per il conto, che ogni controllo abbia probabilità costante e indipendente di accettazione α. Con quattro proposte, il numero medio di token emessi, inclusa correzione o aggiunta finale, è 1 + α + α² + α³ + α⁴. Questa semplificazione non descrive necessariamente un testo reale: l’accettazione cambia con il prefisso.
| α | Token medi/ciclo | Tempo per token | Rapporto velocità |
|---|---|---|---|
| 0,80 | 3,3616 | 19,04 ms | 2,10× |
| 0,20 | 1,2496 | 51,22 ms | 0,78× |
Lo stesso schema matematicamente corretto può quindi accelerare o rallentare. Aumentare k allunga il lavoro del draft e può rendere più costosa la verifica; i candidati dopo un rifiuto sono lavoro sprecato. Risposte molto brevi, arresti su EOS, prefill e rete possono ridurre ulteriormente il beneficio sul tempo totale.
Una piccola prova verificabile
Il codice seguente usa solo la libreria standard Python. È stato eseguito per questo articolo: verifica esattamente con frazioni razionali la distribuzione del nostro esempio e riproduce i due budget di tempo. Non implementa l’intero decoder e non misura prestazioni di modelli.
from fractions import Fraction as F# Distribuzioni ipotetiche, a prefisso fisso.p = [F(5, 10), F(3, 10), F(2, 10)] # targetq = [F(7, 10), F(2, 10), F(1, 10)] # draftassert sum(p) == sum(q) == 1accepted = [min(a, b) for a, b in zip(p, q)]rejected = 1 - sum(accepted)residual = [ max(a - b, F(0)) for a, b in zip(p, q)]correction = [r / sum(residual) for r in residual]exact = [ a + rejected * r for a, r in zip(accepted, correction)]naive = [ a + rejected * t for a, t in zip(accepted, p)]assert exact == passert naive != pprint('Corretto:', [float(x) for x in exact])print('Ingenuo:', [float(x) for x in naive])# Budget ipotetico, non benchmark di un LLM.k, draft_ms, verify_ms = 4, 3, 50overhead_ms, baseline_ms = 2, 40cycle_ms = k * draft_ms + verify_ms + overhead_msfor alpha in (0.8, 0.2): # Accettazione indipendente e costante, per ipotesi. emitted = sum(alpha ** i for i in range(k + 1)) speedup = emitted * baseline_ms / cycle_ms print(f'alpha={alpha}: token={emitted:.4f}, ' f'ms/token={cycle_ms / emitted:.2f}, ' f'speedup={speedup:.2f}')
Le prime due righe stampano rispettivamente [0.5, 0.3, 0.2] e [0.6, 0.26, 0.14]; le successive restituiscono 19,04 e 51,22 ms per token. Modifica q, mantenendola normalizzata e diversa da p, e controlla che l’asserzione sull’esito esatto continui a passare. La normalizzazione del residuo richiede un ramo dedicato quando p = q.
Dove le cose si complicano
Sul piano operativo servono anche spazio per il draft, i suoi stati e i candidati temporanei. Dopo un rifiuto, la KV cache deve restare coerente con il prefisso confermato: gli stati delle continuazioni scartate non possono diventare passato valido. Il riutilizzo della cache e la gestione dei blocchi appartengono quindi al problema di correttezza oltre che a quello di velocità.
La lunghezza della proposta può essere adattata. Nel benchmark Hugging Face e Intel dell’8 ottobre 2024, la coppia CodeGen-6B-mono/350M-mono, in generazione di codice greedy su RTX 4090, passa da un rapporto 0,89× con la strategia euristica a 1,09× con quella dinamica. Sono risultati degli autori, non della nostra prova: mostrano quanto la politica di proposta possa contare.
Per una valutazione sul proprio servizio confronterei lo stesso target, quantizzazione e politica di campionamento, con e senza speculazione, sugli stessi prompt italiani, di codice e misti. Registrerei token proposti e accettati, tempo del draft e della verifica, latenza p50/p95, tempo al primo token, throughput e memoria di picco. Ripeterei il confronto sotto il carico previsto: una riduzione della latenza individuale non prova un aumento della capacità complessiva.
Una checklist pratica
- Quale algoritmo di verifica è implementato, e quale garanzia dichiara?
- Tokenizer, prefissi, vincoli e distribuzioni sono compatibili?
- Quanto costa davvero proporre e verificare ciascun blocco?
- Il guadagno resta con la concorrenza e le lunghezze di risposta reali?
- Ho distinto tempo risparmiato, memoria aggiuntiva e costo economico?
Il modello piccolo può far risparmiare attese al grande quando le sue proposte valgono più del lavoro necessario per produrle e verificarle. La correttezza del sampler e la convenienza del sistema vanno dimostrate separatamente.
DALLA MIA BIBLIOTECA
- Sebastian Raschka — Sviluppare Large Language Model. Costruire da zero LLM su misura. Per ricostruire tokenizzazione, attenzione e generazione autoregressiva prima di affrontarne le ottimizzazioni; le fonti specifiche sullo speculative decoding seguono qui sotto.
Per approfondire
- Leviathan, Kalman, Matias — Fast Inference from Transformers via Speculative Decoding, ICML 2023: algoritmo 1 e analisi di accettazione e tempi.
- Chen e colleghi — Accelerating Large Language Model Decoding with Speculative Sampling, arXiv v1, 2 febbraio 2023: verifica probabilistica e condizioni hardware.
- Hugging Face — Assisted Generation, 11 maggio 2023; Faster Assisted Generation with Dynamic Speculation, 8 ottobre 2024, con collegamento al benchmark riproducibile degli autori.
- vLLM — Speculative Decoding, documentazione corrente consultata il 26 ottobre 2026: garanzie e limiti di implementazione.
← Torna al percorso Dentro l’IA


Lascia un commento