Skip to content

Commit 6bc0866

Browse files
[graph_trainer][aot_fx_trace][graph_pp] Add multiplexed graph coverage and DualPipeV validation
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, and keeps FakeTensor shape metadata on the destination ShapeEnv while preserving raw symbolic metadata relationships. Add multiplex unit coverage for metadata transfer, output ordering, prebuild/compile behavior, full-Inductor overlap construction, and unsupported BACKWARD_INPUT overlap sub-actions. Extend DeepSeek V3 GraphPP numerics and integration coverage for DualPipeV and document the GraphPP execution contract. 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. stack-info: PR: #3090, branch: sanketpurandare/stack/9
1 parent 6e1befc commit 6bc0866

8 files changed

Lines changed: 901 additions & 51 deletions

File tree

torchtitan/experiments/graph_trainer/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,48 @@ 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; `GraphPPRunner` only executes the prebuilt callable for each PP
70+
schedule action.
71+
72+
```bash
73+
NGPU=8 MODULE=graph_trainer.deepseek_v3 CONFIG=graph_trainer_deepseek_v3_debugmodel ./run_train.sh \
74+
--compile.mode aot_fx_trace \
75+
--parallelism.pipeline_parallel_degree 2 \
76+
--parallelism.pipeline_parallel_schedule Interleaved1F1B \
77+
--parallelism.data_parallel_shard_degree 4 \
78+
--parallelism.expert_parallel_degree 2
79+
```
80+
81+
Supported runtime schedules include `Interleaved1F1B`, `ZBVZeroBubble`, and
82+
`DualPipeV`. GraphPP builds stage-local forward/backward graphs, optional FSDP
83+
`UNSHARD` / `REDUCE_GRAD` graphs, optional dI/dW graphs for split backward
84+
schedules, and multiplexed graphs for `OVERLAP_F_B` actions. Regional and full
85+
Inductor compilation reuse the existing GraphTrainer compilation passes on the
86+
extracted GraphPP callables; GraphPP-specific handling stays in the GraphPP
87+
stack before those passes are invoked. Multiplexed graphs keep the forward graph
88+
as the destination module and ShapeEnv, insert backward placeholders/compute
89+
before it, and transfer backward metadata into that ShapeEnv. This preserves
90+
forward collective-size provenance for full Inductor without changing the shared
91+
compiler passes.
92+
93+
GraphPP follows the same subclass boundary as the non-PP GraphTrainer tracer.
94+
Extracted graphs run on flat plain tensor leaves. Values exposed to the PP
95+
runtime are rewrapped from tracer metadata: stage forward outputs, input
96+
gradients sent to previous stages, and parameter gradients before assignment to
97+
live `param.grad`. Internal values remain flat because they never leave GraphPP
98+
graph execution: saved-for-backward tensors, unsharded FSDP params, raw grad
99+
leaves, reduce-grad inputs, and multiplexed intermediate outputs.
100+
101+
Current limitations: GraphPP does not load precompile artifacts yet, CUDA graph
102+
capture should target the `GraphPPRunner` steady-state path in a future change,
103+
and EP-overlap annotations will be composed with GraphPP in a later PR.
104+
63105
### Compiler Optimizations
64106

65107
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: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@
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,
7+
from torchtitan.experiments.graph_trainer.graph_pp.graph_multiplex import (
8+
multiplex_fw_bw_graph,
129
)
1310
from torchtitan.experiments.graph_trainer.graph_pp.partition import (
1411
GraphMeta,
@@ -18,12 +15,19 @@
1815
GraphPPDiDwSplit,
1916
split_di_dw_graph,
2017
)
18+
from torchtitan.experiments.graph_trainer.graph_pp.split_fsdp_collectives import (
19+
GraphPPFSDPBackwardSplit,
20+
GraphPPFSDPForwardSplit,
21+
split_backward_fsdp_collectives,
22+
split_forward_fsdp_collectives,
23+
)
2124

2225
__all__ = [
2326
"GraphPPDiDwSplit",
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: 40 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
"""
@@ -43,6 +43,9 @@
4343
split_backward_fsdp_collectives,
4444
split_forward_fsdp_collectives,
4545
)
46+
from torchtitan.experiments.graph_trainer.graph_pp.graph_multiplex import (
47+
multiplex_fw_bw_graph,
48+
)
4649
from torchtitan.experiments.graph_trainer.graph_pp.partition import (
4750
GraphMeta as PartitionGraphMeta,
4851
partition_joint_graph,
@@ -1083,14 +1086,43 @@ def _build_graph_pp_overlap_graphs(
10831086
*,
10841087
compile_config: GraphTrainerCompileConfig,
10851088
) -> dict[tuple[int, int], GraphPPOverlapGraphs]:
1086-
del compile_config
1087-
required_pairs = _required_multiplex_pairs(schedule)
1088-
if required_pairs:
1089-
raise NotImplementedError(
1090-
"GraphPP OVERLAP_F_B requires multiplexed graph support, which is "
1091-
"introduced in the follow-up DualPipeV commit."
1089+
"""Build multiplexed graphs required by ``OVERLAP_F_B`` schedule actions."""
1090+
1091+
stage_index_to_stage = {
1092+
stage.stage_index: cast(GraphPipelineStage, stage) for stage in schedule._stages
1093+
}
1094+
overlap_graphs: dict[tuple[int, int], GraphPPOverlapGraphs] = {}
1095+
for fw_stage_idx, bw_stage_idx in _required_multiplex_pairs(schedule):
1096+
pair = (fw_stage_idx, bw_stage_idx)
1097+
fw_stage = stage_index_to_stage[fw_stage_idx]
1098+
bw_stage = stage_index_to_stage[bw_stage_idx]
1099+
fw_graphs = _require_graph_trainer_stage_graphs(fw_stage, "OVERLAP_F_B")
1100+
bw_graphs = _require_graph_trainer_stage_graphs(bw_stage, "OVERLAP_F_B")
1101+
if fw_graphs.compiled or bw_graphs.compiled:
1102+
raise ValueError(
1103+
"GraphPP overlap graphs must be built before stage graphs are compiled."
1104+
)
1105+
multiplexed_graph = multiplex_fw_bw_graph(
1106+
fw_graphs.modules.fw,
1107+
bw_graphs.modules.full_bw,
1108+
)
1109+
_annotate_graph_pp_graph(
1110+
multiplexed_graph,
1111+
stage_index=fw_stage_idx,
1112+
callable_name="multiplex",
1113+
action_name="OVERLAP_F_B",
1114+
)
1115+
compiled_graph = _compile_graph_pp_module(
1116+
multiplexed_graph,
1117+
compile_config=compile_config,
1118+
graph_name=f"stage_{fw_stage_idx}_fw_stage_{bw_stage_idx}_bw_multiplex",
1119+
)
1120+
overlap_graphs[pair] = GraphTrainerOverlapGraphs(
1121+
fw_graphs=fw_graphs,
1122+
bw_graphs=bw_graphs,
1123+
multiplexed_graph=compiled_graph,
10921124
)
1093-
return {}
1125+
return overlap_graphs
10941126

10951127

10961128
def _example_args_from_stage_metadata(stage: GraphPipelineStage) -> tuple[Any, ...]:
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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" and new_node.target in bw_get_attr_remap:
145+
new_node.target = bw_get_attr_remap[str(new_node.target)]
146+
old_to_new[node] = new_node
147+
insert_point = new_node
148+
149+
multiplexed_output = multiplexed_gm.graph.find_nodes(op="output")[0]
150+
bw_output_node = bw_gm.graph.find_nodes(op="output")[0]
151+
bw_outputs = [
152+
old_to_new[value] if isinstance(value, fx.Node) else value
153+
for value in bw_output_node.args[0]
154+
]
155+
fw_outputs = list(multiplexed_output.args[0])
156+
multiplexed_output.args = (tuple(bw_outputs + fw_outputs),)
157+
158+
multiplexed_gm.graph.eliminate_dead_code()
159+
multiplexed_gm.graph.lint()
160+
multiplexed_gm.recompile()
161+
trace_graph_pp_graph("graph_pp_multiplexed_graph", multiplexed_gm)
162+
return multiplexed_gm

0 commit comments

Comments
 (0)