Skip to content

Fix gradient accumulation under FSDP2: defer cross-replica all-reduce - #162

Open
Sullivan07043 wants to merge 6 commits into
NVIDIA:mainfrom
Sullivan07043:fix-fsdp2-accum
Open

Fix gradient accumulation under FSDP2: defer cross-replica all-reduce#162
Sullivan07043 wants to merge 6 commits into
NVIDIA:mainfrom
Sullivan07043:fix-fsdp2-accum

Conversation

@Sullivan07043

Copy link
Copy Markdown
Contributor

Fixes #161 (ddp_sync_grad is a silent no-op under FSDP2, so trainer.grad_accum_iter saves no communication).

Change. Add an FSDP2 branch to ddp_sync_grad: on non-boundary micro-steps, defer only the cross-replica all-reduce via FSDPModule.set_requires_all_reduce(enabled, recurse=False) on every FSDPModule. Restore it in the finally block. The intra-node reduce-scatter still runs each micro-step, so gradients stay sharded and per-rank memory does not grow. DDP behavior is unchanged.

Validation (2x 8xA100-80GB, Ethernet, HSDP shard=8 replicate=2, batch 64/rank, fleet whose accum=1 step is 22.2 s):

Config s/step
accum=1 22.2
accum=4, without this fix (measured behavior = 4x) 88.8
accum=4, with this fix 54.3 / 54.3 / 54.9

Decomposition from the two measured points: compute 10.8 s + sync 11.4 s per micro-batch. Throughput 1.63x at accum=4. Loss stayed in the expected band.

One commit, 18 added lines, no API change.

ddp_sync_grad only handled DistributedDataParallel. Under FSDP2
(fully_shard) it was a silent no-op, so every micro-step of an
accumulation window ran the full cross-node gradient reduction and
grad_accum_iter saved no communication (measured: accum=4 step =
4x the accum=1 step on 2x8 A100 over Ethernet).

Defer only the cross-replica all-reduce via
FSDPModule.set_requires_all_reduce on non-boundary micro-steps. The
intra-node reduce-scatter still runs every micro-step, so gradients
stay sharded and per-rank memory does not grow.

Validated on 2x 8xA100-80GB (HSDP shard=8 replicate=2): accum=4 step
54.5 s vs 88.8 s unamortized on a fleet whose accum=1 step is 22.2 s.
@lfengad

lfengad commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the fix — the overall approach makes sense to me. Just two small things I think we should address before merging:

  1. The FSDP branch always restores set_requires_all_reduce(True), instead of restoring the previous state like the DDP branch does. Could we preserve the old state here as well? Otherwise nested or external sync contexts could be changed unexpectedly.
  2. The FSDP modules are updated before entering the try/finally. If one of those calls raises midway, the modules already updated would not be restored. Could we move that setup into the protected block?

Thanks!

lfengad and others added 2 commits August 6, 2026 20:48
…finally

- Capture each FSDP module's prior all_reduce_grads (mirroring the
  setter's write target) and restore exactly that value on exit, so
  nested or external sync contexts are preserved.
- Record (module, prev) as mutations happen inside the protected block;
  if the setup loop raises midway, the finally block restores exactly
  the modules already changed.
- Same treatment for the DDP branch via a mutation flag.

Verified with a single-rank fully_shard test: flip/restore, nested
contexts preserving outer state, exception-path restore, enabled=True
no-op.
@Sullivan07043

Copy link
Copy Markdown
Contributor Author

Thanks for the review — both points addressed in 799dbba:

  1. The context now captures each FSDP module's prior all_reduce_grads (mirroring the setter's write target) and restores exactly that value on exit, so nested or external sync contexts keep their state. The DDP branch behavior is unchanged; it now restores through a mutation flag for the same exactness.
  2. All module mutations happen inside the try/finally block, recording (module, prev) as they land — if the setup loop raises midway, the finally restores exactly the modules already changed.

Verified with a single-rank fully_shard semantic test: flip/restore, nested contexts preserving an outer False, exception-path restore, and enabled=True as a state no-op.

foreverlms pushed a commit that referenced this pull request Aug 7, 2026
The locked hf 1.25.1 in `cosmos_framework/utils/hf_cli` fails on
repositories migrated to Hugging Face Xet storage. Reproduced with the
framework's own download path (`checkpoint_db._hf_download`) against
`nvidia/Cosmos-Guardrail1`:

```
RuntimeError: Task error: Unable to parse string as hex hash value
```

The action-policy inference server hard-requires that download at
startup, so serving is currently broken on a fresh machine.

**Change.** Bump the pin to `hf==1.26.0` and regenerate `uv.lock` with
the documented procedure from the file header (`uv lock
--upgrade-package hf`). Two files, no code change.

**Validation.** On a fresh 8xA100 instance, cleared the model from the
HF cache and re-ran the framework's exact download command with the new
lock: full 3.5 GB snapshot (102 files) downloaded clean and returned the
snapshot path.

Found while validating #162 on rented hardware.

Co-authored-by: lfengad <liangf@nvidia.com>
Comment thread cosmos_framework/utils/distributed.py
…or uninitialized

Every non-DDP model takes the FSDP branch, including plain single-GPU
runs. Guard it with dist.is_available()/is_initialized(): a build
without distributed no longer hits the fsdp import on every micro-step,
and initialized-but-plain runs skip the per-step modules() walk.

Verified: with dist uninitialized the context yields without walking
modules(), and exceptions propagate cleanly through the early exit.
@lfengad
lfengad self-requested a review August 9, 2026 03:49

@lfengad lfengad left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wait for the latest sync release of the framework code, then could merge. Thx!

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.

trainer.grad_accum_iter saves no communication under FSDP2: ddp_sync_grad is a no-op for fully_shard modules

2 participants