Repository navigation
Conversation
AutoEP's grouped experts run the gate and up projections of the same input as two grouped GEMMs, and in the backward pass add the two input-gradient GEMM results. expert_parallel.gate_up_impl="fused" concatenates w1 and w3 on each call and runs one torch._grouped_mm with twice the output columns. A single input-gradient GEMM then accumulates both projections in FP32. The activation reads the two halves of the packed output in place: swiglu through a new Triton kernel, swiglu_packed, with the same FP32 math as the existing swiglu kernel; the other registered forms through their eager expression on views. Parameters keep the separate w1/w3 layout, so checkpoints, expert tensor parallelism and optimizer state are unchanged. The default, "separate", keeps the existing path. "fused" is rejected when the experts would not run torch._grouped_mm. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: yh0903 <helloyu0903@gmail.com>
…bine In the DeepEP backward, the experts' input gradient is combined back to the token ranks while their weight gradients are independent of it, yet a grouped GEMM's backward computes both before the combine starts. With expert_parallel.overlap_weight_grad, the experts' backward returns the input gradient first and leaves its weight-gradient GEMMs in an ExpertWeightGradSlot. The DeepEP dispatch, which takes the expert weights as extra inputs, launches its backward combine asynchronously, runs those GEMMs on the compute stream with comm_num_sm SMs withheld through PyTorch's SM carveout so the combine has room to run, waits on the combine and returns the weight gradients through autograd. The GEMMs are the products autograd forms for torch._grouped_mm, so gradients are those of the serial backward. The activation registry gains optional standalone backward functions for the fused kernels, provided for swiglu; forms without one are recomputed and differentiated. The option is rejected unless comm_backend is deepep with autoep_size > 1, the experts run torch._grouped_mm, ZeRO stage is 1 or 2, and the SM carveout API exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: yh0903 <helloyu0903@gmail.com>
…e grouped GEMM in the DeepEP case Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: yh0903 <helloyu0903@gmail.com>
0z5a
left a comment
There was a problem hiding this comment.
The documented contract here says overlap_weight_grad is supported with ZeRO stage 1 or 2, but the validation only rejects stage 3, so ZeRO-0 currently passes through.
Could we either explicitly reject zero_stage not in (1, 2), or add ZeRO-0 correctness coverage and document it as supported? Since this path reroutes expert dW through the dispatch autograd node, I'd prefer failing closed for an unvalidated optimizer path.
…dai#8733) AutoEP's grouped experts run the gate (`w1`) and up (`w3`) projections of the same routed rows as two grouped GEMMs. In the backward pass, they compute two input-gradient GEMMs and add the two BF16 results. This adds an opt-in `expert_parallel.gate_up_impl="fused"`, which runs both projections as one `torch._grouped_mm` over the two weights, concatenated on each call. ### Change - `GroupedExperts(gate_up_impl="fused")` concatenates `w1` and `w3` per call and runs one grouped GEMM with `2 * hidden_dim` output columns. - The backward pass therefore has one input-gradient GEMM, which accumulates both projections in FP32, and no separate addition. - The activation reads the two halves of the packed output in place: - `swiglu` uses a new Triton kernel, `swiglu_packed`, with the same FP32 math as the existing `swiglu` kernel. - The other registered forms (`geglu_tanh`, `swiglu_clamped`, `swiglu_oai`) run their eager expression on views of the halves. - New forms can register a `packed_fused_fn`. - The parameters keep their separate `w1`/`w3` layout. Checkpoints, universal checkpoints, expert tensor parallelism and optimizer state, including Muon's per-expert treatment, are unchanged. - The default, `"separate"`, keeps the existing path. - `"fused"` is rejected rather than ignored when the experts would not run `torch._grouped_mm`: - `use_grouped_mm=false`, at config validation and in `GroupedExperts`; - devices that select the Triton grouped GEMM, unless `disable_triton_grouped_mm=true`. - Docs: `autoep.rst` and the config reference. ### Numerical and autograd contract The forward expression and parameter-gradient semantics are unchanged. Exact output/weight-gradient equality is an observation in the tested H100 cases, not a guarantee across GEMM shapes, implementations or hardware. The input gradient can differ because it is accumulated in one FP32 GEMM rather than as two rounded BF16 results added. The FP64 regression now uses the same represented BF16 weights as the tested paths, rather than the original pre-quantization FP32 weights; its existing relative-L2 bound (fused error <= 1.05 * separate error) is unchanged. This is not a universal accuracy-improvement or bitwise-equivalence claim. Cross-GEMM output and parameter-gradient assertions now use the existing grouped-GEMM BF16 policy (`rtol=0.02`, `atol=0.02 * max(abs(reference))`) rather than zero tolerance. The skewed/empty-expert forward and smooth-form parameter gradients use the independent FP64 reference. Clamped-form parameter gradients use an independent dense BF16 reference, because rounding a projection across a clamp boundary changes its derivative discontinuously; the bound is not enlarged to absorb those discontinuities. The input-gradient relative-L2 comparison is unchanged. The multi-step test uses the same policy for comparisons across algorithms, while saved/reloaded parameters, optimizer momentum tensors and parameter-group configuration must still match exactly. Exact parity checks between the two implementations of the same elementwise SwiGLU kernel remain unchanged. The packed SwiGLU Triton path has first-order gradients only: `create_graph=True` raises rather than silently dropping its second derivative. Forward and backward select the input's CUDA device and its current stream, restoring the caller's current device. The eager expressions for the other activation forms and the default separate path are unchanged. ### Tests - CPU, `TestFusedGateUp` in `tests/unit/v1/moe/test_autoep_expert_activation.py`: - packed versus separate halves for every registered form, forward and backward; - only `swiglu` has a packed kernel; - the default is unchanged; - unsupported settings are rejected (bad value, `use_grouped_mm=false`, Triton grouped GEMM path), at config validation and in `GroupedExperts`; - the config reaches the replaced experts, whose parameters remain `w1`, `w2`, `w3`. - Earlier GPU evidence, on one H100 (305 passed): `test_swiglu_triton.py`, `test_autoep_expert_activation.py` and `test_autoep_unit.py`. - `swiglu_packed` matches the existing `swiglu` kernel's values exactly, forward and backward (`rtol=0`, `atol=0`, not a signed-zero bit-pattern assertion). Covered: FP32/FP16/BF16, odd widths, 3-D inputs, empty inputs, non-contiguous input, and an odd last dimension (rejected). - The fused path joins `test_grouped_gemm_paths_match_the_for_loop` for every form. - New `test_matches_separate_with_skewed_and_empty_experts`, for every form, with an empty expert and unaligned rows: - output and `w1`/`w2`/`w3` gradients match exactly in the tested shapes; - the original relative-L2 FP64 error comparison is retained with the corrected represented-weight reference described above. - Not part of this change: the 2 failures are `TestMiniMaxM3Block::test_replaced_block_matches_hf_block`. That preset requires Transformers >= 5.15.0, and the image has 5.12.0. - The shipped `GroupedExperts`, one H100, Qwen3-30B-A3B per-rank expert shape (131,072 routed rows, hidden 2048, expert hidden 768, 16 local experts, BF16): | Per call (ms) | Forward, no grad | Forward | Backward | Total | |---|---:|---:|---:|---:| | `"separate"` | 2.149 | 2.292 | 4.857 | 9.298 | | `"fused"` | 2.224 | 2.236 | 4.321 | 8.781 | Output and all weight gradients matched exactly; the input gradient differed by 2.9e-3 relative. - End to end, Qwen3-30B-A3B on 8 H100s: - Setup: expert parallel size 8, micro-batch 4, 16 accumulation steps, sequence length 4096, DeepEP, reentrant activation checkpointing, BF16 parameters and Adam state, 30 steps with 10 warmup. - Measured with an equivalent benchmark patch on our benchmark base: the same concatenation and the same packed SwiGLU kernel. - Separate 22,018.8 / 22,018.5 ms against fused 21,601.2 / 21,608.4 ms, in two pairs in the same job: **-414 ms (-1.9%)**. - Step-1 loss identical. Later losses agree within same-arm repeat noise. - Peak allocated memory unchanged; reserved memory about 0.4 GB higher. ### Review follow-up validation - Current-master targeted validation on NVIDIA H100 80GB HBM3, torch 2.10/CUDA 13.0, Triton 3.6.0 and transformers 5.12.0: **83 passed, 0 skipped** (the full 63-case SwiGLU file plus 20 `TestFusedGateUp` cases). The SwiGLU file also passed **63/63 with no skips under `pytest --forked`**. Cross-device cases used two GPUs; the lifecycle cases use the actual native grouped GEMM. - With only the same new tests applied to the prior runtime, all four focused guards fail as intended: both FP16/BF16 cross-device cases reproduce Triton's inaccessible-pointer error, and both FP32/BF16 higher-order cases fail because `create_graph=True` was not rejected. This verifies the actual root causes, not an unrelated collection or test-API failure. - Regressions cover strided packed inputs and upstream gradients, two-device/current-stream selection and caller restoration, explicit higher-order rejection, the corrected FP64 reference, and two SGD updates with two accumulation micro-batches per step. All four activations run with and without reentrant recomputation; a model/optimizer state-dict round trip between updates keeps the separate `w1`/`w2`/`w3` names and shapes and checks the next update against the independent separate path. - The SwiGLU test file imports its runtime in the worker, not during collection, so its Triton driver probe is deferred until after `pytest --forked` starts the worker. - The follow-up changes launch-device selection and unsupported-autograd handling, not the GEMM implementation, kernel arithmetic, launch configuration, default or parameter layout. The earlier local and E2E performance evidence is reused without retiming; the historical percentages are not new measurements of these guards. ### AI review follow-up - Changed tests and documentation only; the runtime is byte-identical to the previous validated head. - The numerical-contract comment is addressed by the reference/tolerance policy above, plus a regression that accepts a single BF16 representable-step change but rejects a clearly incorrect value. On NVIDIA H100 80GB HBM3, torch 2.10/CUDA 13.0, Triton 3.6.0 and transformers 5.12.0, the focused fused-gate-up/grouped-GEMM suite passed **33/33 with no skips**. - A synthetic one-ULP perturbation of the fused down-GEMM output passed all **12/12** reference/lifecycle cases with the contract-level checks; the old exact checks failed all **12/12** under the same perturbation. An independently injected 25% output/gradient error was rejected by all **four** reference cases. These are small test controls, not new GEMM-backend or performance measurements; they demonstrate that legitimate rounding is allowed without accepting an incorrect computation. - The DCO comment was checked against the actual GitHub PR commit list and local ancestry: every PR commit has the configured author's `Signed-off-by` trailer, and DCO passes. The SHA cited in that comment is not in the PR and is not found by GitHub's commit API, so no commit amendment or history rewrite is needed. ### CI and scope DCO, test collection and changed-files pre-commit passed on the published follow-up. Full GPU CI and infrastructure-smoke jobs were skipped on the PR event; they are not reported as full GPU CI acceptance. The targeted H100 runs above provide the device evidence for this follow-up. This branch inherits recent upstream `master`. The backward-overlap child (deepspeedai#8739) remains a separate stacked PR and is not included here; no recompute-skip, buffer-slice, row-weighting-activation or communication optimization is added by this PR. The new optimizer round trip is a focused SGD test, not an independent qualification of every optimizer, expert-TP/universal-checkpoint combination, ZeRO stage or accelerator. The E2E evidence covers one model/configuration on H100, not long-run convergence or a universal speedup. --------- Signed-off-by: yh0903 <helloyu0903@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve merged gate-up, checkpoint replay and exact-size buffer behavior. Enforce ZeRO stages 1/2 and forward the actual engine stage into validation; keep the disabled default unchanged. Restore SM carveout and wait for the async combine even if dW fails, reject incomplete higher-order derivatives, and retain input-device standalone backward launches. Read accumulated parameter gradients before clipping in the real stage-1/2 regression rather than accepting released param.grad as missing coverage. The human author reviewed this exact staged tree before signoff; targeted H100 acceptance and CPU controls passed without a new performance sweep. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: yh0903 <helloyu0903@gmail.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
🔵 Needs a closer look
The distributed asynchronous autograd scheduling and private PyTorch SM-carveout integration warrant final human review despite extensive targeted testing.
0 open findings
What changed in this PR
Adds opt-in overlap of AutoEP expert weight-gradient GEMMs with DeepEP’s backward combine, building on the merged fused gate/up work while keeping activation weighting separate.
Changes:
- Adds deferred expert weight-gradient computation with async DeepEP combine and SM carveout.
- Validates supported DeepEP, grouped-GEMM, ZeRO, and PyTorch configurations.
- Adds activation backward helpers, documentation, and CPU/H100 coverage.
| File | Description |
|---|---|
deepspeed/moe/ep_experts.py |
Implements deferred expert weight gradients. |
deepspeed/module_inject/auto_ep_comm.py |
Overlaps combine with gradient GEMMs. |
deepspeed/module_inject/auto_ep_config.py |
Validates overlap requirements. |
deepspeed/module_inject/auto_ep_layer.py |
Connects overlap to AutoEP routing. |
deepspeed/module_inject/auto_ep_presets/base.py |
Adds the configuration option. |
deepspeed/ops/triton_ops/swiglu_triton.py |
Exposes standalone SwiGLU backward helpers. |
deepspeed/runtime/engine.py |
Passes the actual ZeRO stage. |
docs/code-docs/source/autoep.rst |
Documents behavior and performance. |
docs/_pages/config-json.md |
Documents the configuration field. |
tests/unit/module_inject/test_auto_ep_comm.py |
Tests scheduling, recovery, and checkpointing. |
tests/unit/v1/moe/test_autoep_deepep_parity.py |
Tests real DeepEP gradient parity. |
tests/unit/v1/moe/test_autoep_expert_activation.py |
Tests deferral and validation. |
tests/unit/v1/ops/triton_ops/test_swiglu_triton.py |
Tests backward helper parity and device selection. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Thanks @0z5a. Overlap now explicitly rejects ZeRO 0/3 and accepts only stages 1/2, with the engine forwarding its actual stage to validation. Error paths restore the carveout and wait for the pending async combine! |
tohtana
left a comment
There was a problem hiding this comment.
Hi @yh0903,
Thank you for submitting this PR!
I wasn't able to run this with real DeepEP setting, but the code change overall looks good.
One small change I want to request: the engine now passes zero_stage to the AutoEP config validator, but an existing validator test func rejects that keyword. A minimal signed test-only repair would be like commit e452d350.
Retain the merged clamped activation kernels and tests while resolving the worker-fixture imports as a union with the standalone backward helpers. Keep both sets of imports inside the worker, preserving fork-safe collection. Accept and record zero_stage in the existing sequence-parallel validator test double and assert the actual stage is forwarded. This fixes the reproduced unexpected-keyword failure without changing the product runtime or weakening existing assertions. The human author reviewed the exact staged tree before signoff. Targeted CPU configuration, deferred-gradient, engine and comm/replay checks and changed-files pre-commit passed. Reuse earlier device evidence without claiming a fresh GPU or end-to-end run. Signed-off-by: yh0903 <helloyu0903@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks, Masahiro! Applied the minimal test-double repair you suggested in 894a116. Also synchronized with master, resolving the SwiGLU fixture conflict by retaining both the standalone backward and clamped activation helpers inside the worker-only fixture. This follow-up introduces no manual product-runtime changes. |
The fused gate+up prerequisite, #8733, has merged. This branch is synchronized with current master and retains only the backward-overlap feature delta; no force-rewrite of the earlier submitted commits is required.
In AutoEP's DeepEP backward, the experts compute their input gradient (dX) and their weight gradients (dW). DeepEP's combine then sends dX back to the token ranks. dW and the combine are independent, yet they run serially: a grouped GEMM's backward computes dX and dW together, and only then does the dispatch backward start the combine. In a kernel profile of Qwen3-30B-A3B on 8 H100s, DeepEP ran concurrently with no compute kernel at all.
This adds an opt-in
expert_parallel.overlap_weight_grad, which runs the combine and the dW GEMMs side by side.Change
GroupedExperts.forwardtakes an optionalExpertWeightGradSlot._DeferredWeightGradExpertsruns the same forward as thetorch._grouped_mmpath, for separate or fused gate+up and every registered activation.torch._grouped_mm(_grouped_mm_mat1_backward,_grouped_mm_mat2_backward).async_with_compute_stream=True;torch._grouped_mmlaunches persistent kernels sized to every SM, which leaves the combine nowhere to run.comm_num_smSMs withheld throughtorch._C._set_sm_carveout_experimental.finallyblock as well, so a dW error does not leave the queued communication unaccounted for.swigluprovides them, via newswiglu_backwardandswiglu_packed_backward, which the existing autograd functions now also call.create_graph=Trueraises rather than returning an incomplete derivative.overlap_weight_gradis rejected, rather than ignored, unless all of these hold:comm_backend="deepep"withautoep_size > 1;use_grouped_mm=true, and the experts do not select the Triton grouped GEMM;autoep.rstand the config reference.Tests
TestDeferredExpertWeightGrad:w1/w2/w3gradients match the plain path's values exactly in the tested CPU cases, for every activation, separate and fused gate+up, FP32 and BF16, with weight gradients immediate or deferred, including an empty expert. This does not assert universal bit-pattern equivalence.torch._grouped_mmrequirement, and config validation for every rejected setting.TestWeightGradOverlap, the route against a DeepEP contract buffer:comm_num_smwithheld, then wait.test_overlapped_weight_gradients_match_the_serial_backward, on 4 GPUs with DeepEP, uniform and skewed routing, separate and fused gate+up: input and every parameter gradient within 1e-2 relative L2, the shared parity checks, and no leaked carveout.swiglu_backwardandswiglu_packed_backwardbitwise against autograd.TestMiniMaxM3Block::test_replaced_block_matches_hf_block. That preset needs Transformers >= 5.15.0, and the image has 5.12.0.Setup: expert parallel size 8, micro-batch 4, 16 accumulation steps, sequence length 4096, DeepEP, reentrant checkpointing, fused gate+up, 30 steps with 10 warmup.
Measured with an equivalent benchmark patch on our benchmark base: the same deferral, async combine and carveout.
Two pairs in one job:
A kernel profile showed DeepEP running alongside grouped GEMMs for 1,325 ms of the step, against none before.
Step-1 loss identical. Later losses agree within same-arm repeat noise.
Peak allocated memory unchanged; reserved memory about 0.7 GB higher.
A follow-up sweep in one job: withholding more SMs than DeepEP uses (24 with
comm_num_sm=12) was 0.9% slower. That supports tying the carveout tocomm_num_sm.The overlap's gain depends on how much backward communication is left to hide. A second job separated the SM budget from the overlap:
comm_num_sm=12comm_num_sm=20comm_num_sm=20comm_num_sm=28On this single node, more DeepEP SMs speed up every DeepEP call regardless of this option. The overlap saves 472 ms at 12 SMs and 246 ms at 20.
Review follow-up
zero_stage not in (1, 2)is rejected while the disabled default still permits the ordinary stage-0 path.safe_get_full_gradbeforeengine.step(), including stage-2 cases whereparam.gradis released. It does not accept an empty expert-gradient dictionary as proof of parity.AI assistance was used for review and test preparation. The human author reviewed the exact task-specific diff before signoff/publication. This updates an existing PR rather than opening a duplicate feature implementation.
DCO, test collection and changed-files pre-commit passed on the published head. Full GPU CI and infrastructure smoke jobs were skipped; they are not described as a full GPU CI run. On-device evidence for this review follow-up is the targeted H100 execution above.
Latest review follow-up
Applied the requested minimal test-double repair:
record_validateaccepts and recordszero_stage, and the existing sequence-parallel engine test asserts the actual stage is0. It no longer rejects the engine's keyword argument, and it would still catch missing stage forwarding instead of silently ignoring it. The original test reproducedTypeError: ... unexpected keyword argument 'zero_stage'before the repair.Synchronized with master to resolve the test-fixture import conflict. The worker-only fixture retains both this PR's standalone SwiGLU backward helpers and master's merged clamped activation helpers and tests. The only manual edits in this follow-up are in those two test files; no manual kernel, autograd, scheduling, numerical-threshold or default changes were made. The activation-weighting child remains separate.
Validation on 1x Apple M4 Pro CPU, macOS 26.7, Python 3.11.15, Torch 2.14.1, pytest 9.1.1; CUDA and accelerator driver are not applicable:
--forked. Three selected SwiGLU cases reach the merged worker fixture and skip explicitly because CUDA/Triton is unavailable; this is import/worker coverage, not kernel execution.CPU reproduction command, from the repository root:
The prior H100 and E2E results above are reused evidence for the overlap implementation, not a new device run of this merged tree. GPU integration, throughput, memory and convergence experiments were not repeated for this test-only review repair and import-conflict resolution. No additional ZeRO/offload/TP/PP or hardware combinations are claimed.