Skip to content

MoE dispatch: fix silent Tutel + TP corruption, speed up native and Tutel paths - #8195

Open
jinyouzhi wants to merge 3 commits into
deepspeedai:masterfrom
jinyouzhi:moe
Open

MoE dispatch: fix silent Tutel + TP corruption, speed up native and Tutel paths#8195
jinyouzhi wants to merge 3 commits into
deepspeedai:masterfrom
jinyouzhi:moe

Conversation

@jinyouzhi

Copy link
Copy Markdown
Contributor

Follow-up to #8174. Three independent changes to deepspeed/moe/sharded_moe.py.

1. Speed up Tutel route extraction for k > 2

topkgating rebuilt a dense [s, e] one-hot mask for every route, so gating cost
grew linearly with k. On launch-bound configurations Tutel therefore lost to the
native path once k > 2. Since the top-k columns already name the selected experts,
the per-route index/location/gate values are now a gather. Routes are handed to
Tutel as contiguous int32 so its own casts become no-ops, and the capacity tensor is
resolved once instead of forcing a device-to-host sync per use.

Routing output is bit-identical.

2. Shard the Tutel dispatch buffer along capacity under tensor parallelism

Tutel's encode() returns a flat [e * c, m] buffer, whereas the dense path
produces [e, c, m]. The drop_tokens(dispatched_input, dim=1) that follows
therefore sliced the model dim instead of the capacity dim. The result still
reshaped cleanly into the expert input (e * c * m / tp divided by e * m happens to
yield c / tp), so there was no error — experts silently ran on a hidden-state slice
reinterpreted as capacity, producing wrong results.

Reshaping the buffer to [e, c, m] before sharding fixes it. This is a pre-existing
bug on master, not introduced by #8174, but #8174 widens its reach by lifting the
k == 1 restriction on use_tutel.

3. Dispatch MoE tokens by index instead of a dense one-hot einsum

The native path materialised a dense [s, e, c] one-hot and ran
einsum("sec,sm->ecm"), costing O(s * e * c * m) to move only s * k * m elements.
Tokens are now routed through their capacity slots directly, so dispatch and combine
each become a single gather.

The sparse routing metadata already existed for Tutel, so the gate always returns it
and MOELayer selects a backend. The dense return path is kept because
deepspeed/ops/transformer/inference/moe_inference.py still consumes it.

Combine accumulates in fp32, matching the einsum it replaces — the einsum accumulated
in fp32 inside the tensor-core matmul, so a naive low-precision gather would have been
less accurate than the code it replaced.

Results

Measured on 2 GPUs, bf16, forward+backward.

Tutel/native step-time ratio (change 1):

k before after
2 0.947 0.937
3 1.078 1.01
4 1.158 1.04

Native path (change 3), s=8192 m=2048 e=16 k=3:

step time peak memory
before 58.33 ms 4353 MB
after 37.25 ms 2918 MB

The native path now matches Tutel (37.25 vs 37.49 ms).

Testing

tests/unit/moe/: 50 passed with Tutel installed, 48 passed / 2 skipped without.

  • test_sparse_dispatch_matches_dense (k=1/2/3) checks the index path against the
    dense einsum for both dispatch and combine.
  • TestMOETensorParallelTutel checks Tutel against the dense path under tensor
    parallelism; it fails without change 2 and passes with it.

Cross-checked end-to-end against master (fp32 max diff 1.2e-07) and verified that
bf16/fp16 accuracy versus an fp32 golden reference does not regress.

TestMOETensorParallelTutel requires Tutel and is skipped in CI, which does not
install it. Change 2 was validated locally on 2 GPUs: max diff 7.27e-01 before the
fix, 0.00e+00 after.

jinyouzhi and others added 2 commits July 29, 2026 17:28
Building a dense [s, e] mask per route made gating cost grow with k, so Tutel
lost to the native path once k > 2. Gather the per-route index/location/gate
values instead and hand Tutel contiguous int32 routes. Routing output is
bit-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: iLeGend <824040212@qq.com>
Tutel's encode() returns a flat [e * c, m] buffer, so the drop_tokens() that
follows sliced the model dim instead of the capacity dim and experts silently
ran on mismatched data. Reshape to [e, c, m] to match the dense path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: iLeGend <824040212@qq.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: 28353d198d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread deepspeed/moe/sharded_moe.py Outdated
Comment on lines 311 to 312
if sparse_routes and TUTEL_INSTALLED:
locations1 = tutel_moe.fast_cumsum_sub_one(mask1)

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 Keep native top-1 routing off Tutel

For default k=1 MoE, MOELayer now asks the gate for sparse routes before it chooses the backend, so sparse_routes=True is passed even when the layer was constructed with use_tutel=False. Because this condition keys off TUTEL_INSTALLED instead of the selected backend, any environment that merely has Tutel installed still invokes tutel_moe.fast_cumsum_sub_one on the nominal native path, removing the documented way to opt out of Tutel kernels and polluting native-vs-Tutel behavior. Please keep this on torch.cumsum unless the Tutel backend is actually enabled, or pass the backend choice separately.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

The dense [s, e, c] one-hot einsum cost O(s * e * c * m) to move only s * k * m
elements. Route tokens through their capacity slots directly so dispatch and
combine each become a single gather. The dense path stays for the inference
gate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: iLeGend <824040212@qq.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.

1 participant