Skip to content

Add an opt-in DeepEP transport for the AutoEP expert all-to-all - #8213

Open
yh0903 wants to merge 3 commits into
deepspeedai:masterfrom
yh0903:autoep-deepep-backend
Open

Add an opt-in DeepEP transport for the AutoEP expert all-to-all#8213
yh0903 wants to merge 3 commits into
deepspeedai:masterfrom
yh0903:autoep-deepep-backend

Conversation

@yh0903

@yh0903 yh0903 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What this adds

An optional DeepEP transport for the AutoEP expert all-to-all, selected by environment variable and defaulting to the existing NCCL path.

DEEPSPEED_AUTOEP_COMM_BACKEND=nccl     # default, unchanged behaviour
DEEPSPEED_AUTOEP_COMM_BACKEND=deepep
DEEPSPEED_AUTOEP_COMM_SMS=<n>          # default 12

Why

The expert all-to-all is the largest single cost in an AutoEP step. Replaying routing captured from real SFT runs on 16 H100s across two nodes:

Routing Skew (max receive / mean) NCCL DeepEP Speedup
math, last step 3.01 99.72 ms 48.02 ms 2.08x
code, last step 2.56 100.52 ms 47.91 ms 2.10x
code, worst step 5.09 115.75 ms 46.40 ms 2.49x

Two things are worth separating. Roughly half the gain is deduplication: DeepEP sends a token once per destination rank rather than once per selected expert, which is worth about 1.29x on its own and could in principle be done without changing transport. Its kernels account for the remaining 1.61x, and they are also where the skew immunity comes from — at the most imbalanced step recorded, NCCL degrades to 115.8 ms while DeepEP stays flat.

In whole training steps, with the same model, data and step count and only the backend changed:

nccl:   step=454.01 ms  payload all-to-all=142.92 ms (31.5% of the step)
deepep: step=370.74 ms  payload all-to-all= 29.68 ms ( 8.0% of the step)

end-to-end: 1.225x (18.3% of the step removed)

Why DeepEP v2 only

This wraps ElasticBuffer, the v2 API. The legacy v1 Buffer is deliberately not supported:

  • v1 carries data over NVSHMEM and IBGDA rather than NCCL, which needs either
    the NVreg_EnableStreamMemOPs driver parameter or the GDRCopy /dev/gdrdrv device. Neither is present on the cluster this was developed against, and both require administrator action rather than configuration.
  • v1's published internode bandwidth is markedly lower — 43 GB/s at EP16
    against 153 GB/s intranode — and the node boundary is exactly the cost this change exists to reduce. v2's hierarchical NVLink plus RDMA path targets that case directly.
  • v2 can reuse existing NCCL communicators, which matters in a training step
    that already has ZeRO and data-parallel groups on the fabric.

The backend name is deepep rather than deepep_v2: it names the library, and nothing about it would have to change if v1 were ever added.

Choosing the SM budget

Communication competes with the expert GEMM for SMs, so the default was chosen by sweeping whole training steps rather than the collective in isolation:

Comm SMs Step time
8 340 ms
12 311 ms
16 353 ms
24 360 ms
32 391 ms

At 8 the collective itself degrades; above 12 the step grows because communication takes SMs the rest of the step was using. Tuning this from 24 to 12 moved the end-to-end result from 1.126x to 1.225x.

Scope and safety

git diff -w removes zero lines from the shipped path: the existing NCCL code is unchanged and only moves into an else branch. deep_ep is imported only when the backend is selected, so installations without it are unaffected, and an unparsable backend value warns and falls back rather than breaking a path nobody opted into. DeepEP also requires NCCL 2.30.4 or newer for GIN, which not every cluster has, so a missing package explains what it needs instead of failing deep inside buffer construction.

DeepEP replaces more than the two collectives. It takes tokens before top-k expansion and replicates them itself, groups arrivals by expert for the grouped GEMM, and reduces the weighted sum in its combine, so the expansion and reduction around the collectives are replaced as well. Its backward pass has no separate entry points: the gradient of a combine is a dispatch and the gradient of a dispatch is a combine, both replayed against the handle the forward dispatch produced.

Limitations

The DeepEP path is disabled with a warning when folded tensor parallelism is active (tp_size > 1). Folded TP partitions assignments across lanes and restores them by assignment metadata, which a transport whose combine returns token-major rows cannot satisfy. Such layers fall back to the NCCL path rather than silently producing wrong output.

Testing

13 unit tests cover backend selection (default, empty, unknown, SM budget), the preflight checks, and gradient shape conformance. Correctness was verified on 16 H100s across two nodes for every routing scenario above, including a forward-and-backward round trip through all four payload collectives.

Follow-up work, kept out of this PR so each change stands on its own: communication/compute overlap (measured 1.11x, composes with this) and deduplication on the NCCL path (1.29x, needs no new dependency).

The expert all-to-all is the largest single cost in an AutoEP step. Replaying
real SFT routing on 16 H100s across two nodes, NCCL spends 99.7 ms per step on
payload all-to-all against DeepEP's 48.0 ms, and the gap widens with routing
skew: at the most imbalanced step recorded, NCCL degrades to 115.8 ms while
DeepEP stays at 46.4 ms.

Roughly half of that is deduplication. DeepEP sends a token once per
destination rank rather than once per selected expert, worth about 1.29x on
its own; its kernels account for the remaining 1.61x. The skew immunity comes
entirely from the kernels.

In whole training steps the step time drops from 454.0 ms to 370.7 ms, an
18.3% reduction, with the payload all-to-all falling from 31.5% of the step to
8.0%.

DeepEP replaces more than the two collectives. It takes tokens before top-k
expansion and replicates them itself, groups arrivals by expert for the
grouped GEMM, and reduces the weighted sum in its combine, so the expansion
and reduction around the collectives are replaced too. Its backward pass has
no separate entry points: the gradient of a combine is a dispatch and the
gradient of a dispatch is a combine, both replayed against the handle the
forward dispatch produced.

The transport is selected by environment variable and defaults to NCCL:

    DEEPSPEED_AUTOEP_COMM_BACKEND=nccl     (default)
    DEEPSPEED_AUTOEP_COMM_BACKEND=deepep
    DEEPSPEED_AUTOEP_COMM_SMS=<n>          (default 12)

A job that sets nothing behaves exactly as before, deep_ep is imported only
when it is selected, and an unparsable value warns and falls back rather than
breaking a path nobody opted into. DeepEP also needs NCCL 2.30.4 or newer for
GIN, which not every cluster has, so a missing package explains what it
requires instead of failing deep inside buffer construction.

The default SM budget of 12 was chosen by sweeping whole steps rather than the
collective alone. Communication competes with the expert GEMM for SMs: at 8
SMs the collective itself degrades, and above 12 the step grows because
communication takes SMs the rest of the step was using. The measured steps
were 340, 311, 353, 360 and 391 ms at 8, 12, 16, 24 and 32 SMs.

Signed-off-by: yh0903 <helloyu0903@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e3325bba1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread deepspeed/module_inject/auto_ep_comm.py Outdated
Comment on lines +262 to +264
def backward(ctx, grad_received):
grad_tokens = ctx.exchange.combine(grad_received.contiguous(), ctx.handle)
return None, _conform_rows(grad_tokens, ctx.tokens_shape), None, None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve router-score gradients through DeepEP

When training with the DeepEP backend, the original router scores enter the custom autograd function as topk_weights, but this backward unconditionally returns None for that input. The received weights later read from exchange.last_recv_weights were produced inside the custom forward and are outside its autograd outputs, while _DeepEPCombine.backward also returns no weight gradient, so the MoE loss silently stops contributing gradients to the router gate for both pre- and post-score modes.

Useful? React with 👍 / 👎.

Comment on lines +747 to +748
if self.comm_backend == DEEPEP_BACKEND:
expert_output = self._deepep_route(x, ro)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not feed token-combined rows into folded restore

When AutoEP is used with folded TP (tp_size > 1) and DeepEP is selected, this call ignores the assignment partition created above and returns a fully combined [T, H] tensor on every TP lane. The subsequent restore_combined expects one row per lane-local assignment, gathers those rows, applies routing weights, and scatters by assignment metadata, so it either encounters incompatible row counts or weights and scatters already-combined token outputs as if they were assignment outputs, corrupting the result.

Useful? React with 👍 / 👎.

num_experts=self.num_experts,
top_k=self.top_k,
hidden_size=self.hidden_size,
num_max_tokens_per_rank=tokens.shape[0],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resize the DeepEP buffer for larger batches

When a later forward has more tokens than the first forward, the retained exchange is still configured with the first batch's token count as num_max_tokens_per_rank. Because the buffer is only created when _deepep_exchange is None, variable sequence lengths, dynamic batching, or a short warm-up batch followed by a larger batch exceed its declared capacity and cause the DeepEP dispatch to fail; recreate or grow the exchange when tokens.shape[0] exceeds the stored capacity.

Useful? React with 👍 / 👎.

Comment on lines +765 to +767
elif self.comm_backend == DEEPEP_BACKEND:
# Already reduced over top-k and back in token order.
output = expert_output.reshape(bsz, seqlen, hdim)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the local combine when EP size is one

When DEEPSPEED_AUTOEP_COMM_BACKEND=deepep is set but ep_size == 1, the DeepEP route is never called and expert_output still contains one row per top-k assignment from the local path. This backend-only branch nevertheless skips combine_from_routed and reshapes those T * top_k rows directly to [B, S, H], which fails for top_k > 1 and omits the routing reduction even when the shape happens to fit.

Useful? React with 👍 / 👎.

@@ -0,0 +1,293 @@
# SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the required Signed-off-by trailer

This is a non-merge commit, but its commit message has no Signed-off-by trailer, so it does not satisfy the repository's mandatory commit and CI requirement. Recreate the proposed commit with --signoff using the configured Git identity.

AGENTS.md reference: AGENTS.md:L8-L8

Useful? React with 👍 / 👎.

Comment on lines +14 to +15
DEEPSPEED_AUTOEP_COMM_BACKEND=nccl (default)
DEEPSPEED_AUTOEP_COMM_BACKEND=deepep

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Document the new backend in user-facing documentation

The new opt-in backend, its environment variables, dependency, NCCL requirement, and supported configurations are only described in an internal module docstring; the commit adds no user-facing documentation, so users cannot reliably discover or configure the feature. The repository explicitly requires documentation updates for new features.

AGENTS.md reference: AGENTS.md:L26-L26

Useful? React with 👍 / 👎.

@yh0903
yh0903 force-pushed the autoep-deepep-backend branch 2 times, most recently from df6e5d4 to e566d6f Compare August 4, 2026 17:59
Six issues raised in review, all confirmed against the code:

Router scores stopped receiving gradients. Both autograd functions returned
None for their topk_weights input, and the received weights were read off the
exchange rather than returned as an output, leaving them outside the graph.
The MoE loss therefore contributed nothing to the router gate in either score
mode. Dispatch now returns the received weights so autograd carries their
gradient back, and both backward passes reduce and return it. This fails
silently -- training runs and the loss falls while the gate never learns -- so
two tests pin the return arity that carries it.

The buffer kept the first batch's token count as its capacity. Variable
sequence lengths or a small warm-up batch followed by a larger one would
exceed it; the exchange is now rebuilt when a batch outgrows it.

Skipping the routed combine was keyed on the backend being selected rather
than on DeepEP having run. With ep_size == 1 the local path runs instead and
still has one row per assignment, so the reduction was skipped on rows that
needed it. It is now keyed on the route actually producing the output.

Folded tensor parallelism partitions assignments across lanes and restores
them by assignment metadata, which a transport whose combine returns
token-major rows cannot satisfy. Such layers now warn and use the NCCL path
rather than corrupting the result.

The backend, its environment variables, its NCCL requirement and its limits
are now documented in the AutoEP page rather than only in a module docstring.

Signed-off-by: yh0903 <helloyu0903@gmail.com>
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.

2 participants