Speculative Decoding: Drafting, Verification, and Real Speed Gains
When generating a long answer, the target model normally runs again after each token is chosen to obtain the probabilities for the next one. Speculative decoding uses cheaper computation to propose a sequence, then has the target verify its candidates in parallel. A verification round can commit several tokens, spreading the cost of expensive serial steps across them. Leviathan et al.'s original paper gives an algorithm that preserves the target sampling distribution.
Start with prefix dependencies in autoregressive generation and the prefill, decode, and throughput boundaries in inference performance. The question here is how to reduce target calls during decode, and when fewer calls save elapsed time.
Known candidates make parallel verification possible
In ordinary generation, choosing the second token requires the first, and choosing the third requires both. The KV cache retains the existing prefix's state; the target still processes each new position.
Let be the committed prefix and let the draft propose . All candidate prefixes are now known, so a causal target forward pass can obtain:
Verify against , against , and so on: the offset between logits and candidate positions matters. If all three are accepted, sample a bonus token from . If the second is rejected, retain only the first and add a correction at the second position. Discard the original third candidate and its verification result because they depend on the rejected prefix. EOS or an output limit can end the round early.
The target verification is parallel; a linear draft may still generate candidates sequentially. Verifying more positions adds computation. The benefit depends on amortizing reads of weights and historical state.
Acceptance and rejection preserve the distribution
The rule in Section 2.3 and Appendix A.1 of the original paper uses normalized target distribution and actual proposal distribution for the same prefix over the same token identifiers. Here, includes the target's temperature, top-k, top-p, or other sampling transformations; must likewise describe the draft's actual proposal procedure.
Accept a candidate sampled from with probability:
A proposed token has . At the first rejection, sample a replacement from the remaining probability mass:
When , every candidate is accepted and the zero-denominator correction branch is never entered. Applying these rules correctly at successive prefixes preserves the target sequence distribution. Rejecting low-probability candidates and simply resampling from generally changes it.
Account for all probability mass with three tokens
Consider a constructed vocabulary A, B, C with and . Their acceptance probabilities are . Multiplying by proposal probabilities gives accepted mass and total acceptance . The remaining goes entirely to B, restoring .
This Python 3 example checks the probabilities using exact fractions, then replays one verification round with fixed candidates and draws. Here, and are normalized distributions over the same finite vocabulary, each proposed token has , and draws lie in . The toy distributions are identical at every prefix to keep the arithmetic simple; real model distributions change with the prefix. The code samples from the correction distribution at the first rejection, or from for a bonus token if all candidates are accepted. When , it skips the correction distribution. The default inputs reject C and replace it with B with probability 1.
from fractions import Fraction as F
tokens = ['A', 'B', 'C']
p = dict(zip(tokens, map(F, ['0.5', '0.3', '0.2'])))
q = dict(zip(tokens, map(F, ['0.6', '0.1', '0.3'])))
accepted = {x: min(p[x], q[x]) for x in tokens}
alpha = sum(accepted.values())
if alpha < 1:
residual = {x: max(p[x] - q[x], 0) / (1 - alpha) for x in tokens}
restored = {x: accepted[x] + (1 - alpha) * residual[x] for x in tokens}
else:
residual = None
restored = accepted
assert restored == p
print('accepted mass:', [float(accepted[x]) for x in tokens])
print('acceptance:', float(alpha))
print('residual:', None if residual is None else [float(residual[x]) for x in tokens])
print('reconstructed:', [float(restored[x]) for x in tokens])
def sample(distribution, u):
assert 0 <= u < 1
cumulative = F(0)
for x in tokens:
cumulative += distribution[x]
if u < cumulative:
return x
raise ValueError('distribution must sum to 1')
draft = ['A', 'C', 'B']
draws = list(map(F, ['0.7', '0.9', '0.2']))
final_draw = F('0.2')
assert len(draft) == len(draws)
emitted = []
for i, (x, u) in enumerate(zip(draft, draws)):
assert q[x] > 0 and 0 <= u < 1
threshold = min(F(1), p[x] / q[x])
ok = u < threshold
print(f'{x}: u={float(u):.1f}, threshold={float(threshold):.3f}, accept={ok}')
if not ok:
assert residual is not None
emitted.append(sample(residual, final_draw))
print('discard:', draft[i:])
break
emitted.append(x)
else:
bonus = sample(p, final_draw)
emitted.append(bonus)
print('bonus:', bonus)
print('emit:', emitted)
accepted mass: [0.5, 0.1, 0.2]
acceptance: 0.8
residual: [0.0, 1.0, 0.0]
reconstructed: [0.5, 0.3, 0.2]
A: u=0.7, threshold=0.833, accept=True
C: u=0.9, threshold=0.667, accept=False
discard: ['C', 'B']
emit: ['A', 'B']
The round commits A, B. Although the last draft B happens to match the correction token, its original state depended on prefix A, C and must be discarded.
Identical distributions do not require word-for-word identical answers with the same seed: algorithms can consume random numbers in different orders. vLLM's lossless-guarantee documentation also retains qualifications about floating-point precision, batch size, and logprob stability. The guarantee concerns decoding from a fixed target; changes introduced by target quantization or approximate caching require a separate comparison.
A good draft must also be cheap
At a fixed prefix, acceptance is . Similar draft and target distributions make acceptance more likely. The draft's ability to answer questions on its own does not substitute for this measure.
Use a simplified cost budget to decide whether speculation is worth testing. Suppose each round proposes candidates, each position has conditional acceptance probability , acceptance events are independent and identically distributed, and there is no early stopping. Including a correction or bonus token, expected committed tokens per round are:
These are the simplifying conditions used in Section 3.1 of the original paper. When acceptance varies by position and task, measure actual tokens committed per round rather than inserting an overall average acceptance rate without checking those conditions.
The following timings are all hypothetical: ordinary target decode takes 10 ms per token, , a whole target verification round takes 12 ms, and state handling takes 2 ms. With draft cost ms per token and no overlap between these stages, a cycle costs ms. For a sufficiently long generation, dividing that budget by gives the long-run average time per token.
gamma = 4
baseline_ms = 10
verify_ms = 12
state_ms = 2
for alpha, draft_ms in [(0.8, 1), (0.9, 3)]:
expected = sum(alpha ** j for j in range(gamma + 1))
cycle_ms = gamma * draft_ms + verify_ms + state_ms
per_token = cycle_ms / expected
speedup = baseline_ms / per_token
print(f'alpha={alpha:.1f} tokens={expected:.4f} cycle_ms={cycle_ms} '
f'ms/token={per_token:.3f} speedup={speedup:.3f}x')
alpha=0.8 tokens=3.3616 cycle_ms=18 ms/token=5.355 speedup=1.868x
alpha=0.9 tokens=4.0951 cycle_ms=26 ms/token=6.349 speedup=1.575x
The first draft costs 1 ms per token; the second costs 3 ms. The second commits more tokens on average but runs slower: about 1.575 times baseline speed, compared with 1.868 for the first. Real verification time can also vary with candidate count, batch size, context, and tree shape. Insert measured costs before tuning draft length; acceptance, drafting speed, verification, and state handling belong in the same calculation.
Chains, trees, and self-speculation describe different choices
The September 21, 2026 DeepSeek-V4 adaptation preprint discusses linear chains and candidate trees. The original LayerSkip paper provides a concrete self-speculation example: training uses layer dropout and an early-exit loss; inference drafts with early layers, then verifies and corrects with the remaining layers. Good early exits therefore cannot be assumed for an arbitrary unadapted model.
The first two rows describe candidate shape; the third describes where the draft comes from. A candidate tree ultimately commits one continuous path, with acceptance and sampling rules that must still preserve the target distribution. The chain's single-candidate correction rule cannot simply be applied to an arbitrary tree, and selecting the highest-probability branch is not target sampling. This differs from beam search, which explores prefixes to optimize a sequence score.
KV state must follow the accepted path
The KV-cache note explains reuse and compression. Speculation additionally requires cache contents to represent the committed prefix: remove a rejected suffix in a chain; in a tree, expose only the committed context and each node's ancestors, never sibling branches. Reuse shared prefixes and isolate temporary state after branching. If a newly sampled correction or bonus token has not yet passed through the model, compute its KV state later rather than reusing a rejected candidate's state.
Sections 2–4 of the DeepSeek-V4 preprint explain that CSA (Compressed Sparse Attention) and HCA (Heavily Compressed Attention) also compress history along the sequence. Branches can produce different compressed states, so a tree-shaped attention mask alone is insufficient. The implementation isolates branches in a temporary scratch pad, then refreshes the chosen path's KV, compressed state, and intermediate buffers. This compression across positions differs from MLA (multi-head latent attention), which reduces the width of each token's cached representation.
The paper tests DeepSeek-V4-Flash on eight NVIDIA GPUs, with verification budgets 5–8, batch sizes 1–64, and GSM8K, MBPP, and ShareGPT. It reports a peak decode-throughput improvement of about 18.5% over matched-budget linear speculation, on ShareGPT with s3_k2_d6 and batch size 4. That baseline already uses speculation. As budgets grow, accepted length can continue rising while throughput plateaus: branch management and state refresh consume the gains.
Compare matched workload and quality
The official vLLM documentation identifies medium-to-low QPS, memory-bound workloads as a setting for reducing inter-token latency. First check model, draft, and backend compatibility using local inference runtimes. Make two comparisons: speculation off versus on, then chains versus trees at a matched verification budget when linear speculation is already available.
Fix the target checkpoint, precision, tokenizer, prompt template, sampling and stopping rules, cache policy, hardware, and runtime version. Use the same prompt set and input-length distribution, record actual output lengths, and compare at the same concurrency or the same arrival rate in separate tests. Measure low-load latency separately from saturated throughput.
Acceptance measures how well the draft matches. Cycle time divided by committed tokens connects that match to speed. Report results with the measurement boundaries in inference performance to show whether savings occur in decode, the whole request, or the service as a whole.