Skip to content

AutoEP + AutoTP folding: Universal Checkpointing under ZeRO-3 - #8848

Draft
pengdurice wants to merge 1 commit into
deepspeedai:masterfrom
pengdurice:peng-autoep-folding-ucp-v1
Draft

pengdurice wants to merge 1 commit into
deepspeedai:masterfrom
pengdurice:peng-autoep-folding-ucp-v1

Conversation

@pengdurice

Copy link
Copy Markdown
Contributor

AutoEP + AutoTP folding: Universal Checkpointing under ZeRO-3

Part of: #8672 (Q4 2026 roadmap), "Mixture-of-Expert (MoE) Training Scaling and Kernel Development": "AutoEP Performance Optimization & Parallel Folding (AutoEP + AutoTP)" (PoC @tohtana @pengdurice). Follows #8670.

Problem

#8670 makes ds_to_universal refuse a folded ZeRO-3 checkpoint: "Universal checkpoint conversion of an AutoEP +
AutoTP folded ZeRO-3 checkpoint is not supported yet. Load it with the same topology." So a folded run can only resume
with the same TP size, EP size and number of GPUs.

Three things stood in the way: two in the conversion, one in the load.

  1. File keys. The AutoEP expert step of the ZeRO-3 conversion keyed checkpoint files by the ZeRO data-parallel rank
    alone (_zero3_rank_from_file). ZeRO-3 files are named zero_pp_rank_{dp}_mp_rank_{tp}. Under folding the files of
    different TP ranks share a data-parallel rank, so they collided. That is why AutoEP + AutoTP folding under ZeRO-3 #8670 refused.
  2. The TP merge's strict check. _merge_tp_slice_files asserts that every pattern in AutoTP's universal info was
    used. AutoTP lists the AutoEP expert weights (experts.w1/w2/w3) among tp_replicated_parameter_patterns. Those
    weights are merged by the AutoEP step, not by the TP merge, so their patterns are never used and the assertion
    fails.
  3. The layout check on load. _load_checkpoint compares the folding layout recorded in the model-states file with
    the runtime's (_validate_autoep_folding_checkpoint_metadata). This rejects any other TP size, EP size or an
    unfolded runtime, although a universal load re-cuts every tensor for the runtime's layout.

Change

  • deepspeed/checkpoint/ds_to_universal.py:
    • The AutoEP ZeRO-3 files are keyed by (TP rank, DP rank), with _zero3_tp_dp_ranks_from_file, which the dense
      merge already uses. _zero3_rank_from_file is no longer used and is removed, along with the folding refusal.
    • _merge_tp_slice_files no longer counts a pattern as unused when it only matches parameters excluded from this
      merge. The ZeRO-3 path now passes the AutoEP expert names as excluded; the ZeRO-1/2 path already did.
    • The expert reassembly itself is unchanged. It already uses each file's EP rank and ZeRO partition rank. Under
      folding each expert slice exists on exactly one rank, and the existing duplicate and completeness checks still
      apply.
  • deepspeed/runtime/engine.py _load_checkpoint: skip the folding-layout check when checkpoint.load_universal is
    set. Rigid loads keep it.
  • Loading tensors needs no change. For each parameter, load_hp_checkpoint_state in stage3.py cuts the EP slice,
    then the TP shard, then the ZeRO partition in the parameter's own group (dense data-parallel or expert
    data-parallel). That is what folding needs.
  • Docs: docs/code-docs/source/autoep.rst and docs/_pages/config-json.md.
  • Tests, in tests/unit/v1/moe/test_autoep_autotp_sharded_attention.py:
    • TestZero3FoldedCheckpoint no longer expects the conversion to be refused.
    • New TestZero3FoldedUniversalCheckpoint. A distributed fixture trains a folded TP 2 x EP 2 run on 4 ranks for one
      AdamW step, saves, converts, and records every parameter and both Adam states at full size, then the losses and
      parameters of the next step. Each target layout loads the universal checkpoint and must restore every recorded
      tensor exactly. Targets with the source's 2 data-parallel groups read the same batches, so they must also repeat
      the next step. Targets: the same layout, EP 4, TP 4, 8 ranks with TP 4, and AutoEP without AutoTP; each in fp32
      and bf16.

ZeRO-1/2 conversion of folded checkpoints is not covered: it stores experts in per-expert files, one per TP rank,
and is still refused in autoep_universal.py (resolve_expert_ckpt_path).

Tests

On master d371f33cc with this change, one node of 8 NVIDIA H200 GPUs, torch 2.8.0+cu126 (and master
without the change).

File Result
tests/unit/v1/moe/test_autoep_autotp_sharded_attention.py (includes the 10 new universal-checkpoint cases) 43 passed, 2 failed: TestTiedOutputHead::test_row_parallel_tied_head_is_refused (2 cases) fails the same on master d371f33cc without this change: the test expects AutoTP to refuse a row-parallel output head tied to the embedding, and master no longer does
tests/unit/v1/moe/test_autoep_checkpoint.py 43 passed
tests/unit/checkpoint/test_autotp_uc_checkpoint.py 38 passed, 1 failed, 6 errors: the same tests fail on master without this change (see below)
tests/unit/runtime/tensor_parallel/test_autotp_universal_checkpoint.py 33 passed
tests/unit/checkpoint/test_autoep_affine_universal.py 20 passed
tests/unit/checkpoint/test_universal_checkpoint.py 144 errors: the same on master without this change (see below)

The failures in these two files do not come from this change. Master d371f33cc without this change gives the same
failing tests:

  • 6 errors in test_autotp_uc_checkpoint.py and all 144 in test_universal_checkpoint.py are "fixture ... not found"
    for fixtures defined as DistributedFixture classes (affine_resume_checkpoint, baseline_ws2, baseline_ws4).
    This environment has pytest 9.1.1, which does not recognize the _pytestfixturefunction attribute that
    tests/unit/common.py sets to register those classes as fixtures.
  • 1 failure in test_autotp_uc_checkpoint.py (TestRealCheckpointUniversalConversionTPxPP::test): gloo
    "Connection closed by peer" in the conversion's worker pool.

On master 5f266e2 (2026-10-15; 1 node of 8 H200, driver 580.126.09, torch 2.8.0+cu126), the failing
tests are the same with and without this change:

File With this change Master 5f266e2 without it
test_autoep_autotp_sharded_attention.py 43 passed, 2 failed (TestTiedOutputHead, 2 cases) 33 passed, 2 failed (the same 2)
test_autoep_checkpoint.py 43 passed not rerun
test_autotp_uc_checkpoint.py 38 passed, 1 failed, 6 errors 38 passed, 1 failed, 6 errors (the same tests)
test_autotp_universal_checkpoint.py 33 passed not rerun
test_autoep_affine_universal.py 20 passed not rerun

The 6 errors are "fixture ... not found" (affine_resume_checkpoint 4, uneven_vocab_checkpoint 2) and the failure is
gloo "Connection closed by peer" in TestRealCheckpointUniversalConversionTPxPP::test, on both sides.

Further checks (not part of the PR's tests)

A wider version of the new test: 7 target layouts. Besides the 5 above: 8 ranks with TP 2 x EP 4, and AutoEP EP 4
without AutoTP. Run in fp32 and bf16, with the optimizer-offload PR for folding applied, and in 3 offload
combinations: saved and loaded without offload, saved and loaded with offload, saved without and loaded with offload.
All 42 loads restore every parameter and both Adam states exactly. Every target with the source's 2 data-parallel
groups repeats the next step: losses equal, and parameters within 1e-4 relative, leaving out the attention key bias.
In bf16 only the same layout repeats it.

The key bias is left out of the next-step comparison because its true gradient is zero. Its computed gradient is
rounding noise that another layout adds up in another order, and AdamW turns that noise into steps of about lr
(observed differences 6e-4 to 1.5e-3 relative).

Duplicate check

Searched open and recent pull requests on 2026-10-15 ("universal checkpoint", "ds_to_universal", "folding", "AutoEP"): none converts folded ZeRO-3 checkpoints. #8800 plans live expert transfers between placements; #8637 links ZeRO-1/2 fragments.

Proposal

New functionality: per CONTRIBUTING.md it needs an accepted proposal issue first. Draft:
descriptions/r3/peng-autoep-folding-ucp-v1-proposal-issue.md. TODO(user): open the issue, wait for acceptance, then link it here.

AI assistance

This change, its tests and this description were prepared with AI assistance (Claude).

Signed-off-by: pengdurice <pengduhit@gmail.com>

@0z5a 0z5a 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.

Thanks for picking this up, it fills an important gap for the folding work. I went through the converter path for folded ZeRO-3: routed experts aren't TP-sliced under folding (expert TP = 1), fragments don't collide since each global rank has a unique (ep_rank, edp_rank), ZeRO padding is stripped via ds_zero_partition_groups, and TP-replicated params go through replicated_map.rebuild. A synthetic folded grid over 7 layouts with odd sizes (so EDP padding occurs) reassembles fp32 / exp_avg / exp_avg_sq bitwise for me. The engine change is a no-op for non-folded runs, and comparing against the source engine's live state in the GPU test is the right reference.

1. [blocking] Existing unit test now fails with KeyError: 0 — ds_to_universal.py:709, test_autoep_unit.py:941

_autoep_expert_param_names_by_rank now keys metadata_by_rank by (tp_rank, dp_rank), but test_universal_converter_detects_zero3_partitioned_autoep_model_state still indexes metadata_by_rank[0]. It fails on CPU (DS_ACCELERATOR=cpu), and it's the one failure in the cpu-torch-latest check on 9362efc. Could you update it to metadata_by_rank[(0, 0)] and note the key shape in the helper's docstring, since other tests import it?

2. [should-fix] Docs promise module-only loads that aren't tested — config-json.md:1147

The new text promises weights-only / module-only universal loads at a different autotp_size, but TestZero3FoldedUniversalCheckpoint only exercises the default load_checkpoint. Module-only goes through load_module_checkpoint_state_from_checkpoint_dir_stage3, which never sees a folded source here. Could you add one load_module_only=True target that compares fp32 against the recorded state, or narrow the docs? Also, autoep.rst around L597 still says only autoep_size / DP size can change via universal checkpointing; worth mentioning autotp_size for folded runs.

3. [should-fix] CI covers a narrow set of layout changes — test_autoep_autotp_sharded_attention.py:529-535

On the 4-GPU runners (8, 4, 2) skips, so no CI-run target grows the dense DP degree; only (4, 4, 2) changes it, down to 1. A 4-rank (4, 1, 4) target would move dense DP 2→4, EDP 2→1 and TP 2→1 in one case. Also, with ATTENTION_ONLY_TP_SPECS only q/k/v (+bias) and o_proj are TP-sliced; the default-spec layouts (MLP gate/up column, down row, row-parallel bias, vocab-parallel embedding/head) never reach this converter path. One use_default_specs target, or a note in the description that they're out of scope?

4. [question] Experts are published as TP-replicated in the AutoTP UC metadata — ds_to_universal.py:608-611

Under folding, collect_autotp_universal_checkpoint_info publishes ...experts.w[123] as TP-replicated with a replicated_map over the EP-local shape, but TP peers hold different experts (with TP=EP=2 the groups coincide). Filtering those patterns out of the strict check works for this converter, but the metadata itself stays wrong. Would it be cleaner to skip AutoEP expert params when collecting AutoTP UC info? Anything that later plans from AFFINE_MAP in UC info (e.g. the shard-to-shard planner / executor) would otherwise trust a wrong map.

5. [nit] k_proj.bias exclusion — test_autoep_autotp_sharded_attention.py:577

The key bias only has a zero true gradient on its non-rotary half. With partial_rotary_factor=0.5 the rotary half is rotated by position-dependent RoPE; in fp64 on this tiny GLM-4-MoE I get max |grad| ≈ 1e-4 on the rotary dims (same order as q_proj.bias) vs ≈ 4e-11 on the pass-through dims. Could you exclude only [..., int(head_dim * partial_rotary_factor):] of each head instead of dropping the whole tensor?

6. [nit] Fallback partition values — ds_to_universal.py:870-871

If ds_zero_partition_groups were missing, this falls back to the dense DP size / rank, which is wrong for expert sub-groups (partitioned over EDP). Every partition-native AutoEP ZeRO-3 checkpoint writes it, so raising here seems safer than guessing.

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>
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Oct 26, 2026
…pspeedai#8800)

## Summary

deepspeedai#8623 plans shard-to-shard copies between two `ParamAffineMap`s. Today,
though, the only AutoEP maps come from checkpoint metadata, so nothing
at runtime can feed the planner. This PR closes that gap: it builds the
source map from the placement the live `AutoEPMoELayer` actually holds,
and builds the target map from a target placement descriptor. Both maps
then go straight to `plan_transfer`.

```
AutoEP runtime placement -> source / target ParamAffineMap -> plan_transfer (deepspeedai#8623) -> copy descriptors
```

Maps of what each process actually stores under expert data parallelism
(EDP) and ZeRO are left to a follow-up, which builds them by nesting
these layout maps with ZeRO's storage maps.

Related: deepspeedai#8252 (§8.4 AutoEP expert placement), deepspeedai#8230, deepspeedai#8544.

## Changes

- `AutoEPMoELayer.expert_affine_maps()` returns the affine maps of the
live `w1`/`w2`/`w3`.
- Map ranks are EP-local ranks. The docstring explains how to translate
them with `dist.get_global_rank(self.ep_group, rank)`.
- Each shard is the unpartitioned packed `[local_experts, ...]` tensor.
ZeRO-3/EDP fragments stay outside the map, and the shape comes from
`ds_shape`.
  - No collectives.
- `AutoEPMoELayer.plan_expert_transfer(target_placement, limits=None, *,
replica_selector=None)` returns one `TransferPlan` per expert parameter.
- It only plans: it moves no data, swaps no parameters and migrates no
optimizer state.
- `autoep_affine.plan_autoep_placement_transfer(source_placement,
target_placement, logical_shape, ...)` is the layer-independent form.
Both placements go through the existing
`autoep_placement_to_affine_map`, so no second placement→map conversion
is introduced.
- Placements are compared by explicit rank IDs rather than list order
(`autoep_placements_equal`), and each parameter's published
`DS_AUTOEP_UC_META` placement and affine map must equal the live
lowering.
- The live placement must match how `repack_expert_weights` actually
packed the weights; today that is contiguous and uniform. A descriptor
that disagrees raises `NotImplementedError`, rather than producing a map
that addresses shards the weights do not have.
- A placement the descriptor can express is not necessarily one the
runtime can execute. The target placement is only used for planning.

## Not in scope

- Executing plans or transporting data (deepspeedai#8809), parameter swap,
optimizer-state migration.
- Storage maps of EDP replicas and ZeRO-3 fragments (follow-up PR), and
replica selection policy (deepspeedai#8252).
- Placement generation/versioning: the runtime does not change placement
after construction yet.
- Runtime support for non-uniform placements.

## Tests

`tests/unit/v1/moe/test_autoep_unit.py`:
- `test_live_expert_maps_address_the_packed_runtime_weights`: for each
rank, the map's shard shape and contents match the real packed
`w1/w2/w3`. The reference is the full tensor from
`repack_expert_weights(ep_size=1)`, which does not depend on any affine
map.
-
`test_plan_expert_transfer_moves_live_shards_into_target_packed_order`:
from EP=2 into 3 ranks with reordered, uneven, empty and replicated
placements; executing the plan reproduces the target shards, every
element is written once, and planning never calls `rebuild`/`extract`.
- `test_live_expert_maps_reject_a_placement_the_weights_do_not_have`,
`test_live_expert_maps_identify_placement_ranks_by_id`,
`test_live_expert_maps_reject_a_published_map_the_placement_does_not_produce`.

`tests/unit/checkpoint/test_autoep_affine.py`:
`test_placements_equal_identifies_ranks_by_id`.

Results (Intel Arc Pro B70):
- `test_autoep_unit.py`, `test_autoep_affine.py`,
`test_autoep_affine_universal.py`, `test_affine_transfer_plan.py`,
`test_affine_shard_map.py`, `test_autoep_zero3_metadata.py`: 432 passed,
6 skipped.
- `test_autoep_checkpoint.py` (8× B70): 32 passed, 2 skipped.


## Contribution checklist

**Related work.**
- This PR builds on deepspeedai#8623 (`plan_transfer`).
- Plan execution is deepspeedai#8809; replica policy is deepspeedai#8252.
- deepspeedai#8844 and deepspeedai#8848 change Universal Checkpointing for AutoEP. They do not
produce runtime maps.

A search for AutoEP placement and transfer and for affine maps found no
overlapping PR.

**Commands and results.** Hardware and software: Intel Arc Pro B70 (XPU,
XCCL), Level Zero driver 1.15.39122 (compute runtime 26.27.39122.11),
torch 2.14.0.dev20260804+xpu.
```
pytest -p no:randomly tests/unit/v1/moe/test_autoep_unit.py tests/unit/checkpoint/test_autoep_affine.py \
  tests/unit/checkpoint/test_autoep_affine_universal.py tests/unit/checkpoint/test_affine_transfer_plan.py \
  tests/unit/checkpoint/test_affine_shard_map.py tests/unit/checkpoint/test_autoep_zero3_metadata.py
pytest -p no:randomly tests/unit/v1/moe/test_autoep_checkpoint.py
```
Results are listed under Tests above.

**Numerics, performance, memory.** Training is not affected. The new
methods only run when called and use no collectives.

**Compatibility matrix.**

| | Validated | Not validated |
|---|---|---|
| ZeRO | Independent: maps describe the unpartitioned packed tensors |
Storage under EDP/ZeRO (follow-up) |
| Parallelism | EP=2; target placements on 3 ranks | |
| Precision | Dtype-independent: maps describe shapes and offsets | |
| Accelerator | XPU | CUDA, HPU |

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

---------

Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants