Skip to content

[BugFix] Fix DP token padding in dflash attention metadata - #56181

Merged
njhill merged 1 commit into
vllm-project:mainfrom
TheEpicDolphin:fix-dflash-draft-attention-dp-padding
Sep 11, 2026
Merged

njhill merged 1 commit into
vllm-project:mainfrom
TheEpicDolphin:fix-dflash-draft-attention-dp-padding

Conversation

@TheEpicDolphin

@TheEpicDolphin TheEpicDolphin commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR follows up on the fix in #55458, but for dflash-derived speculators. While serving DFlash/DSpark models with DP > 1, the extra padding tokens from the DP sync can cause a mismatch between the number of query tokens reported in query_start_loc_cpu and the number reported by num_tokens in the attention metadata. When this happens, it can trigger the following assertion for the FlashInfer backend:

File "vllm/v1/worker/gpu/model_runner.py", line 2020, in sample_tokens
  draft_tokens = self.speculator.propose(
File "vllm/v1/worker/gpu/spec_decode/dflash/speculator.py", line 491, in propose
  self._generate_draft(
File "vllm/v1/worker/gpu/spec_decode/dflash/speculator.py", line 255, in _run_model
  last_hidden_states = self.model(
File "vllm/model_executor/models/qwen3_dflash.py", line 348, in forward
  hidden_states = self.self_attn(
File "vllm/v1/attention/backends/flashinfer.py", line 2213, in forward
  prefill_wrapper.run(
File "flashinfer/prefill.py", line 2825, in run
  raise ValueError
ValueError: q.shape[0] (52) does not match qo_indptr[-1] (48). For paged prefill,
q must have shape [total_tokens, num_heads, head_dim] where total_tokens = qo_indptr[-1].

I refactored the way we build the attention metadata for the speculators so that this mismatch no longer happens in practice. I created a helper (_build_uniform_attn_metadata) which is called by the DFlash and DSpark speculators during proposal of their N draft tokens, and during the last N-1 single draft decodes for MTP and EAGLE. _build_uniform_attn_metadata ensures that the attention metadata is built with the padded tokens count (from the batch descriptor) during FULL cudagraph, and the actual number of query tokens otherwise.

After this fix, the above error is no longer triggered by DFlash/DSpark during startup.

Test Plan

Benchmark: vllm bench serve --dataset-name speed_bench --dataset-path <speed-bench> --speed-bench-dataset-subset throughput_2k --speed-bench-category low_entropy --speed-bench-output-len 1024 --num-prompts 256 --num-warmups 32 --max-concurrency 64 --request-rate inf --temperature 0
GSM8K: tests/evals/gsm8k/gsm8k_eval.py --num-questions 200 --temperature 0
"before" = this PR's parent (6983a08). 256/256 requests OK and 0 server errors in every row.

Base + draft DP Accept. length Accept. rate (%) Output tok/s Mean TPOT (ms)
Qwen3-Coder-30B-A3B-Instruct + -DFlash 1 2.76 → 2.76 58.71 → 58.81 5241 → 5303 11.02 → 10.97
Qwen3-Coder-30B-A3B-Instruct + -DFlash 2 crash → 2.73 crash → 57.71 crash → 4944 crash → 11.94
Inkling-Small-NVFP4 (MTP-8), TP=2 1 3.72 → 3.69 33.97 → 33.68 4395 → 4443 13.24 → 13.02
Inkling-Small-NVFP4 (MTP-8), TP=2, eager 2 3.65 → 3.65 33.14 → 33.14 1397 → 1406 40.42 → 40.51
Qwen3.8-27B + Qwen3.8-27B-DFlash2 1 3.92 → 3.89 41.65 → 41.29 3063 → 3042 18.48 → 18.62
DeepSeek-V4-Flash + MTP, TP=4 1 2.64 → 2.63 54.78 → 54.29 4122 → 4743¹ 14.59 → 12.60¹
DeepSeek-V4-Flash-DSpark, TP=4 1 3.89 → 3.87 41.29 → 41.01 4185 → 4516¹ 13.84 → 13.11¹

Acceptance length is unchanged or within 0.03 everywhere; acceptance rate within 0.5pp;
throughput and TPOT within ±1.7% except ¹. GSM8K accuracy unchanged within run-to-run
variance (±3pp at n=200, measured by repeating identical configs).

Comment on lines -295 to -320
def _build_draft_attn_metadata(
self,
num_reqs: int,
num_reqs_padded: int,
num_tokens_padded: int,
seq_lens_cpu_upper_bound: torch.Tensor,
step: int,
num_query_per_req: int | None = None,
causal: bool | Mapping[int, bool] = False,
query_start_loc_np: np.ndarray | None = None,
dcp_local_seq_lens: torch.Tensor | None = None,
) -> dict[str, Any] | None:
if not self.draft_attn_layer_names:
return None
assert num_query_per_req is None # Omitted for DFlash, read from self instead
return super()._build_draft_attn_metadata(
num_reqs,
num_reqs_padded,
num_tokens_padded,
seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
step=step,
num_query_per_req=self.num_query_per_req,
causal=causal,
query_start_loc_np=query_start_loc_np,
dcp_local_seq_lens=dcp_local_seq_lens,
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed

@TheEpicDolphin
TheEpicDolphin force-pushed the fix-dflash-draft-attention-dp-padding branch 2 times, most recently from 0020859 to 0e86e6a Compare September 10, 2026 07:11
@TheEpicDolphin
TheEpicDolphin marked this pull request as ready for review September 10, 2026 07:12

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@WoosukKwon WoosukKwon added the ready ONLY add when PR is ready to merge/full CI is needed label Sep 10, 2026
@WoosukKwon

Copy link
Copy Markdown
Collaborator

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #88210 for commit 0e86e6ac67e1.

@mergify

mergify Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @TheEpicDolphin.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 11, 2026
Signed-off-by: Giancarlo Delfin <gdelfin@inferact.ai>
@TheEpicDolphin
TheEpicDolphin force-pushed the fix-dflash-draft-attention-dp-padding branch from 0e86e6a to 1a56609 Compare September 11, 2026 17:40
@TheEpicDolphin

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #88386 for commit 1a566092100e.

@mergify mergify Bot removed the needs-rebase label Sep 11, 2026
@njhill
njhill merged commit 9dcf6bf into vllm-project:main Sep 11, 2026
119 of 120 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Sprint - DFlash Sep 11, 2026
@TheEpicDolphin
TheEpicDolphin deleted the fix-dflash-draft-attention-dp-padding branch September 11, 2026 21:39
lucifer1004 added a commit to lucifer1004/vllm that referenced this pull request Sep 11, 2026
Drop the DFlash _build_draft_attn_metadata override following upstream
(vllm-project#56181) which moved its logic into DraftModelSpeculator and switched
propose() to _build_uniform_attn_metadata.

Co-authored-by: Kimi Code <noreply@moonshot.cn>
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
ItsRoy69 pushed a commit to ItsRoy69/vllm that referenced this pull request Sep 15, 2026
Tflowers-0129 pushed a commit to vllm-project/vllm-ascend that referenced this pull request Sep 23, 2026
### What this PR does / why we need it?

#### Change Summary

The PR advances the main2main lane to vLLM v0.30.0 (commit
`ced6857afa0ea7b2e3f0846a62e1394e90f15607`), adapting vllm-ascend to
every upstream change in the `84030bbe` -> `4991f97` -> `ced6857` range.
Because both CI lanes now install vLLM v0.30.0, all
`vllm_version_is("0.29.0")` forks are permanently false and are
collapsed to the main behavior.

| Files | Upstream vLLM change | vllm-ascend adaptation |
|-------|---------------------|------------------------|
| `.github/vllm-main-verified.commit` | — | Updated verified main commit
hash `84030bbe` -> `4991f97` -> `ced6857` (v0.30.0 tag commit) |
| `.github/vllm-release-tag.commit` | — | Bumped the release boundary to
`v0.30.0` |
| `.github/workflows/pr_test.yaml` | — | Added a dual-version cpu-ut
matrix (`vllm_versions`) for the main2main lane; temporarily commented
out pre-commit/mypy and set `cpu-ut` to `if: false`; dropped the
`needs.cpu-ut` requirement from the ready gate; added a
main2main-specific cpu-ut failure hint |
| `Dockerfile` | — | `VLLM_TAG` default -> `v0.30.0` |
| `Dockerfile.310p` | — | `VLLM_TAG` default -> `v0.30.0` |
| `Dockerfile.310p.openEuler` | — | `VLLM_TAG` default -> `v0.30.0` |
| `Dockerfile.a3` | — | `VLLM_TAG` default -> `v0.30.0` |
| `Dockerfile.a3.openEuler` | — | `VLLM_TAG` default -> `v0.30.0` |
| `Dockerfile.a5` | — | `VLLM_TAG` default -> `v0.30.0` |
| `Dockerfile.a5.openEuler` | — | `VLLM_TAG` default -> `v0.30.0` |
| `Dockerfile.openEuler` | — | `VLLM_TAG` default -> `v0.30.0` |
| `README.md` | — | CI notes updated to v0.30.0 |
| `README.zh.md` | — | CI notes updated to v0.30.0 |
|
`tests/e2e/nightly/single_node/ops/singlecard_ops/triton/test_num_nans.py`
| — | `VLLM_VERSION` default 0.29.0 -> 0.30.0 |
| `tests/ut/_310p/test_model_runner_v2_310p.py` | v0.30.0 boundary |
Dropped the `vllm_version_is` gate test; `_needs_kv_cache_zeroing_310p`
always uses `spec_config.use_eagle_block_drop()` |
| `tests/ut/core/test_dyntra_lb_scheduler.py` | — | Removed the 0.29
`KVConnectorBlockState.block_ids` assertions; assert the `req_ids` form
only |
| `tests/ut/core/test_scheduler_connector_block_state.py` | — | Removed
the 0.29 `block_ids` snapshot branch |
| `tests/ut/kv_offload/test_native_cpu_offload.py` | — | Block -> chunk
API: assert `spec.num_chunks` |
| `tests/ut/kv_offload/test_npu_offload_spec.py` | — |
`num_blocks`/`kv_bytes_per_block` -> `num_chunks`/`kv_bytes_per_chunk` |
| `tests/ut/models/test_deepseek_v41_registration.py` |
[vllm#56741](vllm-project/vllm#56741) | Removed
the V4.1 0.29 skip; registration is always active |
| `tests/ut/patch/platform/test_patch_mamba_block_aligned_split.py` | —
| Removed the 0.29 gate for `mamba_fine_grained_prefix_cache` |
| `tests/ut/patch/platform/test_patch_speculative_config_dspark.py` |
[vllm#55914](vllm-project/vllm#55914) /
[vllm#56930](vllm-project/vllm#56930) | Removed
the DeepSeek V4.1 0.29 `pytest.skip` |
| `tests/ut/patch/platform/test_patch_use_v2_model_runner.py` |
[vllm#54523](vllm-project/vllm#54523) | Deleted
the 0.28.0 PCP+DP validator workaround tests; reworked the
version-routing fixtures |
| `tests/ut/patch/worker/test_patch_dspark_pp.py` | — | Parametrize
`legacy: bool` instead of version strings |
| `tests/ut/quantization/configs/test_modelslim_config.py` |
[vllm#56741](vllm-project/vllm#56741) | Removed
the V4.1 0.29 skip |
| `tests/ut/spec_decode/test_dspark_proposer.py` |
[vllm#56741](vllm-project/vllm#56741) | Removed
the V4.1 0.29 skip marker |
| `tests/ut/spec_decode/test_eagle_proposer.py` | — | `uses_xdrope_dim`
-> `mrope_num_dims` |
| `tests/ut/test_compressed_prefix_cache.py` | — | `replay_boundaries`
now unconditional |
| `tests/ut/worker/test_encoder_acl_graph.py` | — | `axis_keys=()` now
unconditional |
| `tests/ut/worker/test_model_runner_v1.py` |
[vllm#56741](vllm-project/vllm#56741) | Removed
the V4.1 0.29 skips and the `vllm_version_is` patches |
| `tests/ut/worker/test_model_runner_v2.py` | v0.30.0 boundary |
Version-routing fixture rework |
| `tests/ut/worker/test_pcp_manager_v2.py` |
[vllm#56107](vllm-project/vllm#56107) /
[vllm#53867](vllm-project/vllm#53867) | Dropped
the 0.29 `req_states` coverage / added `padded_num_reqs` coverage;
removed the 0.28/0.29 branches |
| `tests/ut/worker/v2/test_pp_utils.py` | — | Replaced the
version-routing matrix with `use_legacy_spec_pp() is False` |
| `vllm_ascend/_310p/model_runner_310p.py` | — | Removed the 0.29
xdrope-position branch |
| `vllm_ascend/_310p/spec_decode/llm_base_proposer_310.py` | — | Removed
the 0.29 xdrope `target_positions[0]` squeeze |
| `vllm_ascend/_310p/worker/v2/model_runner.py` |
[vllm#57270](vllm-project/vllm#57270) | Removed
the 0.29 `max_seq_len_np` kwarg and the 0.29 eagle-block-drop branch |
| `vllm_ascend/_310p/worker/v2/rope.py` | — | mrope `num_dims =
model_config.mrope_num_dims` (dropped the 0.29 constant 3) |
| `vllm_ascend/_310p/worker/v2/states.py` |
[vllm#56908](vllm-project/vllm#56908) |
`UvaBuffer.uva` property -> method; final v0.30.0 form |
| `vllm_ascend/attention/mla_v1.py` |
[vllm#56181](vllm-project/vllm#56181) | Draft
TND_NTD layout forced via `_EXTRA_CTX.is_draft_model`; dropped the 0.29
gate |
| `vllm_ascend/attention/utils.py` |
[vllm#55353](vllm-project/vllm#55353) /
[vllm#56157](vllm-project/vllm#56157) |
Ascend-owned
`_seq_lens_cpu`/`_num_computed_tokens_cpu`/`dcp_local_seq_lens_cpu` are
now unconditional |
| `vllm_ascend/batch_invariant.py` | — | `reduce_sum` accepts
NumPy-style `axis` + `dtype`, rejects `dim`+`axis` together, forwards
`dtype` to the native fallback |
| `vllm_ascend/core/dyntra_lb_scheduler.py` | — |
`KVConnectorBlockState` always uses `req_ids`+`resolve_block_ids`
(dropped the 0.29 `block_ids` snapshot) |
| `vllm_ascend/core/kv_cache_interface.py` |
[vllm#53906](vllm-project/vllm#53906) | MLA
`get_storage_block_size` override unconditional; dropped the 0.29
`storage_block_size` property |
| `vllm_ascend/core/recompute_scheduler.py` | — | Dropped the xdrope
kwarg and the 0.29 `block_ids` snapshot |
| `vllm_ascend/core/scheduler_profiling_chunk.py` | — | Dropped the 0.29
`block_ids` snapshot |
| `vllm_ascend/distributed/kv_transfer/kv_pool/kv_offload/native/npu.py`
| — | Block -> chunk API (`num_chunks`, `kv_bytes_per_chunk`)
unconditional |
| `vllm_ascend/lora/punica_npu.py` |
[vllm#53555](vllm-project/vllm#53555) + boundary
| `add_lora_logits` per-adapter matmul fallback for heads smaller than
the rank; final state drops the `apply_lora_full_linear` binding (both
supported targets predate #53555) |
| `vllm_ascend/models/__init__.py` |
[vllm#56741](vllm-project/vllm#56741) | V4.1
registration (`DeepseekV41ForCausalLM`/`DSparkModel`) unconditional |
| `vllm_ascend/models/deepseek_v41/engram/embedding.py` |
[vllm#56741](vllm-project/vllm#56741) |
`deepseek_v4_1` -> `deepseek_v41` import unconditional |
| `vllm_ascend/models/deepseek_v41/engram/hash_state.py` |
[vllm#56741](vllm-project/vllm#56741) |
`deepseek_v4_1` -> `deepseek_v41` import unconditional |
| `vllm_ascend/models/deepseek_v41/engram/parallel.py` |
[vllm#56741](vllm-project/vllm#56741) |
`deepseek_v4_1` -> `deepseek_v41` import unconditional |
| `vllm_ascend/models/deepseek_v41/model.py` |
[vllm#56741](vllm-project/vllm#56741) |
`deepseek_v4_1` -> `deepseek_v41` import unconditional |
| `vllm_ascend/models/deepseek_v41/vl_model.py` |
[vllm#56741](vllm-project/vllm#56741) /
[vllm#56554](vllm-project/vllm#56554) |
`deepseek_v41` imports; drop `IMAGE_PAD_ID`/alignment-pad handling on
main |
| `vllm_ascend/ops/mla.py` |
[vllm#56157](vllm-project/vllm#56157) |
`MLAAttention.supports_pcp_dcp = True` set on the class, unconditional |
| `vllm_ascend/ops/rotary_embedding.py` |
[vllm#56446](vllm-project/vllm#56446) | YaRN
mscale signature adaptation; v0.29 branch collapsed |
| `vllm_ascend/patch/__init__.py` |
[vllm#55914](vllm-project/vllm#55914) /
[vllm#56930](vllm-project/vllm#56930) | Registry
entry for the new draft-EP patch; dropped version gates |
| `vllm_ascend/patch/platform/__init__.py` |
[vllm#56741](vllm-project/vllm#56741 Engram
| `patch_engram_config` imported unconditionally |
| `vllm_ascend/patch/platform/patch_balance_schedule.py` | — | Dropped
the 0.29 `block_ids` snapshot |
| `vllm_ascend/patch/platform/patch_kv_cache_coordinator.py` |
[vllm#54736](vllm-project/vllm#54736) + boundary
| Accept/forward `allow_partial_hash_hits`; collapsed the 0.29 gate |
| `vllm_ascend/patch/platform/patch_parallel_config.py` |
[vllm#54523](vllm-project/vllm#54523) | Removed
the 0.29 `_validate_parallel_config` PCP+DP workaround; keeps
`use_sequence_parallel_moe` |
| `vllm_ascend/patch/platform/patch_speculative_config.py` |
[vllm#55914](vllm-project/vllm#55914) without
[vllm#56930](vllm-project/vllm#56930) | Skip
`_verify_with_expert_parallelism` for non-MoE draft
(`runner_type=="draft"`) |
| `vllm_ascend/patch/platform/patch_use_v2_model_runner.py` |
[vllm#54523](vllm-project/vllm#54523) | Removed
the 0.28.0 PCP+DP validation workaround |
| `vllm_ascend/patch/worker/patch_bind_kv_cache.py` |
[vllm#53781](vllm-project/vllm#53781) | Ascend
`bind_kv_cache_to_layers` (assign the raw allocation); collapsed the
0.29 gate |
| `vllm_ascend/patch/worker/patch_deepseek_v2.py` |
[vllm#53781](vllm-project/vllm#53781) |
Accept/ignore `index_group_builder`; `SparseMLAIndexGroupBuilder` import
collapse |
| `vllm_ascend/patch/worker/patch_mamba_utils.py` |
[vllm#56898](vllm-project/vllm#56898) |
`GPUInputBatch` import source version-gated, then collapsed to
`gpu_input_batch.InputBatch` |
| `vllm_ascend/patch/worker/patch_v2/patch_attn_utils.py` |
[vllm#53781](vllm-project/vllm#53781) | Register
Ascend `bind_kv_cache_to_layers`; expose tuple element 0 for the device
filter; collapsed the 0.29 gate |
| `vllm_ascend/patch/worker/patch_v2/patch_dspark.py` | — | Legacy
Spec+PP bypass comment (inactive on v0.30.0) |
| `vllm_ascend/patch/worker/patch_v2/patch_spec_pp.py` |
[vllm#56888](vllm-project/vllm#56888) | Alias
`async_tensor_h2d as async_copy_to_gpu`; collapsed the 0.29 gate |
| `vllm_ascend/patch/worker/patch_v2/patch_uva.py` |
[vllm#56908](vllm-project/vllm#56908) | `uva`
property vs method; final v0.30.0 method form |
| `vllm_ascend/spec_decode/llm_base_proposer.py` |
[vllm#56254](vllm-project/vllm#56254) +
[vllm#55914](vllm-project/vllm#55914) /
[vllm#56930](vllm-project/vllm#56930) + boundary
| Gate the V4.1 DSpark import by `HAS_TRITON`; static
`_draft_embed_accepts_mm` check instead of the runtime `embed_input_ids`
probe; collapsed version gates |
| `vllm_ascend/utils.py` | — | `vllm_version_is` docstring 0.29 -> 0.30;
Kimi MLA custom-op registration unconditional |
| `vllm_ascend/worker/model_runner_v1.py` |
[vllm#56741](vllm-project/vllm#56741) | V4.1 dsa
metadata/cache imports unconditional; removed xdrope position handling |
| `vllm_ascend/worker/v2/aclgraph_utils.py` |
[vllm#51700](vllm-project/vllm#51700) |
`ModelAclGraphManager.__init__` accepts/forwards `ubatch_runner`;
`UBatchRunner` import collapse |
| `vllm_ascend/worker/v2/model_runner.py` |
[vllm#56888](vllm-project/vllm#56888) /
[vllm#53867](vllm-project/vllm#53867) /
[vllm#51700](vllm-project/vllm#51700) /
[vllm#57270](vllm-project/vllm#57270) +
[vllm#56181](vllm-project/vllm#56181) + boundary
| Async-copy alias; pass `BatchExecutionDescriptor` to
`maybe_partition_pcp_batch`; `ubatch_runner`; `make_dummy(is_padding)`;
keep the replicated PCP draft on the global batch; collapsed version
gates. Also restores the `_check_oproj_tp_graph_step` guard and the PD
decode-recompute `gather_batch_req_state` override accidentally removed
by the v0.28.0-boundary cleanup (`e30adae80`) |
| `vllm_ascend/worker/v2/pcp_manager.py` |
[vllm#56888](vllm-project/vllm#56888) /
[vllm#56107](vllm-project/vllm#56107) /
[vllm#53867](vllm-project/vllm#53867) +
[vllm#56181](vllm-project/vllm#56181) + boundary
| Async alias; drop `req_states`; forward `padded_num_reqs`;
`prepare_draft_prefill` no-op / `restore_for_sampling` skip; collapsed
version gates |
| `vllm_ascend/worker/v2/pp_utils.py` | — | `use_legacy_spec_pp()`
returns `False` |
| `vllm_ascend/worker/v2/spec_decode/autoregressive/speculator.py` |
[vllm#56181](vllm-project/vllm#56181) | Route
through `_build_uniform_attn_metadata`/`_build_attn_metadata`; re-hook
the Ascend rotary-positions injection onto the new methods; dropped the
0.29 gate |
| `vllm_ascend/worker/v2/spec_decode/dflash/speculator.py` |
[vllm#56181](vllm-project/vllm#56181) | Same
`BatchExecutionDescriptor` routing; dropped the 0.29 gate |
| `vllm_ascend/worker/v2/spec_decode/dspark/speculator.py` |
[vllm#56181](vllm-project/vllm#56181) | Same
routing; dropped the 0.29 gate |

### Does this PR introduce _any_ user-facing change?

### How was this patch tested?


- vLLM main:
vllm-project/vllm@84030bb

---------

Signed-off-by: hfadzxy <starmoon_zhang@163.com>
Co-authored-by: zhao-stack <80399320+zhao-stack@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dflash mrv2 Model Runner V2 specific ready ONLY add when PR is ready to merge/full CI is needed speculative-decoding

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants