Repository navigation
[Spec Decode][MRV2] Skip drafting in steps where no request can use the drafts - #62784
Merged
Merged
Conversation
…he drafts In steps where no scheduled request can use the drafts proposed after the step (non-final prefill chunks, and requests sure to reach max_tokens), the scheduler sets SchedulerOutput.skip_drafts. DP ranks AND the flag in the existing batch-descriptor all-reduce, and the V2 model runner then proposes with K=0: the AR speculator skips draft sampling and the draft decode steps, and DFlash stores the context K/V and skips its query pass. Continues vllm-project#48244. Co-authored-by: Nick Hill <nickhill123@gmail.com> Signed-off-by: Nick Hill <nickhill123@gmail.com> Signed-off-by: zixi-qi <zixi@inferact.ai> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zixi-qi
force-pushed
the
zixi/skip-drafting
branch
from
October 21, 2026 01:47
dd571c3 to
64f2091
Compare
zixi-qi
marked this pull request as ready for review
October 21, 2026 16:18
zixi-qi
requested review from
ApostaC,
TheEpicDolphin,
WoosukKwon,
ZJY0516,
alexm-redhat,
heheda12345,
ivanium,
njhill,
orozery,
robertgshaw2-redhat,
yewentao256 and
ywang96
as code owners
October 21, 2026 16:18
Member
|
/ci run |
|
✅ Triggered Buildkite CI #96559 for commit ℹ️ This PR is 85 commits behind upstream |
Collaborator
Author
|
/ci run |
|
✅ Triggered Buildkite CI #96571 for commit |
6 tasks done
Collaborator
Author
|
/ci retry |
|
✅ Queued 2 failed job(s) for retry in Buildkite CI #96571. |
Member
|
The failing tests seem related, but should be an easy fix |
The DP all-reduce now carries a seventh row, each rank's allow_skip_drafts vote, so the stubbed reduction in test_gpu_ubatch_slicing.py must have seven rows too. The stub votes no, which keeps these microbatching tests unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: zixi-qi <zixi@inferact.ai>
Collaborator
Author
|
/ci run |
|
✅ Triggered Buildkite CI #96715 for commit ℹ️ This PR is 51 commits behind upstream |
Collaborator
Author
|
/ci retry |
|
✅ Queued 2 failed job(s) for retry in Buildkite CI #96715. |
6 tasks done
6 tasks done
6 tasks done
5 tasks
Morrowmake
pushed a commit
to Morrowmake/vllm-cmp170hx
that referenced
this pull request
Oct 26, 2026
Upstream vLLM vllm-project#62784 makes DFlashSpeculator.propose store only the context K/V when a step schedules zero drafts, on every data-parallel rank. Remove the fork hook that did the same for one rank only, and let the DSpark confidence code follow the upstream rule.
Morrowmake
pushed a commit
to Morrowmake/vllm-cmp170hx
that referenced
this pull request
Oct 26, 2026
DFlashSpeculator.propose skips the draft step on every data-parallel rank when a step schedules zero drafts (upstream vLLM vllm-project#62784), and drafts_nothing follows that rule. The slot test still asserted the removed one-rank rule.
linfeng-yuan
pushed a commit
to vllm-project/vllm-ascend
that referenced
this pull request
Oct 27, 2026
### What this PR does / why we need it?
Port `vllm-ascend` to vLLM main `544f182d` (bumped from `c0b6f74`) while
keeping the
`v0.31.0` release lane green. The upstream window drops
`replace_parameter(..., prefer_copy=...)`,
fuses the Kimi-K3 output gate into the QKV-A projection, adds the ROCm
Kimi-K3
specialized-decode hooks, widens the EPLB expert-weight and
rejection-sample contracts, and
adds the DP-drafter `num_reqs_override`, the MRv2 `skip_sample` drafting
skip, the HiSparse
`topk_buffer_width`, the SimpleCPUOffload CPU-block RPC and the abstract
`AsyncModelRunnerOutput.get_kv_connector_output`. Every gate below is
either a
`vllm_version_is("0.31.0")` split or an attribute/signature probe.
| Files | Upstream vLLM change (why it breaks vllm-ascend) | vllm-ascend
adaptation |
|-------|--------------------------------------------------|------------------------|
| `.github/vllm-main-verified.commit` | — (main-lane pin only) |
Verified main pin bumped `c0b6f74` -> `544f182d` |
| `tests/ut/attention/test_mla_prolog_v3_route.py` |
[vllm#60805](vllm-project/vllm#60805) makes
`MLAAttention.get_kv_cache_spec` read `topk_buffer_width` (set in
`__init__`);
[vllm#59634](vllm-project/vllm#59634) makes
`_mla_preprocess` return a 3-tuple | Set `owner.topk_buffer_width =
None` on the stub; mock `_mla_preprocess` as `(None, None, None)` |
| `tests/ut/attention/test_mla_v1.py` |
[vllm#59634](vllm-project/vllm#59634) fuses the
Kimi-K3 output gate as a third QKV-A shard | Add tests for
`qkv_a_gate_fused`/`use_output_gate` and `_split_fused_qkv_lora` gate
peeling; unpack the 3-tuple |
| `tests/ut/models/minimax_m3/test_minimax_m3_vit.py` |
[vllm#60238](vllm-project/vllm#60238) makes
`_mark_tower_model` read `use_v2_model_runner` | Add
`use_v2_model_runner=False` to the constructed runner stub |
| `tests/ut/models/test_kimi_k3_adapter.py` |
[vllm#60683](vllm-project/vllm#60683) adds the
ROCm specialized-decode hooks (`init_specialized_decode`,
`get_model_state_cls`, the `step` forward argument, and `layer.kv_cache`
reassignment via `clear_layer_kv_caches`) | Add main-only tests (`skipif
vllm_version_is("0.31.0")`) covering the inherited hooks |
| `tests/ut/ops/test_routed_experts.py` |
[vllm#61786](vllm-project/vllm#61786) registers
expert weights via `w.view(torch.uint8) if w.element_size() == 1 else w`
| New UT asserting `EplbExpertTensorList.element_size()/view()` keep the
container and its aliasing semantics |
| `tests/ut/patch/platform/test_patch_structured_output.py` |
[vllm#49625](vllm-project/vllm#49625) makes
`StructuredOutputManager.grammar_init` read `backend_name` | Set
`manager.backend_name = None` on the stub |
| `tests/ut/runtime_guard/test_unified_wiring.py` |
[vllm#56430](vllm-project/vllm#56430) adds
`AsyncModelRunnerOutput.get_kv_connector_output` | Add a stub
`get_kv_connector_output` to the fake async output |
| `tests/ut/simple_kv_offload/test_simple_kv_offload.py` |
[vllm#52921](vllm-project/vllm#52921) adds the
`get_simple_cpu_offload_num_cpu_blocks` worker RPC | New main-only UTs
for the aligned / no-group / no-connector paths |
| `tests/ut/spec_decode/test_dflash2_v2.py` |
[vllm#58984](vllm-project/vllm#58984) adds
`num_reqs_override` to `prepare_dflash_inputs_triton` | Update the
expected kwarg; new test for positional forwarding |
| `tests/ut/worker/test_model_runner_v1.py` |
[vllm#60805](vllm-project/vllm#60805)
`topk_buffer_width` | Set `layer.topk_buffer_width = None` on the layer
stub |
| `tests/ut/worker/test_model_runner_v2.py` | Same
[vllm#60805](vllm-project/vllm#60805) | Same
stub attribute |
| `tests/ut/worker/v2/test_speculator.py` |
[vllm#62784](vllm-project/vllm#62784) adds
`skip_sample` to the drafter `_prefill` | Expect/forward `skip_sample`
on main (7 -> 8 positional args) |
| `tests/ut/worker/v2/test_v2_block_table.py` |
[vllm#60767](vllm-project/vllm#60767) makes
`init_block_table_layout_tensors` read `_sliding_window_reach` | Set
`tables._sliding_window_reach = [0, 0]` |
| `vllm_ascend/attention/mla_v1.py` |
[vllm#59634](vllm-project/vllm#59634) fuses the
Kimi-K3 output gate into the QKV-A projection (three shards, no
standalone `g_proj`);
[vllm#62326](vllm-project/vllm#62326) drops
`replace_parameter(..., prefer_copy=)` | Detect the gate-fused layout
from `fused_qkv_a_proj.output_sizes`, disable MLAPO/PROLOG_V3 for it,
bound the KV-A slice with `_fused_qkv_a_kv_end`, peel the gate in
`_split_fused_qkv_lora`; version-split `replace_parameter` |
| `vllm_ascend/attention/sfa_v1.py` |
[vllm#62326](vllm-project/vllm#62326) drops
`replace_parameter(..., prefer_copy=)` | Version-split
`replace_parameter` (keep `prefer_copy=True` on v0.31.0) |
| `vllm_ascend/models/deepseek_v4/indexer.py` |
[vllm#52670](vllm-project/vllm#52670) keeps
`STR_DTYPE_TO_TORCH_DTYPE["fp8"]` as the uint8 storage dtype, so
`kv_cache_dtype_str_to_dtype` cannot validate the indexer's fp8 compute
dtype | Validate `indexer_kv_dtype` against the platform default string
and return the compute dtype directly |
| `vllm_ascend/models/kimi_k3.py` |
[vllm#60683](vllm-project/vllm#60683) adds the
ROCm specialized-decode hooks and reassigns `layer.kv_cache` on teardown
| Add a `kv_cache` setter, call `init_specialized_decode` + override
`get_model_state_cls` to the Ascend hybrid state on main, forward `step`
|
| `vllm_ascend/observability/runtime_guard/hooks.py` |
[vllm#56430](vllm-project/vllm#56430) makes
`AsyncModelRunnerOutput.get_kv_connector_output` required | Implement it
on `AscendAsyncOutput` (main only) |
| `vllm_ascend/ops/fused_moe/routed_experts.py` |
[vllm#61786](vllm-project/vllm#61786) moves
1-byte expert tensors as uint8 views | Add `element_size()`/`view()` to
`EplbExpertTensorList`; widen the `get_expert_weights` return type |
| `vllm_ascend/ops/gdn.py` |
[vllm#62326](vllm-project/vllm#62326) drops
`replace_parameter(..., prefer_copy=)` | Version-split
`replace_parameter` for the packed conv weight |
| `vllm_ascend/ops/kimi_kda.py` | Same
[vllm#62326](vllm-project/vllm#62326) | Same
version-split for the packed conv weight |
| `vllm_ascend/ops/linear.py` | Same
[vllm#62326](vllm-project/vllm#62326) | Same
version-split for `weight_fp32` |
| `vllm_ascend/ops/triton/v2/spec_decode/prepare_dflash_inputs.py` |
[vllm#58984](vllm-project/vllm#58984) forwards
the DP-synced request count during dummy runs | Accept
`num_reqs_override`, cap `num_reqs` and slice `num_scheduled_tokens` |
| `vllm_ascend/worker/v2/spec_decode/autoregressive/speculator.py` |
[vllm#62784](vllm-project/vllm#62784) adds
`skip_sample` to `_prefill` | Version-split `_prefill`, forward
`skip_sample` on main |
| `vllm_ascend/worker/v2/spec_decode/dflash/speculator.py` |
[vllm#58984](vllm-project/vllm#58984) adds
`num_reqs_override` | Forward `num_reqs_override` through both
`prepare_dflash_inputs` wrappers |
| `vllm_ascend/worker/v2/spec_decode/gemma4/speculator.py` |
[vllm#62784](vllm-project/vllm#62784) adds
`skip_sample` | Accept and forward `skip_sample` |
| `vllm_ascend/worker/v2/spec_decode/rejection_sampler_utils.py` |
[vllm#53987](vllm-project/vllm#53987) adds the
FLy entropy-gated verification mode; upstream also threads the
watermarking kwargs | Accept both kwarg groups, but raise
`NotImplementedError` for FLy and watermarking on NPU |
| `vllm_ascend/worker/worker.py` |
[vllm#52921](vllm-project/vllm#52921) adds the
`get_simple_cpu_offload_num_cpu_blocks` RPC to the worker | Implement it
(main only) via
`_find_simple_cpu_offload_connector().get_aligned_num_cpu_blocks()` |
### Does this PR introduce _any_ user-facing change?
### How was this patch tested?
---
### Checklist
#### Testing
- [ ] **Boundary / edge-case coverage**: I reviewed whether this change
involves
boundary conditions (e.g. empty input, single/zero element, min/max
values,
off-by-one, sequence-length limits, integer overflow). Where applicable,
I
added unit tests covering these edge cases in `tests/ut/` or
`tests/e2e/`.
- [ ] **Regression test**: If this PR fixes a bug, I added a regression
test
that would fail without the fix.
- [ ] **Error / failure path coverage**: I covered abnormal paths
(exceptions,
timeouts, out-of-range, division by zero) in addition to the happy path.
- [ ] **Meaningful & reliable tests**: New tests assert real behavior
and are
not written merely to satisfy CI.
- [ ] **Performance impact**: If this PR touches a performance-sensitive
path,
I attached benchmark results and added performance regression tests.
- vLLM main:
vllm-project/vllm@c0b6f74
---------
Signed-off-by: zhangxinyuehfad <starmoon_zhang@163.com>
Signed-off-by: hfadzxy <starmoon_zhang@163.com>
bantianyue
pushed a commit
to bantianyue/vllm
that referenced
this pull request
Oct 28, 2026
…he drafts (vllm-project#62784) Signed-off-by: Nick Hill <nickhill123@gmail.com> Signed-off-by: zixi-qi <zixi@inferact.ai> Co-authored-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: vllm-agent <claw@inferact.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Continues #48244 by @njhill (co-author). In steps where no scheduled request can use the
drafts proposed after the step, the speculator now proposes with K=0 instead of
drafting. Such requests are non-final prefill chunks and requests sure to reach
max_tokens. This covers every request on a P/D prefill instance, which runs withmax_tokens=1.Related: #39266 (draft) skips EAGLE drafting on MRV2 for P/D prefill instances and
non-final chunks; this PR decides in the scheduler and also covers requests about to
finish, DFlash and DP. #53426 makes the K=0 draft sync forward optional; this PR keeps
that forward.
Claims
speculator runs with
num_speculative_tokens=0:in sync, but skip draft sampling and the draft decode steps.
LiLiCorr share this code.
(Qwen3-8B, 64 x 16,384-token prompts, 1 output token, one GB300 GPU) improves by
+1.9% with EAGLE3 and +7.3% with DFlash at
max_num_batched_tokens=512, and by+1.4% and +2.7% at 2048. On a 1P1D prefill instance every step proposes with K=0,
and prefill throughput improves by +2.1% (EAGLE3) and +3.4% (DFlash) at 512, and by
+0.6% and +1.9% at 2048.
spec-decode counter are identical to
main.Validation
Qwen3-8B with
AngelSlim/Qwen3-8B_eagle3(K=3) andz-lab/Qwen3-8B-DFlash-b16(K=16),--attention-backend FLASH_ATTN, GB300. Baseline ismainat532a7d19d2, the base ofthis PR; every run below used the rebased commit.
An earlier version of this description reported +9.0% (EAGLE3) and +3.4% (DFlash) at 512.
Those runs were CPU-starved (the benchmark processes were confined to one CPU core),
which inflated host-side costs. The numbers below replace them.
Prefill-only throughput (total tokens/s; mean of 3 reps on one engine after a warm-up
pass). The workload matches
vllm bench throughput --dataset-name random --random-input-len 16384 --random-output-len 1 --num-prompts 64 --max-model-len 20480.main and this PR ran at the same time on two GPUs of one node, then swapped GPUs. The
table shows the GPU whose reps stayed within 1%; on the other GPU, reps of both builds
varied by up to 30%. With the PR, every proposal in these runs used K=0.
max_num_batched_tokensThe skipped work is draft sampling and the draft decode steps (EAGLE3), or the query pass
(DFlash). Host time spent in
propose()at 512 drops from 1.41 to 0.98 ms per step forEAGLE3 and from 1.05 to 0.92 ms for DFlash; the rest of the saving is GPU time. The
drafter's prefill pass still runs: it writes the draft KV cache that later steps, and on
a P/D decode instance the transferred KV, depend on.
1P1D prefill throughput. P and D each on one GPU with
NixlConnector, the samedrafter on both, P at the given
max_num_batched_tokens. Through the toy proxy, 3phases of 128 random 8,192-token prompts at concurrency 32 (
max_tokens=8). P's waitingqueue was non-empty in at least 89% of samples. main and this PR ran side by side on the
node's two GPU pairs, then swapped; tokens/s and deltas are means over both placements.
max_num_batched_tokensEquivalence (offline, greedy). There are 80 requests: 24 long prompts chunked at 512
tokens with
max_tokens1, 2 or 64, run alone and then mixed with 32 short decoderequests.
VLLM_BATCH_INVARIANT=1VLLM_BATCH_INVARIANT=1Without batch invariance, two runs of
mainalready differ on 23/80 EAGLE3 outputs, andmain vs this PR differ on 31/80. DFlash is deterministic across runs of
main, but mainvs this PR differ on 31/80: in a K=0 step DFlash stores all context K/V in one eager
pass instead of partly inside the FULL graph, so rounding can differ, and acceptance then
changes batch shapes.
Existing e2e tests give the same results on both:
tests/v1/e2e/spec_decode/eagle/test_eagle_correctness.py -k "medium and qwen3_eagle3 and not transformers"tests/v1/e2e/spec_decode/acceptance_rates/dflash/test_dflash.py(MRV2 reference acceptance)tests/v1/e2e/spec_decode/acceptance_rates/dflash/test_dflash.py::test_dflash_correctness[qwen3-mrv2]Serving equivalence. 1P1D with
NixlConnectorused EAGLE3 K=3 on both sides and CUDAgraphs on. DP=2 used EAGLE3 K=3. Requests were sent one at a time.
There were no hangs with DP ranks voting to skip.
Unit tests.
tests/v1/core/test_scheduler.py,tests/v1/core/test_async_scheduler.py,tests/v1/worker/test_gpu_dp_utils.pyandtests/v1/worker/test_gpu_target_dependent_ar_speculator.pypass (281 tests), except twopre-existing PP scheduler tests that fail the same way on
main.Details
SchedulerOutput.skip_draftswhen no scheduled request can usethe step's drafts, on the V2 runner and for drafters that reserve lookahead slots.
_drafts_unusedcounts only the output tokens a request is sure to have, assumingevery draft is rejected, so it never skips drafts a later step could verify.
runs, giving
DPSyncState.skip_drafts. The runner then proposes with K=0. A rankrunning a dummy batch votes no, so an idle DP rank disables the skip for that step.
slots are the next chunk's first positions, which are allocated one step later
anyway. For a finishing request it saves at most about one block for one step.
Keeping it safely means masking every draft KV write, including inside captured
FULL draft-decode graphs; the fused decode path is the default on FlashAttention,
FlashInfer and Triton, and its captured graphs compute their own slot mappings.
num_speculative_tokens == 0path, which already skipsthe AR draft decode steps, instead of a separate flag in each speculator.
skip_sampleand the DFlash early return also apply to K=0 from dynamic speculativedecoding.
still draft in these steps and the drafts go unused, as before. N-gram reserves no
lookahead slots, so the flag is never set for it. MTP, DSpark, DFlash2 and LiLiCorr
share the changed code but were not run; all validation used Qwen3-8B. MRV1 is
unchanged.
Throughput script (one engine, warm-up then timed reps)
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
AI assistance (Claude Code) was used for the rebase, the rework and the validation. I
reviewed and can defend every change.
🤖 Generated with Claude Code