[Feat] FSDP Fullgraph Overlap — whole-graph weight all-gather / compute overlap#41
Merged
Merged
Conversation
jiahy0825
reviewed
Jul 15, 2026
jiahy0825
reviewed
Jul 16, 2026
jiahy0825
reviewed
Jul 16, 2026
jiahy0825
reviewed
Jul 17, 2026
… pre-MoE all-gathers
…Config, cross-rank fail-fast
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🗂️ PR Category
📝 Description
Summary
This PR adds an end-to-end, compile-time FSDP weight all-gather / compute overlap feature for the single-fullgraph (non-split) compilation path. When
enable_fsdp_fullgraph_overlap=True(requiresdisable_graph_split=Trueandcudagraph_mode=NONE), MagiCompiler:prim_redistributenodes into explicit functionalall_gather_into_tensor+wait_tensorcollectives on the FX graph (passes/fsdp_overlap/redistribute_lowering.py).all_gather_into_tensor_coalescedper (compute-region, process-group, dtype), with an optional per-bucket size cap (passes/fsdp_overlap/bucket_all_gather.py, entry pointlower_and_bucket_full_graph).passes/fsdp_overlap/reorder.py, replaces Inductor's builtinraise_comms/sink_waits): a single back-to-front two-pointer sweep places each all-gather launch (wait stays put) at the latest position whose upstream compute still hides the collective (compute_window >= comm * contention_factor + slack).profiling/runtime_estimator.py): Inductor's analytical roofline is unusable for our nodes (0 for custom ops, ~60x low for deep fused pointwise, ~1500x high for matmul — seescripts/demo/research_estimate_op_runtime_findings.md), so we measure real kernel times instead.Cost model:
ProfilingRuntimeEstimatorA callable
snode -> nanosecondspassed directly to the reorder pass ascost_fn(deliberately not installed at the globalconfig.estimate_op_runtime, which Inductor's own scheduling/caching consults in ways that specialize dynamic shapes).(target op, input shapes/dtypes)— not the per-node name — so isomorphic ops across repeated layers share one measurement (O(#distinct kernels), not O(#nodes)).scheduler.benchmark_fused_nodes(codegens the same fused kernel production emits; verified 0.995–1.15x of the compiled-graph kernel). Skipped (analytical fallback) while the graph still has free symbols, since that path would specialize dynamic dims.size_hint(no guards added), under@torch._dynamo.disable+no_grad, inside a ShapeEnv sandbox (_shapeenv_sandbox+suppress_guards) so measurement never leaks anEq(sym, hint)specialization. Safe even on the dynamic base compile.__call__(per-rank compilation is not co-scheduled; issuing NCCL there desyncs ranks → watchdog hang). They are seeded with Inductor's static analytical estimate and, inprofile_syncmode, later overridden by a real rank-lockstep measurement (below).profiling/benchmark_inputs.py): ops whose replay needs value-consistent metadata (e.g. split sizes feeding an internal CPall_to_all) register a hook viaregister_benchmark_inputs(op_name, fn, has_internal_collective=...); MagiCompiler holds no model-specific op names.Multi-rank correctness
The reorder decides how far to hoist each gather from costs; all FSDP gathers run on one process group, so NCCL matches the Nth call positionally — ranks must issue them in identical relative order or they deadlock (observed and root-caused via flight recorder on 8-GPU gaga4). Rank-consistency requires both:
Shard(0)params no longer give trailing ranks extraconstant_pad_ndnodes.fsdp_overlap_cost_mode:profile_sync(default): the estimator'swarm_and_sync()re-measures every distinct op in rank-lockstep (barrier + fixed iteration counts, so ops with internal collectives issue the same number of NCCL calls on every rank), then MAX-reduces the tables over a dedicated gloo group — real measured costs that are identical on every rank.analytical: Inductor roofline (deterministic by construction, zero overhead, less accurate).New config options (
CompileConfig)disable_graph_splitFalseenable_fsdp_fullgraph_overlapFalsedisable_graph_split=True,cudagraph_mode=NONE).fsdp_fullgraph_bucket_mode"none""none"(N individual gathers + waits) or"coalesced"(one coalesced gather per region/group/dtype).fsdp_fullgraph_bucket_size_mibfsdp_overlap_cost_mode"profile_sync"profile_sync|analytical|profile(see multi-rank section).fsdp_overlap_slack_ns5000.0fsdp_overlap_comm_contention_factor1.5Observability
repositioned M/N weight all-gather launch(es) (cost table: D distinct ops, measured=X reused=Y);rank-synchronized profiling done (N cost entries reconciled).cur -> target, dep floor,comm/acc_upstream/need, verdicthidden/COMPUTE-LIMITED) answering "why didn't this gather move earlier"; the full op->time table with grep-friendlyESTLINE|kind|label|per_call_us|calls|total_us|measuredrows diffable against an nsys trace (scripts/demo/compare_estimate_vs_nsys.py).Misc
utils/visualize/visualizer.py: graphviz label sanitization/truncation (backslash escapes, 16384-char label limit) and render failures downgraded to a warning — visualization can no longer abort compilation.Tests
tests/feature_tests/test_fsdp_overlap_lowering.py—prim_redistribute→ all_gather+wait lowering (incl. unconditional pad / uneven shard determinism).tests/feature_tests/test_fsdp_overlap_bucket.py— per-region coalesced bucketing, size caps, group/dtype partitioning.tests/feature_tests/test_fsdp_overlap_reorder.py— sweep placement, dep floors, order clamp, validation (viafsdp_overlap_helper/reorder_helper.py).tests/feature_tests/test_fsdp_overlap_e2e.py— end-to-end overlap on a real compile, including a world=2 spawn test exercisingprofile_sync/warm_and_syncrank-lockstep without deadlock.tests/feature_tests/test_profiling_estimator.py— table memoization,_static/_realize_arg, extern measurement accuracy vs independent CUDA-event timing, deepcopy safety;fsdp_overlap_helper/estimator_collective_helper.pycovers the collective seed →warm_and_syncmeasured-override path (distributed).tests/feature_tests/test_profiling_registry.py—register_benchmark_inputshook registry &has_internal_collectiveflagging.