[MRV2][Spec] Fuse AR speculator multi-step decodes back into one CUDA graph - #46849
Conversation
cf2e0b8 to
f418e74
Compare
|
Thanks for this optimization, it would be really awesome to have this. However, i believe we need to gate this fully-captured decode loop functionality on whether all attention backends for the model support vllm/vllm/v1/attention/backends/flash_attn.py Lines 505 to 521 in 735def4 Right now only sparse SWA seems to support this refresh. To safely enable this optimization for Deepseek v4 MTP we need to make sure that all of its backends support this refresh. |
|
This pull request has merge conflicts that must be resolved before it can be |
149c3ec to
048cc52
Compare
@TheEpicDolphin Thanks for the review! Now I understand it's not just about Firstly, a commit will add the configuration and per-backend capability gating with an automatic fallback to the existing per-step rebuild path. The next commit will add the required metadata refresh for FA3, and then I will audit the remaining EAGLE/MTP attention backends for similar sequence- or position-dependent state and see if there are other adjustments required. |
c58fa87 to
23b21e6
Compare
23b21e6 to
6ff1d40
Compare
Signed-off-by: Yizhou Liu <liu_yizhou@outlook.com>
Signed-off-by: Yizhou Liu <liu_yizhou@outlook.com>
Signed-off-by: Yizhou Liu <liu_yizhou@outlook.com>
Signed-off-by: Yizhou Liu <liu_yizhou@outlook.com>
Signed-off-by: Yizhou Liu <liu_yizhou@outlook.com>
This became necessary after vllm-project#48892 made padded idx_mapping entries persist as -1 for all draft sampling modes. Signed-off-by: Yizhou Liu <liu_yizhou@outlook.com>
6dd1a39 to
fc8ceaf
Compare
Signed-off-by: Yizhou Liu <liu_yizhou@outlook.com>
|
/ci run |
|
✅ Triggered Buildkite CI #83294 for commit |
TheEpicDolphin
left a comment
There was a problem hiding this comment.
LGTM! cc: @ZJY0516
Builds on vLLM PR vllm-project#46849, which restores fused multi-step draft decode graphs for autoregressive speculative decoding when every attention backend can update step-dependent draft metadata in place. That PR left DeepSeek V4 ROCm sparse SWA disabled because the AMD path also materializes ROCm-specific ragged SWA indices and indptrs. Enable the ROCm DeepSeek V4 AITER SparseSWA metadata builder for fused draft decode by refreshing its ragged SWA representation after the generic dense SWA update. The refresh writes directly into the persistent metadata buffers returned by build(), avoiding temporary ragged flat/indptr allocations so captured HIPGraph replay keeps stable tensor storage addresses. Add focused tests for the direct-to-buffer helper and in-place metadata update. Validation used TP4 on MI355X against patched and control images built from vllm/main@c4e9692. Runtime logs confirmed the patched image used the fused speculator graph path, while control logged fallback because DEEPSEEK_SPARSE_SWA did not support fused multi-step draft decode metadata updates. Smoke, GSM8K, c16, and c128 runs completed with no HIPGraph/cudagraph replay errors, illegal memory access, stale metadata symptoms, or failed benchmark requests. Serve command shape: VLLM_ROCM_USE_AITER=1 SAFETENSORS_FAST_GPU=1 \ vllm serve deepseek-ai/DeepSeek-V4-Flash \ --tensor-parallel-size 4 \ --attention_backend ROCM_AITER_UNIFIED_ATTN \ --compilation-config '{"mode":3,"cudagraph_mode":"FULL_DECODE_ONLY"}' \ --speculative-config '{"method":"mtp","num_speculative_tokens":3,"attention_backend":"ROCM_AITER_UNIFIED_ATTN"}' \ --kv-cache-dtype fp8 \ --distributed-executor-backend mp \ --max_model_len 4096 \ --max-num-batched-tokens 8192 \ --max_num_seqs 256 \ --tokenizer-mode deepseek_v4 \ --tool-call-parser deepseek_v4 \ --trust-remote-code Accuracy command shape: lm_eval --model local-completions \ --tasks gsm8k \ --num_fewshot 8 \ --model_args model=deepseek-ai/DeepSeek-V4-Flash,base_url=http://localhost:<PORT>/v1/completions,num_concurrent=16,tokenized_requests=False Accuracy results, full GSM8K 8-shot, no limit: patched flexible/strict: 0.9492 / 0.9500 control flexible/strict: 0.9416 / 0.9424 Benchmark command shapes: vllm bench serve --model deepseek-ai/DeepSeek-V4-Flash \ --dataset-name random \ --random-input-len 512 \ --random-output-len 1024 \ --temperature 0 \ --ignore-eos \ --max-concurrency 16 \ --num-prompts 160 \ --num-warmups 16 vllm bench serve --model deepseek-ai/DeepSeek-V4-Flash \ --dataset-name random \ --random-input-len 512 \ --random-output-len 1024 \ --temperature 0 \ --ignore-eos \ --max-concurrency 128 \ --num-prompts 1024 \ --num-warmups 128 Performance results, output throughput: c16 patched/control: 2000.58 / 1958.82 tok/s (+2.13%) c128 patched/control: 7852.55 / 7751.43 tok/s (+1.30%) Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Tres Popp <tres.popp@amd.com>
Purpose
This PR restores fused multi-step CUDA graph execution for autoregressive speculative decoding in Model Runner V2.
#41162 fixed stale attention metadata by rebuilding it and replaying a separate CUDA graph for every draft step. While correct, that design reintroduces per-step Python dispatch, metadata construction, and CUDA graph launch overhead.
This PR instead captures the post-prefill draft loop into one CUDA graph. Attention metadata is built once, and backend-owned step-dependent metadata is updated in place between draft steps.
Additional Background
Our local tests suggest the historical illegal memory access is not fully explained by stale draft metadata:
Design
Design of #41162:
sequenceDiagram participant Spec as AutoRegressiveSpeculator participant Inputs as Draft Input Buffers participant Builder as Attention Metadata Builder participant Graph as Draft CUDA graph loop draft_step = 0..N-1 Spec->>Builder: _build_draft_attn_metadata() Builder-->>Spec: fresh attn_metadata Spec->>Graph: replay per-step draft graph Spec->>Inputs: update_draft_inputs() endDesign of this PR:
sequenceDiagram participant Spec as AutoRegressiveSpeculator participant Input as Draft input buffers participant Attn as Attention metadata builder participant Graph as Fused draft CUDA graph Spec->>Attn: Build metadata once Spec->>Graph: Replay fused multi-step graph loop Each captured draft step Graph->>Graph: Draft model forward and sampling Graph->>Input: Advance draft inputs and positions Graph->>Attn: `update_draft_decode_metadata()` Attn-->>Attn: Update backend-derived metadata endBackends opt in through
supports_draft_decode_metadata_update. The default is disabled, so unadapted backends retain the split-graph path and rebuild metadata between draft steps.update_draft_decode_metadata()is executed while the fused graph is being captured. Implementations must emit CUDA graph capture-safe operations and keep replayed tensor state in persistent storage; Python code is not executed during replay.Current backend handling:
Test Plan
Tested on:
Online serving: device-bound workload
The online serving sweep used:
The baseline forces
use_fused_multi_step_decode=False; the fused runs use normal backend capability selection. All 24 runs completed without failed requests.For DeepSeek-V4-Flash:
For Qwen3.5-9B:
The acceptance-normalized ITL delta compares
median ITL * acceptance length, which approximates the time per speculative scheduler step and removes the effect of small acceptance-rate differences between paired runs.The normalized results fluctuate in both directions and show no systematic regression so we cannot conclude it has performance gain. My guess is that these workloads remain device-bound, so reducing host dispatch helps little.
Host-bound profiling
To expose host dispatch overhead, Qwen3.5-9B was profiled with TP=2 and
torch_profiler_with_stack=True.The table below averages rank 0
ProfilerStep#5throughProfilerStep#7, excluding the first profiled iteration.proposeCPU spanWith three speculative tokens, the split path launches one target graph, one draft-prefill graph, and two draft-decode graphs per scheduler step. The fused path replaces the two draft-decode launches with one multi-step graph.
Trace screenshots
Split graph: Each post-prefill draft step returns to Python, rebuilds attention metadata, and launches a separate draft graph. In the stable rank 0 iterations,
_multi_step_decodetakes 1.929–2.149 ms.Fused graph: Both post-prefill draft steps execute inside one CUDA graph. Input advancement and backend metadata updates are captured as part of that graph. In the same rank 0 interval,
_fused_multi_step_decodetakes 0.848–0.883 ms.Raw traces for both TP ranks and the profiler summary outputs are attached here:
fused-graph.zip
split-graph.zip
Per-step Metadata Check
The following values were captured with temporary debug instrumentation. The
final PR does not keep this logging path.
With refresh enabled, SWA metadata advances from draft step 1 to draft step 2:
With refresh disabled, draft step 2 keeps step-1 SWA metadata:
So the refresh path updates the same SWA metadata that the split-graph path would rebuild.
is_valid_tokenstays unchanged, as expected, because the padded request set is unchanged across draft steps.Other things
AI assistance was used for code review, performance-data aggregation. The human author made and reviewed the changes and ran the evaluations above.
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.