Speculative decoding
Draft k tokens with a small model, verify them in a single parallel forward pass of the target model, and prove the acceptance-rejection scheme preserves the target distribution.
Autoregressive decoding is inherently sequential: each token depends on all previous ones, so the decode steps cannot be parallelized. The target model sits idle for most of the decode phase -- it is memory-bandwidth bound, not compute bound.
Speculative decoding (Leviathan et al., 2023; Chen et al., 2023) exploits this idle compute: a small, fast draft model generates tokens in parallel (or near-parallel), then the large target model verifies all in a single forward pass[1, 2].
The algorithm
-
Draft: the draft model generates candidate tokens autoregressively. The draft model is much smaller ( draft for a target; the target model itself can be used with early exit as the draft).
-
Verify: run the target model on the tokens (the last accepted token + all drafts) in a single parallel forward pass, producing distributions .
-
Accept or reject: for each draft token , accept or reject using the ratio :
- Sample .
- If : accept and move to token .
- Otherwise: reject, sample a corrected token from the adjusted distribution, and stop (tokens are discarded).
-
Correction: if token is rejected, sample from:
This ensures the corrected sample is drawn from , not .
The acceptance-rejection scheme preserves the target distribution
Claim: the accepted tokens are sampled exactly from (the target distribution), regardless of the draft distribution .
Proof sketch for one token: Let be the target distribution and the draft.
The probability of accepting draft token :
The probability of a token being the output (either accepted or sampled from adjusted):
With and :
The scheme is lossless: the output distribution is exactly the target, not an approximation.
Expected speedup
Each verification call takes one target-model forward pass (same cost as a single decode step, but processes positions in parallel). Expected accepted tokens per verification:
where is the expected overlap between the draft and target distributions.
Speedup: .
If the draft model is faster and , :
- Tokens per time unit without speculative decoding: 1.
- Tokens per time unit with speculative decoding: .
Real-world speedups: 2-4x for code generation (drafts predict common patterns well); 1.5-2x for diverse generation (lower acceptance rate).
Implementation (conceptual)
import numpy as np
def speculative_decode_step(target, draft, context, k=4):
"""
target, draft: callable context -> (vocab_size,) probabilities
context: list of token ids so far
Returns: newly accepted token ids
"""
rng = np.random.default_rng()
# 1. Draft k tokens
draft_tokens = []
draft_probs = []
ctx = list(context)
for _ in range(k):
p = draft(ctx)
t = rng.choice(len(p), p=p)
draft_tokens.append(t)
draft_probs.append(p[t])
ctx.append(t)
# 2. Verify all k drafts in parallel
# Target processes [context + draft_tokens] in one pass, returns q at each draft position
all_ctx = list(context) + draft_tokens
q_list = target.parallel_forward(all_ctx, n_positions=k) # list of k distributions
# 3. Accept-reject
accepted = []
for i, (x, p_x, q) in enumerate(zip(draft_tokens, draft_probs, q_list)):
q_x = q[x]
r = rng.random()
if r <= q_x / (p_x + 1e-9):
accepted.append(x)
else:
# Sample from adjusted distribution
adj = np.maximum(0, q - draft(list(context) + accepted))
adj /= adj.sum() + 1e-9
accepted.append(rng.choice(len(adj), p=adj))
break # stop at first rejection
# 4. One extra token from target if all accepted
if len(accepted) == k:
q_last = target(list(context) + accepted)
accepted.append(rng.choice(len(q_last), p=q_last))
return acceptedVariants
Self-speculative decoding: use the target model with early exit as the draft -- faster early layers generate drafts, full model verifies. No separate model needed.
Medusa: add parallel prediction heads to the target model that predict tokens ahead simultaneously. Lower hardware cost than a separate draft model.
EAGLE: predicts draft tokens by conditioning on the target model's hidden states at the last accepted position, achieving higher acceptance rates than independent draft models.
Exercises
Exercise 1. For a single token the draft and target distributions over three outcomes are and . Compute the per-token acceptance probability , and confirm the adjusted distribution is a valid distribution.
Solution
Step 1. Take the elementwise minimum of and : , , . Their sum is
Step 2. The unnormalized residual is , summing to . Note , as it must, since the accepted mass and the residual mass partition the target. Normalizing gives , which is nonnegative and sums to , a valid distribution.
Exercise 2. A draft model proposes tokens, the draft is faster than the target, and the per-token acceptance rate is . Using the speedup expression with the draft speed ratio, compute the speedup, then explain why pushing to at the same does not multiply the gain by five.
Solution
Step 1. Substitute , , :
Step 2. At the same formula gives , not . The numerator grows linearly in , but so does the denominator (each drafted token still costs draft time), and the expected number of accepted tokens is bounded by the geometric truncation at the first rejection: once a draft is rejected the remaining drafts are discarded, so the realized acceptance run averages , which saturates near tokens regardless of how large gets. Beyond that, longer drafts mostly add wasted draft work.
Exercise 3 (code). Verify the losslessness claim empirically. With and from Exercise 1, run the single-token accept-reject scheme many times in numpy and check the empirical output distribution matches .
Solution
import numpy as np
rng = np.random.default_rng(0)
p = np.array([0.5, 0.3, 0.2])
q = np.array([0.6, 0.1, 0.3])
Z = np.maximum(0, q - p).sum()
p_adj = np.maximum(0, q - p) / Z
N = 400_000
counts = np.zeros(3)
for _ in range(N):
x = rng.choice(3, p=p) # draft a token
if rng.random() <= q[x] / p[x]: # accept with prob min(1, q/p)
counts[x] += 1
else:
counts[rng.choice(3, p=p_adj)] += 1 # else sample the residual
emp = counts / N
assert np.allclose(emp, q, atol=0.01) # output distribution equals the target
print("empirical:", emp.round(3), " target:", q)The empirical frequencies land on within sampling noise: the accept-reject correction reproduces the target distribution exactly, independent of the draft .
The next lesson cuts the other half of the inference bill, the weight memory itself, by quantizing the model below 16-bit precision.
References
- [1]Leviathan, Y., Kalman, M., & Matias, Y. (2023). "Fast inference from transformers via speculative decoding". ICML. arXiv:2211.17192. Introduces the draft-verify scheme and proves it preserves the target distribution.↩
- [2]Chen, C., et al. (2023). "Accelerating large language model decoding with speculative sampling". arXiv:2302.01318. Concurrent formulation of speculative sampling for large autoregressive models.↩