Skip to content

[zero] Run ZeRO-3's rank-agreement checks and barriers over the partition group - #9076

Open
LiRunGuo wants to merge 2 commits into
deepspeedai:masterfrom
LiRunGuo:zero3-partition-group
Open

LiRunGuo wants to merge 2 commits into
deepspeedai:masterfrom
LiRunGuo:zero3-partition-group

Conversation

@LiRunGuo

@LiRunGuo LiRunGuo commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

ZeRO-3's rank-agreement checks and init barriers ran over the world. They now run over the parameters' partition group, so a ZeRO-3 engine can train on a subset of the ranks while the other ranks hold no engine. With the default group the same ranks take part as before.

This is the first commit of #9074, sent on its own because it does not depend on RFC #9026 and is small (31 lines added and 19 removed in deepspeed/), plus a regression test as a second commit (85 lines in tests/unit/v1/zero/test_zero.py).

What breaks on master. Give deepspeed.initialize an mpu whose data-parallel group is a subset of the world, for example ranks 1-2 of three, and leave rank 0 without an engine (a trainer next to inference-only ranks, or two trainer layouts in the same processes):

  • DeepSpeedZeroOptimizer_Stage3._setup_for_real_optimizer calls dist.barrier() over the world, twice, and _create_fp16_partitions_with_defragmentation once more. The trainer ranks wait for rank 0, which never calls it.
  • assert_ints_same_as_other_ranks broadcasts from global rank 0 over the world (assert_lst_len_same_as_other_ranks, get_lst_from_rank0). It is called when a trace is recorded (PartitionedParameterCoordinator), in the coalesced all-gather (Init._all_gather) and, with enable_sanity_checks, in the gradient reduction. Rank 0 is outside the group and never sends.

Root cause. These calls predate engines over a subgroup: the data path of ZeRO-3 already uses the partition group (all_gather, reduce_scatter and the parameter broadcasts all pass it), and only the checks and barriers around it still assumed the world.

The change

  • zero/utils.py: assert_ints_same_as_other_ranks, assert_lst_len_same_as_other_ranks and get_lst_from_rank0 take an optional group and broadcast from the group's first rank. group=None is the old behaviour, so other callers are unaffected.
  • stage3.py: the three barriers and the sanity check pass self.dp_process_group.
  • partition_parameters.py: the two checks in _all_gather pass the parameters' ds_process_group.
  • partitioned_param_coordinator.py: takes process_group (default None) and passes it to its three trace checks; parameter_offload.py hands it the optimizer's dp_process_group.

Validation

One node with 4× RTX 3090 (24 GB), driver 595.58.03, torch 2.13.0+cu130, NCCL 2.29.7.

Regression test TestZero3OverSubgroup (in tests/unit/v1/zero/test_zero.py): a world of three, ZeRO-3 over ranks 1-2 through an mpu, rank 0 without an engine. Three SGD steps are compared with plain torch SGD on the same model and batch. SGD with gradient_clipping set to 0, so that the comparison sees the scale of the averaged gradient (an engine clips to norm 1.0 by default, and Adam hides a constant scale).

Tree Tests (python -m pytest -q -rfEs, from tests/) Result
master + the test commit unit/v1/zero/test_zero.py::TestZero3OverSubgroup does not finish; killed after 5 minutes
this PR, GPU (NCCL) same 1 passed in 9.4 s
this PR, CPU (gloo, DS_ACCELERATOR=cpu) same 1 passed in 30 s
this PR, GPU unit/v1/zero/test_zero.py, unit/v1/zero/test_zero_init_pg_contradiction.py, unit/runtime/zero/test_zero_context.py 137 passed, 1 skipped (marked "not working" on master)

I did not look at where the run on master blocks; the first world barrier in _setup_for_real_optimizer is the first call rank 0 never answers.

Compatibility

  • Default group: an engine's default dp_process_group holds every rank, so the same ranks take part in every barrier and check as before. The ZeRO test files above run at the default group.
  • Validated on a subgroup: ZeRO-3, fp32, SGD, no offload, GPU (NCCL) and CPU (gloo), one node. [rollout] In-memory relayout between sharded layouts (two-stage relay, pipelined execution) and ZeRO-3 training-state move #9074 adds Adam on a subgroup in its tests (a trainer on one of two ranks, and a trainer moving between layouts) and a bf16 model trained on two of four ranks in its DP4 → DP2 → DP4 run.
  • Not validated on a subgroup: fp16, parameter or optimizer offload, NVMe, enable_sanity_checks, several nodes.
  • Not covered: save_checkpoint and _commit_decoupled_checkpoint in engine.py still call dist.barrier() over the world, so a subgroup engine cannot save a checkpoint while the other ranks stay out. That is a separate change.
  • ZeRO-1/2 are not touched.

AI assistance: AI assistance was used. The code, the test and this description were drafted with an AI coding agent (Claude Code). The submitter reviewed every changed line and ran the tests above.

cc @Achyuthan-S @jinyouzhi

…tion group

A ZeRO-3 engine whose data-parallel group is a subset of the world (two
trainer layouts living in the same processes, or a trainer next to
inference-only ranks) hung or failed its trace checks: the checks broadcast
from global rank 0 over the world and the barriers waited for every rank.
They now run over the parameters' partition group; with the default group
nothing changes.

Signed-off-by: RunguoLi <li19107254665@gmail.com>
@Achyuthan-S

Achyuthan-S commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Thanks @LiRunGuo. I read the diff against master: it covers every caller of the three check helpers (the two in Init._all_gather, the three trace checks in PartitionedParameterCoordinator, and the sanity check in stage 3) and all three world barriers in stage3.py, and group=None keeps today's behaviour. dist.get_global_rank(group, 0) exists in deepspeed.comm, so the first-rank lookup works for any group. Looks right to me.

One related case this PR leaves out, which I hit on master while rebuilding ZeRO-3 engines in the same processes for the layout-change runs: ZeRO-3 numbers modules and parameters with process-wide counters, DeepSpeedZeRoOffload._register_deepspeed_module(self, module, count=[0]) (a mutable default) and the class attribute zero.Init.param_id, and neither is reset between engines. So after a phase in which only some ranks built an engine (DP4 → DP2 on two of four ranks), a new engine over all four ranks numbers its modules differently on the ranks that sat out, and the first trace check fails with "Detected a disagreement on list contents between rank0 and rank2". This PR doesn't touch those counters, so a subgroup engine followed by a new full-group engine should hit the same check; #9074 avoids it by keeping both engines alive. Resetting the counters per engine isn't safe in general, because parameter ids must stay unique across several zero.Init contexts for one model, so I'd treat it as a separate issue. I can open one with the reproduction if that's useful.

TestZero3OverSubgroup trains ZeRO-3 on ranks 1-2 of a world of three through an mpu, with no engine on rank 0, and
compares three SGD steps with plain torch SGD on the same model and batch. Without the previous commit the test
does not finish: the world barriers and the broadcasts from global rank 0 wait for rank 0.

Signed-off-by: RunguoLi <li19107254665@gmail.com>
@LiRunGuo

Copy link
Copy Markdown
Contributor Author

Thanks @Achyuthan-S for reading the diff.

The test is in. The second commit (54680fd) adds TestZero3OverSubgroup to tests/unit/v1/zero/test_zero.py: a world of three, ZeRO-3 on ranks 1-2 through an mpu, no engine on rank 0, three SGD steps compared with plain torch SGD. On master it does not finish; with the first commit it passes on GPU (NCCL) and on CPU (gloo). The description has the table.

The counters. Both are on master as you describe (_register_deepspeed_module(self, module, count=[0]) and Init.param_id). I have not reproduced the failure, but from the code I expect it with this PR too:

  • The trace checks compare the raw numbers ([m.ds_id for m in self.__submodule_order] and [p.param.ds_id for p in self.__param_order]). This PR only changes which ranks compare them.
  • So a new engine fails whenever its group mixes ranks that have registered different numbers of ZeRO-3 modules or parameters before. That is wider than a full-group engine after a subgroup one: DP2 on ranks 0-1 followed by a new DP3 on ranks 1-3 would hit it as well.
  • [rollout] In-memory relayout between sharded layouts (two-stage relay, pipelined execution) and ZeRO-3 training-state move #9074 avoids it only because DP4 → DP2 → DP4 returns to an engine that is still alive.

Yes please open the issue with your reproduction; I agree it is separate from this PR. One direction, untested: the checks could compare each id's position among the engine's own ids instead of the process-wide number. Ids stay unique across zero.Init contexts, and the check no longer depends on what a rank built earlier.

I've marked the PR ready for review.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T17:31:45.443169Z 54680fd Draft marked ready
ℹ️ 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.

@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: 54680fd9a0

ℹ️ 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".

zero_quantized_nontrainable_weights=self.zero_quantized_nontrainable_weights,
fast_sharding_for_leaf_module=self.fast_sharding_for_leaf_module,
log_trace_cache_warnings=self.log_trace_cache_warnings,
process_group=self.dp_process_group,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate traces across the parameter partition groups

In supported zero.Init-before-AutoTP setups, the optimizer DP group is not necessarily the parameters' partition group: tests/unit/checkpoint/test_zero_affine.py:430-443 explicitly builds world-partitioned parameters before creating TP2/DP2. Wiring the coordinator to self.dp_process_group lets its submodule and parameter-order checks pass independently in each DP lane, while its later all_gather_coalesced calls still communicate those parameters over param.ds_process_group; if TP lanes record different orders, equal-sized tensors can be mixed or the all-gather can hang instead of the disagreement being reported. Validate the trace for every represented parameter partition group rather than only the optimizer DP group.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

with get_accelerator().stream(self.reduce_and_partition_stream):
if self.enable_sanity_checks:
assert_ints_same_as_other_ranks([p.ds_id for p in params_in_bucket])
assert_ints_same_as_other_ranks([p.ds_id for p in params_in_bucket], self.dp_process_group)

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 Run the gradient check on the bucket's partition group

When a parameter's partition group differs from the optimizer DP group, as in the world-partitioned zero.Init-before-AutoTP path, this check compares only DP-lane peers even though the reduction immediately below uses bucket.process_group or _get_param_partition_group(params_in_bucket[0]) (stage3.py:1945-1960 and 1994-2013). With enable_sanity_checks, a different bucket order among the actual reduction peers can therefore go undetected and equal-shaped gradients can be reduced into the wrong parameters; pass the bucket's partition group here.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

@gpu-test-labeler gpu-test-labeler Bot added the skip-gpu-test Requires positive evidence that every changed behavior can be validated without a GPU. label Nov 14, 2026
@LiRunGuo

Copy link
Copy Markdown
Contributor Author

A correction to my comment above. I wrote that I had not reproduced the counter failure; I had met it before and forgot. In an early run of the DP4 ↔ DP2 switch, an engine built on a subset of the ranks first, and one over all ranks after it, failed the trace check with "disagreement on list contents". #9074 avoids it by building the engine over every rank first. That matches what the code suggests.

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

skip-gpu-test Requires positive evidence that every changed behavior can be validated without a GPU.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants