[Feature] Add DeepEPv2 (ElasticBuffer) MoE A2A backend - #35634
Conversation
|
/tag-and-rerun-ci |
ch-wan
left a comment
There was a problem hiding this comment.
Summary
This reland adds a standalone deepep_v2 MoE A2A backend (ElasticBuffer dispatch/combine, masked-slab Triton kernels, DeepGEMM permute adapters, server-args/env knobs, and capturer classification). The DeepSeek path looks carefully built: layout is chosen by inference phase, the handle lifecycle is fail-closed, the boot-time chunk-vs-cap check is real, and the packed-UE8M0 / CUDA-graph kernel tests guard previously-broken cases. The dominant correctness gap is that deepep_v2 is still not a DeepEP-family backend in is_deepep_class_backend(), so non-DeepSeek MoE models that key A2A forward/setup on that helper (or a raw is_deepep() list) will run FusedMoE's new dispatcher and then extra-reduce. Remaining notes are DX (NCCL symmetric memory) and test-convention nits, not kernel bugs.
Issue counts by severity
- bugs: 1
- suggestions: 5
- nits: 0
| return b.is_deepep() or b.is_mooncake() or b.is_mori() or b.is_pplx() | ||
| return ( | ||
| b.is_deepep() | ||
| or b.is_deepep_v2() |
There was a problem hiding this comment.
[bug] is_deepep_class_backend() now includes deepep_v2, which is enough for Qwen3 (and Qwen2's extra-reduce scale) because those sites call the helper. It is not enough for models that still keep their own A2A-family lists. Those lists were the other half of the original "skip A2A / extra-reduce" bug, and they still omit v2:
glm4_moe.py:533_enable_a2a_moe(deepep/mooncake/nixl/mori/ascend_fuseep/flashinfer) — GLM-4.5 takesforward_normal.glm4_moe_lite.py:296andmimo_v2.py:458— same pattern.qwen2_moe.py:654(is_deepep() or is_mori()) — Qwen2-MoE never enters_forward_deepep; shared-expert TP1 atqwen2_moe.py:369is also missed.
DeepSeek's parallel_enable_a2a_moe(deepseek_v2.py:845) was updated in this PR; the copies were not.FusedMoEstill buildsDeepEPv2Dispatcher, soforward_normalruns A2A dispatch/combine and then a post-experts TP all-reduce.should_skip_post_experts_all_reducedoes not skipdeepep_v2(only flashinfer/pplx), so this is a silent double-count, not a clean fallback.
Suggestion: Add or get_moe_a2a_backend().is_deepep_v2() to every A2A-family list that already includes mooncake/nixl/mori/flashinfer (the DeepSeek _enable_a2a_moe / shared-expert TP1 shape). For Qwen2, either add is_deepep_v2() next to is_deepep() on the forward, ep_size, and shared-expert TP1 checks, or route those three sites through is_deepep_class_backend() — do not replace GLM4's list with the helper, because that list also admits flashinfer/nixl/ascend_fuseep.
There was a problem hiding this comment.
Thanks, this made it clear that treating v2 as a global A2A dispatcher would require a broader audit of model-specific forward contracts. I chose the narrower landing instead of adding is_deepep_v2() to every backend list in this PR. 20806a2aee now accepts only the three architectures exercised here: DeepseekV3ForCausalLM, DeepseekV4ForCausalLM, and Qwen3MoeForCausalLM. Every other architecture fails in _handle_a2a_moe() before weight loading, so Qwen2/GLM/MiMo cannot enter the dispatcher and then take an extra reduction. The boundary tests cover unsupported and missing architectures, including the official Qwen3.8 Qwen3_5MoeForCausalLM config. Supporting another model can now be a separate change with its model-specific A2A path and accuracy tests.
| deepep_v2_cap = ( | ||
| envs.SGLANG_DEEPEP_V2_NUM_MAX_DISPATCH_TOKENS_PER_RANK.get() | ||
| ) | ||
| if self.chunked_prefill_size > deepep_v2_cap: |
There was a problem hiding this comment.
[suggestion] The boot-time capacity guard only compares per-rank chunked_prefill_size to SGLANG_DEEPEP_V2_NUM_MAX_DISPATCH_TOKENS_PER_RANK. Decode CUDA graph is left enabled, and _handle_gpu_memory_settings often sets cuda_graph_config.decode.max_bs to 256/512 while the default cap is 128. The error text tells users to lower --chunked-prefill-size; doing that (and leaving max_bs alone) still blows the per-rank dispatch assert on graph capture. Prefill was called out because smoke traffic may never hit a full chunk; decode graph warmup does hit max_bs, but the failure is the opaque dispatch ValueError rather than this handler's message.
Suggestion: Also reject when per-rank cuda_graph_config.decode.max_bs (and/or max_running_requests) exceeds the cap, and mention --cuda-graph-max-bs in the error alongside the chunked-prefill hint.
There was a problem hiding this comment.
Good catch on decode graph warmup. 20806a2aee extends the startup check beyond the prefill chunk: it now covers the resolved decode CUDA-graph max batch size, the per-DP-rank running-request bound, and speculative width, and the error points to both --chunked-prefill-size and --cuda-graph-max-bs. I kept the calculation limited to canonical startup bounds rather than duplicating scheduler batching logic; the dispatcher also validates its actual row count at runtime, which remains the authoritative guard for dynamic/adaptive batches. Tests cover the exact graph boundary, DP division, speculative width, adaptive graph, and disabled-graph cases.
|
|
||
|
|
||
| class DeepEPv2Buffer: | ||
| _buffer: Optional[ElasticBuffer] = None |
There was a problem hiding this comment.
[suggestion] DeepEPv2Buffer stores the process-wide ElasticBuffer on class attributes. DeepEP v1's DeepEPBuffer already parks the same kind of singleton on get_resources().buffers so reset_context() and sequential Engine rebuilds release it. destroy() only nulls the Python refs (deepep_v2.py:202); a key-mismatch recreate drops the old object without an explicit DeepEP teardown, so NCCL/CUDA state can leak across tests or a second init in-process.
Suggestion: Follow the DeepEP v1 facade: keep DeepEPv2Buffer.get_buffer / destroy as the API, put _buffer / _buffer_key in get_resources().buffers, and if ElasticBuffer exposes a close/destroy, call it from destroy().
There was a problem hiding this comment.
Updated in f550b88142. The facade now stores its ElasticBuffer state under get_resources().buffers, includes the ProcessGroup identity in the cache key, and publishes a replacement only after collective construction succeeds. destroy() removes the runtime-context entry and local references. I checked the 0.1.2 lifecycle before adding an explicit native teardown: communicator destruction is tied to DeepEP's explicit-destroy mode rather than a normal per-facade close API, so calling it here would be unsafe. The new tests cover reuse, key/group replacement, construction failure, facade destroy, and reset_context().
| topk_weights = topk_output.topk_weights | ||
| topk_ids = topk_output.topk_ids.to(torch.int64) | ||
| self._validate_common(hidden_states, topk_ids) | ||
| # Layout follows inference phase, not comm mode: expanded wins on decode and |
There was a problem hiding this comment.
[suggestion] "expanded wins on decode and regresses prefill. Keeping it off the comm mode is what makes the masked-GEMM + CUDA-graph decode path available under hybrid too" narrates the layout/comm-mode split instead of stating the invariant. The next two lines already say use_masked = use_expand_layout.
Suggestion: Replace with the invariant only, e.g. that expand vs contiguous follows get_is_extend_in_batch() (decode expand + masked GEMM is graph-safe under both direct and hybrid).
There was a problem hiding this comment.
Agreed. f550b88142 reduces this to the invariant: layout follows the inference phase, so decode uses expand plus masked GEMM and extend uses contiguous, independently of direct versus hybrid. The surrounding implementation carries the rest.
| # semantic token limit; large prefill/chunked-prefill workloads may need a | ||
| # larger value. | ||
| SGLANG_DEEPEP_V2_NUM_MAX_DISPATCH_TOKENS_PER_RANK = EnvInt(128) | ||
| # 0 lets DeepEP v2 ElasticBuffer choose the communication SM count. |
There was a problem hiding this comment.
[nit] The descriptor comment says 0 lets ElasticBuffer choose the SM count. _resolve_num_sms_qps must resolve 0 in SGLang because ElasticBuffer treats 0 as "zero SMs/QPs", not auto (deepep_v2.py:242).
Suggestion: Say that 0 means SGLang calls get_theoretical_num_sms / get_theoretical_num_qps (ElasticBuffer does not treat 0 as auto).
There was a problem hiding this comment.
I rechecked this against the pinned sgl-deep-ep==0.1.2, whose ElasticBuffer now handles num_sms=0 natively: dispatch resolves the theoretical SM/QP counts and combine reuses the handle's selected settings. f550b88142 therefore removes SGLang's duplicate _resolve_num_sms_qps, passes zero through, and updates the descriptor comment to match the current package behavior. Explicit positive values are still forwarded unchanged.
cb3eb78 to
29c9e7e
Compare
This reverts commit 1270204, restoring sgl-project#29525, with the CI break that caused that revert fixed in the following commit. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
register_cuda_ci(runner_config="deepep-8-gpu-h200") produced the suite name base-c-test-deepep-8-gpu-h200, which no runner config defines. run_suite.py validates every registered test's suite up front, so this one bad name aborted suite collection for unrelated lanes too -- base-b-test-cpu-arm64 and the NPU lane both failed with "Tests registered to invalid suites" and main had to be reverted. Use 8-gpu-h200, the config other 8-GPU tests already use. Running the tests as single files (python3 <file> -f) never exercises run_suite.py, which is why local runs stayed green; `python3 test/run_suite.py --hw cpu --suite base-b-test-cpu-arm64` reproduces it in one command. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
get_server_args() was removed by the config namespace split (sgl-project#35025-sgl-project#35027), which also lowered the direct process-global read baseline to zero, so test_global_config_read_ratchet failed base-a-test-cpu and fast-fail took the 1-gpu-small and jit-kernel lanes with it. deepep_v2_mode is declared NS(exec.moe), so read it from that bag. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
The test asks for a 256-token per-rank dispatch buffer but never set --chunked-prefill-size, so the default 8192 -- halved to 4096 per rank by DP attention -- exceeded it and deepep_v2's boot-time capacity check refused to start the server, erroring setUpClass on base-c-test-8-gpu-h200. The prompts here are a dozen tokens each, so ask for 256 (128 per rank, half the buffer). Set for both backends so the deepep and deepep_v2 classes stay comparable. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
…conventions is_deepep_class_backend() now includes deepep_v2. It shares legacy DeepEP's scattered token topology, and models that route on that helper -- Qwen3-MoE among them -- otherwise take the TP forward path, which never feeds the dispatcher, so dispatch waits on counts that stay zero. Verified on Qwen3-30B-A3B-FP8: with the classification in place deepep_v2 and deepep both score GSM8K 0.930; with it removed the server does not come up. The DeepSeek paper-over that duplicated this helper is gone; the remaining `or is_deepep_v2()` clauses there are not duplicates -- those sites spell out their own backend lists and that clause is the only thing admitting v2. Also from review: - Seed NCCL_CUMEM_ENABLE in the deepep_v2 branch. _set_envs_and_config runs after this handler and only fills it in when unset, so this enables symmetric memory without --enable-symm-mem's NVLS and 4GB prealloc. - Use envs.<name>.override() instead of patch.dict(os.environ) so the descriptor restores _set_to_none exactly. - Guard tearDownClass on cls.process existing; a setUpClass failure before popen_launch_server used to raise in teardown and hide the real error. - Move the capturer classification test to base-a-test-cpu: it only mocks get_moe_a2a_backend and slices a CPU tensor. - Point the readback e2e at 4-gpu-h100; it runs tp2/dp2. - Trim comments that narrate design history rather than state an invariant. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
moe_runner_backend is a converted field, so a bare assignment never reaches the declaration stash and the projection does not see the write (test_resolution_declarations::test_converted_fields_are_not_assigned_bare). A declaration replays last, so resolving auto off the raw field would outrank a model declaration that already claimed the runner. Read the resolving view first: declare deep_gemm only when nothing claimed it, and keep failing fast otherwise. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
Limit DeepEP v2 to the model and runner contracts validated by this landing: DeepSeek V3/V4 and Qwen3 MoE with DeepGEMM and dynamic 128x128 blockwise FP8 experts. Reject unsupported architectures, deterministic inference, speculative draft usage, incompatible quantization, and undersized prefill/decode dispatch capacity before execution. Keep a runtime row-count guard as the authoritative protection for dynamic batches. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
Store ElasticBuffer state in RuntimeContext resources so context reset and sequential engine construction release Python ownership consistently with other communication backends. Include the ProcessGroup in the cache key and publish a new buffer only after collective construction succeeds. Delegate NUM_SMS=0 to sgl-deep-ep 0.1.2 native auto-selection, reuse dispatch handle settings for combine, retain only invariant-focused comments, and cover buffer reuse, replacement, failure, and reset lifecycles. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
Exercise the architecture allowlist, including explicit Qwen3.8 rejection, deterministic and speculative constraints, NGRAM behavior, runner and quantization contracts, and instance-connector handling. Cover prefill, decode CUDA graph, DP, speculative-width, adaptive-graph, and runtime dispatch-capacity boundaries so unsupported configurations fail with actionable errors instead of entering A2A execution. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
The sgl-deep-ep 0.1.2 ElasticBuffer extension requires an NCCL 2.30.7-or-newer runtime, while the 4-GPU H100 CI pool currently loads 2.29.7. Conditionally skip only the DeepEP v2 readback class when the runtime is too old. Keep the legacy DeepEP class and test registration active, and automatically restore the v2 coverage when the runner is upgraded. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
Remove review history, restated control flow, and assertion narration from the DeepEP v2 implementation and tests. Keep concise WHY comments only for non-obvious contracts such as single-use handles, capture-safe overflow behavior, expanded versus contiguous psum layout, empty-rank notification, collective argument consistency, and scale layout. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
materialize_declarations() is gone from arg_groups/overrides.py on main; resolution now ends at _resolution_finished and readers use resolved_view. Update the two DeepEP v2 tests that still asserted on materialized fields. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
762ef5b to
c517349
Compare
|
/tag-and-rerun-ci |
test_resolution_reads_the_declarations (new on main) rejects raw self.<field> reads inside resolution-reachable handlers: the field holds the operator's raw input, so a handler reading it misses declarations another resolver made. Route the DeepEP v2 reads through the views already bound in each scope: cfg (resolving_view) in _handle_a2a_moe, view (resolved_view) in _validate_deepep_v2_dispatch_token_budget, and resolved_view(self) in _validate_deepep_v2_model_architecture. This also makes the TBO/SBO and enforce-shared-experts-fusion guards see declared values rather than only what was typed on the CLI. Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com>
…5634) Co-authored-by: menyu <menyu@nvidia.com> Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com> Co-authored-by: Cheng Wan <cheng.wan@radixark.ai>
…5634) Co-authored-by: menyu <menyu@nvidia.com> Co-authored-by: Jinyan Chen <93358689+liz-badada@users.noreply.github.com> Co-authored-by: Han Yu <helloyu0903@gmail.com> Co-authored-by: Cheng Wan <cheng.wan@radixark.ai>
Motivation
Add DeepEP v2
ElasticBufferas a standalone MoE A2A backend nameddeepep_v2, alongside the existingdeepepbackend. Its fixed-capacitycommunication shapes make decode CUDA-graph capturable with both single-node
and multi-node topologies.
Supported scope
DeepseekV3ForCausalLM,DeepseekV4ForCausalLM,Qwen3MoeForCausalLMdeep_gemmsgl-deep-ep==0.1.2(deep_ep 2.1.0)directandhybridselect the ElasticBuffer topology; they are not aliasesfor legacy DeepEP
low_latencyandnormal. Decode uses the expanded maskedlayout, while extend/prefill uses the contiguous layout in either mode.
This intentionally does not claim support for other MoE architectures or
quantization contracts. In particular, Qwen3.8
(
Qwen3_5MoeForCausalLM), BF16 experts, MXFP8, non-DeepGEMM runners,deterministic inference, and a speculative draft worker using
deepep_v2arenot supported by this landing. These configurations now fail before A2A
execution instead of reaching a model-specific forward path with incorrect
post-expert reduction or incompatible activation scales.
Implementation
DeepEPv2Dispatcherusing ElasticBuffer dispatch andcombine. Legacy
deepepobjects and mode semantics are not reused.and masked decode, including Hopper FP32 scales and Blackwell packed UE8M0
scales.
RuntimeContextresources so context reset andsequential engine construction release ownership consistently. The cache key
includes the process group and a new buffer is published only after collective
construction succeeds.
settings for combine.
SGLANG_DEEPEP_V2_NUM_SMS=0delegates automatic SM/QPselection to
sgl-deep-ep==0.1.2.os.environ.setdefault("NCCL_CUMEM_ENABLE", "1"); an explicit user settingstill wins, and
--enable-symm-memremains optional.Capacity and fail-fast behavior
SGLANG_DEEPEP_V2_NUM_MAX_DISPATCH_TOKENS_PER_RANKconfigures ElasticBuffer'sper-rank capacity; it is a memory reservation, not a semantic token limit.
Startup checks cover the canonical prefill budget, decode CUDA-graph batch,
per-DP-rank running-request bound, and speculative width. The dispatch adapter
also checks the actual runtime row count, which remains the authoritative guard
for dynamic batches.
Errors include actionable guidance for adjusting the capacity,
--chunked-prefill-size, or--cuda-graph-max-bs. Unsupported model, runner,quantization, deterministic, and draft-backend combinations fail at startup or
MoE layer initialization.
Validation
directandhybridbooted and returned correct capital,arithmetic, and translation answers.
(
direct) and 0.965 (hybrid) versus 0.955 for legacy DeepEP.Representative back-to-back serving results:
Multi-node
hybridwas also validated on 2 x H20 x8 over RoCE at 4,610 tok/sdecode and 20,688 in_tok/s prefill. There is no same-fabric legacy DeepEP
baseline because its NVSHMEM data plane did not complete on that setup.
Checklist
Co-authored-by: @jianxiongzhang
CI States
Latest PR Test (Base): 🚫 Run #33033834791
Latest PR Test (Extra): ⏳ Run #33034831627
Latest PR Test (AMD ROCm 7.2): 🚫 Run #33033834696