[speculative decoding] Add dynamic speculation length with confidence-threshold early exit#35301
[speculative decoding] Add dynamic speculation length with confidence-threshold early exit#35301jmamou wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a confidence-threshold-based Dynamic Speculative Length (DSL) for speculative decoding, a valuable optimization that can improve throughput by avoiding unnecessary computation when the draft model's confidence is low. The implementation includes the addition of a draft_confidence_threshold configuration, the core early-exit logic in the proposer, and comprehensive observability through new metrics. The changes are well-structured, thoroughly tested, and accompanied by clear documentation and benchmark results demonstrating significant performance gains. Overall, this is a high-quality contribution that enhances vLLM's speculative decoding capabilities.
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels. Just a reminder: PRs would not trigger full CI run by default. Instead, it would only run You ask your reviewers to trigger select CI tests on top of Once the PR is approved and ready to go, your PR reviewer(s) can run CI to test the changes comprehensively before merging. To run CI, PR reviewers can either: Add If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. 🚀 |
|
This pull request has merge conflicts that must be resolved before it can be |
voipmonitor
left a comment
There was a problem hiding this comment.
Bug: CUDA index-out-of-bounds crash with GDN attention backend
When DSL triggers early exit, draft_token_ids_list has fewer elements than num_speculative_tokens. After torch.stack, the resulting tensor shape is [batch_size, N] where N < num_speculative_tokens. Downstream code (specifically gdn_attn.py:237 — block_table_tensor[spec_state_indices]) expects the full width and crashes with:
/pytorch/aten/src/ATen/native/cuda/IndexKernel.cu:111: operator(): block: [0,0,0], thread: [11,0,0]
Assertion `-sizes[i] <= index && index < sizes[i] && "index out of bounds"` failed.
Repro: Qwen3.5-397B-A17B-NVFP4, TP4, --speculative-config '{"method":"qwen3_next_mtp","num_speculative_tokens":20, "draft_confidence_threshold": 0.6}'
Fix
Pad draft_token_ids to full num_speculative_tokens width after torch.stack. The zero-padded positions will be rejected during verification anyway, so quality is unaffected.
# [batch_size, num_speculative_tokens]
draft_token_ids = torch.stack(draft_token_ids_list, dim=1)
# Pad to full num_speculative_tokens width if DSL early exit produced
# fewer tokens — downstream (GDN attention, scheduler) expects fixed shape.
if draft_token_ids.shape[1] < self.num_speculative_tokens:
pad_width = self.num_speculative_tokens - draft_token_ids.shape[1]
draft_token_ids = torch.nn.functional.pad(
draft_token_ids, (0, pad_width), value=0)
Tested on RTX PRO 6000 Blackwell (SM120), 4× GPUs. Crash gone, DSL early exit works correctly.
|
This pull request has merge conflicts that must be resolved before it can be |
Update: Padding Fix and Async DSL Sync supportChanges since initial submissionTwo issues were addressed after the initial benchmarks were posted:
Updated BenchmarksHardware: 1× NVIDIA A100 80 GB PCIe
Set A —
|
| Mode | Spec tokens | Threshold | Throughput (tok/s) | Speedup vs target | Speedup vs SD |
|---|---|---|---|---|---|
| target | — | — | 91.10 | 1.00× | — |
| SD | 5 | — | 59.85 | 0.66× | 1.00× |
| SD w/DSL | 10 | 0.4 | 104.28 | 1.14× | 1.74× |
| SD w/DSL | 20 | 0.8 | 163.72 | 1.80× | 2.74× |
Best DSL configuration: 20 tokens, threshold=0.8 → 163.72 tok/s, 1.80× vs target, 2.74× vs SD
SD/5 is slower than target (0.66×) at output_len=1024 — a known characteristic of speculative
decoding at long outputs where the overhead of generating and verifying K draft tokens
outweighs the gains. DSL/20/0.8 avoids this by exiting early, achieving 1.80× vs target.
Model pair 2: facebook/opt-6.7b + facebook/opt-125m
| Mode | Spec tokens | Threshold | Throughput (tok/s) | Speedup vs target | Speedup vs SD |
|---|---|---|---|---|---|
| target | — | — | 95.81 | 1.00× | — |
| SD | 5 | — | 192.60 | 2.01× | 1.00× |
| SD w/DSL | 20 | 0.8 | 210.56 | 2.20× | 1.09× |
Best DSL configuration: 20 tokens, threshold=0.8 → 210.56 tok/s, 2.20× vs target, 1.09× vs SD
Set B — mt-bench, 80 prompts, output_len=512, max_concurrency=1
Model pair 1: meta-llama/Llama-3.1-8B-Instruct + meta-llama/Llama-3.2-1B-Instruct
| Mode | Spec tokens | Threshold | Throughput (tok/s) | Speedup vs target | Speedup vs SD |
|---|---|---|---|---|---|
| target | — | — | 40.43 | 1.00× | — |
| SD | 5 | — | 73.33 | 1.81× | 1.00× |
| SD w/DSL | 10 | 0.6 | 102.00 | 2.52× | 1.39× |
| SD w/DSL | 15 | 0.6 | 101.05 | 2.50× | 1.38× |
Best DSL configuration: 10 tokens, threshold=0.6 → 102.00 tok/s, 2.52× vs target, 1.39× vs SD
With
output_len=512at single concurrency, SD/5 already gives a large boost (1.81×) because
mt-bench prompts are highly predictable. DSL improves further by exiting early once confidence
drops, recouping the overhead of larger draft budgets.
Set C — random synthetic, 80 prompts, input_len=128, output_len=128, max_concurrency=8
Note: The server is configured with
--max-num-seqs 1, which serialises all requests
regardless of client concurrency. The async early-exit loop significantly improves high-k
DSL performance even in this setting by skipping up to K−k_valid draft forward passes per
step. A rerun with--max-num-seqs≥max_concurrencywould show further gains from
batch-level confidence averaging.
Model pair 1: meta-llama/Llama-3.1-8B-Instruct + meta-llama/Llama-3.2-1B-Instruct
| Mode | Spec tokens | Threshold | Throughput (tok/s) | Speedup vs target | Speedup vs SD |
|---|---|---|---|---|---|
| target | — | — | 72.96 | 1.00× | — |
| SD | 5 | — | 139.62 | 1.91× | 1.00× |
| SD w/DSL | 20 | 0.6 | 219.00 | 3.00× | 1.57× |
Set D — mt-bench, 80 prompts, output_len=512, max_concurrency=4
Note: Same
--max-num-seqs 1caveat as Set C applies here. As in Set C, the async
early-exit loop substantially improves high-k DSL configurations.
Model pair 1: meta-llama/Llama-3.1-8B-Instruct + meta-llama/Llama-3.2-1B-Instruct
| Mode | Spec tokens | Threshold | Throughput (tok/s) | Speedup vs target | Speedup vs SD |
|---|---|---|---|---|---|
| target | — | — | 116.31 | 1.00× | — |
| SD | 5 | — | 154.41 | 1.33× | 1.00× |
| SD w/DSL | 15 | 0.8 | 222.73 | 1.91× | 1.44× |
| SD w/DSL | 20 | 0.8 | 190.42 | 1.64× | 1.23× |
Best DSL configuration: 15 tokens, threshold=0.8 → 222.73 tok/s, 1.91× vs target, 1.44× vs SD
Observations
-
Async early-exit impact (Sets D & E): The most dramatic improvement from the early-exit
loop is visible at high concurrency with large K. Set C DSL/20/0.6 improves from 21.36 → 219.00
tok/s (+925%); Set D DSL/15/0.8 improves from 34.42 → 222.73 tok/s (+547%). At k=20
with typical exits at step 3–5, the loop now skips 15–17 forward passes per decode step — the
dominant cost in these configurations. The 1-byte async D→H copy adds negligible overhead. -
Single-concurrency scenarios (Sets A & C): DSL consistently beats SD at c=1.
Set B: 2.52× vs target, 1.39× vs SD (k=10/τ=0.6). Set A Llama: DSL/20/0.8 at
1.80× vs target, 2.74× vs SD — note SD/5 is itself slower than target (0.66×) at
output_len=1024, showing that DSL's early exit rescues speculative decoding at long outputs. -
OPT: DSL beats SD at mt-bench: Set A OPT DSL/20/0.8 achieves 2.20× vs target,
1.09× vs SD (210.56 tok/s). -
Best confirmed speedups (output tok/s):
- Set C Llama random (c=8): 3.00× vs target, 1.57× vs SD (DSL k=20/τ=0.6)
- Set D Llama mt-bench (c=4): 1.91× vs target, 1.44× vs SD (DSL k=15/τ=0.8)
- Set B Llama mt-bench (c=1): 2.52× vs target, 1.39× vs SD (DSL k=10/τ=0.6)
- Set A Llama mt-bench (c=1, long): 1.80× vs target, 2.74× vs SD (DSL k=20/τ=0.8)
- Set A OPT mt-bench (c=1): 2.20× vs target, 1.09× vs SD (DSL k=20/τ=0.8)
-
Threshold / token-count sensitivity: DSL/20/0.6 excels on short-output high-concurrency
workloads (Set C: 3.00×) where early exit triggers quickly and skips most drafts. High
thresholds (0.8) with moderate K (15) are the most consistently safe choice across diverse
workloads.
|
The speedups are super interesting! A couple of questions:
|
|
@tomasruizt thanks for the valuable comments!
Correct. Synthetic inputs are used here primarily to measure throughput under controlled load. They do understate acceptance rates compared to real workloads. I plan to run additional exp on real workloads.
I agree the baseline should also be evaluated at its optimal K. Note that that I reported also vanilla (target row). Anyway, I plan to add a sweep over K for baseline SD.
Our benchmarks were run at different concurrencies but I have not yet swept batch size. I will add a batch size sweep.
Correct, for short outputs, prefill dominates and DSL savings are diluted. For long outputs, decode dominates and DSL's per-step savings compound over more steps, making the speedup more visible. This is why we report output_length as a variable — it shows where DSL's benefit saturates and is most relevant. |
|
@jmamou About the output_length point: If you compare the TPOT metric rather than tok/s you can cleanly separate the decoding performance gains of DSL from the prefill phase (which is captured in the TTFT metric). |
|
@tomasruizt I ran additional experiments with optimal K for SD, real workloads and added TPOT. I added evaluations on ShareGPT and mt-bench to complement the random-input results (see below). Train-phase K sweep (total token throughput, tok/s):
We applied the same train/test discipline for DSL, running a grid search over K∈{5,10,15,20} × τ∈{0.4,0.6,0.8} on the same train split:
Test-set results (80 prompts, seed=123, c=1, output_len=512, Llama-3.1-8B + Llama-3.2-1B)
DSL with train-optimised (K, τ) outperforms best-K SD by 34–42% in throughput and 6–18% in TPOT on held-out test data across both real-text datasets. DSL still requires tuning (K, τ), so the number of hyperparameters is the same as SD+K. The practical advantage is that DSL with a reasonably large K (e.g. K=15) and a moderate τ (e.g. 0.6–0.8) is robust across datasets: the confidence threshold self-regulates at runtime by exiting early when the draft model is uncertain. A fixed-K SD with suboptimal K has no such recourse — e.g. SD/K=7 drops to 0.65× vanilla throughput on ShareGPT test (slower than vanilla decoding), while DSL/K=15/τ=0.8 is at 1.65×. |
|
@jmamou Thanks for the quick reply! Two questions:
|
The vLLM server has
I picked the last (2nd invacation) value, so the cache-inflated 222 tok/s was reported. The test run (table 2, 80 diverse prompts, seed=123) also has The K-selection remains valid: all K values in the train sweep share the same warm-cache
The same issue — K=6 and K=7 ran in the same server session, so their prefix cache states diverged:
The TPOT is almost identical — the entire 2.4× gap comes from TTFT. K=6 got a warm cache (65 ms The K=6 selection from the train sweep is not confirmed. The lower TPOT for K=7 (6.15 ms vs 6.47 ms) and |
|
@jmamou Thanks for being so transparent! I suggest using |
|
I have looked through the benchmarks here and I feel there is more noise than signal. Let's align directly, either via DM or in the spec decoding SIG meeting. My high-level concerns are:
|
Signed-off-by: jmamou <[email protected]>
- Add confidence calculation with _greedy_sample_with_confidence method - Implement dynamic k adjustment: stop when min confidence drops below threshold - Handle early stopping with zero-padding to maintain fixed output shape - Use simple batch-wide strategy: stop all when any sequence confidence drops This completes the DSL feature for CPU speculative decoding, building on the draft_confidence_threshold config parameter already in the dsl branch.
Signed-off-by: jmamou <[email protected]>
|
@jmamou Hi jmamou, thank you for your excellent work. I am currently conducting tests based on your commits, running the Llama-8B + Eagle-3 configuration on vLLM version 0.18.1. The results from two specific experimental groups—one with |
@easonfzw, is my understanding correct that vanilla SD ( |
|
@keyboardAnt Thank you for responding to my issue. Based on my current experiments—conducted using Eagle3 with |
@easonfzw, can you please try to rerun your benchmark with a larger cap on the number of drafts (e.g., |
|
@easonfzw Thanks for running this — the result is consistent with what we'd expect for DSL in this regime, and it's a useful data point. DSL's speedup comes from skipping draft forward passes when the draft model's confidence collapses early. The savings scale roughly as
So at To be upfront: DSL is a weaker fit for Eagle than for draft-model SD. The cheap-draft point (1) caps DSL's upper-bound benefit regardless of how well it's tuned, and the calibration point (2) just means τ values that work for draft-model SD probably don't transfer — Eagle3's logits still correlate with acceptance (they go through the target LM head), they're just noisier as an exit signal, so τ would need to be re-swept for Eagle3 specifically. Where DSL could still help with Eagle is at very large To see whether DSL recovers anything in your setup, two things would help:
If you can share TPOT numbers for:
all with prefix caching off and a fixed seed, that would be the cleanest comparison. Could you also share which device you ran on? The DSL bookkeeping vs. draft-skip trade-off shifts with device compute/memory bandwidth, so that affects the interpretation. |
Purpose
This PR introduces confidence-threshold based Dynamic Speculative Length (DSL) support for speculative decoding and adds end-to-end DSL observability, inspired by DISCO (Mamou et al., 2024) and the HuggingFace blog post.
Functional changes
draft_confidence_thresholdto speculative config (default0.0, disabled by default).Why this matters
draft_confidence_threshold=0.0).Usage
Enable DSL by adding
draft_confidence_thresholdto the speculative configuration:Or via CLI:
Setting
draft_confidence_threshold=0.0(default) disables DSL and restores standard speculative decoding behaviour.Algorithm
Based on the dynamic speculation lookahead approach of DISCO (Mamou et al., 2024) (blog). At each draft token step$i$ (from 0 to
num_speculative_tokens - 2), the proposer:draft_confidence_threshold), drafting stops and the tokens generated so far are returned.The drafted sequence is always padded to
num_speculative_tokensby the caller regardless of how many tokens were actually generated; the unused slots are masked by the verifier.Batch policy (mean): the exit condition is the average confidence over the batch, making a single low-confidence request less likely to cause an early exit and giving higher-confidence requests more benefit of the doubt.
Known limitations / trade-offs
.item()on a GPU scalar, forcing a host–device synchronisation at every token step when DSL is active. This is negligible at small batch sizes but may become a bottleneck under high concurrency. A pure device-side solution (e.g., storing the flag in a GPU tensor and avoiding the sync) would remove this overhead.Test Plan
Scope
ModelRunnerOutputEagleProposerCommands
Benchmark
Hardware: 1× NVIDIA A100 80 GB PCIe
Dataset:
philschmid/mt-bench(10 prompts,output_len=1024,temperature=0,max_concurrency=1)vLLM version: v0.1.dev14014+g6963a1497 (branch
dsl, mean batch policy)Model pair 1:
meta-llama/Llama-3.1-8B-Instruct+meta-llama/Llama-3.2-1B-InstructBest DSL configuration: 20 tokens, threshold=0.6 → 1.77× vs target, 1.44× vs spec_decode
Model pair 2:
facebook/opt-6.7b+facebook/opt-125mBest DSL configurations:
Hardware: 1× NVIDIA A100 80 GB PCIe
Dataset: random synthetic (80 prompts,
input_len=1024,output_len=512,temperature=0,max_concurrency=1)vLLM version: v0.1.dev14014+g6963a1497 (branch
dsl, mean batch policy)Model pair 1:
meta-llama/Llama-3.1-8B-Instruct+meta-llama/Llama-3.2-1B-Instruct(random, c=1)Best DSL configuration: 15 tokens, threshold=0.8 → 508.75 tok/s, 1.92× vs target, 1.48× vs SD
Hardware: 1× NVIDIA A100 80 GB PCIe
Dataset:
philschmid/mt-bench(80 prompts,output_len=512,temperature=0,max_concurrency=1)vLLM version: v0.1.dev14014+g6963a1497 (branch
dsl, mean batch policy)Model pair 1:
meta-llama/Llama-3.1-8B-Instruct+meta-llama/Llama-3.2-1B-Instruct(mt-bench 80-prompt, c=1)Best DSL configuration: 10 tokens, threshold=0.6 → 166.31 tok/s, 1.62× vs target, 1.53× vs SD
Hardware: 1× NVIDIA A100 80 GB PCIe
Dataset: random synthetic (80 prompts,
input_len=128,output_len=128,max_concurrency=8)vLLM version: v0.1.dev14014+g6963a1497 (branch
dsl, mean batch policy)Model pair 1:
meta-llama/Llama-3.1-8B-Instruct+meta-llama/Llama-3.2-1B-Instruct(random, c=8)Best DSL configuration: 15 tokens, threshold=0.8 → 960.55 tok/s, 3.60× vs target, 1.52× vs SD
Hardware: 1× NVIDIA A100 80 GB PCIe
Dataset:
philschmid/mt-bench(80 prompts,output_len=512,temperature=0,max_concurrency=4)vLLM version: v0.1.dev14014+g6963a1497 (branch
dsl, mean batch policy)Model pair 1:
meta-llama/Llama-3.1-8B-Instruct+meta-llama/Llama-3.2-1B-Instruct(mt-bench, c=4)Best DSL configuration: 20 tokens, threshold=0.4 → 181.35 tok/s, 1.32× vs target, 1.63× vs SD
Note: SD is slower than target at c=4 (0.81×); 10-token DSL configs mostly unstable (>70% failures).
Model pair 2:
facebook/opt-6.7b+facebook/opt-125m(mt-bench, c=4, output_len=256)Best DSL configuration: 10 tokens, threshold=0.8 → 299.28 tok/s, 1.55× vs SD
Observations
output_len=512at single concurrency, SD/5 is roughly on par with target for Llama (1.06×). DSL exits early, avoiding unnecessary speculative compute: 10t/0.6 → 1.62× vs target, 1.53× vs SD.output_len=1024(original 10-prompt runs), DSL yields large gains (up to +44% on Llama, up to +64% on OPT). Long outputs give many draft steps for confident exits.anytomeanmatters most at concurrency > 1. A single uncertain request no longer short-circuits drafting for the whole batch.References
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.cc: @orenpereg @keyboardAnt