Speculative Decoding: Draft, EAGLE, MTP
Draft and verify with no quality loss. Vanilla, Medusa, EAGLE-3 and MTP drafts, speedup math, vLLM config, gates.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Comparisons
- Defaults and numbers
- Troubleshooting
- Questions and tips
Draft types side by side#
| Factor | Vanilla draft | Medusa | EAGLE-3 | MTP (DeepSeek-V3) |
|---|---|---|---|---|
| Draft | Separate small LLM | Extra LM heads on verifier | Multi-layer feature fusion + TTT | 1 transformer block + projection |
| Trained when | Reuse an existing model | SFT stage on the verifier | Post-pre-training | During pre-training |
| Training cost | None | ~1B SFT tokens | ~60-200B tokens | About 10% slower pre-training |
α | 0.55-0.70 | 0.65-0.75 | 0.88-0.92 | 0.80+ at depth 1 |
| Speedup | 1.8-2.3× | 2-3× | 3.5-6.5× | 1.8× |
| Second model | Yes | No | Tiny draft head | No, part of the model |
| Existing weights | Yes | Needs fine-tune | Yes | No, re-train |
From Lesson 10.15, Lesson 10.18, Lesson 07.16
The loop in code#
for i, d in enumerate(drafts): u = rng.random() ratio = q_prob / p_prob if p_prob > 0 else float("inf")q verifier, p draft if u < min(1.0, ratio): emitted.append(d) kv.extend(1)accepted: cache grows by one else: correction = sample(residual(q, p), rng)(q - p)+ renormalized emitted.append(correction) kv.truncate_to(prefix_len + len(emitted))KV rollback return emitted, 1one verifier forwardbonus = sample(q, rng)all N accepted: free tokenemitted.append(bonus)From 10.15 code/main.py
Commands#
| Command | What it does |
|---|---|
vllm serve <model> --speculative-config '{"method": "eagle", | EAGLE draft head |
--speculative-config '{"method": "draft_model", | Vanilla draft model |
--speculative-config '{"model": "<draft>", | EAGLE-3 draft |
python main.py --vocab 64 --alpha 0.75 --k 4 --samples 50000 | Rejection rule, alpha sweep, tree mask demo |
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| No speedup after deploy | Spec decode is opt-in | Set speculative_config explicitly |
| Mean ITL drops, P99 ITL rises | Rejected drafts take two passes | Watch P99 ITL, tune before ship |
α below 0.55 on traffic | Draft trained on general data | Train a domain draft or disable |
α collapses | High-temperature creative sampling | Fall back to plain decode |
| No gain at high concurrency | Batching already fills the FLOPs | Measure break-even at that concurrency |
| Cache wrong after a reject | Entries past j-1 stay written | Truncate to the accepted prefix |
| Gain is small on a tiny model | Draft not much cheaper | Skip spec decode under 3B |
One speculative step#
From Lesson 07.16, Lesson 10.15
Serving stack#
From Lesson 19.14
Functions in the lessons#
| Task | Code |
|---|---|
| Residual on rejection | residual(q, |
| One step with KV rollback | spec_step(q, |
| Tokens per verify | expected_tokens_per_verify(alpha, |
| Wall time per token | wall_time_per_token(alpha, |
| Speedup for a config | expected_speedup(p) |
| Tree verify mask | tree_attention_mask(tree) |
| Distribution check | chi_square(observed, |
| Measured acceptance | m.acceptance_rate(k) |
From 07.16 code/main.py, 10.15 code/main.py, 10.18 code/main.py, 10.25 code/main.py, 17.05 code/main.py, 19.14 code/main.py
Speedup math#
- Tokens per verify
- \mathbb{E}[\text{accepted}] = \frac{1 - \alpha^{N+1}}{1 - \alpha}
- Example
α = 0.8, K = 4gives 3.36 tokens per forward.- Wall time per token
(N * c + 1) / E[accepted], withc = cost(draft) / cost(verifier).- Biggest lever
- acceptance rate
αmoves the speedup more than draft lengthN. - Production form
S(alpha, K) = (1 + K*alpha) / (1 + verify_overhead)- Break-even
alpha_breakeven = verify_overhead / K. It rises with concurrency.- MTP loss
- L_{\text{MTP}} = \frac{\lambda}{D} \sum_{k=1}^{D} L_k,
lambda0.3 then 0.1.
Script defaults#
| Setting | Default | Note |
|---|---|---|
--vocab | 32 | Vocab size, at least 2 |
--alpha | 0.75 | Target acceptance rate in (0, 1] |
--k | 4 | Draft length, at least 1 |
--samples | 20000 | Sample count |
--seed | 0 | Random seed |
n_tokens | 1000 | simulate_tail tokens per run |
Use each draft when#
- Use a vanilla draft for a fast prototype with no training.
- Use Medusa heads when you can fine-tune the verifier.
- Use EAGLE-2 or EAGLE-3 in production for maximum speed.
- Use lookahead when you want no draft, no training and no extra params. Expect 1.3 to 1.6×.
- Use MTP when you control pre-training and hidden size is at least 4096.
- Use spec decode for chat latency, code and JSON output, and long generations.
- Skip it for outputs under 50 tokens and batch-1 offline jobs.
Abbreviations#
- α
- acceptance rate. Per-token chance that the verifier accepts a draft.
- K or N
- draft length. Typical 4-8 tokens per target forward.
- EAGLE
- tiny draft conditioned on the verifier's hidden states.
- TTT
- training-time test. The draft sees its own outputs during training.
- MTP
- multi-token prediction. A module predicts a token
kpositions ahead. - P99 ITL
- the tail metric to watch after you enable spec decode. Not P50.
- HPA
- autoscales the server on queue wait, not CPU.
Questions#
- Does spec decode change output quality?
- No. The output matches the verifier distribution exactly.
- Why does decode leave room for this?
- Decode is memory-bound. Compute sits idle.
- How do I read alpha in vLLM V1?
- Divide
spec_decode_metrics.accepted_tokens_per_requestby draft length. - What did EAGLE-3 change?
- It drops the feature loss and trains with training-time test.
- Why is MTP a good draft?
- Sequential modules keep the causal chain, like a decoder.