Skip to content

Synchronize pre-collective step failures across the process group - #71

Merged
thad0ctor merged 4 commits into
mainfrom
fix/precollective-failure-sync
Jul 15, 2026
Merged

Synchronize pre-collective step failures across the process group#71
thad0ctor merged 4 commits into
mainfrom
fix/precollective-failure-sync

Conversation

@thad0ctor

@thad0ctor thad0ctor commented Jul 15, 2026

Copy link
Copy Markdown
Owner

What & why

In sharded/FSDP2 runs, GefenMuon.step() and GefenMuonHybrid.step() ran closure() and the AMP overflow-skip before the collective section — the codebook full_tensor() all-gather in exact/distributed modes, and the Parallel-Muon dist.all_reduce/broadcast in the distributed step. Base already synchronized gradient presence, but not:

  • a closure() that raises on one rank (e.g. a data-loader or loss error on a single rank), or
  • an AMP found_inf that differs across ranks (each rank scans its own FSDP shard, so overflow is genuinely rank-local).

Either one makes a rank exit early while its peers enter the next collective → indefinite hang / NCCL timeout.

The fix

Synchronize failure across the process group before entering any collective, so every rank raises or skips symmetrically, without introducing a new hang and preserving fail-before-mutation.

  • Reusable primitive _synchronize_step_failure(local_failed, process_group) — all-reduces an int32 flag with ReduceOp.MAX over a supplied process group (mesh / dist.group.WORLD), guarded on is_initialized() / world_size < 2. _step_failure_collective_device places the flag on the right device per backend (nccl→cuda, gloo/default→cpu, honoring bound_device_id). _synchronize_step_control_range agrees min/max control bounds in one collective. The primitive takes the PG as an argument so other scopes (e.g. the convention's codebook-scope path) can re-point onto it rather than keep a divergent copy.
  • Step wiring: closure(), the capturable check, gradient-structure validation, and the AMP found_inf/grad_scale parse are wrapped and synchronized before work partitioning, codebook learning, or any state mutation. _step_failure_process_groups selects a control group that encloses every upcoming collective participant (or abstains, returning ()), so the sync itself cannot deadlock. CUDA-graph capture returns () (host-readable flags can't be captured; captured steps require an eager warmup).
  • AMP agreement: _prepare_synchronized_amp_step agrees on (present, overflow, scale_present, scale_value) across ranks before any unscale, raising symmetric errors on rank-divergent scaler config.
  • Hybrid: runs the preflight once at the composite level and suppresses the muon child's duplicate via an exception-safe _gefen_hybrid_precollective_preflight marker — no double-collective.

Tests

  • New tests/test_precollective_failure_sync.py (gloo, world=2, muon_exact / muon_distributed / hybrid_exact): a one-rank closure() raise and a divergent found_inf both make all ranks fail fast (per-step liveness deadline + no-hang assertion) with symmetric messages, and leave optimizer/param state byte-for-byte unchanged. Would catch a re-introduced hang (removing the sync blocks rank 1 in the collective past the deadline). 3/3 pass.
  • Full CPU suite: 456 passed / 0 failed. GPU (2×3090 NCCL): distributed_checkpoint_safety, grad_presence, capturable_fsdp2, fsdp2_parity, fused_multirank all pass.

Scope

Sharded / FSDP2 (sharded_mode exact or distributed) users. This is a standalone extraction onto main; the primitive is factored so the convention's codebook-scope failure sync re-points onto it (paired convention PR).

Summary by CodeRabbit

  • Bug Fixes
    • Improved distributed synchronization for Muon sharded preflight, including consistent propagation of pre-step closure failures and errors across process groups.
    • Strengthened AMP/GradScaler step gating by validating cross-rank AMP presence and control values (overflow/grad scale) before unscaling and deciding to step.
    • When stepping is skipped, optimizer parameters remain unchanged while post-step hooks still run.
  • Tests
    • Added distributed Gloo tests covering closure-failure sync and AMP-control mismatch/overflow scenarios, verifying unchanged optimizer state and hook behavior on all ranks.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e11ce2c4-230f-4ac7-816a-0286f18a2a22

📥 Commits

Reviewing files that changed from the base of the PR and between 58ca39d and dbbc001.

📒 Files selected for processing (1)
  • src/gefen/gefen_muon.py
📝 Walkthrough

Walkthrough

Gefen adds distributed preflight and AMP-control synchronization for Muon and hybrid optimizer steps, including backend-aware reductions, coordinated failure propagation, synchronized GradScaler decisions, and distributed Gloo coverage for closure and AMP-control scenarios.

Changes

Distributed AMP step synchronization

Layer / File(s) Summary
AMP control parsing and reductions
src/gefen/gefen.py
Adds backend-aware failure and control-range reductions, separates GradScaler control parsing from gradient mutation, validates scales, and updates unscaling behavior.
Muon sharded preflight protocol
src/gefen/gefen_muon.py
Selects participating process groups, synchronizes preflight failures and AMP controls, and gates sharded steps on collective decisions.
Hybrid optimizer integration
src/gefen/hybrid.py
Routes Muon hybrid steps through synchronized preflight, propagates errors, preserves post-hooks, and manages temporary child-step state.
Distributed synchronization coverage
tests/test_precollective_failure_sync.py
Adds Gloo multiprocessing tests for closure failures, divergent AMP controls, overflow, unchanged optimizer state, timeouts, cleanup, and post-hooks.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant GefenMuonHybrid
  participant GefenMuon
  participant ProcessGroup
  participant GradScalerControls
  Caller->>GefenMuonHybrid: step()
  GefenMuonHybrid->>GefenMuon: select process groups and preflight
  GefenMuon->>ProcessGroup: synchronize failures and AMP control ranges
  ProcessGroup-->>GefenMuon: collective step decision
  GefenMuon->>GradScalerControls: parse and unscale controls
  GefenMuon-->>GefenMuonHybrid: proceed or skip step
  GefenMuonHybrid-->>Caller: return loss or completed step
Loading

Possibly related PRs

  • thad0ctor/Gefen-X#4: Introduces the composite GefenMuonHybrid optimizer used by this step-flow refactor.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: synchronizing pre-collective step failures across the process group.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/precollective-failure-sync

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
src/gefen/gefen_muon.py (1)

1389-1509: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Missing @torch._dynamo.disable on the new host-branching sync wrappers.

_synchronize_sharded_step_flag, _synchronize_sharded_step_error, _synchronize_sharded_step_control_range, and _prepare_synchronized_amp_step all perform Python-level branching/raising on values derived from dist.all_reduce results, yet none carries @torch._dynamo.disable — unlike the sibling _step_failure_process_groups (line 1308) and the pre-existing _assert_sharded_grad_presence_consistent (line 1510), which both use it for the same kind of host-branching-around-collectives pattern. Raw dist.all_reduce(tensor, group=process_group) calls are a known Dynamo pain point (frequently causing graph breaks or Unsupported errors when traced), so under torch.compile+capturable=True (a combination this class's docstring explicitly advertises), these undecorated wrappers risk extra graph breaks/recompiles compared to the established pattern in this same file.

Consider adding @torch._dynamo.disable to these four methods for consistency with _step_failure_process_groups/_assert_sharded_grad_presence_consistent.

♻️ Proposed fix
+    `@torch._dynamo.disable`
     `@staticmethod`
     def _synchronize_sharded_step_flag(local_value, process_groups) -> bool:
         ...

+    `@torch._dynamo.disable`
     def _synchronize_sharded_step_error(
         self, error, phase: str, process_groups
     ) -> None:
         ...

+    `@torch._dynamo.disable`
     `@staticmethod`
     def _synchronize_sharded_step_control_range(local_control, process_groups):
         ...

+    `@torch._dynamo.disable`
     def _prepare_synchronized_amp_step(self, optimizer, process_groups) -> bool:
         ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gefen/gefen_muon.py` around lines 1389 - 1509, Add `@torch._dynamo.disable`
to _synchronize_sharded_step_flag, _synchronize_sharded_step_error,
_synchronize_sharded_step_control_range, and _prepare_synchronized_amp_step,
matching the existing decoration pattern used by _step_failure_process_groups
and _assert_sharded_grad_presence_consistent. Preserve their current
synchronization, branching, and error behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/gefen/gefen_muon.py`:
- Around line 1389-1509: Add `@torch._dynamo.disable` to
_synchronize_sharded_step_flag, _synchronize_sharded_step_error,
_synchronize_sharded_step_control_range, and _prepare_synchronized_amp_step,
matching the existing decoration pattern used by _step_failure_process_groups
and _assert_sharded_grad_presence_consistent. Preserve their current
synchronization, branching, and error behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 606cadce-c148-404a-b3fd-6595a647feb9

📥 Commits

Reviewing files that changed from the base of the PR and between 0f489b8 and d02874e.

📒 Files selected for processing (4)
  • src/gefen/gefen.py
  • src/gefen/gefen_muon.py
  • src/gefen/hybrid.py
  • tests/test_precollective_failure_sync.py

_synchronize_sharded_step_flag, _synchronize_sharded_step_error,
_synchronize_sharded_step_control_range, and _prepare_synchronized_amp_step
all branch/raise on values derived from dist.all_reduce, so they must not be
traced -- matching the existing @torch._dynamo.disable on the sibling
_step_failure_process_groups and _assert_sharded_grad_presence_consistent.
Under torch.compile + capturable (advertised in the class docstring) the raw
all_reduce + host branching otherwise forces avoidable graph breaks/recompiles.
Tracing-only change; no runtime behavior change (precollective test still 3/3).
thad0ctor added a commit that referenced this pull request Jul 15, 2026
Match the mainline fix (#71): the sharded sync wrappers
(_synchronize_sharded_step_flag/_error/_control_range, _prepare_synchronized_amp_step)
and their codebook-scope analogs (_synchronize_codebook_scope_failure,
_prepare_scoped_amp_optimizer_step) all branch/raise on dist.all_reduce results,
so they carry @torch._dynamo.disable like the sibling _step_failure_process_groups
and _assert_sharded_grad_presence_consistent. Tracing-only; no runtime change
(precollective + scoped-agreement tests still 8/8).
…vice

Two distributed-correctness fixes in _step_failure_process_groups (found in
review):

* Deadlock on overlapping meshes: the old logic selected a single mesh, or
  abstained (returned ()) only when a rank saw multiple non-enclosing meshes.
  With overlapping meshes (e.g. members {0,1,2} and {2,3}) a rank in both
  abstained while a rank in only one entered that mesh's all-reduce, so the
  shared rank never joined and the control collective hung. Sync over EVERY
  sharded mesh the rank participates in, deduplicated by process-group name and
  ordered consistently -- exactly the meshes _assert_sharded_grad_presence_-
  consistent already enters, so participation is symmetric and cannot desync.

* Wrong flag device: the flag used the ambient torch.cuda.current_device(),
  which can differ from the parameter's local shard device and target the wrong
  GPU. Derive the device from the shard (as the grad-presence preflight does)
  and thread it through as _synchronize_step_failure/_control_range's optional
  collective_device.

test_precollective_failure_sync + muon distributed suite stay green (466 passed).

@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: 58ca39d5a8

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

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gefen/gefen_muon.py
Comment thread src/gefen/gefen_muon.py Outdated
The previous rewrite flattened every sharded mesh's groups and sorted them by
process-group name. For a multi-dim (HSDP/TP) mesh whose group names do not
lexically follow the dimension order, that could make one rank enter a row
all-reduce while a peer is blocked in its column all-reduce -- a lock-ordering
deadlock in the preflight itself. Mirror _assert_sharded_grad_presence_-
consistent exactly instead: dedup meshes by content key, iterate them in
sorted-key order, and within each mesh keep the get_all_groups() dimension
order. Each group still carries the local shard device.

(Single pass per group, like the grad-presence preflight -- deeply overlapping
non-enclosing meshes keep that preflight's existing cross-mesh limitation.)

Full suite with GPU (2x3090 NCCL): 761 passed.
@thad0ctor
thad0ctor merged commit 937a85d into main Jul 15, 2026
15 checks passed
thad0ctor added a commit that referenced this pull request Jul 15, 2026
…ier-2 convention side) (#72)

* Preserve legacy vmean loads on the rank-local sharded path (#70)

* Preserve legacy vmean loads on the rank-local sharded path

_unwrap_rank_local_sharded_checkpoint validated the payload with the strict
default (allow_legacy_vmean_counter=False), so a rank-local (DTensor/FSDP)
checkpoint carrying vmean without the separate vmean_step counter -- a
pre-counter state the step-time resume path backfills from step -- was rejected
at load. The native load path already opts into that tolerance
(allow_legacy_vmean_counter=True); the rank-local path did not, so the two
disagreed and an otherwise valid legacy resume failed only on the sharded path.
This regressed against pre-load-atomicity behavior, which had no such check.

Pass allow_legacy_vmean_counter=True at the rank-local call site to match the
native path. This only relaxes the vmean-without-vmean_step case; a current
checkpoint (which carries vmean_step) is unaffected, and the inverse
vmean_step-without-vmean corruption check is unchanged.

tests/test_cpu_step_checkpoint.py::test_rank_local_validator_tolerates_legacy_vmean_without_step
pins the validator tolerance both ways (the strict default rejects the
pre-counter payload; the tolerance the rank-local path now opts into accepts it).

* Guard the rank-local unwrap path directly, not just the validator

Add test_rank_local_unwrap_tolerates_and_backfills_legacy_vmean: a single-rank
gloo world builds a rank-local (rank_local_dtensor_v2) checkpoint, drops the
separate vmean_step counter to simulate a pre-counter state, loads it through
_unwrap_rank_local_sharded_checkpoint (the call site the fix touches), and
asserts the first resumed step backfills vmean_step from step.

The existing validator test calls _validate_rank_local_states directly, so it
stays green if the unwrap call site regresses to strict validation. This test
fails without allow_legacy_vmean_counter=True on the rank-local path (verified:
reverting the fix raises "block second moment is missing vmean_step" at load).
CPU-only, no multiprocessing (~2s).

* Synchronize pre-collective Muon step failures

* Disable Dynamo tracing on the pre-collective sync wrappers

Match the mainline fix (#71): the sharded sync wrappers
(_synchronize_sharded_step_flag/_error/_control_range, _prepare_synchronized_amp_step)
and their codebook-scope analogs (_synchronize_codebook_scope_failure,
_prepare_scoped_amp_optimizer_step) all branch/raise on dist.all_reduce results,
so they carry @torch._dynamo.disable like the sibling _step_failure_process_groups
and _assert_sharded_grad_presence_consistent. Tracing-only; no runtime change
(precollective + scoped-agreement tests still 8/8).

* Close pre-collective sync gaps on the convention side

Four distributed-correctness fixes (found in review), on top of re-pointing the
codebook scope onto the mainline primitive:

* Overlapping-mesh deadlock + wrong flag device: same _step_failure_process_-
  groups rewrite as the mainline PR -- sync over every participated mesh
  (deduped, ordered) with the shard device, mirroring
  _assert_sharded_grad_presence_consistent. Threads collective_device through
  _synchronize_step_control_range too.

* Scoped Gefen AMP presence hang: plain Gefen.step gated
  _prepare_scoped_amp_optimizer_step() on local found_inf/grad_scale, so with a
  multi-member codebook scope a rank with those attributes entered the presence
  all-gather while a rank without them skipped it -- deadlock. Run the scoped
  AMP agreement on EVERY member of a multi-member scope, matching the
  unconditional Muon/Hybrid preflight.

* Dropped post-closure binding recheck (the CI failure): the merged Tier-2 step
  rewrite lost the convention's _assert_finalized_binding_layout() recheck after
  closure() on the mesh (non-scoped) branch, so a closure that rebinds
  param_groups was not caught -- test_finalized_layout_guard_rechecks_after_-
  closure[muon] failed. Re-assert it inside the preflight try (caught +
  synchronized) so a closure rebind fails before mutation and on every member.

Full convention suite: 1095 passed, 0 failed.

* Preserve mesh dimension order + close hybrid preflight gaps

Three more review fixes:

* DeviceMesh dimension order (same as the mainline PR): mirror
  _assert_sharded_grad_presence_consistent -- dedup meshes by content key,
  sorted-key order, get_all_groups() dimension order per mesh -- instead of
  flattening and sorting all groups by name, which could reorder a 2-D
  HSDP/TP mesh's row/column groups per rank and lock-order deadlock the
  preflight.

* Hybrid post-closure binding recheck (unscoped path): the binding-is-None
  branch stepped children without re-asserting _assert_finalized_binding_layout()
  after the closure/pre-hooks, so a rank-local closure swapping a same-shaped
  backup parameter passed the gradient scan and stepped under stale routing.
  The scoped path already rechecked; re-assert in the unscoped preflight try
  (caught + synchronized).

* Hybrid capture readiness: the _gefen_hybrid_precollective_preflight marker
  suppresses the muon child's own _assert_codebook_capture_ready() guard, and
  the hybrid preamble only checked devices, so a hybrid capturing before its
  Muon codebook initialized could run host-driven codebook init during capture.
  Call the muon capture/codebook readiness guard in the hybrid preflight.

Full convention suite with GPU (2x3090 NCCL): 1420 passed.
@thad0ctor
thad0ctor deleted the fix/precollective-failure-sync branch July 15, 2026 16:50
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.

1 participant