[DeepSeek V2] Reorder dual-stream MoE to main-first to avoid CUDA graph stream explosion - #30460
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
/rerun-test test/registered/models_e2e/test_deepseek_v3_fp4.py test/registered/spec/eagle/test_deepseek_v3_fp4_mtp_small.py test/registered/models_e2e/test_deepseek_v32_fp4_mtp_tp.py test_moe_ep_extra.py |
|
Results for 🚀 🚀 ⛔ |
|
/rerun-test test/registered/quant/test_kimi_k25_nvfp4_eagle.py |
|
Results for 🚀 |
…ph stream explosion DeepseekV2MoE.forward_normal_dual_stream enqueued the shared-expert branch on the alt stream *before* the main (routed) branch. During decode/target- verify CUDA graph capture this alt-first ordering makes cudaGraphInstantiate allocate a fresh side stream per layer, exploding to ~61 streams for Kimi K2.5 (one per layer) -- the same mechanism fixed for the DSA indexer in #30025. Alt-first was originally required (#29463) because the routed deep_gemm pre-permute calls dispose_tensor(hidden_states), which set_()s the storage to empty; a later shared-expert kernel reading hidden_states would then capture data_ptr()==0 into the graph and replay from null. Fix: issue the routed branch first, then the shared expert on the alt stream, and take a storage alias `shared_in = hidden_states[:]` before the routed call so the shared expert reads live storage past dispose_tensor. This satisfies all three constraints at once: no stream explosion (capture reuses one alt stream), PDL overlap preserved (routed is the last main-stream kernel, fuses with the residual add), and the deep_gemm dispose_tensor hazard is avoided. Since shared_output is now computed after the deferred-finalize decision, gate that decision on a precomputed has_shared_output flag instead. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
b765c9e to
bf43480
Compare
bf43480 to
4aa6ec3
Compare
4aa6ec3 to
727d67f
Compare
Follow-up refactor of #30460. Instead of keeping hidden_states alive past the routed deep_gemm's dispose_tensor set_() via a `shared_in = hidden_states[:]` alias, add CaptureFlags.disable_dispose_tensor: model_capture_mode() sets it for the duration of decode/spec graph capture, and dispose_tensor() is a no-op while set. So the routed deep_gemm no longer frees hidden_states mid-capture and the dual-stream shared expert reads it directly. Same effect, no aliasing trick. Keeps #30460's main-first ordering (no CUDA-graph stream explosion). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
727d67f to
29007eb
Compare
|
/rerun-test test/registered/models_e2e/test_deepseek_v3_fp4.py test/registered/spec/eagle/test_deepseek_v3_fp4_mtp_small.py test_moe_ep_extra.py test/registered/quant/test_kimi_k25_nvfp4_eagle.py |
|
Results for 🚀 🚀 |
|
/rerun-failed-ci |
…first-dual-stream # Conflicts: # python/sglang/srt/models/deepseek_v2.py
…E to main-first to avoid CUDA graph stream explosion (sgl-project#30460) (sgl-project#30714) Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jiminator <jimmysh341@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ph stream explosion (sgl-project#30460) Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jiminator <jimmysh341@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Motivation
Profiling Kimi-K2.5-NVFP4 (TP8, EAGLE3 spec decode,
flashinfer_trtllmMoE) showed the target-verify decode CUDA graph fanning out across ~61 streams — one per model layer — instead of the intended 2 (main + alt).Root cause is in
DeepseekV2MoE.forward_normal_dual_stream: the shared-expert branch was enqueued on the alt stream before the main (routed) branch. During CUDA graph capture this alt-first, per-layer ordering makescudaGraphInstantiateallocate a fresh side stream for every layer — the same "stream explosion" mechanism fixed for the DSA indexer in #30025.Alt-first was originally required by #29463: the routed
deep_gemmpre-permute callsdispose_tensor(hidden_states), whichset_()s the storage to empty. A shared-expert kernel readinghidden_statesafterward would capturedata_ptr() == 0into the decode graph and replay from null.Change
Issue the main (routed) branch first, then the shared expert on the alt stream. To keep
deep_gemmcorrect, take a storage aliasshared_in = hidden_states[:]before the routed call runsdispose_tensor, so the shared expert reads live storage. This satisfies all three constraints simultaneously:deep_gemmdispose_tensorhazard avoided — shared readsshared_in, a reference taken pre-dispose.The alias keeps the buffer alive past
dispose_tensor, but decode/verifyhidden_statesis tiny (bs * num_draft_tokensrows vs thousands in prefill), so the lost early free is negligible.Since
shared_outputis now computed after the deferred-finalize decision, that decision is gated on a precomputedhas_shared_outputflag instead ofshared_output is not None.Results
Re-profiled with the same TP8 Kimi-K2.5 config + client. Per full
TARGET_VERIFYforward, TP-0:{2:78, 3:4462, 4:140, 61:63, 62:15}{2:124, 3:32}Perf/correctness unchanged (within noise), confirming the alias preserves shared-expert input:
Test plan
deep_gemmEP gsm8k (test_moe_ep_extra.py) to validate the alias against the [DeepSeek V3] Reland: run routed experts on main stream in dual-stream MoE #29463 path.Refs #29463, #30025.
Made with Cursor
Profile
Before

After
CI States
Latest PR Test (Base): 🚫 Run #29049292993
Latest PR Test (Extra): ❌ Run #29049292883