Repository navigation
AutoEP + AutoTP folding: Universal Checkpointing under ZeRO-3 - #8848
pengdurice wants to merge 1 commit into
Conversation
Signed-off-by: pengdurice <pengduhit@gmail.com>
0z5a
left a comment
There was a problem hiding this comment.
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.
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>
…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>
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_universalrefuse 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.
alone (
_zero3_rank_from_file). ZeRO-3 files are namedzero_pp_rank_{dp}_mp_rank_{tp}. Under folding the files ofdifferent TP ranks share a data-parallel rank, so they collided. That is why AutoEP + AutoTP folding under ZeRO-3 #8670 refused.
_merge_tp_slice_filesasserts that every pattern in AutoTP's universal info wasused. AutoTP lists the AutoEP expert weights (
experts.w1/w2/w3) amongtp_replicated_parameter_patterns. Thoseweights are merged by the AutoEP step, not by the TP merge, so their patterns are never used and the assertion
fails.
_load_checkpointcompares the folding layout recorded in the model-states file withthe runtime's (
_validate_autoep_folding_checkpoint_metadata). This rejects any other TP size, EP size or anunfolded runtime, although a universal load re-cuts every tensor for the runtime's layout.
Change
deepspeed/checkpoint/ds_to_universal.py:_zero3_tp_dp_ranks_from_file, which the densemerge already uses.
_zero3_rank_from_fileis no longer used and is removed, along with the folding refusal._merge_tp_slice_filesno longer counts a pattern as unused when it only matches parameters excluded from thismerge. The ZeRO-3 path now passes the AutoEP expert names as excluded; the ZeRO-1/2 path already did.
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 whencheckpoint.load_universalisset. Rigid loads keep it.
load_hp_checkpoint_stateinstage3.pycuts 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/code-docs/source/autoep.rstanddocs/_pages/config-json.md.tests/unit/v1/moe/test_autoep_autotp_sharded_attention.py:TestZero3FoldedCheckpointno longer expects the conversion to be refused.TestZero3FoldedUniversalCheckpoint. A distributed fixture trains a folded TP 2 x EP 2 run on 4 ranks for oneAdamW 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
d371f33ccwith this change, one node of 8 NVIDIA H200 GPUs, torch 2.8.0+cu126 (and masterwithout the change).
tests/unit/v1/moe/test_autoep_autotp_sharded_attention.py(includes the 10 new universal-checkpoint cases)TestTiedOutputHead::test_row_parallel_tied_head_is_refused(2 cases) fails the same on masterd371f33ccwithout this change: the test expects AutoTP to refuse a row-parallel output head tied to the embedding, and master no longer doestests/unit/v1/moe/test_autoep_checkpoint.pytests/unit/checkpoint/test_autotp_uc_checkpoint.pytests/unit/runtime/tensor_parallel/test_autotp_universal_checkpoint.pytests/unit/checkpoint/test_autoep_affine_universal.pytests/unit/checkpoint/test_universal_checkpoint.pyThe failures in these two files do not come from this change. Master
d371f33ccwithout this change gives the samefailing tests:
test_autotp_uc_checkpoint.pyand all 144 intest_universal_checkpoint.pyare "fixture ... not found"for fixtures defined as
DistributedFixtureclasses (affine_resume_checkpoint,baseline_ws2,baseline_ws4).This environment has pytest 9.1.1, which does not recognize the
_pytestfixturefunctionattribute thattests/unit/common.pysets to register those classes as fixtures.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:
test_autoep_autotp_sharded_attention.pyTestTiedOutputHead, 2 cases)test_autoep_checkpoint.pytest_autotp_uc_checkpoint.pytest_autotp_universal_checkpoint.pytest_autoep_affine_universal.pyThe 6 errors are "fixture ... not found" (
affine_resume_checkpoint4,uneven_vocab_checkpoint2) and the failure isgloo "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).