Skip to content

Commit 11f8dd1

Browse files
[graph_trainer][aot_fx_trace][graph_pp] Add multiplexed graph coverage and DualPipeV validation (#3090)
Add GraphPP forward/backward multiplex graph construction and wire it into OVERLAP_F_B runtime execution. The multiplexed graph preserves the flat GraphPP calling convention, prebuilds and compiles before schedule execution, keeps FakeTensor shape metadata on the destination ShapeEnv, and validates copied backward get_attr targets instead of silently skipping missing remaps. Harden multiplex metadata transfer by preserving raw symbolic metadata relationships, rejecting unsupported symbolic metadata, and documenting the upstream ShapeEnv transfer gap. Add multiplex unit coverage for metadata transfer, output ordering, prebuild/compile behavior, full-Inductor overlap construction, unsupported BACKWARD_INPUT overlap sub-actions, unsupported symbolic metadata, and missing get_attr remaps. Extend DeepSeek V3 GraphPP numerics and integration coverage for DualPipeV and document the GraphPP execution contract with links to the GraphPP and CUDAGraphable GraphPP RFC issues. The numerics comparison uses eager Interleaved1F1B as the baseline because torch.compile is incompatible with the eager ZBV and DualPipeV schedules required by FlexAttention. It disables local-rank gradient clipping so schedule-dependent clip coefficients do not mask graph execution equivalence, and compares rank-local TensorBoard losses from the ranks that actually own loss so core metrics remains unchanged.
1 parent 21e7954 commit 11f8dd1

8 files changed

Lines changed: 929 additions & 45 deletions

File tree

torchtitan/experiments/graph_trainer/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,52 @@ NGPU=8 MODULE=graph_trainer.llama3 CONFIG=graph_trainer_llama3_8b ./run_train.sh
6060
NGPU=8 MODULE=graph_trainer.deepseek_v3 CONFIG=graph_trainer_deepseek_v3_16b ./run_train.sh --parallelism.data_parallel_shard_degree=4 --parallelism.tensor_parallel_degree=2 --parallelism.expert_parallel_degree=2
6161
```
6262

63+
### GraphPP Pipeline Parallelism
64+
65+
GraphPP is the `aot_fx_trace` pipeline-parallel path for GraphTrainer models.
66+
It reuses TorchTitan's eager PP module splitting and PyTorch PP schedules, then
67+
traces one representative microbatch per local stage with GraphTrainer's
68+
`minimal_fx_tracer`. The resulting per-stage graph bundles are reused for later
69+
microbatches; `GraphPipelineRuntime` only executes the prebuilt callable for each PP
70+
schedule action.
71+
72+
Design references:
73+
- GraphPP RFC: https://github.com/pytorch/torchtitan/issues/3780
74+
- CUDAGraphable GraphPP RFC: https://github.com/pytorch/torchtitan/issues/3820
75+
76+
```bash
77+
NGPU=8 MODULE=graph_trainer.deepseek_v3 CONFIG=graph_trainer_deepseek_v3_debugmodel ./run_train.sh \
78+
--compile.mode aot_fx_trace \
79+
--parallelism.pipeline_parallel_degree 2 \
80+
--parallelism.pipeline_parallel_schedule Interleaved1F1B \
81+
--parallelism.data_parallel_shard_degree 4 \
82+
--parallelism.expert_parallel_degree 2
83+
```
84+
85+
Supported runtime schedules include `Interleaved1F1B`, `ZBVZeroBubble`, and
86+
`DualPipeV`. GraphPP builds stage-local forward/backward graphs, optional FSDP
87+
`UNSHARD` / `REDUCE_GRAD` graphs, optional dI/dW graphs for split backward
88+
schedules, and multiplexed graphs for `OVERLAP_F_B` actions. Regional and full
89+
Inductor compilation reuse the existing GraphTrainer compilation passes on the
90+
extracted GraphPP callables; GraphPP-specific handling stays in the GraphPP
91+
stack before those passes are invoked. Multiplexed graphs keep the forward graph
92+
as the destination module and ShapeEnv, insert backward placeholders/compute
93+
before it, and transfer backward metadata into that ShapeEnv. This preserves
94+
forward collective-size provenance for full Inductor without changing the shared
95+
compiler passes.
96+
97+
GraphPP follows the same subclass boundary as the non-PP GraphTrainer tracer.
98+
Extracted graphs run on flat plain tensor leaves. Values exposed to the PP
99+
runtime are rewrapped from tracer metadata: stage forward outputs, input
100+
gradients sent to previous stages, and parameter gradients before assignment to
101+
live `param.grad`. Internal values remain flat because they never leave GraphPP
102+
graph execution: saved-for-backward tensors, unsharded FSDP params, raw grad
103+
leaves, reduce-grad inputs, and multiplexed intermediate outputs.
104+
105+
Current limitations: GraphPP does not load precompile artifacts yet, CUDA graph
106+
capture should target the `GraphPipelineRuntime` steady-state path in a future change,
107+
and EP-overlap annotations will be composed with GraphPP in a later PR.
108+
63109
### Compiler Optimizations
64110

65111
The `aot_fx_trace` mode has a built-in pass pipeline controlled by dedicated flags.

torchtitan/experiments/graph_trainer/graph_pp/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
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.graph_multiplex import (
8+
multiplex_fw_bw_graph,
9+
)
710
from torchtitan.experiments.graph_trainer.graph_pp.partition import (
811
GraphMeta,
912
partition_joint_graph,
@@ -24,6 +27,7 @@
2427
"GraphPPFSDPBackwardSplit",
2528
"GraphPPFSDPForwardSplit",
2629
"GraphMeta",
30+
"multiplex_fw_bw_graph",
2731
"partition_joint_graph",
2832
"split_backward_fsdp_collectives",
2933
"split_di_dw_graph",

torchtitan/experiments/graph_trainer/graph_pp/graph_builder.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
before assigning to live ``param.grad``.
1515
3. Internal graph values stay flat because they never escape GraphPP graph
1616
execution: saved-for-backward values, unsharded FSDP params, raw grad
17-
leaves, and reduce-grad inputs.
17+
leaves, reduce-grad inputs, and multiplexed intermediate outputs.
1818
4. DTensor and other traceable tensor subclasses use the existing tracer layout
1919
metadata. GraphPP must not add a separate DTensor-specific wrapping path.
2020
"""
@@ -46,6 +46,9 @@
4646
split_backward_fsdp_collectives,
4747
split_forward_fsdp_collectives,
4848
)
49+
from torchtitan.experiments.graph_trainer.graph_pp.graph_multiplex import (
50+
multiplex_fw_bw_graph,
51+
)
4952
from torchtitan.experiments.graph_trainer.graph_pp.partition import (
5053
GraphMeta as PartitionGraphMeta,
5154
partition_joint_graph,
@@ -1168,14 +1171,49 @@ def _build_graph_pp_overlap_graphs(
11681171
*,
11691172
compile_config: GraphTrainerCompileConfig,
11701173
) -> dict[tuple[int, int], GraphPPOverlapGraphs]:
1171-
del compile_config
1172-
required_pairs = _required_multiplex_pairs(schedule)
1173-
if required_pairs:
1174-
raise NotImplementedError(
1175-
"GraphPP OVERLAP_F_B requires multiplexed graph support, which is "
1176-
"introduced in the follow-up DualPipeV commit."
1174+
"""Build multiplexed graphs required by ``OVERLAP_F_B`` schedule actions."""
1175+
1176+
stage_index_to_stage = {
1177+
stage.stage_index: cast(GraphPipelineStage, stage) for stage in schedule._stages
1178+
}
1179+
overlap_graphs: dict[tuple[int, int], GraphPPOverlapGraphs] = {}
1180+
for fw_stage_idx, bw_stage_idx in _required_multiplex_pairs(schedule):
1181+
pair = (fw_stage_idx, bw_stage_idx)
1182+
fw_stage = stage_index_to_stage[fw_stage_idx]
1183+
bw_stage = stage_index_to_stage[bw_stage_idx]
1184+
if fw_stage.graphs is None or bw_stage.graphs is None:
1185+
raise ValueError(
1186+
"GraphPP overlap graph construction requires both stage "
1187+
f"graphs first: forward_stage={fw_stage_idx}, "
1188+
f"backward_stage={bw_stage_idx}."
1189+
)
1190+
fw_graphs = cast(GraphTrainerStageGraphs, fw_stage.graphs)
1191+
bw_graphs = cast(GraphTrainerStageGraphs, bw_stage.graphs)
1192+
if fw_graphs.compiled or bw_graphs.compiled:
1193+
raise ValueError(
1194+
"GraphPP overlap graphs must be built before stage graphs are compiled."
1195+
)
1196+
multiplexed_graph = multiplex_fw_bw_graph(
1197+
fw_graphs.modules.fw,
1198+
bw_graphs.modules.full_bw,
1199+
)
1200+
_annotate_graph_pp_graph(
1201+
multiplexed_graph,
1202+
stage_index=fw_stage_idx,
1203+
callable_name="multiplex",
1204+
action_name="OVERLAP_F_B",
1205+
)
1206+
compiled_graph = _compile_graph_pp_module(
1207+
multiplexed_graph,
1208+
compile_config=compile_config,
1209+
graph_name=f"stage_{fw_stage_idx}_fw_stage_{bw_stage_idx}_bw_multiplex",
1210+
)
1211+
overlap_graphs[pair] = GraphTrainerOverlapGraphs(
1212+
fw_graphs=fw_graphs,
1213+
bw_graphs=bw_graphs,
1214+
multiplexed_graph=compiled_graph,
11771215
)
1178-
return {}
1216+
return overlap_graphs
11791217

11801218

11811219
def _example_args_from_stage_metadata(stage: GraphPipelineStage) -> tuple[Any, ...]:
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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+
7+
import copy
8+
from itertools import dropwhile
9+
10+
import torch.fx as fx
11+
from torch.fx.graph_module import _assign_attr, _get_attr
12+
13+
from torchtitan.experiments.graph_trainer.graph_pp.utils import (
14+
_find_fake_mode,
15+
_MetaShapeEnvTransfer,
16+
trace_graph_pp_graph,
17+
)
18+
19+
20+
def _copy_prefixed_get_attrs(
21+
dst: fx.GraphModule,
22+
src: fx.GraphModule,
23+
*,
24+
prefix: str,
25+
) -> dict[str, str]:
26+
remap: dict[str, str] = {}
27+
for node in src.graph.find_nodes(op="get_attr"):
28+
attr_name = str(node.target)
29+
if attr_name in remap:
30+
continue
31+
base_name = f"{prefix}{attr_name.replace('.', '_')}"
32+
new_attr_name = base_name
33+
suffix = 0
34+
# GraphModule attributes share one namespace; probe it directly to
35+
# avoid colliding with existing get_attr targets.
36+
while hasattr(dst, new_attr_name):
37+
suffix += 1
38+
new_attr_name = f"{base_name}_{suffix}"
39+
_assign_attr(copy.deepcopy(_get_attr(src, attr_name)), dst, new_attr_name)
40+
remap[attr_name] = new_attr_name
41+
return remap
42+
43+
44+
def multiplex_fw_bw_graph(
45+
fw_gm: fx.GraphModule,
46+
bw_gm: fx.GraphModule,
47+
) -> fx.GraphModule:
48+
"""Concatenate backward and forward graphs into one boxed GraphPP callable.
49+
50+
Contract:
51+
``OVERLAP_F_B`` schedule actions need one callable that performs a
52+
backward action for one stage and a forward action for another stage. The
53+
returned graph preserves both flat calling conventions by ordering
54+
placeholders as ``bw_inputs + fw_inputs`` and outputs as
55+
``bw_outputs + fw_outputs``.
56+
57+
Pseudocode:
58+
deep-copy the forward graph as the destination
59+
transfer backward metadata into the forward FakeTensorMode/ShapeEnv
60+
insert copied backward placeholders before the forward placeholders
61+
copy backward get_attr targets with a prefix to avoid attr collisions
62+
copy backward compute nodes before the forward compute nodes
63+
replace the output tuple with backward outputs followed by forward outputs
64+
65+
The forward graph remains the destination module because its ShapeEnv owns
66+
the dynamic collective-size constraints needed by full Inductor for MoE
67+
all-to-all outputs. The backward graph is inserted in topological order
68+
before the existing forward compute, with disjoint placeholders and
69+
prefixed attributes, so this helper does not need dependency analysis or a
70+
scheduling policy. EP-overlap annotations are intentionally not applied
71+
inside this graph; pass ordering keeps EP-overlap on the standalone no-FSDP
72+
forward/backward graphs.
73+
74+
Args:
75+
fw_gm (fx.GraphModule): Forward graph whose ShapeEnv and module
76+
namespace become the destination for the multiplexed graph.
77+
bw_gm (fx.GraphModule): Backward graph copied before the forward graph
78+
inside the multiplexed callable.
79+
80+
Returns:
81+
fx.GraphModule: One graph module with placeholders ordered as
82+
``bw_inputs + fw_inputs`` and outputs ordered as
83+
``bw_outputs + fw_outputs``.
84+
85+
Raises:
86+
ValueError: If the forward graph does not contain the placeholders or
87+
output node needed to build the multiplexed callable.
88+
"""
89+
old_to_new: dict[fx.Node, fx.Node] = {}
90+
# Preserve the forward ShapeEnv exactly as traced. Reconstructing it from
91+
# copied metadata can lose collective-size hints that full Inductor needs.
92+
multiplexed_gm = copy.deepcopy(fw_gm)
93+
dst_fake_mode = _find_fake_mode(multiplexed_gm)
94+
meta_transfer = _MetaShapeEnvTransfer(dst_fake_mode)
95+
for node in bw_gm.graph.nodes:
96+
meta_transfer.collect(node.meta)
97+
meta_transfer.seed()
98+
bw_get_attr_remap = _copy_prefixed_get_attrs(
99+
multiplexed_gm,
100+
bw_gm,
101+
prefix="bw",
102+
)
103+
104+
fw_placeholders = multiplexed_gm.graph.find_nodes(op="placeholder")
105+
if not fw_placeholders:
106+
raise ValueError("GraphPP forward graph has no placeholders to multiplex")
107+
first_fw_placeholder = fw_placeholders[0]
108+
insert_point: fx.Node | None = None
109+
for node in bw_gm.graph.find_nodes(op="placeholder"):
110+
if insert_point is None:
111+
with multiplexed_gm.graph.inserting_before(first_fw_placeholder):
112+
new_placeholder = multiplexed_gm.graph.placeholder(f"bw_{node.name}")
113+
else:
114+
with multiplexed_gm.graph.inserting_after(insert_point):
115+
new_placeholder = multiplexed_gm.graph.placeholder(f"bw_{node.name}")
116+
new_placeholder.meta = meta_transfer.copy_meta(node.meta)
117+
old_to_new[node] = new_placeholder
118+
insert_point = new_placeholder
119+
120+
first_fw_compute = next(
121+
(node for node in multiplexed_gm.graph.nodes if node.op != "placeholder"),
122+
None,
123+
)
124+
if first_fw_compute is None:
125+
raise ValueError("GraphPP forward graph has no output node to multiplex")
126+
127+
bw_nodes = iter(bw_gm.graph.nodes)
128+
bw_nodes = dropwhile(lambda node: node.op == "placeholder", bw_nodes)
129+
insert_point = None
130+
for node in bw_nodes:
131+
if node.op == "output":
132+
break
133+
if insert_point is None:
134+
with multiplexed_gm.graph.inserting_before(first_fw_compute):
135+
new_node = multiplexed_gm.graph.node_copy(
136+
node, lambda arg: old_to_new[arg]
137+
)
138+
else:
139+
with multiplexed_gm.graph.inserting_after(insert_point):
140+
new_node = multiplexed_gm.graph.node_copy(
141+
node, lambda arg: old_to_new[arg]
142+
)
143+
new_node.meta = meta_transfer.copy_meta(node.meta)
144+
if new_node.op == "get_attr":
145+
attr_name = str(new_node.target)
146+
if attr_name not in bw_get_attr_remap:
147+
raise ValueError(
148+
"GraphPP multiplexed graph missing copied get_attr target "
149+
f"for backward attribute {attr_name!r}."
150+
)
151+
new_node.target = bw_get_attr_remap[attr_name]
152+
old_to_new[node] = new_node
153+
insert_point = new_node
154+
155+
multiplexed_output = multiplexed_gm.graph.find_nodes(op="output")[0]
156+
bw_output_node = bw_gm.graph.find_nodes(op="output")[0]
157+
bw_outputs = [
158+
old_to_new[value] if isinstance(value, fx.Node) else value
159+
for value in bw_output_node.args[0]
160+
]
161+
fw_outputs = list(multiplexed_output.args[0])
162+
multiplexed_output.args = (tuple(bw_outputs + fw_outputs),)
163+
164+
multiplexed_gm.graph.eliminate_dead_code()
165+
multiplexed_gm.graph.lint()
166+
multiplexed_gm.recompile()
167+
trace_graph_pp_graph("graph_pp_multiplexed_graph", multiplexed_gm)
168+
return multiplexed_gm

0 commit comments

Comments
 (0)