Skip to content

Overlap FSDP communication with compute - #5719

Merged
wujingyue merged 8 commits into
NVIDIA:mainfrom
wujingyue:mfsdp-prefetch-on-5513
Jul 16, 2026
Merged

Overlap FSDP communication with compute#5719
wujingyue merged 8 commits into
NVIDIA:mainfrom
wujingyue:mfsdp-prefetch-on-5513

Conversation

@wujingyue

@wujingyue wujingyue commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

This implements the explicit prefetching approach used in MFSDP v1 for MFSDP v2 (mfsdp_v2). The current implementation assumes that static module orders match the execution order closely enough for one-step prefetch.

Each FsdpModule's pre-forward hook prefetches the next FsdpModule's parameter all-gather from forward_order. During backward, pre-hooks follow a separately computed backward_order instead of assuming backward is strictly the reverse of forward_order, which matters for nested module trees.

An example trace from the added unit test:

image

Implementation

Forward prefetch

Each FsdpModule tracks materialization with _unshard_event. _unshard_parameter_groups() is idempotent: if _unshard_event is already set, the FsdpModule was already unsharded or prefetched; otherwise it enqueues the all-gather on the shared all-gather stream and records _unshard_event after materialization.

pre_forward waits on _unshard_event instead of waiting on the whole all-gather stream, so compute depends only on the current FsdpModule's materialization and not on later release work queued on that stream. It then issues the next FsdpModule's all-gather from forward_order. _reshard_parameter_groups() queues release on the same all-gather stream where unsharded storage was allocated, then clears _unshard_event so the next user enqueues a fresh all-gather.

Backward overlap

The shared FSDP context maintains separate static forward_order and backward_order sequences. Each backward pre-hook:

  1. calls the same idempotent unshard path and waits only for that FsdpModule's _unshard_event;
  2. prefetches the next FsdpModule needed by backward on the all-gather stream;
  3. allows that all-gather to overlap the current FsdpModule's gradient computation.

Gradient reduction is split into allocation, packing, and communication stages. Once a module's gradients are packed, its reduce-scatter is launched immediately on a separate reduce-scatter stream, allowing it to overlap subsequent backward GEMMs and all-gathers. Existing symmetric-memory scaling, mixed gradient dtypes, first-backward assignment, and microbatch accumulation semantics are preserved.

An autograd-end callback registered by the root makes the caller's current stream wait for the reduce-scatter stream before backward() returns. This cannot rely on the root module's own post-backward hook: that hook tracks gradients for parameters owned by the root and may run before descendants finish, or may not run at all when the root owns no trainable parameters.

Verification

  • test_fully_shard.py passes with two CUDA ranks after the _unshard_event change: 14 passed per rank.
  • The nested-order unit test verifies the static forward and backward orders for a nested module tree, including the backward DFS traversal.
  • The profiler test covers one forward and backward pass. For four child FsdpModules, it requires all 6 expected all-gather/GEMM overlaps and all 3 expected reduce-scatter/GEMM overlaps, rather than accepting any single overlap.
  • The profiler test also verifies that all-gathers use one stream, reduce-scatters use another, and both streams are distinct from compute.
  • A local Nsight Systems capture confirms backward all-gathers and reduce-scatters overlap GEMM execution on separate streams.
  • Diff and byte-compilation checks pass for the latest edits.

@copy-pr-bot

copy-pr-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@wujingyue
wujingyue changed the base branch from pull-request/5513 to main July 9, 2026 03:09
@wujingyue
wujingyue force-pushed the mfsdp-prefetch-on-5513 branch from 356bf43 to 2d43993 Compare July 9, 2026 04:01
@copy-pr-bot

copy-pr-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@wujingyue
wujingyue force-pushed the mfsdp-prefetch-on-5513 branch from 2d43993 to 700c4f0 Compare July 9, 2026 04:59
@wujingyue wujingyue changed the title Prototype: forward all-gather prefetch in experimental Megatron-FSDP (stacked on #5513) Prototype: forward all-gather prefetch in FSDP Jul 9, 2026
@wujingyue
wujingyue force-pushed the mfsdp-prefetch-on-5513 branch from 700c4f0 to 68aa5a2 Compare July 9, 2026 06:01
Comment thread megatron/core/distributed/fsdp/src/megatron_fsdp/experimental/module.py Outdated
Comment thread megatron/core/distributed/fsdp/src/megatron_fsdp/experimental/module.py Outdated
@wujingyue
wujingyue force-pushed the mfsdp-prefetch-on-5513 branch from 9045a0d to 4d2f7de Compare July 9, 2026 23:26
@wujingyue wujingyue linked an issue Jul 9, 2026 that may be closed by this pull request
@wujingyue
wujingyue force-pushed the mfsdp-prefetch-on-5513 branch from 4d2f7de to 29f3f1f Compare July 9, 2026 23:35
@wujingyue wujingyue changed the title Prototype: forward all-gather prefetch in FSDP Overlap experimental FSDP communication with compute Jul 9, 2026
@wujingyue
wujingyue marked this pull request as ready for review July 9, 2026 23:40
@wujingyue
wujingyue requested review from a team as code owners July 9, 2026 23:40
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the Final Review PR is in the "final review" stage label Jul 14, 2026
all-gather stream (where the buffer was allocated), gated on a consumer
event recorded on the compute stream so the free never races the kernels
that read the buffer.
"""

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.

Should we collect the states of unsharded models and release module parameter from all models except the current and next ones?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When would that be useful? Some FsdpModule's post-forward or post-backward failed to be triggered? That sounds like a bug that we shouldn't hide.

In pre_forward, issue the next FSDP unit's all-gather on the comm stream
(static module-tree order) so AG_{i+1} launches before F_i finishes, instead
of relying on delayed releases for overlap.

Since prefetch now drives overlap, the delayed-release deque is dropped: each
unit frees its own unsharded storage on the all-gather stream right after its
own compute (event-gated on a compute-stream consumer event so the free never
races the consuming kernels), rather than queuing releases. This bounds peak
in-flight memory to the current unit plus the prefetched next.

Each unit tracks its own materialized state via FsdpModule._is_unsharded, so
pre_forward can skip a unit an earlier unit already prefetched -- no central
id-keyed set or per-forward reset.

Applied on top of the FSDP all-gather overlap work (NVIDIA#5513). Verified against the
mfsdp_v2 unit tests: no new failures vs the base, and forward-peak-memory-bounds
now passes (it failed with the deque). resting-forward (32 MB) and flaky-overlap
fail on the base too. Profiling: prefetch raises concurrent AG/compute overlap
but not forward wall-clock because the SM-based all-gather contends with the
persistent full-occupancy GEMM; the real lever is SM-free (CTA-zero) comm.
Static module order assumes forward order == registration order.

Signed-off-by: Jingyue Wu <jingyuew@nvidia.com>
Reuse the recorded forward order in reverse during backward to prefetch each upcoming FSDP unit's parameters on the all-gather stream. Wait only for the current unit before compute so later all-gathers can overlap backward GEMMs.

Split gradient reduction into allocation, packing, and reduce-scatter stages, and launch each reduction immediately on a dedicated stream. Register an autograd-final callback from the root backward pre-hook to order optimizer work after communication while preserving mixed-dtype accumulation and symmetric-memory averaging.

Extend the profiler test to require the expected all-gather and reduce-scatter overlap counts and verify that communication and compute use distinct streams.

Signed-off-by: Jingyue Wu <jingyuew@nvidia.com>
Signed-off-by: Jingyue Wu <jingyuew@nvidia.com>
Signed-off-by: Jingyue Wu <jingyuew@nvidia.com>
Signed-off-by: Jingyue Wu <jingyuew@nvidia.com>
The overlapped backward path allocates the reduce-scatter input buffer
(torch.empty) on a dedicated reduce_scatter_stream inside post_backward.
Under torch.cuda.graph() capture that stream is not part of the capture at
the first allocation, so the allocator falls back to a raw cudaMalloc, which
is illegal during capture and invalidates the graph.

Fork the reduce-scatter stream from the current stream once at the start of
backward (root pre_backward) so it joins the capture before the first
reduce-scatter allocation. Later modules are covered by the post-copy fork
each preceding module already issues, and the existing post-backward final
callback supplies the matching join.

Signed-off-by: Jingyue Wu <jingyuew@nvidia.com>
Add FsdpContext.current_stream() to centralize the repeated
torch.cuda.current_stream(<stream>.device) lookups and replace the five
duplicated call sites. Also inline a redundant context local in
_unshard_parameter_groups.

Signed-off-by: Jingyue Wu <jingyuew@nvidia.com>
test_overlaps_communication_and_compute demanded the exact theoretical
maximum overlap (2*(num_children-1) all-gathers, num_children-1 reduce-scatters)
on every rank, with all ranks required to pass. SM-based NCCL collectives share
SMs with the GEMMs, so communication overlaps compute only partially and the count
varies run to run (CI's coverage instrumentation and tighter runners shift it
enough to fail), making the exact-maximum assertion flaky.

Assert only that communication meaningfully overlaps compute (all-gather >= 2,
reduce-scatter >= 1); the deterministic stream-topology checks already prove the
overlap machinery is wired correctly. NCCL symmetric-memory collectives, which run
with little to no SM usage, would relieve the contention and let these thresholds
be tightened again.

Signed-off-by: Jingyue Wu <jingyuew@nvidia.com>
@svcnvidia-nemo-ci

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started!

You can track the progress here: https://github.com/NVIDIA/Megatron-LM/actions/runs/29476715799

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MFSDP v2 overlap, prefetching, and double-buffering schedule support

5 participants