Skip to content

Commit 5e43112

Browse files
[graph_trainer][aot_fx_trace][graph_pp] Add FSDP collective splitting
Add GraphPP extraction for explicit FSDP UNSHARD and REDUCE_GRAD graphs. The splitters use shared FSDP trace-pattern helpers to identify validated all-gather/wait reconstruction chains and reduce-scatter epilogues, keep the no-FSDP forward/backward graphs on the GraphPP flat calling convention, and leave fused edge graphs out of the runtime contract. Teach the default SAC policy to force-save the same helper-selected unshard output when FSDP runs with reshard_after_forward=False, matching the AutoParallel force-save behavior instead of saving every all-gather target. Add FSDP split tests for collective extraction, view and split-cat reconstruction, malformed unshard chains, no-op paths, input-source metadata, reduce-grad input naming, and pre-reduce cast placement. stack-info: PR: #3088, branch: sanketpurandare/stack/7
1 parent 2d9fe15 commit 5e43112

6 files changed

Lines changed: 861 additions & 11 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
"""Pattern helpers for GraphPP's current simple-FSDP collective traces.
7+
8+
The matchers intentionally follow c10d functional traces produced by FSDP2:
9+
10+
param_shard -> all_gather -> wait -> view*/split-cat -> compute
11+
local_grad -> cast/view* -> reduce_scatter -> wait -> param_grad
12+
local_grad -> cast/view* -> all_reduce -> wait -> param_grad
13+
14+
They are structural helpers for today's trace shape. A future upstream FSDP or
15+
torch.pipelining annotation should replace this with explicit collective-region
16+
metadata instead of broader pattern matching.
17+
"""
18+
19+
import operator
20+
from typing import Any
21+
22+
import torch
23+
import torch.fx as fx
24+
25+
26+
def is_wait_tensor(node: fx.Node) -> bool:
27+
return (
28+
node.op == "call_function"
29+
and node.target == torch.ops._c10d_functional.wait_tensor.default
30+
)
31+
32+
33+
def is_all_gather_into_tensor(node: fx.Node) -> bool:
34+
return (
35+
node.op == "call_function"
36+
and node.target == torch.ops._c10d_functional.all_gather_into_tensor.default
37+
)
38+
39+
40+
def is_reduce_scatter_tensor(node: fx.Node) -> bool:
41+
return (
42+
node.op == "call_function"
43+
and node.target is torch.ops._c10d_functional.reduce_scatter_tensor.default
44+
)
45+
46+
47+
def is_all_reduce(node: fx.Node) -> bool:
48+
return (
49+
node.op == "call_function"
50+
and node.target is torch.ops._c10d_functional.all_reduce.default
51+
)
52+
53+
54+
def is_reduce_grad_collective(node: fx.Node) -> bool:
55+
return is_reduce_scatter_tensor(node) or is_all_reduce(node)
56+
57+
58+
def _find_last_all_gather_in_chain(start_node: fx.Node) -> fx.Node | None:
59+
"""Find the final all-gather in a linear FSDP unshard launch chain."""
60+
node = start_node
61+
last_all_gather = None
62+
while True:
63+
if len(node.users) != 1:
64+
break
65+
user = next(iter(node.users))
66+
if len(user.all_input_nodes) > 1:
67+
break
68+
node = user
69+
if is_all_gather_into_tensor(node):
70+
last_all_gather = node
71+
return last_all_gather
72+
73+
74+
def _find_last_user_in_wait_chain(wait_node: fx.Node) -> fx.Node:
75+
"""Find the last FSDP unshard node before the value enters real compute.
76+
77+
The traced FSDP unshard has a mostly linear shape:
78+
79+
flat_param -> ... -> all_gather -> wait -> view* -> compute
80+
81+
Some models reshape the gathered flat buffer through a split/cat fanout:
82+
83+
wait -> split -> getitem_0 --+
84+
-> getitem_1 --+-> cat -> view* -> compute
85+
86+
In both cases the FSDP region ends before the first consumer with multiple
87+
FX inputs. The split/getitem/cat fanout is still part of reconstructing the
88+
unsharded parameter value, so it is included in the chain.
89+
"""
90+
node = wait_node
91+
while True:
92+
if len(node.users) != 1:
93+
if (
94+
node.op == "call_function"
95+
and node.target == torch.ops.aten.split.Tensor
96+
and all(
97+
user.op == "call_function"
98+
and user.target == operator.getitem
99+
and len(user.users) == 1
100+
for user in node.users
101+
)
102+
):
103+
getitem_users = [next(iter(user.users)) for user in node.users]
104+
potential_cat = getitem_users[0]
105+
if all(user == potential_cat for user in getitem_users) and (
106+
potential_cat.op == "call_function"
107+
and potential_cat.target == torch.ops.aten.cat.default
108+
):
109+
node = potential_cat
110+
continue
111+
break
112+
113+
user = next(iter(node.users))
114+
if len(user.all_input_nodes) > 1:
115+
break
116+
node = user
117+
return node
118+
119+
120+
def _find_last_non_view_node_in_chain(node: fx.Node) -> fx.Node:
121+
"""Return the value GraphPP should pass across the unshard boundary."""
122+
result = node
123+
while hasattr(result.target, "is_view") and result.target.is_view:
124+
if len(result.all_input_nodes) != 1:
125+
raise ValueError(f"View node {result.name} should have exactly one input")
126+
result = result.all_input_nodes[0]
127+
return result
128+
129+
130+
def find_fsdp_unshard_output(param_placeholder: fx.Node) -> fx.Node | None:
131+
"""Return the extracted unshard output for one flat parameter input.
132+
133+
GraphPP and the SAC force-save policy must agree on this node. It is the
134+
same value AutoParallel saves for ``reshard_after_forward=False``: the last
135+
non-view node in the FSDP all-gather/wait reconstruction chain. Parameters
136+
without an all-gather are replicated or otherwise already local, so callers
137+
should keep the original placeholder as that parameter's unsharded value.
138+
139+
TODO(sanketpurandare): requires upstream change: FSDP trace/passes should
140+
annotate unshard collective regions for downstream graph extraction.
141+
"""
142+
last_all_gather = _find_last_all_gather_in_chain(param_placeholder)
143+
if last_all_gather is None:
144+
return None
145+
146+
if len(last_all_gather.users) != 1:
147+
raise ValueError(
148+
f"Expected one wait_tensor user for all_gather node {last_all_gather.name}, "
149+
f"got {len(last_all_gather.users)}"
150+
)
151+
wait_node = next(iter(last_all_gather.users))
152+
if not is_wait_tensor(wait_node):
153+
raise ValueError(
154+
f"Expected wait_tensor after all_gather node {last_all_gather.name}, "
155+
f"got {wait_node.name}"
156+
)
157+
158+
wait_chain_user = _find_last_user_in_wait_chain(wait_node)
159+
return _find_last_non_view_node_in_chain(wait_chain_user)
160+
161+
162+
def find_fsdp_unshard_save_node(param_placeholder: fx.Node) -> fx.Node | None:
163+
return find_fsdp_unshard_output(param_placeholder)
164+
165+
166+
def find_fsdp_reduce_grad_input(param_grad_output: Any) -> fx.Node | None:
167+
"""Return the split point before an FSDP reduce-grad epilogue.
168+
169+
The backward FSDP/DDP/HSDP tail is traced as a unary chain ending in the
170+
synced grad output:
171+
172+
local_grad -> cast/view* -> reduce_scatter -> wait -> sharded_grad
173+
local_grad -> cast/view* -> all_reduce -> wait -> replicated_grad
174+
local_grad -> cast/view* -> all_reduce -> wait -> reduce_scatter
175+
-> wait -> grad
176+
177+
GraphPP splits at the input to the earliest grad-sync collective in that
178+
suffix. The cast remains in ``bw_no_fsdp`` so microbatch accumulation
179+
happens in FSDP's reduce dtype, and ``reduce_grad`` contains only the
180+
scheduled collective epilogue. Values that are not FX nodes, such as
181+
``None`` parameter-grad slots, are not collective outputs and are preserved
182+
by the caller.
183+
184+
TODO(sanketpurandare): requires upstream change: FSDP trace/passes should
185+
annotate reduce-grad collective regions for downstream graph extraction.
186+
"""
187+
if not isinstance(param_grad_output, fx.Node):
188+
return None
189+
190+
node = param_grad_output
191+
reduce_grad_input = None
192+
while isinstance(node, fx.Node) and len(node.all_input_nodes) == 1:
193+
input_node = node.all_input_nodes[0]
194+
if len(input_node.users) > 1:
195+
break
196+
previous_node = node
197+
node = input_node
198+
if is_reduce_grad_collective(previous_node):
199+
reduce_grad_input = node
200+
return reduce_grad_input

torchtitan/experiments/graph_trainer/graph_pp/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
from torchtitan.experiments.graph_trainer.graph_pp.split_fsdp_collectives import (
8+
GraphPPFSDPBackwardSplit,
9+
GraphPPFSDPForwardSplit,
10+
split_backward_fsdp_collectives,
11+
split_forward_fsdp_collectives,
12+
)
713
from torchtitan.experiments.graph_trainer.graph_pp.partition import (
814
GraphMeta,
915
partition_joint_graph,
@@ -15,7 +21,11 @@
1521

1622
__all__ = [
1723
"GraphPPDiDwSplit",
24+
"GraphPPFSDPBackwardSplit",
25+
"GraphPPFSDPForwardSplit",
1826
"GraphMeta",
1927
"partition_joint_graph",
28+
"split_backward_fsdp_collectives",
2029
"split_di_dw_graph",
30+
"split_forward_fsdp_collectives",
2131
]

0 commit comments

Comments
 (0)