Speculative decoding
Draft tokens with a small model, verify with the large one — speedups when acceptance rates stay high; when to enable per route.
Idea
Autoregressive decode is serial on the big model: one forward pass → one (or few) tokens. Speculative decoding lets a cheap draft model propose several tokens; the target model verifies them in parallel (one forward over the draft span). Accepted tokens are “free” speed; rejected tokens fall back and you continue.
sequenceDiagram
participant Draft
participant Target
Draft->>Draft: propose k tokens
Draft->>Target: verify block
Target-->>Draft: accept prefix / reject
Mathematically, verification is designed so the output distribution matches the target (lossless when implemented correctly) — you are not “approximating” the big model’s distribution if acceptance sampling is done right. You are buying wall-clock speed when the draft is often right.
Interview cue: Speculative decoding speeds decode when draft acceptance is high. It is not a substitute for continuous batching or better routing.
Why decode is the pain point
| Phase | Speculative impact |
|---|---|
| Prefill / TTFT | Usually little help (wrong bottleneck) |
| Decode / TPOT | Primary win condition |
| Queueing | Orthogonal — still need capacity |
If your traffic is RAG-heavy with huge prompts and short answers, fix KV / prefill and routing first.
Algorithm sketch
- Draft model generates (k) candidate tokens given the current prefix.
- Target model runs one forward pass that scores those positions (plus maybe a bonus token).
- Accept the longest prefix consistent with target probabilities (scheme-dependent).
- On rejection, keep the corrected token from the target and repeat.
flowchart TD
Prefix[Current prefix] --> Draft[Draft proposes t1..tk]
Draft --> Verify[Target verifies in parallel]
Verify --> Acc{Accept length?}
Acc -->|m of k| Emit[Emit m tokens]
Acc -->|0| Fix[Emit target correction]
Emit --> Prefix
Fix --> Prefix
Variants you will hear about
| Variant | Draft source | Notes |
|---|---|---|
| Independent draft model | Smaller sibling / distilled | Common in engines |
| Medusa / draft heads | Extra heads on target | No second weight set — different ops trade |
| N-gram / lookup draft | Prompt or code table | Strong on boilerplate / code |
| Tree / multi-candidate | Several draft branches | Higher complexity |
Engines (vLLM, TensorRT-LLM, etc.) expose different knobs — read their docs for the exact acceptance rule and whether lossless mode is on.
Acceptance rate is the gate
Measure acceptance rate = tokens accepted / tokens drafted (plus mean accepted stretch).
| Acceptance (rough) | Likely outcome |
|---|---|
| ≥ 0.7 | Often worth enabling on that route |
| ~0.4–0.6 | Measure net tok/s carefully |
| ≤ 0.3 | Overhead usually loses |
# Shape — log per request
span = {
"draft_tokens": k_total,
"accepted_tokens": m_total,
"acceptance_rate": m_total / max(k_total, 1),
"mean_accept_stretch": mean_stretch,
"tok_s_vs_baseline": ratio,
}Ship rule: gate enablement on acceptance + net tok/s, not on a blog’s “2× faster” claim.
When it wins
| Condition | Why |
|---|---|
| Draft well-aligned with target | Same family / distilled → high acceptance |
| Predictable outputs | Code, templated answers, boilerplate HTML |
| Decode-bound workloads | Extra draft cost amortized |
| Stable temperature / decoding | Wild sampling lowers acceptance |
| Expensive target | Absolute wall-clock savings matter |
When it loses
| Condition | Why |
|---|---|
| High-entropy creative tasks | Many rejects |
| Draft too weak / wrong family | Overhead dominates |
| Prefill-dominated turns | You optimized the wrong phase |
| Tiny batches already memory-starved | Extra draft weights hurt |
| Ops complexity not worth it | Batching + quant + cache already enough |
| Tokenizer / template mismatch | Garbage acceptance, “speedups” with wrong tokens |
flowchart LR
Traffic[Traffic mix] --> Pred{Predictable?}
Pred -->|yes| Try[Try speculative]
Pred -->|no| Skip[Prefer batching + quant + route]
Try --> Acc{Acceptance high?}
Acc -->|yes| Keep[Keep + monitor]
Acc -->|no| Disable[Disable for that route]
Cost model (intuition)
Per “successful” span of (m) accepted tokens you paid roughly:
- Draft compute for (k) tokens
- One target forward over the span (not (m) serial target steps)
If (m) is usually close to (k), wall-clock decode drops. If (m\approx 0), you paid draft + target for almost nothing.
Choosing (k)
| (k) | Trade |
|---|---|
| Too small | Little parallelism benefit |
| Too large | More rejects → wasted draft + verify |
| Tuned | Maximize net tok/s on your mix |
Sweep (k) offline; keep per-route configs.
Ship rule: enable speculative decoding per route / model pair, not globally. Creative chat and JSON tool calls may disagree.
Pairing with the rest of the stack
| Lever | Relationship |
|---|---|
| Continuous batching | Orthogonal; still needed under concurrency |
| Quantization | Draft and target quants both affect acceptance |
| Prefix cache | Helps TTFT; speculative helps decode |
| Cost routing | Maybe only enable on the expensive target model |
| Tensor parallel | Draft placement / VRAM must be planned |
See quantization for inference and cost/latency routing.
flowchart TB
GW[Gateway route] --> Spec{Spec enabled for route?}
Spec -->|yes| Pair[Draft + target on engine]
Spec -->|no| TargetOnly[Target only]
Pair --> Metrics[Acceptance + tok/s]
TargetOnly --> Metrics
VRAM reality check
Loading two models (draft + target) costs memory you could have spent on concurrency. Sometimes a smaller draft on CPU/other device is offered — measure end-to-end, including PCIe. Sometimes Medusa-style heads avoid a second model — different failure modes.
Observability
Log:
- Acceptance rate (overall + per route)
- Mean accepted stretch length
- Tokens/sec vs baseline canary
- TTFT (should be ~unchanged; if not, look at scheduler interactions)
- Quality canary — confirm lossless config is actually enabled
- VRAM / max_num_seqs before vs after
Canary plan
- Shadow: run speculative on a copy of traffic offline; compare outputs if claiming lossless.
- Online: 5–10% of a predictable route; watch acceptance + user thumbs / task success.
- Kill switch: feature flag per route.
Failure modes
- Draft/target tokenizer or chat-template mismatch → garbage acceptance
- Claiming speedup without holding quality/distribution constant
- Enabling on RAG-heavy prefills and declaring “speculative doesn’t work”
- Oversized (k) with low acceptance → pure overhead
- Ignoring VRAM of loading two models on the same GPU
- Global enable across creative + tool routes
- No acceptance metric → flying blind
Micro-project
Draw a diagram for your traffic mix (e.g. 50% FAQ, 30% RAG, 20% creative). Mark where speculative decoding helps vs hurts; list the metric you would gate enablement on (acceptance rate threshold) and a kill-switch plan.
Related
- vLLM · Continuous batching
- Serve your SLM
- TensorRT-LLM and SGLang for engine-specific features