Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions torchtitan/experiments/graph_trainer/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,18 @@ def apply_compile(
parallelism.fsdp_reshard_after_forward, parallel_dims.pp_enabled
)

if compile_config.precompile_artifact_dir and mode != "aot":
if compile_config.precompile_artifact_dir and mode not in ("aot", "aot_fx_trace"):
logger.warning(
"--compile.precompile_artifact_dir is only supported with "
f"--compile.mode=aot, but mode is '{mode}'. Ignoring precompile."
f"--compile.mode=aot or aot_fx_trace, but mode is '{mode}'. "
"Ignoring precompile."
)
compile_config = dataclasses.replace(compile_config, precompile_artifact_dir="")

if compile_config.precompile_artifact_dir and not (
_SERIALIZABLE_PASSES & set(compile_config.passes)
if (
compile_config.precompile_artifact_dir
and mode == "aot"
and not (_SERIALIZABLE_PASSES & set(compile_config.passes))
):
raise ValueError(
"--compile.precompile_artifact_dir requires at least one pass that "
Expand All @@ -281,10 +284,18 @@ def apply_compile(
)
elif mode == "aot_fx_trace":
# aot_fx_trace traces fwd+loss+bwd together inside forward_backward_step,
# so no model-level wrapping is needed here.
logger.info(
"aot_fx_trace compile mode: graph capture will happen at training time"
)
# so no model-level wrapping is needed here. If precompile_artifact_dir
# is set, the precompiled artifact will be loaded lazily in
# GraphTrainer._make_fx_forward_backward_step.
if compile_config.precompile_artifact_dir:
logger.info(
"aot_fx_trace compile mode: precompiled artifact will be loaded "
f"from {compile_config.precompile_artifact_dir}"
)
else:
logger.info(
"aot_fx_trace compile mode: graph capture will happen at training time"
)
return model
else:
raise ValueError(
Expand Down
49 changes: 31 additions & 18 deletions torchtitan/experiments/graph_trainer/cudagraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def __init__(
example_inputs: Sequence[Any],
static_input_indices: tuple[int] | None = None,
should_check_address: bool = False,
tensor_input_indices: list[int] | None = None,
):
_cg_manager.maybe_initialize()
_cg_manager.register_wrapper(self)
Expand All @@ -132,11 +133,16 @@ def __init__(
self._static_input_indices = OrderedSet(
static_input_indices if static_input_indices is not None else []
)
self._input_indices_to_copy = [
i
for i, inp in enumerate(example_inputs)
if isinstance(inp, torch.Tensor) and i not in self._static_input_indices
]
if tensor_input_indices is not None:
self._input_indices_to_copy = [
i for i in tensor_input_indices if i not in self._static_input_indices
]
else:
self._input_indices_to_copy = [
i
for i, inp in enumerate(example_inputs)
if isinstance(inp, torch.Tensor) and i not in self._static_input_indices
Comment thread
bobrenjc93 marked this conversation as resolved.
]
self._cudagraph: torch.cuda.CUDAGraph | None = None
self._has_warmup = False

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

def _check_input_types(self, inputs) -> None:
def _validate_inputs(self, inputs) -> None:
"""Validate that all inputs are of supported types.

Opaque inputs (e.g. DeviceMesh from SimpleFSDP/DTensor) are
inherently static and already excluded from copying (only
tensors appear in ``_input_indices_to_copy``), so no special
handling is needed beyond accepting them here.
"""
for i, inp in enumerate(inputs):
if not (
isinstance(inp, (torch.Tensor, int, float, torch._C.Generator))
or is_opaque_value(inp)
):
raise ValueError(
"args must be tensor, integer (for dynamic shapes), "
"float (for scalar constants), "
"Generator (for random number generator), "
"or opaque object, "
f"but found {type(inp)} with value {inp!r} at index {i}"
)
if isinstance(inp, (torch.Tensor, int, float, torch._C.Generator)):
continue
if is_opaque_value(inp):
Comment thread
bobrenjc93 marked this conversation as resolved.
continue
raise ValueError(
"args must be tensor, integer (for dynamic shapes), "
"float (for scalar constants), "
"Generator (for random number generator), "
"or opaque object, "
f"but found {type(inp)} with value {inp!r} at index {i}"
)

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

if self._cudagraph is None:
self._check_input_types(args)
self._validate_inputs(args)
self._args = args
self._input_addresses = [
x.data_ptr() if isinstance(x, torch.Tensor) else None for x in args
Expand Down
24 changes: 22 additions & 2 deletions torchtitan/experiments/graph_trainer/make_fx_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from collections.abc import Callable, Generator
from contextlib import contextmanager
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any

import torch
Expand Down Expand Up @@ -307,6 +307,7 @@ class TracedResult:
num_flat_outputs: int
output_subclass_layouts: dict[int, SubclassLayout]
output_spec: pytree.TreeSpec
tensor_input_indices: list[int] = field(default_factory=list)

@property
def num_static_inputs(self) -> int:
Expand Down Expand Up @@ -412,7 +413,23 @@ def fn_with_subclass_handling(*plain_args: Any) -> list:

ctx = TracingContext(fake_mode)
# preserve_node_meta propagates fx.traceback.annotate metadata to traced nodes
with fake_mode, tracing(ctx), preserve_node_meta(), _skip_nested_compile():

# Disable autograd multithreading so that backward tracing
# runs on the calling thread. Without this, the C++ autograd
# engine dispatches backward to a worker thread that has a
# fresh contextvars.Context, making the compile_on_one_rank
# ContextVar invisible and causing _sym_get_coordinate to
# bake rank 0's concrete coordinates into the backward graph.
# TODO: Move set_multithreading_enabled(False) to global init.
# Forcing backward onto the main CPU thread is a good default
# for both tracing and runtime, not just the tracing path.
with (
fake_mode,
tracing(ctx),
preserve_node_meta(),
_skip_nested_compile(),
torch.autograd.set_multithreading_enabled(False),
Comment thread
bobrenjc93 marked this conversation as resolved.
):
traced = make_fx(
fn_with_subclass_handling,
record_stack_traces=True,
Expand All @@ -435,6 +452,9 @@ def fn_with_subclass_handling(*plain_args: Any) -> list:
num_flat_outputs=num_flat_outputs,
output_subclass_layouts=output_layouts,
output_spec=output_spec,
tensor_input_indices=[
i for i, x in enumerate(fake_args) if isinstance(x, torch.Tensor)
],
)

return _trace_with_args
Expand Down
59 changes: 45 additions & 14 deletions torchtitan/experiments/graph_trainer/passes.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,20 @@
from torchtitan.tools.logging import logger


def construct_default_graph_passes(
def compile_time_passes(
traced_result: "TracedResult",
) -> list[Callable]:
"""Build the default pass list for the aot_fx_trace compile path.

Per-pass configuration (e.g. ``static_input_indices`` for cudagraph) is
bound here via ``functools.partial`` so that ``apply_graph_passes``
stays a generic pass runner with no pass-specific parameters.
"""Cleanup, FlexAttention annotation, and regional_inductor passes.

Args:
traced_result: The traced graph and metadata from ``trace_train_step``.
If precompile is enabled, these are applied before serialization so
that compiled Triton kernels are baked into the artifact. Otherwise
they run at trace time via ``construct_default_graph_passes``.

Returns:
An ordered list of graph passes ready to apply.
cudagraph is excluded — it needs real tensors and devices at runtime.
Comment thread
bobrenjc93 marked this conversation as resolved.
"""
from torchtitan.models.common.attention import FlexAttention

passes: list[Callable] = [
return [
functools.partial(tlparse_log_graph_pass, graph_name="make_fx_graph_traced"),
remove_detach_pass,
remove_identity_view_pass,
Expand All @@ -82,19 +78,37 @@ def construct_default_graph_passes(
regional_inductor_pass,
]

# cudagraph should be the last pass.

Comment thread
bobrenjc93 marked this conversation as resolved.
def construct_default_graph_passes(
traced_result: "TracedResult",
*,
precompiled: bool = False,
) -> list[Callable]:
"""Build the pass list for the aot_fx_trace path.

When ``precompiled=False`` (default), returns the full list: cleanup,
FlexAttention annotation, regional_inductor, and cudagraph.

When ``precompiled=True``, the artifact already has cleanup and
regional_inductor baked in, so only cudagraph is returned.
"""
from torchtitan.experiments.graph_trainer.cudagraph import is_cudagraph_compatible

passes: list[Callable] = []
if not precompiled:
passes.extend(compile_time_passes(traced_result))

# cudagraph should be the last pass.
if is_cudagraph_compatible(traced_result.gm):
static_input_indices = list(range(traced_result.num_static_inputs))
passes.append(
functools.partial(
cudagraph_pass,
is_forward=True,
static_input_indices=static_input_indices,
tensor_input_indices=traced_result.tensor_input_indices,
)
)

return passes


Expand Down Expand Up @@ -266,6 +280,7 @@ def cudagraph_pass(
*,
is_forward: bool,
static_input_indices: list[int] | None = None,
tensor_input_indices: list[int] | None = None,
) -> torch.fx.GraphModule:
"""
Apply cudagraph.
Expand All @@ -284,7 +299,18 @@ def cudagraph_pass(
when ``static_input_indices`` is not provided.
static_input_indices: Explicit list of input indices with stable tensor
addresses. When provided, ``is_forward`` is not used for inference.
tensor_input_indices: Indices of graph inputs that are tensors (as
opposed to opaque values like DeviceMesh). Used to compute which
inputs need copying for cudagraph replay. When not provided, this
is inferred from ``example_inputs``.
"""
if not isinstance(gm, torch.fx.GraphModule):
Comment thread
bobrenjc93 marked this conversation as resolved.
raise TypeError(
f"cudagraph_pass requires a GraphModule but got {type(gm).__name__}. "
f"Ensure cudagraph is not combined with passes that replace the "
f"GraphModule (e.g. full_inductor_compilation)."
)

# Lazy import: cudagraph.py runs init_global_graph_pool() at import time,
# which must happen after torch.cuda.set_device(local_rank).
from torchtitan.experiments.graph_trainer.cudagraph import (
Expand All @@ -294,7 +320,12 @@ def cudagraph_pass(

if static_input_indices is None:
static_input_indices = get_static_input_indices(gm, is_forward)
gm.forward = CUDAGraphWrapper(gm.forward, example_inputs, static_input_indices)
gm.forward = CUDAGraphWrapper(
gm.forward,
example_inputs,
static_input_indices,
tensor_input_indices=tensor_input_indices,
)
return gm


Expand Down
Loading
Loading