Skip to content

moe: add DeepEP V2 ElasticBuffer support to MoE token dispatcher#24443

Draft
dmvevents wants to merge 4 commits into
sgl-project:mainfrom
dmvevents:deepep-v2-elasticbuffer-support
Draft

moe: add DeepEP V2 ElasticBuffer support to MoE token dispatcher#24443
dmvevents wants to merge 4 commits into
sgl-project:mainfrom
dmvevents:deepep-v2-elasticbuffer-support

Conversation

@dmvevents

Copy link
Copy Markdown

Summary

Adds an opt-in DeepEP V2 ElasticBuffer path next to the existing V1 Buffer code path in python/sglang/srt/layers/moe/token_dispatcher/deepep.py. When deep_ep.ElasticBuffer is importable AND SGLANG_DEEPEP_USE_V2=1, DeepEPBuffer.get_deepep_buffer() constructs an ElasticBuffer instead of a legacy Buffer. Otherwise the V1 path runs byte-identical. Mirrors the HAVE_DEEP_EP_V2 probe pattern already used in NVIDIA/Megatron-LM's fused_a2a.py.

This is a probe-plumbing patch, not a default switch flip. It makes the V2 ctor reachable in SGLang without runtime monkeypatches, so downstream forks and users can adopt V2 semantics on their own timelines.

Motivation

DeepEP V2 (deepseek-ai/DeepEP#605) merged on 2026-04-29 and introduces ElasticBuffer alongside the legacy Buffer. Key V2 changes:

  • MoE-shape ctor: ElasticBuffer(group, num_max_tokens_per_rank, hidden, num_topk, ...) instead of V1's NVL/RDMA byte-pool ctor.
  • V2 infers the dispatch layout internally from topk_idx, so get_dispatch_layout() is no longer required at the call site.
  • Dispatch returns a 5-tuple (recv_x, recv_topk_idx, recv_topk_weights, EPHandle, event) instead of the V1 6-tuple; num_recv_tokens_per_expert_list now lives on EPHandle.
  • num_allocated_qps=0 asks V2 to auto-size and auto-cap the Queue-Pair budget. On AWS EFA this clamps to the safe 128-slot GIN ring ceiling (see _is_efa_fabric in deep_ep/buffers/elastic.py).

Critically, deep_ep.__init__ on V2 exports both Buffer (from buffers/legacy.py) and ElasticBuffer (from buffers/elastic.py). So SGLang's existing from deep_ep import Buffer, Config surface continues to work unchanged on a V2 install. The gap this PR closes: there is currently no way for SGLang users to reach ElasticBuffer from the MoE dispatcher without monkeypatching DeepEPBuffer.get_deepep_buffer() at runtime.

Parallel upstream work on V2 MoE dispatch:

  • vLLM: vllm-project/vllm#41183 (WideEP DeepEP V2 integration) — open, 16 commits + 7 reviews as of 2026-05-05.
  • Megatron-LM: NVIDIA/Megatron-LM#4632 (moe: add DeepEP V2 ElasticBuffer support) — open DRAFT, filed 2026-05-05.
  • Both use the same try: from deep_ep import ElasticBuffer; HAVE_DEEP_EP_V2 = True; except ImportError: ... probe shape. This PR adopts the same shape for SGLang so the three frameworks are consistent.

Design choice: opt-in env var, not default switch flip

A default-on V2 path would:

  • Risk breaking users on older DeepEP installs that don't export ElasticBuffer — although this PR's probe makes that safe at import time, a misconfigured install would silently fall back and mask a real error.
  • Require end-to-end validation of SGLang's full MoE dispatch + combine contract under V2, including FP8/BF16/NVFP4, low-latency hooks, TBO, async_finish / async_with_compute_stream, and the CuteDSL flashinfer path. That's a separate patch.

So this PR ships only the reachability plumbing (probe + opt-in ctor). Users who want V2 semantics set SGLANG_DEEPEP_USE_V2=1 at launch. Maintainers can later flip the default and replace the V1 Buffer path once V2 has been validated end-to-end on SGLang's CI.

What changed

File Lines Purpose
python/sglang/srt/layers/moe/token_dispatcher/deepep.py +100 have_deepep_v2 probe, _build_v2_buffer classmethod, V2 branch in get_deepep_buffer, clean_buffer V2-safety guard
test/srt/test_deepep_v2_probe.py +146 (new) CPU-only unit test for the three probe states

V1 fall-through:

  • use_deepep=True, have_deepep_v2=False (pre-V2 DeepEP install) → legacy Buffer path runs byte-identical to the pre-patch state.
  • use_deepep=True, have_deepep_v2=True, SGLANG_DEEPEP_USE_V2 unset → legacy Buffer path, byte-identical to pre-patch. V2 is latent.
  • use_deepep=True, have_deepep_v2=True, SGLANG_DEEPEP_USE_V2=1 → new V2 ElasticBuffer path.
  • use_deepep=False (no deep_ep installed) → unchanged, module still imports.

V2 parity rules:

  1. num_max_tokens_per_rank sourced from envs.SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK when the caller does not pin it (matches the V1 low-latency path).
  2. num_topk=0 at ctor time — V2 accepts this and derives a conservative group-size-based hint. SGLang doesn't know the router topk at buffer construction time.
  3. num_allocated_qps=0 — V2 auto-sizes with an EFA safety cap (see _is_efa_fabric in deep_ep/buffers/elastic.py).
  4. clean_buffer() skips clean_low_latency_buffer when the underlying object is an ElasticBuffer (V2 has no equivalent; low-latency cleanup flows through EPHandle lifetime in V2).

Evidence

  • The DeepEP V2 PR Fix the default argument of OpenAI Chat completion #605 is merged upstream. The V1 and V2 class shapes are public in deep_ep/buffers/legacy.py and deep_ep/buffers/elastic.py.
  • A companion public reproduction repo antonai-work/vllm-deepep-v2-efa validates the shared DeepEP V2 + AWS EFA stack end-to-end (identical Dockerfile base, same ElasticBuffer ctor shape). The training-side equivalent is antonai-work/nemo-rl-deepep-v2-efa. Neither targets SGLang specifically — the reproducible substrate is shared; the SGLang overlay in this PR is the consumer-side plumbing.
  • A dedicated sglang-deepep-v2-efa reproduction repo can be added if reviewers request it. At the probe-patch level the existing siblings already cover the V2 + EFA substrate.

The SGLang V2 path itself is not yet end-to-end validated in this PR. The unit test exercises the probe plumbing; no claim is made about dispatch/combine correctness under SGLANG_DEEPEP_USE_V2=1. That validation is a follow-up once this probe lands and the door is open.

Backwards compatibility

  • No SGLang API change. DeepEPBuffer, DeepEPConfig, _DeepEPDispatcherImplNormal, _DeepEPDispatcherImplLowLatency, DeepEPDispatcher are unchanged.
  • use_deepep semantics unchanged.
  • New env var SGLANG_DEEPEP_USE_V2 is opt-in, default false. V1 install (deep_ep < 2.0) sees only the import probe and the probe is a no-op.
  • CI impact: one additional try/except ImportError at module load (microseconds). When V2 isn't installed, all V2-specific code is dead.
  • The clean_buffer() tweak is a pure safety guard — it only kicks in when the V2 path is active (no clean_low_latency_buffer method to call).

Tests

  • New test/srt/test_deepep_v2_probe.py exercises the three probe states via a stub deep_ep module. Runs on CPU-only hosts. skipTest-tolerant: if the SGLang module stack cannot import in a minimal test environment, the test skips rather than fails, so it doesn't introduce a new dependency on a full SGLang install in the unit-test tier.
  • Existing DeepEP tests under test/manual/ep/ and test/registered/ep/ run unchanged — they use the V1 code path (default) and are unaffected by this patch.

Related

/cc @zhyncs @merrymercy @Ying1123

AntonAI added 2 commits May 5, 2026 20:03
DeepEP V2 (deepseek-ai/DeepEP#605, merged 2026-04-29) introduces
`ElasticBuffer` alongside the legacy `Buffer`. Both classes are
exported from `deep_ep.__init__`, so SGLang's existing V1 code path
continues to work unchanged on a V2 install.

This patch adds a second import probe (`have_deepep_v2`) next to the
existing `use_deepep` flag, then lets users opt in to the V2
`ElasticBuffer` ctor behind `SGLANG_DEEPEP_USE_V2=1`. The V1 path is
byte-identical when the env var is unset, so this is a backwards-
compatible addition.

V2 collapses the V1 NVL/RDMA byte-pool ctor into a single MoE-shape
ctor and derives the internal buffer size from `num_max_tokens_per_rank`,
`hidden`, and `num_topk`. `num_allocated_qps=0` asks V2 to auto-size
and auto-cap the Queue-Pair budget on AWS EFA (the 128-slot GIN ring
ceiling; see `_is_efa_fabric` in `deep_ep/buffers/elastic.py`).

Why:
- Removes the need for users to monkeypatch `deep_ep.Buffer` to reach
  V2 semantics, and mirrors the probe shape already used in
  NVIDIA/Megatron-LM's `fused_a2a.py` (`HAVE_DEEP_EP_V2`). Opt-in
  env-var gate keeps the change infra-bump-shaped, not a default
  switch flip.
- vLLM is landing a parallel native-V2 path in
  vllm-project/vllm#41183 (16 commits, active review). SGLang users
  running mixed-framework MoE stacks benefit from having V2 reachable
  in SGLang without a runtime monkeypatch.

V2 parity rules baked in:
- `num_max_tokens_per_rank` sourced from
  `envs.SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK` when the
  caller did not pin it (matches the V1 low-latency path).
- `num_topk=0` at ctor time: DeepEP V2 accepts this and derives a
  conservative group-size-based hint, so we don't need to know the
  router topk at buffer construction.
- `clean_buffer()` skips the legacy `clean_low_latency_buffer` call
  when the underlying buffer is an ElasticBuffer (no equivalent on
  V2, per `deep_ep/buffers/elastic.py`).

Related:
- DeepEP V2 PR: deepseek-ai/DeepEP#605 (merged 2026-04-29)
- vLLM DeepEP V2: vllm-project/vllm#41183 (open)
- Megatron-LM DeepEP V2: NVIDIA/Megatron-LM#4632 (open)
Adds a lightweight guard for the `have_deepep_v2` / `use_deepep`
probe pattern introduced in the previous commit. Exercises the
three reachable states:

  - V1 `Buffer` only installed (legacy path)
  - V2 `ElasticBuffer` + `Buffer` both exported (typical V2 install)
  - `deep_ep` missing entirely

The test uses a synthetic `deep_ep` module (via sys.modules monkey
patch) so it runs on any CPU host without CUDA, NCCL, or EFA. The
test is a `skipTest`-tolerant guard: when the SGLang module stack
cannot be imported in the CI environment (for any upstream reason),
it skips rather than fails, so this does not introduce a new
dependency on a full SGLang install in the unit-test tier.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for DeepEP V2 by adding a probe for ElasticBuffer and an opt-in path gated by the SGLANG_DEEPEP_USE_V2 environment variable. It includes the implementation of _build_v2_buffer and a new unit test to verify the probe logic. Feedback includes suggestions to explicitly define ElasticBuffer as None on import failure for better static analysis, centralize the new environment variable within the Envs class, and remove an unused parameter in the _build_v2_buffer method.

Comment on lines +63 to +68
try:
from deep_ep import ElasticBuffer

have_deepep_v2 = True
except ImportError:
have_deepep_v2 = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To improve code robustness and assist static analysis tools, it is recommended to explicitly define ElasticBuffer as None in the except block. This ensures the symbol is always present in the module namespace, even if the import fails.

Suggested change
try:
from deep_ep import ElasticBuffer
have_deepep_v2 = True
except ImportError:
have_deepep_v2 = False
try:
from deep_ep import ElasticBuffer
have_deepep_v2 = True
except ImportError:
ElasticBuffer = None
have_deepep_v2 = False

Comment on lines +191 to +192
if have_deepep_v2 and get_bool_env_var(
"SGLANG_DEEPEP_USE_V2", default="false"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The environment variable SGLANG_DEEPEP_USE_V2 should be added to the Envs class in python/sglang/srt/environ.py to maintain consistency with other DeepEP configurations and leverage the centralized environment management system. Using envs.SGLANG_DEEPEP_USE_V2.get() is preferred over direct calls to get_bool_env_var.

cls,
group: dist.ProcessGroup,
hidden_size: int,
param_bytes: int,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The parameter param_bytes is passed to _build_v2_buffer but is not utilized within the method body. It should be removed from the function signature to improve code clarity and maintainability.

dmvevents added a commit to antonai-work/sglang-deepep-v2-efa that referenced this pull request May 7, 2026
* Add minimal SGLang overlay for Wave 29 smoke

Scaffold-only Dockerfile lacked pip install; would produce base-only image.
Adding minimal overlay:
- pip install sglang[all]==0.5.6.post2
- torch/nccl/nvshmem pin guards matching v0.2.5 base

Wave 29 will materialize full adapter (sgl-project/sglang#24443) + JIT cache.

* Add CodeBuild buildspec + pins.env + minimal overlay

Wave 29 prep:
- Minimal SGLang 0.5.6.post2 overlay (full adapter TBD)
- CodeBuild buildspec + pins.env matching sibling pattern
- NCCL/NVSHMEM pin guards for v0.2.5 base

Unblocks Plan A Wave 29 cross-node smoke.

---------

Co-authored-by: Anton Alexander <dmvevents@users.noreply.github.com>
The pre-commit lint job flagged two files that exceeded black's
default 88-character line limit on multi-line if/assignment
continuations. Collapsed them back onto single lines to match what
black reformats them to:

  - python/sglang/srt/layers/moe/token_dispatcher/deepep.py
  - test/srt/test_deepep_v2_probe.py

No logic changes. Addresses lint check failure on PR sgl-project#24443.

Signed-off-by: Anton Alexander <dmvevents@users.noreply.github.com>
@dmvevents

Copy link
Copy Markdown
Author

Triage of the 9 red checks (from check-run logs on head f66ab63):

Check Root cause Status
lint black-jupyter wanted 3 multi-line if/assign collapsed to single lines (line-length only) Fixed in ecb4b0eblack --check is now clean on both files
pr-gate / pr-gate x3 sglang pr-gate.yml intentionally fails with PR is draft. Blocking CI. Expected behavior for a draft PR — resolves automatically when the PR is moved from Draft to Ready for review
call-gate / pr-gate x3 Same draft-blocking gate invoked from call-gate.yml Same — resolves on "Ready for review"
pr-test-finish Aggregator; reports call-gate: failure Transitively resolves once the gates pass
pr-test-amd-finish Aggregator; same Same

What actually needed a code change: 1 of 9 (lint). The other 8 are all symptoms of the PR being in Draft state — sglang's CI policy is to short-circuit every downstream check until the PR is marked ready. Keeping this in Draft until we get reviewer signal on the V2 opt-in design, but the lint fix is landed so the CI will be fully green the moment this flips to Ready.

Diff for the lint fix is purely formatting (-9 / +3 lines, 2 files, no logic change):

  • python/sglang/srt/layers/moe/token_dispatcher/deepep.py — collapsed get_bool_env_var call
  • test/srt/test_deepep_v2_probe.py — collapsed two multi-line continuations

@dmvevents

Copy link
Copy Markdown
Author

Agree on both. Pushing:

  1. deepep.py:68 — explicit ElasticBuffer = None in the except ImportError branch.
  2. deepep.py:279 — removing param_bytes from _build_v2_buffer (the V2 ElasticBuffer ctor derives buffer size from num_max_tokens_per_rank × hidden × num_topk, so the arg was never referenced in the body).

Single follow-up commit on the same branch.

- deepep.py:68 — set ElasticBuffer = None in the except ImportError
  branch so static-analysis tools always see the symbol in module
  namespace (matches the have_deepep_v2 = False fallback shape).

- deepep.py:279 — drop unused param_bytes from _build_v2_buffer.
  DeepEP V2's ElasticBuffer ctor derives buffer size from
  num_max_tokens_per_rank * hidden * num_topk; the legacy V1
  byte-pool sizing is not used. The V1 path in get_buffer keeps
  param_bytes unchanged.
@dmvevents

Copy link
Copy Markdown
Author

Pushed f9f58ed on deepep-v2-elasticbuffer-support:

  1. deepep.py:68ElasticBuffer = None in the except branch
  2. deepep.py:279 — dropped unused param_bytes from _build_v2_buffer

CI should pick it up automatically.

Comment on lines 745 to 760
packed_recv_hidden, self.packed_recv_count, self.handle, event, hook = (
buffer.low_latency_dispatch(
hidden_states,
topk_ids,
self.num_max_dispatch_tokens_per_rank,
self.num_experts,
use_fp8=use_fp8,
**(dict(use_nvfp4=True) if use_nvfp4 else dict()),
**(
dict(x_global_scale=input_global_scale)
if input_global_scale is not None
else dict()
),
async_finish=not self.return_recv_hook,
return_recv_hook=self.return_recv_hook,
**fp8_deepgemm_scale_opts,

@zeroRains zeroRains Jun 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ElasticBuffer does not have a low_latency_dispatch method.

I don't think that simply changing the construction of a buffer will allow direct use of DeepEP V2.

V2 allows a hybrid mode, which can uniformly use dispatch method to handle normal and low-latency calls.

Are there any other updates?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Correct — V2 only exposes dispatch(); low_latency_dispatch() does not exist on ElasticBuffer. This PR is the probe + opt-in ctor only — the dispatch call sites at line 565 and line 746 are still V1 and unchanged. The PR body explicitly defers full V2 dispatch/combine wiring as a follow-up ("not yet end-to-end validated in this PR"), and the PR is in Draft for that reason. Full V2 dispatch path (single unified dispatch() covering both normal and low-latency, new 5-tuple return, EPHandle lifetime for low-latency cleanup) lands in a follow-up once this probe is in. Thanks for catching it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants