Problem
On the latency and throughput page, one decode step of an 8B model at small batch reads every weight to produce one token per sequence, and the GPU's arithmetic units sit almost entirely idle. Running the same weights over several positions at once costs nearly the same time, because the bytes read do not change, only the FLOPs. The trouble is that autoregressive decoding needs token t before it can compute token t + 1, so there is nothing to put in those extra positions.
Speculative decoding fills them with guesses. The question is how to use guesses without changing what the model says. A sampled chatbot must still sample from the large model's distribution, not from a blend of it with a weaker model's, or the quality you evaluated is no longer the quality you serve. The decoding page showed one step of the rule; this page covers the whole algorithm.
Intuition
Think of a junior writer and an editor. The junior drafts the next few words quickly. The editor reads the whole draft at once and keeps the longest prefix they would plausibly have written themselves, crosses out the first word they disagree with, and writes their own word in its place. If the editor agrees with every drafted word, they also add the next word, since they have already read the whole draft. Each round therefore costs one read by the editor and produces at least one word, often several.
The probabilistic part is how the editor "disagrees". A drafted token that the target likes at least as much as the drafter did (p ≥ q) is always kept. A token the drafter liked more than the target is kept only with probability p/q, and on rejection the replacement is drawn from the tokens the drafter under-proposed. Those two pieces add up to exactly p, so nothing about the output changes except how many target passes it took.
Mechanics
One round
Let the target model give p(· | prefix) and the drafter q(· | prefix). With draft length γ:
- Draft. Sample x₁ ~ q(· | prefix), x₂ ~ q(· | prefix, x₁), …, up to xγ. This is γ sequential steps of the cheap model.
- Verify. Run the target once over the γ + 1 new positions (the last emitted token and the γ drafts) to get p at every position, p(· | prefix, x₁, …, xi−1) for i = 1 … γ + 1.
- Accept left to right. For each i, draw u ~ Uniform(0, 1) and keep xi if u < min(1, p(xi)/q(xi)). Stop at the first rejection.
- Correct. On a rejection at position i, sample a replacement from the residual r = max(0, p − q) / Σ max(0, p − q), using that position's p and q, and end the round.
- Bonus. If all γ drafts are accepted, sample one more token from p at position γ + 1, which the verification pass already computed.
Every round emits between 1 and γ + 1 tokens, so it is never slower in tokens per target pass than plain decoding; it only spends the drafter's time.
Why the output is exactly the target's
Fix one position with target p and draft q. The token emitted there is x if the drafter proposed x and it survived, or if any proposal was rejected and the residual drew x:
P(emit x) = q(x) · min(1, p(x)/q(x)) + (1 − α) · r(x)
The first term is min(p(x), q(x)). The probability of a rejection is 1 − α, where
α = Σₓ min(p(x), q(x)) = 1 − TV(p, q)
is the acceptance rate (TV is total variation distance). Since min(p, q) + max(0, p − q) = p, summing over x gives Σ max(0, p − q) = 1 − α, so (1 − α) · r(x) = max(0, p(x) − q(x)). The two terms add to min(p, q) + max(0, p − q) = p(x). The emitted token is distributed exactly as p. The next position is then conditioned on a prefix whose distribution is already the target's, so the argument repeats position by position. Rejected and discarded drafts never reach the output, which is why the bonus token, drawn from the target's own p, is safe too.
The rule needs q(x) > 0 whenever the drafter proposes x, which holds by construction. Temperature, top-p and any penalties must be applied to p and q before the test, because the rule preserves exactly the p it is given, no more.
The greedy case
At temperature 0, p puts all its mass on argmax p. Then min(1, p/q) is 1 when the draft equals the target's argmax and 0 otherwise, and the residual is the target's argmax. So greedy verification is: keep drafts while they match the target's argmax at each position, replace the first mismatch with the target's argmax, and add the bonus argmax if all match. The output equals plain greedy decoding token for token, up to floating point: the target evaluates γ + 1 positions in one batched call, which can round differently from one position at a time and flip a near-tie.
Tokens per round and speedup
If every position had the same acceptance rate α, the number of accepted drafts K would satisfy P(K ≥ k) = αk for k ≤ γ, and a round emits K + 1 tokens, so
E[tokens per round] = Σk=0..γ αk = (1 − αγ+1) / (1 − α)
Let c be the cost of one drafter step relative to one target step. A round costs γc + 1 target-step units, and plain decoding emits one token per unit, so the speedup in wall-clock time is
speedup(α, γ, c) = (1 − αγ+1) / ((1 − α)(γc + 1))
The numerator saturates at 1/(1 − α) while the denominator keeps growing with γ, so there is a best draft length: longer for a better-aligned drafter (higher α) and for a cheaper one (lower c). Two assumptions hide in this formula. It assumes verifying γ + 1 positions costs the same as one decode step, which holds only while decoding is bound by memory bandwidth. And real acceptance varies with context, so the i.i.d. formula is an approximation; the worked example measures the gap.
Verification attention and the cache
The verification pass feeds γ + 1 query positions into a model whose KV cache
already holds n keys for the prefix. After appending the new keys there are n + γ + 1 keys, and query
i (0-based among the new ones) may attend to keys 0 … n + i. That is a causal mask aligned to the
lower-right corner of the score matrix. The upper-left alignment, which is_causal=True uses in
scaled_dot_product_attention when the matrix is not square, would let the first new query see only key 0,
which is wrong. PyTorch's causal_lower_right bias encodes the correct variant.
After verification the cache contains entries for all γ + 1 new positions, including drafts that were rejected or discarded. If k drafts were accepted, keep the first n + k + 1 entries (the old prefix, the last token, the accepted drafts) and truncate the rest. The newly sampled replacement or bonus token has no cache entry yet; it is the "last token" of the next round. The drafter's own cache needs the same rollback.
Where drafts come from
- A small model of the same family (the original papers): shares the tokenizer, cheap per step, but must be trained and served alongside and run sequentially for γ steps.
- Extra heads on the target. Medusa adds several decoding heads that each predict a token further ahead from the target's last hidden state. Multi-token-prediction (MTP) modules trained with the model, as in DeepSeek-V3 and MiMo-V2-Flash, can be reused as drafters at inference.
- Feature-level drafters. EAGLE trains a light autoregressive head on the target's hidden features instead of on tokens, which raises α; EAGLE-3 fuses features from several target layers.
- No model at all. Prompt lookup or n-gram drafting copies a continuation from earlier in the context when the last few tokens match. It is nearly free (c ≈ 0) and works well for editing, retrieval and code, where outputs repeat inputs.
Two extensions change the verification. Tree verification (SpecInfer) drafts several candidate branches and verifies the whole tree in one pass with a tree-shaped attention mask, raising expected accepted length at the cost of more positions. Block verification (Sun et al., ICLR 2025) decides on the drafted block jointly instead of token by token, which provably accepts at least as many tokens on average while preserving the target distribution.
Worked example
A three-token vocabulary {a, b, c} where each model's next-token distribution depends only on the previous token.
These are exact fractions, computed by tested helpers in ml_models_docs.py:
| Previous token | Target p (a, b, c) | Draft q (a, b, c) | α = Σ min(p, q) | Residual r |
|---|---|---|---|---|
| a | (3/5, 3/10, 1/10) | (1/2, 2/5, 1/10) | 9/10 | (1, 0, 0) |
| b | (1/5, 1/2, 3/10) | (2/5, 3/10, 3/10) | 4/5 | (0, 1, 0) |
| c | (3/10, 1/5, 1/2) | (1/10, 1/5, 7/10) | 4/5 | (1, 0, 0) |
A round with a rejection
Starting after "a" with γ = 3 and seed 2, the drafter proposes c, c, a.
| Position | After | Draft | p | q | min(1, p/q) | u | Result |
|---|---|---|---|---|---|---|---|
| 1 | a | c | 1/10 | 1/10 | 1 | 0.085 | accept |
| 2 | c | c | 1/2 | 7/10 | 5/7 | 0.835 | reject |
The second draft "c" was over-proposed (q = 7/10 > p = 1/2), and u fell above 5/7. The residual after "c" is (1, 0, 0): of the three tokens, only "a" has p > q there, so the replacement must be "a". The third draft is never checked. The round emits c a, 2 tokens for one target pass. With seed 0 instead, all three drafts (b, c, c) pass and the bonus token makes it 4: b c c c.
The cache held 5 entries before verification and 9 after it. With 1 draft accepted it keeps 7 (the prefix, the last token and the accepted draft) and drops 2.
The output distribution is exactly the target's
Enumerating every draft and every accept or reject outcome of a round from "a" gives 94 distinct emitted sequences. Chaining rounds until two tokens are out, the joint probability of each two-token continuation matches the target's own exactly:
| Next two tokens | Speculative | Target |
|---|---|---|
| a a | 9/25 | 9/25 |
| a b | 9/50 | 9/50 |
| a c | 3/50 | 3/50 |
| b a | 3/50 | 3/50 |
| b b | 3/20 | 3/20 |
| b c | 9/100 | 9/100 |
| c a | 3/100 | 3/100 |
| c b | 1/50 | 1/50 |
| c c | 1/20 | 1/20 |
The tests check the same equality for three tokens from every starting context.
Tokens per round: exact against the formula
From "a", the probability of accepting 0, 1, 2 or 3 drafts is 1/10, 13/100, 61/500, 81/125. So a round emits 1659/500 = 3.32 tokens on average. The i.i.d. formula with α = 9/10 predicts 3.439. It overestimates because only the first position is drafted after "a", where α is 9/10; later positions follow "b" or "c", where it is 4/5. Plug a measured average acceptance length into capacity plans, not a single-position α.
Speedup against draft length
The formula with a drafter costing c = 1/20 of a target step, and c = 1/5. The best γ is searched over 1 to 32:
| c = 1/20 | γ = 1 | γ = 2 | γ = 4 | γ = 6 | γ = 8 | γ = 12 | Best γ |
|---|---|---|---|---|---|---|---|
| α = 0.6 | 1.52× | 1.78× | 1.92× | 1.87× | 1.77× | 1.56× | 4 (1.92×) |
| α = 0.8 | 1.71× | 2.22× | 2.80× | 3.04× | 3.09× | 2.95× | 8 (3.09×) |
| α = 0.9 | 1.81× | 2.46× | 3.41× | 4.01× | 4.38× | 4.66× | 13 (4.67×) |
| c = 1/5 | γ = 1 | γ = 2 | γ = 4 | γ = 6 | γ = 8 | γ = 12 | Best γ |
|---|---|---|---|---|---|---|---|
| α = 0.6 | 1.33× | 1.40× | 1.28× | 1.10× | 0.95× | 0.73× | 2 (1.40×) |
| α = 0.8 | 1.50× | 1.74× | 1.87× | 1.80× | 1.66× | 1.39× | 4 (1.87×) |
| α = 0.9 | 1.58× | 1.94× | 2.28× | 2.37× | 2.36× | 2.19× | 7 (2.37×) |
A cheap, well-aligned drafter wants long drafts: at α = 0.9 and c = 1/20 the best is γ = 13, for 4.67×. A drafter costing a fifth of the target caps the gain near 2.37×, and at α = 0.6 with γ = 8 it is slower than plain decoding (0.95×).
Implementation
One sampled round in PyTorch for a batch of one, with models that take a cache and return logits for every new position.
The cache objects are assumed to support crop(length):
import torch
from torch.nn.attention.bias import causal_lower_right
@torch.inference_mode()
def speculative_round(target, draft, last_tok, t_cache, d_cache, gamma, temp=1.0):
n = t_cache.length # cached prefix length
drafts, q_rows, tok = [], [], last_tok
for _ in range(gamma): # gamma cheap sequential steps
logits, d_cache = draft(tok, cache=d_cache)
q = torch.softmax(logits[:, -1] / temp, dim=-1)
tok = torch.multinomial(q, 1)
drafts.append(tok); q_rows.append(q)
new = torch.cat([last_tok] + drafts, dim=1) # [1, gamma + 1]
mask = causal_lower_right(gamma + 1, n + gamma + 1) # queries see the whole prefix
logits, t_cache = target(new, cache=t_cache, attn_mask=mask)
p_rows = torch.softmax(logits[0] / temp, dim=-1) # [gamma + 1, V]
out = []
for i, tok in enumerate(drafts):
x = tok.item()
p, q = p_rows[i], q_rows[i][0]
if torch.rand(()) < torch.clamp(p[x] / q[x], max=1.0):
out.append(x); continue
residual = torch.clamp(p - q, min=0)
out.append(torch.multinomial(residual / residual.sum(), 1).item())
break
else:
out.append(torch.multinomial(p_rows[gamma], 1).item()) # bonus token
accepted = len(out) - 1
t_cache.crop(n + accepted + 1) # drop rejected drafts
d_cache.crop(n + accepted + 1)
return out, t_cache, d_cache
The drafter's cache in this sketch is one position behind on a full accept, because the last draft was never fed to it; real implementations run one more drafter step or feed it next round. Production servers do this for many sequences at once, where each sequence accepts a different number of tokens, which is what makes batched speculative decoding fiddly: caches of ragged lengths, and padding or paged blocks that can be truncated per sequence.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
causal_lower_right | (seq_len_q, seq_len_kv) | Causal bias aligned so the last query sees every key; what verification over a cached prefix needs. | bias.py 342–377 |
causal_upper_left | (seq_len_q, seq_len_kv) | The standard causal mask anchored at the top-left; wrong for γ + 1 queries after a cache. | bias.py 308–339 |
CausalVariant | UPPER_LEFT, LOWER_RIGHT | The two alignments, with 3 × 4 examples of each in the docstring. | bias.py 33–83 |
CausalBias | variant, seq_len_q, seq_len_kv | A lazy mask object that scaled_dot_product_attention accepts as attn_mask without materializing it. | bias.py 86–306 |
scaled_dot_product_attention | [B, H, L, E], [B, H, S, E], is_causal | Fused attention; its is_causal uses the upper-left alignment when L ≠ S. | functional.py 6414–6587 |
softmax | dim | Turns each position's logits into p or q; apply the same temperature to both. | functional.py 2176–2216 |
torch.multinomial | [V] or [B, V], num_samples | Draws drafts, residual replacements and the bonus token; accepts unnormalized weights. | _torch_docs.py 8089–8142 |
Categorical.sample | sample_shape | The distributions-API route to the same draw, implemented with multinomial. | categorical.py 144–149 |
Tradeoffs
- Gains shrink as the batch grows. The speedup formula assumes verifying γ + 1 positions is as cheap as one step. At large batch, decoding approaches the compute ridge on the roofline, so each extra position costs real FLOPs and rejected drafts become wasted work. Speculation buys per-user latency (interactivity) most at low concurrency, and servers often shorten γ or switch it off as load rises.
- The drafter costs memory and engineering. A separate model needs its own weights, cache and serving path. Heads on the target (MTP, Medusa, EAGLE) share the target's features and avoid a second tokenizer, but must be trained per target model and retrained when it changes.
- Acceptance depends on the workload. Code, extraction and templated text accept long drafts; open-ended creative sampling at high temperature accepts few. Measure acceptance length on your own traffic, not on a benchmark average.
- Exactness is a property of the rule, not the system. Mismatched temperature or top-p between p and q, a drafter applying a penalty the target does not, or nondeterministic kernels each break "identical distribution" quietly.
- It is a latency tool before a cost tool. It spends extra FLOPs to cut wall-clock time per token. Mistral Large 4's report gives the scale a production stack sees: with its DSpark speculator and block verification, decode interactivity rose 90% over no speculation and 15% over an EAGLE-3 drafter, both at eight concurrent requests.
- Distillation helps the drafter. Training the drafter on the target's outputs, as on the distillation page, raises α directly, since α is one minus the total variation distance between the two.
The drafter proposes a token with q = 0.4 where the target has p = 0.1. How often is it kept?
With probability min(1, 0.1/0.4) = 0.25. A token with p ≥ q is always kept.
With α = 0.8 and γ = 4, how many tokens does a round emit on average under the i.i.d. model?
(1 − 0.8⁵)/(1 − 0.8) = 3.36: up to four drafts plus the correction or bonus token.
Why does passing is_causal=True for the verification pass give wrong results?
With γ + 1 queries and n + γ + 1 keys, is_causal aligns the mask to the upper-left, so the first new query sees only the first key and the drafts lose the prefix. Verification needs the lower-right alignment.
Connections
Read first
- Decoding and sampling core
- The KV cache advanced
- Latency and throughput core
Read next
No page builds on this one yet.
References
- Leviathan, Kalman and Matias. Fast Inference from Transformers via Speculative Decoding. ICML, 2023.
- Chen et al. Accelerating Large Language Model Decoding with Speculative Sampling. 2023.
- Stern, Shazeer and Uszkoreit. Blockwise Parallel Decoding for Deep Autoregressive Models. NeurIPS, 2018.
- Miao et al. SpecInfer: Accelerating Large Language Model Serving with Tree-based Speculative Inference and Verification. ASPLOS, 2024.
- Cai et al. Medusa: Simple LLM Inference Acceleration Framework with Multiple Decoding Heads. ICML, 2024.
- Li, Wei, Zhang and Zhang. EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty. ICML, 2024; EAGLE-3: Scaling up Inference Acceleration of Large Language Models via Training-Time Test. NeurIPS, 2025.
- Sun, Mendlovic, Leviathan, Aharoni, Ro, Beirami and Suresh. Block Verification Accelerates Speculative Decoding. ICLR, 2025.
- DeepSeek-AI. DeepSeek-V3 Technical Report (multi-token prediction). 2024.
- Mistral AI. Mistral Large 4. 2026 (technical report), section on inference optimizations and Figure 16.
- PyTorch v2.15.0:
causal_lower_right,scaled_dot_product_attention,torch.multinomial.