ml-models / Inference and deployment / Speculative decoding advanced

Speculative decoding

A large model decoding one token at a time leaves almost all of its arithmetic idle, because each step is bound by reading the weights. Speculative decoding lets a cheap drafter guess several tokens, then checks all of them in one pass of the large model with an accept-or-resample rule that keeps the output distribution exactly the large model's. This page proves that rule, counts the tokens a round yields, finds the draft length that maximizes speedup, and shows the attention mask and cache bookkeeping that make verification work.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. One round
  5. Why the output is exactly the target's
  6. The greedy case
  7. Tokens per round and speedup
  8. Verification attention and the cache
  9. Where drafts come from
  10. Worked example
  11. A round with a rejection
  12. The output distribution is exactly the target's
  13. Tokens per round: exact against the formula
  14. Speedup against draft length
  15. Implementation
  16. Tradeoffs
  17. Connections
  18. Read first
  19. Read next
  20. References

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.

One speculative round: draft 3 tokens, verify them in one target pass, accept, reject and resample Top row: the drafter runs 3 cheap steps from context "a", proposing c, c, a. Middle: one target pass reads the last emitted token and the 3 drafts together and returns the target's distribution at every position. Bottom: the first draft "c" is accepted with probability 1; the second, "c", is accepted only with probability 5/7 and is rejected, so a replacement "a" is drawn from the residual. The third draft is discarded unchecked. The round emits 2 tokens for one target pass. drafter (cost c each) draft 1: “c” draft 2: “c” draft 3: “a” target (1 pass) one forward pass over 4 positions inputs: last token “a”, c, c, a p at pos 1 p at pos 2 p at pos 3 p at pos 4 verify min(1, p/q) = 1 u = 0.085 → keep “c” min(1, p/q) = 5/7 u = 0.835 → reject resample “a” discarded bonus only if all accepted emitted this round: c a (2 tokens, 1 target pass)

Mechanics

One round

Let the target model give p(· | prefix) and the drafter q(· | prefix). With draft length γ:

  1. Draft. Sample x₁ ~ q(· | prefix), x₂ ~ q(· | prefix, x₁), …, up to xγ. This is γ sequential steps of the cheap model.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Verification mask: 4 new queries against 5 cached keys plus 4 new keys Two grids of 4 rows (new queries) by 9 columns (keys). In the left grid, lower-right aligned, the first query sees the 5 cached keys plus itself, and each later query sees one more key, so the rows have 6, 7, 8, 9 visible keys. The right grid, upper-left aligned, gives the rows 1, 2, 3, 4 visible keys, hiding the cached prefix from the drafts. The left is correct. lower-right (correct) q0 q1 q2 q3 0 1 2 3 4 5 6 7 8 cached prefix new: last + drafts upper-left (wrong here) q0 q1 q2 q3 0 1 2 3 4 5 6 7 8 cached prefix new: last + drafts filled = query may attend to that key

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 tokenTarget 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.

PositionAfterDraftpqmin(1, p/q)uResult
1ac1/101/1010.085accept
2cc1/27/105/70.835reject

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 tokensSpeculativeTarget
a a9/259/25
a b9/509/50
a c3/503/50
b a3/503/50
b b3/203/20
b c9/1009/100
c a3/1003/100
c b1/501/50
c c1/201/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γ = 12Best γ
α = 0.61.52×1.78×1.92×1.87×1.77×1.56×4 (1.92×)
α = 0.81.71×2.22×2.80×3.04×3.09×2.95×8 (3.09×)
α = 0.91.81×2.46×3.41×4.01×4.38×4.66×13 (4.67×)
c = 1/5γ = 1γ = 2γ = 4γ = 6γ = 8γ = 12Best γ
α = 0.61.33×1.40×1.28×1.10×0.95×0.73×2 (1.40×)
α = 0.81.50×1.74×1.87×1.80×1.66×1.39×4 (1.87×)
α = 0.91.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.

ComponentShape/ParamsMeaningSource
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
CausalVariantUPPER_LEFT, LOWER_RIGHTThe two alignments, with 3 × 4 examples of each in the docstring.bias.py 33–83
CausalBiasvariant, seq_len_q, seq_len_kvA 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_causalFused attention; its is_causal uses the upper-left alignment when L ≠ S.functional.py 6414–6587
softmaxdimTurns 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_samplesDraws drafts, residual replacements and the bonus token; accepts unnormalized weights._torch_docs.py 8089–8142
Categorical.samplesample_shapeThe 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

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.