Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Your large language model starts its answer promptly, but streams out remaining tokens at a frustrating crawl. Prompt processing isn't the culprit. Each next-token decode step stalls waiting for an expensive target forward pass. Could a cheaper model guess several tokens ahead without altering the output distribution users receive?
The previous chapter deployed a small language model as the primary engine answering queries. Here, a cheap mechanism proposes tokens for the target to verify together. It can be a separate model, a trained prediction head, or a lookup in existing text. An exact sampler commits an accepted prefix and corrects the first rejection. This procedure is speculative decoding. The classic algorithm preserves the target's sequence distribution in exact arithmetic, provided its actual conditional probabilities and stopping rules are respected.[1][2]
The same seed need not produce the same text: the two algorithms consume random draws differently. Floating-point kernels can also change the probabilities evaluated in serial versus verification passes. Nor is every heuristic called speculative decoding exact. We will prove the sampler identity, exercise a complete probability-level round, and distinguish a performance model from a measured serving gain.
Why memory bandwidth throttles autoregressive decoding
Standard target sampling has a sequential dependency: its next-token distribution conditions on tokens already selected. A transformer can score a known continuation in parallel with causal masking, but the next unknown sample is needed before the following ordinary decoding step.
Batch-one dense decoding is often limited by weight traffic, though attention, cache traffic, communication, and launch overhead can dominate particular workloads. Consider a hypothetical model using matrix weights per position in BF16. Assume each weight is fetched once from HBM per pass and multiply-add counts as two FLOPs. That gives 60 GB of weight traffic and about 60 GFLOPs per position. This approximation excludes attention and other work; an embedding lookup does not read every stored embedding row.
Under these assumptions, the weight contribution to arithmetic intensity is:
NVIDIA lists 3.35 TB/s and 80 GB for H100 SXM. Its advertised BF16 rate of 1,979 TFLOP/s includes structured sparsity; the corresponding dense rate is about 989.5 TFLOP/s.[3] The weight-only peak-rate floors are:
The roofline estimate uses the larger floor, not their sum: memory and arithmetic can overlap. The ridge is about FLOP/byte. At intensity one, peak bandwidth supports roughly 0.34% of dense peak arithmetic throughput. That is a rate comparison, not evidence that the GPU is idle for 99.66% of elapsed time. Thin matrix operations need not attain the advertised Tensor Core peak. The 60 GB payload also leaves only part of the nominal device capacity for caches and working memory; it does not establish model fit.
Verification can amortize weight traffic across several known positions. Let be draft depth throughout this lesson. A common cached implementation processes one pending committed token plus proposals, producing next-token distributions. With input positions, a matrix-vector operation becomes a matrix-matrix operation (, with ).
If that pass fetches the weights once and reuses them across all positions, its weight-only intensity becomes:
For and , the compute floor rises to about 0.364 ms while the assumed weight-transfer floor stays 17.91 ms. This explains an opportunity, not a measured verification time. Cache reads/writes, activations, logits, masks, kernel shapes, and communication add costs; tiling need not fetch every weight exactly once. The original papers analyze conditions where short-block scoring stays close to one-step latency.[2]
Does parallel verification violate autoregressive causality?
Answer
No. The probability of proposal k is read from the preceding position's logits, conditioned on the committed prefix and proposals 1 through k-1. The input position holding proposal k may attend to itself and its preceding context; its logits predict the next token. This one-position shift matters. No position can attend to future proposals.
The draft-verify protocol and cache state
The classic chain method alternates sequential drafting with parallel target verification.
Let propose and verify. A hypothetical draft pass fetching 2 GB over the same peak-rate bus has a weight-transfer floor of about 0.6 ms. Five sequential passes have about a 3 ms floor, excluding all other work. Actual drafting includes cache synchronization, kernels, sampling, and any catch-up tokens, so parameter ratio alone cannot predict its cost.
| Phase | Component | Operations performed | Latency impact |
|---|---|---|---|
| Draft | Proposer | Generates candidates and records the actual proposal distributions | Measure sequential drafting and catch-up work |
| Verify | Target | Scores candidates and bonus position, reusing cached context | Measure the actual input layout and kernels |
| Commit | Exact sampler | Accepts in order; corrects the first rejection | Sampling, synchronization, and transfer can matter |
| Synchronize | Serving engine | Keeps valid computed states and processes pending emitted tokens | Crop, gather, block ownership, or recomputation depends on the cache |
Managing key-value (KV) cache state requires strict bookkeeping across these phases:
- Distinguish output from computed state. A token is sampled from logits produced before that token is fed through the model. Emitting a token does not compute its KV entries. In ordinary cached drafting, the last sampled proposal may still be pending in the draft cache.
- Align verification rows. With cached prefix
Cand pending committed tokenu, inputs[u, x1, ..., xK]produce distributions[p1, p2, ..., p(K+1)]. Rowp1scoresx1; the final row supplies the bonus distribution. PrefixCis not recomputed, though attention can still read its cache. - Stop at the first rejection. If
x1throughxksurvive butx(k+1)fails, discard that token and all its descendants. Sample the correction from that position's residual distribution. - Retain valid states only. The target can keep computed KV through
xk; rejected-path states must not be reused as the corrected prefix. Each model's valid computed length can differ. The correction must be processed before its KV exists, often as the pending input to the next pass. A token ID is not a cache entry. - Handle full acceptance and termination. Emit all proposals and a target bonus. The bonus is also pending computation. Draft catch-up and cache compaction remain implementation work. Apply EOS, stop sequences, and output limits in emitted order; stop at the first terminating event rather than publishing the rest of a speculative block.
Here is the dependency in miniature. The decimal accumulator is a stand-in for prefix-dependent state, not transformer KV or a serving-cache implementation:
1proposals = [1, 2, 3]
2states, state = [], 0
3for token in proposals:
4 state = 10 * state + token
5 states.append(state)
6accepted, correction = 2, 4
7retained = states[:accepted]
8committed = proposals[:accepted] + [correction]
9assert states[-1] == 123 # state for the rejected token, not for correction 4
10print(f"committed tokens={len(committed)}, computed states={len(retained)}")
11retained.append(10 * retained[-1] + correction) # process pending token
12assert retained[-1] == 124
13print("after processing correction:", retained)1committed tokens=3, computed states=2
2after processing correction: [1, 12, 124]Suppose our committed prefix is The model and the draft proposes serves with cache. Let represent the draft probability and represent the target probability at each prefix position.
| Proposed token | Draft | Target | Acceptance probability |
|---|---|---|---|
| serves | 0.40 | 0.60 | |
| with | 0.35 | 0.40 | |
| cache | 0.60 | 0.40 |
The first two proposals are accepted unconditionally. The third proposal survives with probability , even though the target model assigns it lower probability than the draft did. In this run, a uniform random draw of 0.90 exceeds , triggering a rejection for cache.

Exact correction sampling and distribution proof
The following proof applies to the exact classic sampler. It preserves target probabilities, including the target's mistakes; verification is not factual checking. Approximate acceptance heuristics and a changed target checkpoint need separate claims about their output behavior.[1][2]
Let be the vocabulary. For a candidate token proposed from distribution , the acceptance probability is:
Because was sampled from , we know , making this ratio well-defined. If , the target desires at least as much probability mass on as the draft supplied, so the token survives unconditionally (). If , the draft overproduced , so we thin its frequency by accepting it with probability .
When a token fails the acceptance test, we discard it along with every subsequent proposal in the draft chain. We immediately sample a replacement token from the normalized residual distribution:
The term extracts only the positive probability deficit, identifying tokens where the target demands more probability mass than the draft provided.

Account for both branches
To prove that the emitted token follows the exact target distribution , trace the two mutually exclusive paths that can generate an output: acceptance or rejection.
Branch 1: Proposal accepted. A token is proposed with probability and accepted with probability . Multiplying these gives the probability of proposing and accepting :
For , the proposal-and-accept contribution is zero without evaluating a ratio.
Summing across all vocabulary tokens yields the total probability of accepting any proposal:
Branch 2: Proposal rejected, correction sampled. The probability of rejecting the proposal is the complement of total acceptance:
For any real numbers and , the identity holds. Substituting and :
The normalization constant of the residual distribution is the exact probability of entering the rejection branch.
When rejection happens, we draw token from the normalized residual . The probability of taking this branch and emitting is:
Combining both branches. By the law of total probability, the marginal probability of emitting token is the sum of both contributions:
Applying the algebraic identity directly confirms:
The sampling scheme recovers the target distribution exactly. If , then , rejection becomes impossible, and proposals survive with probability 1. Tokens that the draft model never proposes () remain reachable through the residual branch.
Verify this arithmetic with exact fractions. Notice how resampling directly from raw target after a rejection distorts the output, over-allocating mass to already-accepted tokens.
1from fractions import Fraction as F
2
3q = [F(6, 10), F(3, 10), F(1, 10)]
4p = [F(4, 10), F(5, 10), F(1, 10)]
5overlap = [min(pi, qi) for pi, qi in zip(p, q)]
6missing = [max(F(0), pi - qi) for pi, qi in zip(p, q)]
7z = sum(missing)
8r = [mass / z for mass in missing]
9correct = [mass + z * ri for mass, ri in zip(overlap, r)]
10wrong = [mass + z * pi for mass, pi in zip(overlap, p)]
11assert correct == p
12assert wrong != p
13print("rejection probability:", float(z))
14print("correction probabilities:", [float(x) for x in r])
15print("correct output:", [float(x) for x in correct])
16print("resampling raw target:", [float(x) for x in wrong])1rejection probability: 0.2
2correction probabilities: [0.0, 1.0, 0.0]
3correct output: [0.4, 0.5, 0.1]
4resampling raw target: [0.48, 0.4, 0.12]Why does sampling from raw p after rejection distort the output distribution?
Answer
The accepted branch already accounted for the overlap min(p, q). If you draw from raw p on rejection, tokens that were already accepted get sampled again in proportion to p. That double-counts overlap tokens and under-represents tokens where the target needed extra mass.
Serving transformations and tokenizer alignment
The mathematical proof assumes and represent the true probabilities governing the generation event.
Serving-time transformations must be incorporated into before computing acceptance ratios. If your production serving engine applies temperature scaling, top- nucleus filtering, top- truncation, min- cutoffs, or repetition penalties, must be the post-transformation, normalized categorical distribution. Comparing pre-transformation logits invalidates the proof.
The two models don't require identical sampling settings. A deterministic proposer, such as prompt lookup decoding, assigns probability to its proposed candidate and 0 elsewhere. Its acceptance probability simplifies to:
If the proposed token is rejected, the residual distribution samples from the remaining target mass with zeroed out:
A stochastic target can safely verify candidates proposed by a deterministic heuristic.[1]
Here's an implementation trap. Suppose the target uses top-2 truncation while the proposer samples without truncation. Only the target distribution receives truncation:
1raw_target = [0.55, 0.30, 0.15]
2q = [0.40, 0.35, 0.25] # the actual, untruncated proposal distribution
3p = [raw_target[0] / 0.85, raw_target[1] / 0.85, 0.0]
4for token_id in (1, 2):
5 wrong = min(1.0, raw_target[token_id] / q[token_id])
6 correct = min(1.0, p[token_id] / q[token_id])
7 print(f"token {token_id}: raw ratio={wrong:.3f}, correct={correct:.3f}")
8assert p[2] == 0.0 # a target-forbidden token must never survive1token 1: raw ratio=0.857, correct=1.000
2token 2: raw ratio=0.600, correct=0.000The proof also requires a common token event space. Blindly comparing ID 4012 in one vocabulary with ID 4012 in another is invalid if they mean different tokens. Identical tokenizers are a convenient direct-implementation requirement, not a mathematical necessity: neither every tokenizer uses byte-pair merges nor every cross-tokenizer scheme requires identical segmentation.
Checked September 22, 2026: vLLM's Token-Level Intersection option maps normalized token strings, restricts drafts to shared tokens, and translates their IDs to the target vocabulary. Its documented heterogeneous-vocabulary path supports greedy draft sampling only. A remapped proposer must record probabilities for the distribution it actually samples; renaming IDs alone does not solve general split/merge alignment.[4]
Implement a complete probability-level round
When every proposal in a depth- round survives, sample a bonus token directly from the target distribution at position . If rejection happens at position , compute the correction using row and row at that exact position.

This CPU implementation constructs a probability-level round over three tokens (cache, latency, batch). The target table changes with preceding context, exposing row-index mistakes. It includes an optional EOS token and output cap, but no transformer, KV cache, GPU parallelism, multi-token stop matcher, or performance benchmark:
1from math import fsum, isclose, isfinite
2from random import Random
3
4VOCAB = ("cache", "latency", "batch")
5
6def checked(row):
7 row = tuple(row)
8 if (len(row) != len(VOCAB)
9 or any(type(x) not in (int, float) or not 0 <= x <= 1
10 or not isfinite(x) for x in row)
11 or not isclose(fsum(row), 1.0, abs_tol=1e-12, rel_tol=0)):
12 raise ValueError("expected a finite normalized vocabulary distribution")
13 total = fsum(row)
14 return tuple(x / total for x in row) # normalize tolerated rounding drift
15
16def sample(row, rng):
17 draw, cumulative = rng.random(), 0.0
18 for token_id, probability in enumerate(row):
19 cumulative += probability
20 if draw < cumulative:
21 return token_id
22 # Only a floating-point rounding gap may reach here. Never choose zero mass.
23 return max(i for i, probability in enumerate(row) if probability > 0)
24
25def target(prefix):
26 rows = {None: (0.4, 0.5, 0.1), 0: (0.1, 0.6, 0.3),
27 1: (0.6, 0.2, 0.2), 2: (0.3, 0.2, 0.5)}
28 return rows[prefix[-1] if prefix else None]
29
30def proposer(prefix):
31 return (0.2, 0.3, 0.5) if prefix and prefix[-1] == 2 else (0.6, 0.3, 0.1)
32
33def speculative_round(prefix, p_model, q_model, depth, rng):
34 if type(depth) is not int or depth < 0:
35 raise ValueError("depth must be a nonnegative integer")
36 prefix = tuple(prefix)
37 draft, q_rows = [], []
38 for _ in range(depth):
39 q = checked(q_model(prefix + tuple(draft)))
40 q_rows.append(q)
41 draft.append(sample(q, rng))
42
43 # A transformer obtains these rows together with the appropriate causal mask.
44 p_rows = [checked(p_model(prefix + tuple(draft[:i])))
45 for i in range(depth + 1)]
46 for i, token in enumerate(draft):
47 p, q = p_rows[i], q_rows[i]
48 if rng.random() < min(1.0, p[token] / q[token]):
49 continue
50 missing = [max(0.0, pi - qi) for pi, qi in zip(p, q)]
51 z = sum(missing)
52 if z <= 0:
53 raise ArithmeticError("rejection requires positive residual mass")
54 correction = sample(checked([x / z for x in missing]), rng)
55 return draft[:i] + [correction], f"reject at {i + 1}"
56 return draft + [sample(p_rows[-1], rng)], "bonus"
57
58def generate(count, rng, depth=2, eos_id=None):
59 if type(count) is not int or count < 0:
60 raise ValueError("count must be a nonnegative integer")
61 if type(depth) is not int or depth < 0:
62 raise ValueError("depth must be a nonnegative integer")
63 if eos_id is not None and (type(eos_id) is not int or not 0 <= eos_id < len(VOCAB)):
64 raise ValueError("EOS must be a vocabulary ID")
65 prefix = []
66 while len(prefix) < count:
67 emitted, _ = speculative_round(prefix, target, proposer, depth, rng)
68 for token in emitted:
69 prefix.append(token)
70 if token == eos_id or len(prefix) == count:
71 return prefix # include EOS, discard the rest of this block
72 return prefix
73
74class Draws:
75 """Scripted uniform draws to exercise particular branches, not random evidence."""
76 def __init__(self, values):
77 self.values = iter(values)
78
79 def random(self):
80 return next(self.values)
81
82# First two draws propose cache, cache. Later draws decide acceptance and output.
83cases = [
84 ([0.05, 0.05, 0.90, 0.10], [1], "reject at 1"),
85 ([0.05, 0.05, 0.10, 0.90, 0.95], [0, 2], "reject at 2"),
86 ([0.05, 0.05, 0.10, 0.10, 0.20], [0, 0, 1], "bonus"),
87]
88for draws, expected, expected_outcome in cases:
89 emitted, outcome = speculative_round([], target, proposer, 2, Draws(draws))
90 assert (emitted, outcome) == (expected, expected_outcome)
91 print(outcome + ":", " ".join(VOCAB[i] for i in emitted))
92assert generate(0, Random(0)) == []
93assert len(generate(7, Random(0))) == 7
94assert generate(7, Draws([0.05, 0.05, 0.10, 0.10, 0.20]), eos_id=0) == [0]1reject at 1: latency
2reject at 2: cache batch
3bonus: cache cache latencyTest sequences across multiple positions
A correct distribution on the first token doesn't guarantee that prefix updates are functioning properly. This empirical test runs 60,000 generations and compares the observed frequencies of all nine two-token sequences against the exact joint target probabilities:
1from collections import Counter
2from itertools import product
3
4rng = Random(2026)
5trials = 60_000
6counts = Counter(tuple(generate(2, rng)) for _ in range(trials))
7errors = []
8for first, second in product(range(3), repeat=2):
9 expected = target(())[first] * target((first,))[second]
10 errors.append(abs(counts[(first, second)] / trials - expected))
11assert max(errors) < 0.006 # reproducible smoke check, not a proof
12
13same, outcome = speculative_round([], target, target, 3, Random(1))
14assert len(same) == 4 and outcome == "bonus" # no zero-residual normalization
15p_only = lambda prefix: (0.0, 1.0, 0.0)
16q_only = lambda prefix: (1.0, 0.0, 0.0)
17assert speculative_round([], p_only, q_only, 2, Random(1)) == ([1], "reject at 1")
18assert len(speculative_round([], target, proposer, 0, Random(1))[0]) == 1
19for invalid in ((0.5, 0.5), (0.5, -0.1, 0.6), (float("nan"), 0, 1),
20 (0, 0, 0), (10**1000, 0, 0), (True, 0, 0)):
21 try:
22 checked(invalid)
23 except ValueError:
24 pass
25 else:
26 raise AssertionError("invalid distribution accepted")
27print(f"two-token maximum absolute frequency error: {max(errors):.4f}")
28print("identical, disjoint, zero-depth, and invalid-input checks passed")1two-token maximum absolute frequency error: 0.0037
2identical, disjoint, zero-depth, and invalid-input checks passedThis fixed-seed run has maximum absolute frequency error about 0.0037. It is a reproducible smoke check, not a statistical proof or evidence about a GPU sampler. The sequence-distribution proof follows by applying the conditional identity at every emitted prefix, then applying the same stopping rules.
What state changes occur when proposal two is rejected?
Answer
Emit proposal one and a residual correction at position two. Discard proposal two and its descendants. Retain each model's already-computed valid prefix; the correction's state must be computed before reuse. The next distributions condition on the corrected output, not the discarded draft.
Speedup dynamics and serving load
How many tokens does a speculative round generate on average?
Assume every reached position has the same conditional acceptance probability . An overall average acceptance rate does not establish this assumption. Before termination/output caps, a round emits one correction or bonus plus its accepted prefix. Reaching accepted proposals then has probability .
Summing across all positions gives the expected token yield per round:[1]
The quotient applies for . At , all proposals survive and ; the finite sum handles both endpoints.
If and , the system yields:
Measure time relative to one ordinary target decode step. Let and let be normalized verification cost. For the simple serial draft/verify model, include sampler, catch-up, transfer, and scheduling time in these costs or add a separate normalized overhead term. Ignoring that extra term, the modeled long-generation speedup is:
If drafting cost is and verification cost remains flat (), depth achieves a speedup of . If verification duration grows slightly (), speedup declines to .
1from math import isfinite
2
3def expected_tokens(alpha, depth):
4 if type(alpha) not in (int, float) or not 0 <= alpha <= 1 or not isfinite(alpha):
5 raise ValueError("alpha must be in [0, 1]")
6 if type(depth) is not int or depth < 0:
7 raise ValueError("depth must be a nonnegative integer")
8 return sum(alpha**i for i in range(depth + 1))
9
10def modeled_speedup(alpha, depth, draft_cost, verify_cost):
11 expected = expected_tokens(alpha, depth)
12 if (type(draft_cost) not in (int, float) or type(verify_cost) not in (int, float)
13 or draft_cost < 0 or verify_cost <= 0):
14 raise ValueError("invalid cost")
15 try:
16 total = verify_cost + depth * draft_cost
17 if not isfinite(draft_cost) or not isfinite(verify_cost) or not isfinite(total):
18 raise ValueError("costs must be finite")
19 except OverflowError as error:
20 raise ValueError("costs exceed this numeric model") from error
21 return expected / total
22
23for depth in (1, 5, 10):
24 flat = modeled_speedup(0.8, depth, 0.1, 1.0)
25 growing = modeled_speedup(0.8, depth, 0.1, 1.0 + 0.05 * depth)
26 print(f"K={depth}: expected={expected_tokens(0.8, depth):.2f}, "
27 f"flat={flat:.2f}x, growing={growing:.2f}x")
28assert expected_tokens(0, 5) == 1
29assert expected_tokens(1, 5) == 6
30assert modeled_speedup(0.45, 1, 0.1, 1.0) > 1 # no universal 60% threshold
31for invalid in (True, float("nan"), float("inf"), -0.1, 1.1, 10**1000):
32 try:
33 expected_tokens(invalid, 5)
34 except ValueError:
35 pass
36 else:
37 raise AssertionError("invalid acceptance probability accepted")
38for cost in (True, float("nan"), float("inf"), 10**1000):
39 try:
40 modeled_speedup(0.8, 5, cost, 1.0)
41 except ValueError:
42 pass
43 else:
44 raise AssertionError("invalid cost accepted")1K=1: expected=1.80, flat=1.64x, growing=1.57x
2K=5: expected=3.69, flat=2.46x, growing=2.11x
3K=10: expected=4.57, flat=2.29x, growing=1.83x
For , expected yield approaches , while positive per-step draft cost keeps growing. At , the limit is five emitted tokens, not five accepted proposals. A useful depth depends on the measured cost curve, acceptance by position, termination, and load. There is no universal three-to-six-token optimum; parallel draft methods do not even incur the same cost pattern.
Why larger batches can change the answer
Concurrency changes the resources available for verification. It can reduce gains or produce a slowdown, but a batch-size cutoff cannot be inferred from this worksheet.
If weights are reused across streams, the same approximation gives:
At , this is only 64 FLOP/byte, still below the worksheet's dense H100 ridge of about 295.4. It cannot prove compute saturation. Attention/cache traffic and actual GEMM efficiency also matter; request concurrency is not necessarily the scheduler's current decode batch.
For a deliberately restrictive model, assume ordinary decoding and verification are fully compute-limited, each position costs the same, and verification processes positions per stream. Then . Since , positive sequential draft cost makes the predicted gain below one for :
That result follows from the stated assumptions, not from . Draft weights, states, scratch space, and graph reservations may compete with serving capacity. Shared-feature or parallel proposers can have different costs; adaptive depth may make different decisions at different loads.
There is a concrete counterexample to a universal batching collapse. EAGLE-3's paper reports 1.38x throughput at batch size 64 for LLaMA-Instruct 3.1 8B on one H100 in SGLang v0.4.4, using MT-Bench and a length-three chain without tree verification. The original EAGLE gave 0.99x in that test. These are historical reported results, not measurements of current engines or every workload.[5]
Compare useful delivered throughput, latency, and capacity under the actual offered load. Speculation and continuous batching can be combined; neither wins automatically.
Verification work isn't committed output
Scheduling budgets count work submitted to verification, not just output later accepted. Engines can reserve a fixed window or adapt its size; inspect the installed scheduler's contract.
If each verified stream passes 1 pending committed token plus proposals, the target forward pass consumes sequence positions per request. A server handling 16 concurrent streams with must evaluate positions simultaneously. If the engine's current step budget is capped at 64 positions, that batch can't be scheduled in one pass, even if acceptance rate is near 100%:
1streams, budget, alpha = 16, 64, 0.8
2for depth in (0, 2, 4, 8):
3 positions = streams * (depth + 1)
4 expected_output = streams * sum(alpha**i for i in range(depth + 1))
5 print(f"K={depth}: positions={positions}/{budget}, "
6 f"expected output={expected_output:.1f}, fits={positions <= budget}")1K=0: positions=16/64, expected output=16.0, fits=True
2K=2: positions=48/64, expected output=39.0, fits=True
3K=4: positions=80/64, expected output=53.8, fits=False
4K=8: positions=144/64, expected output=69.3, fits=FalseIf acceptance drops from 0.80 to 0.45, should you increase draft depth K to compensate?
Answer
Not automatically. Under the constant-conditional-rate model, yield still increases with depth but approaches 1/(1-0.45), about 1.82 tokens. Compare the incremental yield with draft and verification cost. Even alpha 0.45 gives about 1.32x at K=1 if c=0.1 and v=1; that is a modeled counterexample to a universal acceptance threshold, not a benchmark. Inspect mismatch and test depth, method, or disabling speculation under the actual workload.
Speculative tree architectures and heuristic drafting
An early rejection makes later chain proposals unusable. Under constant conditional acceptance, full-chain survival is : at and , about 0.168. Actual acceptance depends on position and context.
A tree spends more verification work on alternatives instead of only one path. It can improve useful yield, but doesn't guarantee a better cost per emitted token.
| Proposer approach | Mechanism | Extra resources | What to check |
|---|---|---|---|
| Separate autoregressive draft | A smaller transformer samples a chain | Weights, draft states, sequential passes | Task match, actual proposal probabilities, placement |
| Medusa | Extra heads predict future positions from target features | Head weights, candidate/tree buffers | Frozen versus jointly changed backbone; exact versus typical acceptance |
| EAGLE-2 / EAGLE-3 | Target-conditioned trained drafter with dynamic proposals | Draft weights/states, extracted features, verification buffers | Exact target/checkpoint compatibility; the two versions have different objectives |
| Native multi-token prediction (MTP) | Model-family prediction modules supply drafts | Architecture-specific modules and state | Whether the engine supports that family's MTP/assistant path |
| Parallel draft model | A trained proposer generates several positions together | Draft weights and block/state buffers | Its joint/conditional proposal scheme and supported verification |
| Prompt lookup / n-gram | Match a suffix and propose a following span | Search/cache structures and verification buffers; no extra neural weights | Repeated text, search cost, and engine-specific lookup scope |
Tree attention masks for parallel verification
How does a target model verify multiple competing tree branches in a single parallel forward pass without cross-branch contamination?
For an explicit tree mask, flatten candidate nodes and construct . Within candidate columns, node can attend to an ancestor or itself:
Sibling branches have disallowed attention scores masked to before softmax, giving zero attention probability. For a model using RoPE, candidate positions use the committed-prefix offset plus tree depth, rather than the flattened index. Other positional schemes and hybrid state require their own compatible handling.
All candidate nodes also attend to the appropriate committed prefix. The mask describes scoring, not an exact stochastic sampler. Greedy decoding can follow the longest path matching target choices; stochastic tree methods need their own proposal ordering and correction algorithm. Arbitrarily picking the longest high-probability path does not preserve .[6][7]
Try a four-node forest attached to the same committed prefix. Nodes 0 and 1 are alternatives; node 2 extends 0 and node 3 extends 1. Self-attention is permitted, but sibling branches stay separate:
1parents = [None, None, 0, 1]
2mask, depths = [], []
3for node in range(len(parents)):
4 ancestors, cursor = set(), node
5 while cursor is not None:
6 ancestors.add(cursor)
7 cursor = parents[cursor]
8 mask.append([int(other in ancestors) for other in range(len(parents))])
9 depths.append(len(ancestors))
10assert mask[2] == [1, 0, 1, 0]
11assert mask[3] == [0, 1, 0, 1]
12for row in mask:
13 print(row)
14print("depths:", depths)1[1, 0, 0, 0]
2[0, 1, 0, 0]
3[1, 0, 1, 0]
4[0, 1, 0, 1]
5depths: [1, 1, 2, 2]This is a visibility worksheet, not a fused attention kernel or a proof of tree sampling. Prefix columns and their position offset are omitted. Candidate input 2's logits predict a continuation after node 2, not the probability of node 2 itself.
Original EAGLE predicts features immediately before the target LM head, using target features and a token sequence shifted one step ahead. The target head turns predicted features into draft token probabilities; EAGLE still proposes tokens. Its paper calls these second-to-top-layer features, rather than a separate earlier transformer block.[8] EAGLE-2 makes the draft tree context-dependent using draft confidence, with expansion and reranking.[7]
EAGLE-3 removes the feature-prediction constraint and directly trains token prediction. It fuses low-, mid-, and high-layer target features and simulates multi-step drafting during training. Describing the entire family as feature prediction misses this important change.[5]
Medusa also needs a distinction: Medusa-1 freezes the backbone, while Medusa-2 trains it jointly with the heads. Joint training can change the target itself. The paper's typical acceptance heuristic explicitly relaxes distribution matching for stochastic generation; the exact residual proof above does not establish its losslessness.[6]
Prompt lookup requires no extra model training. A common scheme matches a trailing token n-gram against prompt text and proposes the following span. Some engines also search generated-text caches. It can be useful when outputs repeat available text, but searching, maintaining indexes, and verifying candidates still cost memory and time. A copied span remains a proposal, not a trusted legal or factual answer.[9][10]
Implementation snapshot, September 22, 2026: vLLM documents MTP and parallel PARD drafting in addition to EAGLE and n-grams; these are distinct trained/model-family paths, not switches that make any checkpoint compatible.[11][12] SGLang documents EAGLE-3, standalone drafting, MTP, and DFlash block verification, among other methods.[10] This is a method map, not a speed ranking.
Record engine version, target/draft revisions, quantization, attention backend, parallel topology, and sampler settings. Check returned-logprob, grammar, penalties, multimodal, graph, and scheduler compatibility for that exact method. vLLM documents finite-precision/logprob variability; neither seed equality nor greedy equality proves arbitrary stochastic correctness.[4]
Measure serving speedup with paired benchmarks
Speculative decoding performance depends heavily on your specific serving environment. Validate improvements using a strictly controlled paired experiment before rolling out to production.
Keep the target model checkpoint, quantization format, prompt distribution, sampling parameters, and server hardware topology identical across both arms. Record non-speculative baseline latency, then run the speculative configuration under matching offered load.
Track the following production metrics:
- Client TTFT and completion latency: Include queueing, target prefill, draft initialization, and delivery. Speculation accelerates generation work; it does not remove prompt processing. Measure any first-token regression against the product requirement.
- Delivered token gaps: Record client timestamps and tail pauses. Tokens from one round may arrive in a burst followed by a long gap; average server time per token does not describe that experience. Separate reasoning tokens from visible answer tokens when relevant.
- Accepted proposals and emitted yield: Log both distributions. Yield includes a correction or bonus and can be reduced by termination. Three accepted proposals is not a universal health threshold.
- Acceptance by reached position: Count accepted/reached at each depth and segment by task, context, and sampling mode. A first-position rate alone cannot decide whether drafting is profitable.
- Useful capacity under load: Sweep representative offered request rates and scheduler batches, beyond 64 if relevant. Track delivered throughput, latency SLO attainment, memory peaks, failures, and queue growth. A sampled crossover has uncertainty and can change with workload or adaptive depth.
Use repeated paired prompt sets, report uncertainty, and account for stochastic output lengths. For a fixed-token timing experiment, state the token cap and EOS treatment; for completed user tasks, report the actual completion-length distribution. Track extra verification positions and draft work separately from useful output. A faster isolated stream is not proof of lower cost per completed task or greater SLO-qualified serving capacity.
Follow-up questions
How do position-dependent acceptance rates affect expected length?
Let denote the conditional probability of accepting the -th candidate given that candidates 1 through were accepted. The expected emitted tokens per round is:
Measure acceptance rates strictly from reached positions, not all drafted positions. If position 1 acceptance drops on challenging reasoning prompts, expected length collapses rapidly across all downstream positions.
Even the arithmetic mean of conditional rates loses crucial information. Compare these depth-two fixtures:
1for rates in ((0.9, 0.1), (0.5, 0.5), (0.1, 0.9)):
2 survival, expected = 1.0, 1.0
3 for rate in rates:
4 survival *= rate
5 expected += survival
6 print(f"rates={rates}, mean={sum(rates)/len(rates):.2f}, yield={expected:.2f}")1rates=(0.9, 0.1), mean=0.50, yield=1.99
2rates=(0.5, 0.5), mean=0.50, yield=1.75
3rates=(0.1, 0.9), mean=0.50, yield=1.19How do you isolate a sampler defect from GPU kernel precision shifts?
First, isolate the probability sampler on synthetic fixed probability distributions across CPU and GPU. Test edge cases including complete agreement (), zero overlap, and scripted rejections at every position. If fixed tables match, compare target model logits produced during serial decoding against logits produced during batched verification.
Model-only discrepancies narrow the investigation to masks, position/logit alignment, cache state, preprocessing, and numerical behavior; they do not identify one cause. Quantized kernels, deterministic reduction-order differences, and batch-dependent rounding can matter without random kernel execution. Check serial/verification logits under matched inputs and tolerances, then test the complete sampler, stopping, and delivery paths. A finite frequency test cannot prove equality across every prompt or tiny-probability event.
What strong answers show
- State the assumptions behind weight-only intensity and distinguish a roofline floor from measured latency or idle time.
- Derive the residual identity for actual conditional sampling distributions; qualify numerics, termination, tokenizer mapping, and approximate acceptance.
- Align verification logits and retain only valid computed states. Explain why a correction or bonus remains pending until processed.
- Compare yield against measured drafting, verification, and scheduling cost under actual load, including burst delivery and serving capacity.
- Distinguish original EAGLE, EAGLE-3, Medusa variants, MTP, parallel drafting, and lookup methods without turning one benchmark into a universal ranking.