Skip to content

AutoEP + AutoTP folding: allow CPU optimizer offload under ZeRO-3 - #8829

Open
0z5a wants to merge 4 commits into
deepspeedai:masterfrom
0z5a:autoep-folding-zero3-offload
Open

0z5a wants to merge 4 commits into
deepspeedai:masterfrom
0z5a:autoep-folding-zero3-offload

Conversation

@0z5a

@0z5a 0z5a commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

#8670 left optimizer and parameter offload rejected for AutoEP + AutoTP folding ("not validated yet ...
run a follow-up proof for per-family replica groups"). This PR provides that proof for ZeRO-3 with the
optimizer offloaded to the CPU, and accepts that configuration:

{
  "zero_optimization": {"stage": 3, "offload_optimizer": {"device": "cpu"}},
  "tensor_parallel": {"autotp_size": 2},
  "expert_parallel": {"enabled": true, "autoep_size": 2}
}

Everything else stays rejected, now with a message that names the unsupported part: optimizer offload
with ZeRO stages 1 and 2, NVMe optimizer offload, parameter offload, and Muon with optimizer offload.
An explicit "device": "none" is treated as no offload (it was rejected before).

Why ZeRO-3 + CPU optimizer offload is already correct

  • ZeRO-3 applies the folded per-family gradient correction (_apply_autoep_folding_tp_correction:
    AVERAGE for TP-replicated parameters, /tp_size for routed experts, SKIP for TP-sharded ones) in
    reduce_independent_p_g_buckets_and_remove_grads, before every reduce-scatter. That path is the same
    with and without offload; offload only changes where the reduced partitions are stored.
  • The offloaded norm (complete_grad_norm_calculation_for_cpu_offload) uses the same folding-aware
    counting as the device norm (_grad_norm_counts_every_rank plus the expert-parallel all-reduce), from
    per-partition norms recorded at the accumulation boundary.

The test below checks the claim end to end instead of relying on this reading.

Changes

  • validate_folding_global / validate_autoep_config: take the offload devices instead of
    "offload configured" booleans (offload_optimizer_device, offload_param_device); accept stage 3 +
    "cpu"; reject the rest as above. Internal helpers; the two in-repo callers are updated.
  • DeepSpeedEngine: passes the configured devices.
  • ZeRO-3: rejects Muon with optimizer offload under folding. The offloaded Muon update gathers full
    gradients on its own path, which this PR does not validate (and which overlaps ongoing Muon offload work).
  • Docs: the AutoEP folding constraint in autoep.rst.

Tests

  • test_autoep_autotp_folding_config.py: accepted and rejected offload combinations (replaces the
    blanket-rejection test; the duplicate in test_autoep_autotp_grad_parity.py is removed).
  • test_autoep_autotp_sharded_attention.py, new TestFoldedZero3ClippedUpdate: reuses AutoEP + AutoTP folding under ZeRO-3 #8670's
    per-gradient comparison against an unfolded ZeRO-0 reference and adds one clipped SGD step. For each
    of 4 folding shapes (world 4; TP x EP = 2x2, 2x4, 4x2, 4x4), zero.Init on/off, and offload on/off,
    it compares losses, every gradient, the global norm, and every parameter's update. Clipping (clip
    1e-2, lr 1/clip) makes the update depend on the norm the optimizer computed, so a wrong offloaded norm
    fails the test; the comparison allows one fp32 ulp of the parameter for recovering an update as
    after - before, and asserts that a 0.1% error in the update is still detected.

Results (on-device)

Hardware: 1 node, 4x NVIDIA H100 (64 GB, NVLink NV6 between all pairs), driver 595.91.07 (CUDA 13.2),
torch 2.12.1+cu130, NCCL 2.29.7, transformers 5.15.1, Python 3.12.1 (MareNostrum 5 ACC).

Command (from tests/) This PR Unpatched base 1c862f7df
pytest unit/v1/moe/test_autoep_autotp_sharded_attention.py -k TestFoldedZero3ClippedUpdate 16 passed offload off: 8 passed; offload on: 8 fail with the old "not validated yet" ValueError (the restriction this PR lifts)
pytest unit/v1/moe/test_autoep_autotp_sharded_attention.py -k "not TestFoldedZero3ClippedUpdate" 18 passed, 15 skipped, 2 failed 18 passed, 15 skipped, 2 failed
pytest unit/v1/moe/test_autoep_autotp_{bf16_folding_parity,checkpoint,deepcompile_reject,dispatch,folding_config,folding_groups,grad_parity,runtime,zero1_overlap_folding}.py 81 passed, 9 skipped 75 passed, 9 skipped
pytest unit/v1/moe/test_autoep_autotp_folding_config.py (CPU) 33 passed —

The 2 failures are TestTiedOutputHead::test_row_parallel_tied_head_is_refused[False/True] ("AutoTP sharded an
output head whose weight is tied to the input embedding"). They fail identically on the unpatched base in
the same job, so they are pre-existing and unrelated to this change (likely AutoTP tied-head detection
with transformers 5.15.1); not addressed here. The global norms match the reference exactly, for example
folded 2.380656 vs reference 2.380656 (TP 2) and 3.552228 vs 3.552228 (TP 4).

8-rank shapes skip on the 4-GPU node. The GPU runs used base 1c862f7df; the branch is rebased onto
5f266e26d without conflicts (the 4 commits in between touch DeepEP slicing, XPU op builders, ZeRO-2
partition storage and a test footprint, none of the paths changed here). pre-commit run --files on the
changed files passes (yapf, flake8, codespell, check-torchdist, check-license, check-torchcuda).

Compatibility matrix

Validated: ZeRO-3 x optimizer offload {none, cpu} x zero.Init {off, on} x fp32 x folding TP {2, 4}
x EP {2, 4} (world 4), torch SGD on the offloaded CPU fp32 partitions.

Not validated (and either rejected or left unchanged): DeepSpeedCPUAdam's offloaded step (the tests run
torch SGD on the CPU fp32 partitions with zero_force_ds_cpu_optimizer: false; DeepSpeedCPUAdam steps
the same partitions but through its own kernel), bf16/fp16 with offload, 8-rank shapes, NVMe offload
(rejected), parameter offload (rejected), ZeRO-1/2 optimizer offload (rejected; its offloaded norm path is
being changed by #8823), Muon with offload (rejected).

Not a duplicate

No open or merged PR enables offload with AutoEP + AutoTP folding (searched "folding offload",
"AutoEP offload", "AutoTP offload"). #8823 fixes ZeRO-1/2 routed-expert clipping under folding and does not
touch ZeRO-3 or offload; ZeRO-1/2 offload is left rejected so the two do not interact. Tracked in the
Q4 roadmap #8672 (AutoEP + AutoTP folding milestone).

Folding rejected all ZeRO offload until the per-family gradient
corrections were shown to survive it. Under ZeRO-3 the corrections run
before every reduce-scatter on the path shared with and without offload,
and the offloaded norm uses the same folding-aware counting as the device
norm, so CPU optimizer offload leaves gradients, norm and update unchanged.

Accept offload_optimizer.device "cpu" with ZeRO-3. Keep rejecting
optimizer offload with ZeRO-1/2 and to NVMe, parameter offload, and Muon
with optimizer offload, each with a specific message. Treat device "none"
as no offload. Test losses, gradients, the global norm and the clipped
update against the unfolded reference, with and without offload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T14:02:02.973040Z 5c6be23 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pengdurice
pengdurice enabled auto-merge October 20, 2026 01:54
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Oct 21, 2026
deepspeedai#8855)

## Summary

Fixes a stale test expectation.
`TestTiedOutputHead::test_row_parallel_tied_head_is_refused[False/True]`
in `tests/unit/v1/moe/test_autoep_autotp_sharded_attention.py` fails on
master:

```
AssertionError: AutoTP sharded an output head whose weight is tied to the input embedding
```

The test (added in deepspeedai#8670) expects AutoTP to refuse a row-parallel output
head whose weight is tied to the input embedding. Since 3c6a5ca
("[AutoTP] Support tied embeddings for row-parallel…"), AutoTP supports
this case: it replaces the embedding with a `HiddenParallelEmbedding`
that shares the head's sharded weight. The two changes landed a day
apart, so the expectation was already stale when deepspeedai#8670 merged.

Change (test only, no runtime code modified):
- Rename the test to `test_row_parallel_tied_head_keeps_the_tie`.
- With and without `zero.Init`, check after `deepspeed.initialize` that
the embedding is a `HiddenParallelEmbedding` and `lm_head.weight is
embed_tokens.weight`.
- Run it in the CPU multi-rank nightly (`cpu-torch-multi-latest`).
Before, it skipped on CPU and needed 4 GPUs, so no nightly ran it: the
GPU nightly (`ci/torch_latest.py`) has exactly 2 GPUs. The check only
builds the engine, so it needs no accelerator: drop the CPU skip and
mark it `sequential`, because that nightly selects tests under `unit/v1`
only through its sequential phase. It keeps 4 ranks, which that nightly
admits (`LOCAL_SIZE=4`).

The original concern of the test still holds: under `zero.Init` AutoTP
gathers the partitioned head weight into a new parameter, and if the
embedding kept the old one the two would train as separate copies. The
identity check catches that. Numerical training parity of a tied
row-parallel head is already covered by
`TestTiedRowParallelOutputHeadTraining` in
`tests/unit/v1/autotp/test_autotp_training.py`.

## Related work

No open PR fixes this test. deepspeedai#8829 and deepspeedai#8848 both list these two failures
as pre-existing and leave them unchanged. I searched open PRs touching
`TestTiedOutputHead` and tied/vocab-parallel output heads.

## Tests

```
cd tests
# CPU, as in cpu-torch-multi-latest
DS_ACCELERATOR=cpu LOCAL_SIZE=4 DS_DISABLE_REUSE_DIST_ENV=1 pytest -p no:randomly --forked -m sequential "unit/v1/moe/test_autoep_autotp_sharded_attention.py::TestTiedOutputHead"
# XPU
CUDA_VISIBLE_DEVICES=4,5,6,7 pytest -p no:randomly -m sequential "unit/v1/moe/test_autoep_autotp_sharded_attention.py::TestTiedOutputHead"
```

| Test | master `cc7b5125d` | This PR |
|---|---|---|
| `TestTiedOutputHead[zero_init=False]` | failed on XPU
(`AssertionError` above) | passed on CPU (4 gloo ranks) and XPU (4
ranks) |
| `TestTiedOutputHead[zero_init=True]` | failed on XPU (`AssertionError`
above) | passed on CPU (4 gloo ranks) and XPU (4 ranks) |

`pre-commit run --files` on the changed file passes.

Hardware and software:
- CPU: Intel Xeon Platinum 8457C, 4 ranks over gloo
- XPU: 4× Intel Arc Pro B70
- Level Zero driver 1.15.39122 (compute runtime 26.27.39122.11)
- torch 2.14.0.dev20260804+xpu, transformers 5.17.0

## Compatibility matrix

Test-only change; no numerics, performance or memory impact.

| | Validated | Not validated |
|---|---|---|
| ZeRO | Stage 3, with and without `zero.Init` (as in the test) | Other
stages (not exercised by this test) |
| Precision | fp32 | |
| Offload | | Not exercised |
| Parallelism | AutoTP 2 × AutoEP 2 folding, world size 4 | |
| Accelerator | CPU, XPU | CUDA (not run locally; the GPU nightly does
not select `sequential` tests) |

## AI assistance

This PR was developed with AI assistance (GitHub Copilot CLI). I
reviewed every changed line and ran the tests above.

cc @pengdurice @0z5a @delock

---------

Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Ma, Guokai <guokai.ma@gmail.com>
Keep the existing clipped-update coverage alongside the upstream sequential
tied-head test and its shared embedding assertions.

Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
auto-merge was automatically disabled October 21, 2026 11:56

Head branch was pushed to by a user without write access

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants