Skip to content

Commit 1c4e18b

Browse files
authored
[graph_trainer] Add CooR precompile support for aot_fx_trace compile mode (#2975)
Stack from [ghstack](https://github.com/ezyang/ghstack/tree/0.14.0) (oldest at bottom): * __->__ #2975 Introduce aot_fx_trace as an alternate precompilation path alongside the existing AOT joint graph export. aot_fx_trace uses make_fx to trace the full fwd+loss+bwd step as a single FX graph, then serializes the GraphModule for loading on all ranks during training. Because cudagraphs is a default pass, we also include cudagraph composability related changes in this PR to get to bitwise equivalence between aot_fx_trace with and without precompile. Changes: - precompile_main.py: Refactor into _common_setup() + _precompile_aot() + _precompile_aot_fx_trace(), route by --compile.mode. Use Python float for global_valid_tokens (matching dist_sum's return type) so make_fx bakes it as a graph constant, identical to the runtime path. - precompile.py: PrecompiledFxTraceArtifact dataclass, save/load fns. Uses GraphPickler (not plain pickle) to preserve SymInt expressions (_runtime_compute_coordinate_on_dim) through serialization — plain pickle evaluates SymInts to concrete trace-time values, baking in rank-specific embedding vocab offsets. - compile.py: Mode-aware validation (aot_fx_trace doesn't need serializable passes), logging for precompile load vs runtime trace - trainer.py: _load_precompiled_fx_trace(), branching in _make_fx_forward_backward_step() - make_fx_tracer.py: Use set_multithreading_enabled(False) so backward tracing runs on the calling thread, keeping the compile_on_one_rank ContextVar visible - cudagraph.py: Defer _input_indices_to_copy when example_inputs is empty (precompile load path), accept opaque values (DeviceMesh) in input validation, skip non-tensors in static address checks. Squashed the separate cudagraph CooR PR into this one because cudagraph activates automatically for aot_fx_trace graphs (all inputs are desugared plain tensors + opaques, no HOPs), so bitwise equivalence testing required both fixes together. - passes.py: Guard cudagraph_pass against non-GraphModule inputs ## Test plan - `pytest torchtitan/experiments/graph_trainer/tests/test_bitwise_deterministic.py -x` — 6 passed, 4 skipped (Llama3, DSv3 × {eager self-deterministic, eager vs aot_fx_trace, precompile vs trace}) - `pytest torchtitan/experiments/graph_trainer/tests/test_precompile.py -x` — all pass - `pytest torchtitan/experiments/graph_trainer/tests/test_trace_module.py -x` — all pass - 8-GPU integration tests via CI (ciflow/8gpu)
1 parent ea6a1cd commit 1c4e18b

10 files changed

Lines changed: 867 additions & 122 deletions

File tree

torchtitan/experiments/graph_trainer/compile.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,18 @@ def apply_compile(
246246
parallelism.fsdp_reshard_after_forward, parallel_dims.pp_enabled
247247
)
248248

249-
if compile_config.precompile_artifact_dir and mode != "aot":
249+
if compile_config.precompile_artifact_dir and mode not in ("aot", "aot_fx_trace"):
250250
logger.warning(
251251
"--compile.precompile_artifact_dir is only supported with "
252-
f"--compile.mode=aot, but mode is '{mode}'. Ignoring precompile."
252+
f"--compile.mode=aot or aot_fx_trace, but mode is '{mode}'. "
253+
"Ignoring precompile."
253254
)
254255
compile_config = dataclasses.replace(compile_config, precompile_artifact_dir="")
255256

256-
if compile_config.precompile_artifact_dir and not (
257-
_SERIALIZABLE_PASSES & set(compile_config.passes)
257+
if (
258+
compile_config.precompile_artifact_dir
259+
and mode == "aot"
260+
and not (_SERIALIZABLE_PASSES & set(compile_config.passes))
258261
):
259262
raise ValueError(
260263
"--compile.precompile_artifact_dir requires at least one pass that "
@@ -281,10 +284,18 @@ def apply_compile(
281284
)
282285
elif mode == "aot_fx_trace":
283286
# aot_fx_trace traces fwd+loss+bwd together inside forward_backward_step,
284-
# so no model-level wrapping is needed here.
285-
logger.info(
286-
"aot_fx_trace compile mode: graph capture will happen at training time"
287-
)
287+
# so no model-level wrapping is needed here. If precompile_artifact_dir
288+
# is set, the precompiled artifact will be loaded lazily in
289+
# GraphTrainer._make_fx_forward_backward_step.
290+
if compile_config.precompile_artifact_dir:
291+
logger.info(
292+
"aot_fx_trace compile mode: precompiled artifact will be loaded "
293+
f"from {compile_config.precompile_artifact_dir}"
294+
)
295+
else:
296+
logger.info(
297+
"aot_fx_trace compile mode: graph capture will happen at training time"
298+
)
288299
return model
289300
else:
290301
raise ValueError(

torchtitan/experiments/graph_trainer/cudagraph.py

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def __init__(
124124
example_inputs: Sequence[Any],
125125
static_input_indices: tuple[int] | None = None,
126126
should_check_address: bool = False,
127+
tensor_input_indices: list[int] | None = None,
127128
):
128129
_cg_manager.maybe_initialize()
129130
_cg_manager.register_wrapper(self)
@@ -132,11 +133,16 @@ def __init__(
132133
self._static_input_indices = OrderedSet(
133134
static_input_indices if static_input_indices is not None else []
134135
)
135-
self._input_indices_to_copy = [
136-
i
137-
for i, inp in enumerate(example_inputs)
138-
if isinstance(inp, torch.Tensor) and i not in self._static_input_indices
139-
]
136+
if tensor_input_indices is not None:
137+
self._input_indices_to_copy = [
138+
i for i in tensor_input_indices if i not in self._static_input_indices
139+
]
140+
else:
141+
self._input_indices_to_copy = [
142+
i
143+
for i, inp in enumerate(example_inputs)
144+
if isinstance(inp, torch.Tensor) and i not in self._static_input_indices
145+
]
140146
self._cudagraph: torch.cuda.CUDAGraph | None = None
141147
self._has_warmup = False
142148

@@ -157,19 +163,26 @@ def _copy_non_static_inputs(self, *args):
157163
for i in self._input_indices_to_copy:
158164
self._args[i].copy_(args[i])
159165

160-
def _check_input_types(self, inputs) -> None:
166+
def _validate_inputs(self, inputs) -> None:
167+
"""Validate that all inputs are of supported types.
168+
169+
Opaque inputs (e.g. DeviceMesh from SimpleFSDP/DTensor) are
170+
inherently static and already excluded from copying (only
171+
tensors appear in ``_input_indices_to_copy``), so no special
172+
handling is needed beyond accepting them here.
173+
"""
161174
for i, inp in enumerate(inputs):
162-
if not (
163-
isinstance(inp, (torch.Tensor, int, float, torch._C.Generator))
164-
or is_opaque_value(inp)
165-
):
166-
raise ValueError(
167-
"args must be tensor, integer (for dynamic shapes), "
168-
"float (for scalar constants), "
169-
"Generator (for random number generator), "
170-
"or opaque object, "
171-
f"but found {type(inp)} with value {inp!r} at index {i}"
172-
)
175+
if isinstance(inp, (torch.Tensor, int, float, torch._C.Generator)):
176+
continue
177+
if is_opaque_value(inp):
178+
continue
179+
raise ValueError(
180+
"args must be tensor, integer (for dynamic shapes), "
181+
"float (for scalar constants), "
182+
"Generator (for random number generator), "
183+
"or opaque object, "
184+
f"but found {type(inp)} with value {inp!r} at index {i}"
185+
)
173186

174187
def _check_static_inputs_address(self) -> None:
175188
for i in self._static_input_indices:
@@ -194,7 +207,7 @@ def __call__(self, *args):
194207
return out
195208

196209
if self._cudagraph is None:
197-
self._check_input_types(args)
210+
self._validate_inputs(args)
198211
self._args = args
199212
self._input_addresses = [
200213
x.data_ptr() if isinstance(x, torch.Tensor) else None for x in args

torchtitan/experiments/graph_trainer/make_fx_tracer.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from collections.abc import Callable, Generator
88
from contextlib import contextmanager
9-
from dataclasses import dataclass
9+
from dataclasses import dataclass, field
1010
from typing import Any
1111

1212
import torch
@@ -307,6 +307,7 @@ class TracedResult:
307307
num_flat_outputs: int
308308
output_subclass_layouts: dict[int, SubclassLayout]
309309
output_spec: pytree.TreeSpec
310+
tensor_input_indices: list[int] = field(default_factory=list)
310311

311312
@property
312313
def num_static_inputs(self) -> int:
@@ -412,7 +413,23 @@ def fn_with_subclass_handling(*plain_args: Any) -> list:
412413

413414
ctx = TracingContext(fake_mode)
414415
# preserve_node_meta propagates fx.traceback.annotate metadata to traced nodes
415-
with fake_mode, tracing(ctx), preserve_node_meta(), _skip_nested_compile():
416+
417+
# Disable autograd multithreading so that backward tracing
418+
# runs on the calling thread. Without this, the C++ autograd
419+
# engine dispatches backward to a worker thread that has a
420+
# fresh contextvars.Context, making the compile_on_one_rank
421+
# ContextVar invisible and causing _sym_get_coordinate to
422+
# bake rank 0's concrete coordinates into the backward graph.
423+
# TODO: Move set_multithreading_enabled(False) to global init.
424+
# Forcing backward onto the main CPU thread is a good default
425+
# for both tracing and runtime, not just the tracing path.
426+
with (
427+
fake_mode,
428+
tracing(ctx),
429+
preserve_node_meta(),
430+
_skip_nested_compile(),
431+
torch.autograd.set_multithreading_enabled(False),
432+
):
416433
traced = make_fx(
417434
fn_with_subclass_handling,
418435
record_stack_traces=True,
@@ -435,6 +452,9 @@ def fn_with_subclass_handling(*plain_args: Any) -> list:
435452
num_flat_outputs=num_flat_outputs,
436453
output_subclass_layouts=output_layouts,
437454
output_spec=output_spec,
455+
tensor_input_indices=[
456+
i for i, x in enumerate(fake_args) if isinstance(x, torch.Tensor)
457+
],
438458
)
439459

440460
return _trace_with_args

torchtitan/experiments/graph_trainer/passes.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,20 @@
5050
from torchtitan.tools.logging import logger
5151

5252

53-
def construct_default_graph_passes(
53+
def compile_time_passes(
5454
traced_result: "TracedResult",
5555
) -> list[Callable]:
56-
"""Build the default pass list for the aot_fx_trace compile path.
57-
58-
Per-pass configuration (e.g. ``static_input_indices`` for cudagraph) is
59-
bound here via ``functools.partial`` so that ``apply_graph_passes``
60-
stays a generic pass runner with no pass-specific parameters.
56+
"""Cleanup, FlexAttention annotation, and regional_inductor passes.
6157
62-
Args:
63-
traced_result: The traced graph and metadata from ``trace_train_step``.
58+
If precompile is enabled, these are applied before serialization so
59+
that compiled Triton kernels are baked into the artifact. Otherwise
60+
they run at trace time via ``construct_default_graph_passes``.
6461
65-
Returns:
66-
An ordered list of graph passes ready to apply.
62+
cudagraph is excluded — it needs real tensors and devices at runtime.
6763
"""
6864
from torchtitan.models.common.attention import FlexAttention
6965

70-
passes: list[Callable] = [
66+
return [
7167
functools.partial(tlparse_log_graph_pass, graph_name="make_fx_graph_traced"),
7268
remove_detach_pass,
7369
remove_identity_view_pass,
@@ -93,19 +89,37 @@ def construct_default_graph_passes(
9389
custom_codegen_pass,
9490
]
9591

96-
# cudagraph should be the last pass.
92+
93+
def construct_default_graph_passes(
94+
traced_result: "TracedResult",
95+
*,
96+
precompiled: bool = False,
97+
) -> list[Callable]:
98+
"""Build the pass list for the aot_fx_trace path.
99+
100+
When ``precompiled=False`` (default), returns the full list: cleanup,
101+
FlexAttention annotation, regional_inductor, and cudagraph.
102+
103+
When ``precompiled=True``, the artifact already has cleanup and
104+
regional_inductor baked in, so only cudagraph is returned.
105+
"""
97106
from torchtitan.experiments.graph_trainer.cudagraph import is_cudagraph_compatible
98107

108+
passes: list[Callable] = []
109+
if not precompiled:
110+
passes.extend(compile_time_passes(traced_result))
111+
112+
# cudagraph should be the last pass.
99113
if is_cudagraph_compatible(traced_result.gm):
100114
static_input_indices = list(range(traced_result.num_static_inputs))
101115
passes.append(
102116
functools.partial(
103117
cudagraph_pass,
104118
is_forward=True,
105119
static_input_indices=static_input_indices,
120+
tensor_input_indices=traced_result.tensor_input_indices,
106121
)
107122
)
108-
109123
return passes
110124

111125

@@ -277,6 +291,7 @@ def cudagraph_pass(
277291
*,
278292
is_forward: bool,
279293
static_input_indices: list[int] | None = None,
294+
tensor_input_indices: list[int] | None = None,
280295
) -> torch.fx.GraphModule:
281296
"""
282297
Apply cudagraph.
@@ -295,7 +310,18 @@ def cudagraph_pass(
295310
when ``static_input_indices`` is not provided.
296311
static_input_indices: Explicit list of input indices with stable tensor
297312
addresses. When provided, ``is_forward`` is not used for inference.
313+
tensor_input_indices: Indices of graph inputs that are tensors (as
314+
opposed to opaque values like DeviceMesh). Used to compute which
315+
inputs need copying for cudagraph replay. When not provided, this
316+
is inferred from ``example_inputs``.
298317
"""
318+
if not isinstance(gm, torch.fx.GraphModule):
319+
raise TypeError(
320+
f"cudagraph_pass requires a GraphModule but got {type(gm).__name__}. "
321+
f"Ensure cudagraph is not combined with passes that replace the "
322+
f"GraphModule (e.g. full_inductor_compilation)."
323+
)
324+
299325
# Lazy import: cudagraph.py runs init_global_graph_pool() at import time,
300326
# which must happen after torch.cuda.set_device(local_rank).
301327
from torchtitan.experiments.graph_trainer.cudagraph import (
@@ -305,7 +331,12 @@ def cudagraph_pass(
305331

306332
if static_input_indices is None:
307333
static_input_indices = get_static_input_indices(gm, is_forward)
308-
gm.forward = CUDAGraphWrapper(gm.forward, example_inputs, static_input_indices)
334+
gm.forward = CUDAGraphWrapper(
335+
gm.forward,
336+
example_inputs,
337+
static_input_indices,
338+
tensor_input_indices=tensor_input_indices,
339+
)
309340
return gm
310341

311342

0 commit comments

Comments
 (0)