Skip to content

Commit 132683a

Browse files
[graph_trainer] Add torch.no_grad() and graph-based SAC to traced execution (#2766)
Stack from [ghstack](https://github.com/ezyang/ghstack) (oldest at bottom): * __->__ #2766 Execute traced graph under torch.no_grad() since it already contains explicit backward ops (from torch.autograd.grad traced by make_fx). Without this, PyTorch builds a redundant autograd graph keeping all forward intermediates alive via grad_fn references. Adds SAC option on aot_fx_trace on GraphTrainer. Adds option to dump the peak memory from CUDA caching allocator. Adds similar test as test_bitwise_equivalency.py for peak memory Here is the run comparison between eager and aot_fx_trace with SAC <img width="1114" height="455" alt="Screenshot 2026-04-14 at 1 18 41 PM" src="https://github.com/user-attachments/assets/bf4ffdd3-8783-47be-ac64-32587787591f" /> Llama3B 8b with dp=4 tp=2 on 10 step run before SAC with cuda graph seq_len=1024 <img width="1728" height="585" alt="Screenshot 2026-04-14 at 10 39 41 PM" src="https://github.com/user-attachments/assets/d8a1fe31-bd91-41a4-8e6f-a0e738cc2b76" /> Llama3B 8b with dp=4 tp=2 on 10 step run after SAC with cuda graph seq_len=1024 <img width="1728" height="552" alt="Screenshot 2026-04-14 at 10 39 26 PM" src="https://github.com/user-attachments/assets/273b6e65-c2a3-473b-a2db-53b67a95be3a" /> Llama3B 8b with dp=4 tp=2 on 10 step run before SAC without cuda graph with seq_len=8192 <img width="1728" height="577" alt="Screenshot 2026-04-15 at 4 07 50 PM" src="https://github.com/user-attachments/assets/ca7108eb-6dc6-476c-b9d9-fa320f96945a" /> Llama3B 8b with dp=4 tp=2 on 10 step run after SAC without cuda graph with seq_len=8192 <img width="1721" height="592" alt="Screenshot 2026-04-15 at 4 07 43 PM" src="https://github.com/user-attachments/assets/80fcdcb0-ca38-4012-b71d-2b091adbfddd" /> Deepseek3 16B before SAC <img width="1728" height="600" alt="Screenshot 2026-04-14 at 10 54 31 PM" src="https://github.com/user-attachments/assets/61180fa4-debd-4c10-834e-e4936cfbd67e" /> Deepseek3 16B after SAC <img width="1728" height="578" alt="Screenshot 2026-04-14 at 10 58 30 PM" src="https://github.com/user-attachments/assets/3e3b4e71-e13f-43b7-9404-ad9a0a04f75c" />
1 parent 5e46f94 commit 132683a

9 files changed

Lines changed: 282 additions & 125 deletions

File tree

.github/workflows/integration_test_8gpu_graph_trainer.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ jobs:
7878
# Run precompile unit tests
7979
pytest torchtitan/experiments/graph_trainer/tests/test_precompile.py -v
8080
81-
# Run bitwise deterministic guardrail test
81+
# Run bitwise deterministic and SAC peak-memory guardrail tests
8282
pytest torchtitan/experiments/graph_trainer/tests/test_bitwise_deterministic.py -v
83+
pytest torchtitan/experiments/graph_trainer/tests/test_sac_peak_memory.py -v
8384
8485
rm -rf $RUNNER_TEMP/artifacts-to-be-uploaded/*/checkpoint

.github/workflows/integration_test_8gpu_graph_trainer_h100.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ jobs:
6868
# Run the MoE numerics tests
6969
NCCL_NVLS_ENABLE=0 pytest torchtitan/experiments/graph_trainer/tests/test_numerics.py::TestGraphTrainerNumerics -v -k "moe"
7070
71-
# Run bitwise deterministic guardrail test (includes H100-only hardcoded-hash tests)
71+
# Run bitwise deterministic and SAC peak-memory guardrail tests
7272
pytest torchtitan/experiments/graph_trainer/tests/test_bitwise_deterministic.py -v
73+
pytest torchtitan/experiments/graph_trainer/tests/test_sac_peak_memory.py -v
7374
7475
rm -rf $RUNNER_TEMP/artifacts-to-be-uploaded/*/checkpoint

tests/integration_tests/run_tests.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ def run_single_test(
9393

9494
def run_tests(args, test_list: list[OverrideDefinitions], module=None, config=None):
9595
"""Run all integration tests to test the core features of TorchTitan"""
96-
9796
exclude_set = set()
9897
if hasattr(args, "exclude") and args.exclude:
9998
exclude_set = {name.strip() for name in args.exclude.split(",")}

torchtitan/experiments/graph_trainer/make_fx_tracer.py

Lines changed: 14 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212
import torch
1313
import torch.nn as nn
1414
import torch.utils._pytree as pytree
15-
from torch._functorch._aot_autograd.logging_utils import (
16-
setup_stacktrace_preservation_hooks,
17-
)
1815
from torch._guards import tracing, TracingContext
1916
from torch._subclasses import FakeTensorMode
2017
from torch.fx.experimental.proxy_tensor import make_fx
@@ -200,52 +197,6 @@ def _remove_cpu_shadow_chains(gm: torch.fx.GraphModule) -> None:
200197
gm.recompile()
201198

202199

203-
@contextmanager
204-
def _patch_engine_run_backward() -> Generator[None, None, None]:
205-
"""Patch _engine_run_backward to install stacktrace preservation hooks.
206-
207-
Why this is needed:
208-
When make_fx traces a function that calls loss.backward(), the backward
209-
pass is decomposed into primitive ATen ops. Normally (in eager autograd),
210-
``setup_stacktrace_preservation_hooks`` is called by the autograd engine
211-
to propagate ``seq_nr`` from forward ops to their corresponding backward
212-
ops. Under make_fx tracing, this hook setup doesn't happen automatically
213-
because the engine path differs, so backward FX nodes end up without
214-
``seq_nr`` metadata. Without ``seq_nr``, we can't correlate backward
215-
nodes back to their forward counterparts (needed by
216-
``_copy_fwd_metadata_to_bw_nodes``).
217-
218-
This context manager patches ``_engine_run_backward`` to call
219-
``setup_stacktrace_preservation_hooks`` before the autograd engine runs,
220-
restoring ``seq_nr`` propagation during tracing.
221-
222-
We must patch the name in both modules since ``torch.autograd.__init__``
223-
imports it via ``from .graph import``.
224-
"""
225-
import torch.autograd
226-
import torch.autograd.graph
227-
228-
_orig_fn = torch.autograd.graph._engine_run_backward
229-
230-
def _patched(t_outputs, *args, **kwargs): # type: ignore[no-untyped-def]
231-
roots = [
232-
t.grad_fn
233-
for t in t_outputs
234-
if isinstance(t, torch.Tensor) and t.grad_fn is not None
235-
]
236-
if roots:
237-
setup_stacktrace_preservation_hooks(roots)
238-
return _orig_fn(t_outputs, *args, **kwargs)
239-
240-
torch.autograd.graph._engine_run_backward = _patched # type: ignore[assignment]
241-
torch.autograd._engine_run_backward = _patched # type: ignore[assignment]
242-
try:
243-
yield
244-
finally:
245-
torch.autograd.graph._engine_run_backward = _orig_fn # type: ignore[assignment]
246-
torch.autograd._engine_run_backward = _orig_fn # type: ignore[assignment]
247-
248-
249200
def _copy_fwd_metadata_to_bw_nodes(fx_g: torch.fx.GraphModule) -> None:
250201
"""Copy forward node metadata (custom) to later nodes sharing the same seq_nr.
251202
@@ -402,33 +353,36 @@ def fn_with_subclass_handling(*plain_args: Any) -> list:
402353

403354
state_for_fn = dict(zip(state_fqns, state_wrapped, strict=True))
404355
user_list = pytree.tree_unflatten(list(user_flat), user_args_spec)
405-
406-
with _patch_engine_run_backward():
356+
with torch.compiler._patch_autograd_grad():
407357
result = fn(state_for_fn, *user_list)
408-
409358
flat_outs, output_spec = pytree.tree_flatten(result)
410359
num_flat_outputs = len(flat_outs)
411360
unwrapped_outs, output_layouts = _unwrap_subclasses(flat_outs)
412361
return unwrapped_outs
413362

414363
ctx = TracingContext(fake_mode)
415364
# preserve_node_meta propagates fx.traceback.annotate metadata to traced nodes
416-
417365
# Disable autograd multithreading so that backward tracing
418-
# runs on the calling thread. Without this, the C++ autograd
366+
# runs on the calling thread. Without this, the C++ autograd
419367
# engine dispatches backward to a worker thread that has a
420368
# fresh contextvars.Context, making the compile_on_one_rank
421369
# ContextVar invisible and causing _sym_get_coordinate to
422370
# bake rank 0's concrete coordinates into the backward graph.
423371
# TODO: Move set_multithreading_enabled(False) to global init.
424372
# Forcing backward onto the main CPU thread is a good default
425373
# for both tracing and runtime, not just the tracing path.
374+
# _skip_nested_compile lets the current make_fx trace inline through
375+
# torch.compile'd FlexAttention kernels instead of erroring.
376+
# _non_strict_tracing_context is required by _patch_autograd_grad() and
377+
# marks this make_fx pass as the non-strict tracing flow, distinct from
378+
# other make_fx-based entry points such as non-strict export.
426379
with (
427380
fake_mode,
428381
tracing(ctx),
429382
preserve_node_meta(),
430383
_skip_nested_compile(),
431384
torch.autograd.set_multithreading_enabled(False),
385+
torch.compiler._non_strict_tracing_context(),
432386
):
433387
traced = make_fx(
434388
fn_with_subclass_handling,
@@ -470,6 +424,10 @@ def run_traced(
470424
This is a reference implementation of traced-graph execution. It keeps the
471425
state handling, subclass unwrapping, and output reconstruction logic
472426
explicit instead of baking those semantics into ``TracedResult`` itself.
427+
Runs under ``torch.no_grad()`` because the graph already contains explicit
428+
backward ops (from ``torch.autograd.grad`` traced by make_fx). Without
429+
this, PyTorch would build a redundant autograd graph on top, keeping all
430+
forward intermediates alive via ``grad_fn`` references.
473431
"""
474432
state_flat = list(state.values())
475433
user_args_flat, _ = pytree.tree_flatten(list(args))
@@ -481,7 +439,8 @@ def run_traced(
481439
all_args = list(state_flat) + list(user_args_flat)
482440
flat_inputs, _ = _unwrap_subclasses(all_args)
483441

484-
flat_outputs = traced_result.gm(*flat_inputs)
442+
with torch.no_grad():
443+
flat_outputs = traced_result.gm(*flat_inputs)
485444
wrapped = _wrap_subclasses(
486445
flat_outputs,
487446
traced_result.num_flat_outputs,

torchtitan/experiments/graph_trainer/passes.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
from torchtitan.tools.logging import logger
5151

5252

53+
def _is_backward_node(node: torch.fx.Node) -> bool:
54+
return node.meta.get("autograd_backward", False)
55+
56+
5357
def compile_time_passes(
5458
traced_result: "TracedResult",
5559
) -> list[Callable]:
@@ -69,6 +73,7 @@ def compile_time_passes(
6973
remove_detach_pass,
7074
remove_identity_view_pass,
7175
remove_identity_slice_pass,
76+
selective_activation_remat_pass,
7277
# FlexAttention HOPs must be compiled (via regional_inductor) to
7378
# produce bitwise identical results to the eager Trainer path.
7479
# When left uncompiled, flex_attention still runs correctly but
@@ -452,6 +457,11 @@ def apply_sac_pass(
452457
if node.op != "call_function":
453458
continue
454459

460+
# Skip backward nodes — they must not carry recompute tags,
461+
# otherwise the remat pass would try to duplicate backward ops.
462+
if _is_backward_node(node):
463+
continue
464+
455465
if node.target in (
456466
operator.getitem,
457467
torch.ops._c10d_functional.wait_tensor.default,
@@ -469,7 +479,6 @@ def apply_sac_pass(
469479
node.meta["recompute"] = parent.meta["recompute"]
470480
node.meta["ac_graph_id"] = parent.meta.get("ac_graph_id", 0)
471481
continue
472-
473482
custom_meta = node.meta.get("custom", {})
474483
ac_region_id = custom_meta.get(_AC_REGION_ID, 0)
475484
node.meta["ac_graph_id"] = ac_region_id
@@ -504,6 +513,27 @@ def apply_sac_pass(
504513
return gm
505514

506515

516+
def selective_activation_remat_pass(
517+
gm: torch.fx.GraphModule, example_inputs: tuple | None = None
518+
) -> torch.fx.GraphModule:
519+
"""Apply graph-based SAC to a traced fwd+loss+bwd graph.
520+
521+
Tags forward nodes with recompute policy via apply_sac_pass (backward
522+
nodes are skipped automatically via ``node.meta["autograd_backward"]``), then
523+
applies remat_using_tags_for_fwd_loss_bwd_graph to duplicate
524+
PREFER_RECOMPUTE forward ops before backward and DCE originals.
525+
526+
The model must have been annotated with annotate_ac_regions before
527+
tracing so that nodes have custom["ac_region_id"] metadata.
528+
"""
529+
from torch._functorch._activation_checkpointing.remat_using_tags_for_fwd_loss_bwd_graph_pass import (
530+
remat_using_tags_for_fwd_loss_bwd_graph,
531+
)
532+
533+
apply_sac_pass(gm)
534+
return remat_using_tags_for_fwd_loss_bwd_graph(gm)
535+
536+
507537
# Apply activation checkpointing on joint graph before partitioner
508538
def fsdp_reshard_after_fwd_pass(
509539
gm: torch.fx.GraphModule,
@@ -604,7 +634,7 @@ def inductor_decomposition_pass(
604634
f"Placeholder count mismatch: {len(orig_placeholders)} vs {len(decomp_placeholders)}"
605635
)
606636

607-
for orig, decomp in zip(orig_placeholders, decomp_placeholders):
637+
for orig, decomp in zip(orig_placeholders, decomp_placeholders, strict=True):
608638
# Copy all metadata from original to decomposed
609639
for key, value in orig.meta.items():
610640
if key not in decomp.meta:
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
from types import SimpleNamespace
8+
9+
import torch
10+
import torch.nn as nn
11+
12+
from torchtitan.components.loss import cross_entropy_loss
13+
from torchtitan.config import ActivationCheckpointConfig
14+
from torchtitan.distributed.utils import get_train_context
15+
from torchtitan.experiments.graph_trainer.trainer import GraphTrainer
16+
from torchtitan.trainer import Trainer
17+
18+
19+
def build_minimal_trainer(
20+
model: nn.Module,
21+
model_config,
22+
trainer_cls: type[Trainer],
23+
*,
24+
activation_checkpoint_mode: str = "none",
25+
compile_enable_passes: bool = True,
26+
compile_passes: list[str] | None = None,
27+
compile_joint_passes: list[str] | None = None,
28+
tokenizer=None,
29+
) -> Trainer:
30+
"""Build the minimal Trainer/GraphTrainer needed for single-GPU test steps."""
31+
trainer = object.__new__(trainer_cls)
32+
trainer.model_parts = [model]
33+
trainer.loss_fn = cross_entropy_loss
34+
trainer.parallel_dims = SimpleNamespace(pp_enabled=False, cp_enabled=False)
35+
trainer.train_context = get_train_context(False)
36+
trainer.model_config = model_config
37+
trainer.device = torch.device("cuda")
38+
trainer.tokenizer = tokenizer
39+
40+
if trainer_cls is GraphTrainer:
41+
trainer.config = SimpleNamespace(
42+
compile=SimpleNamespace(
43+
mode="aot_fx_trace",
44+
enable_passes=compile_enable_passes,
45+
passes=[] if compile_passes is None else list(compile_passes),
46+
joint_passes=[]
47+
if compile_joint_passes is None
48+
else list(compile_joint_passes),
49+
precompile_artifact_dir="",
50+
),
51+
activation_checkpoint=ActivationCheckpointConfig(
52+
mode=activation_checkpoint_mode
53+
),
54+
)
55+
trainer._fwd_bwd_step_module = None
56+
trainer._traced_step = None
57+
else:
58+
trainer.config = SimpleNamespace()
59+
60+
return trainer

torchtitan/experiments/graph_trainer/tests/test_bitwise_deterministic.py

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import tempfile
1818
import unittest
1919
from collections.abc import Callable
20-
from types import SimpleNamespace
2120

2221
import torch
2322
import torch.nn as nn
@@ -27,7 +26,6 @@
2726

2827
from torchtitan.components.loss import cross_entropy_loss
2928
from torchtitan.components.tokenizer import HuggingFaceTokenizer
30-
from torchtitan.distributed.utils import get_train_context
3129
from torchtitan.experiments.graph_trainer.deepseek_v3 import (
3230
model_registry as dsv3_model_registry,
3331
)
@@ -38,6 +36,9 @@
3836
model_registry as llama3_model_registry,
3937
)
4038
from torchtitan.experiments.graph_trainer.llama3.parallelize import annotate_llama
39+
from torchtitan.experiments.graph_trainer.tests._trainer_test_utils import (
40+
build_minimal_trainer,
41+
)
4142
from torchtitan.experiments.graph_trainer.trainer import GraphTrainer
4243
from torchtitan.models.common.attention import FlexAttention
4344
from torchtitan.tools.utils import has_cuda_capability
@@ -60,43 +61,6 @@ def _set_deterministic(seed: int = SEED) -> None:
6061
_TOKENIZER_PATH = "./tests/assets/tokenizer"
6162

6263

63-
def _build_trainer(
64-
model: nn.Module,
65-
model_config,
66-
trainer_cls: type,
67-
*,
68-
enable_passes: bool = True,
69-
) -> Trainer:
70-
"""Build a minimal Trainer/GraphTrainer for single-GPU non-distributed testing.
71-
72-
Uses object.__new__ to bypass __init__ because the full Trainer constructor
73-
requires a distributed environment, job config, and checkpoint manager that
74-
are unnecessary for single-GPU numerical verification. The attributes set
75-
below are the minimal set required by forward_backward_step().
76-
"""
77-
trainer = object.__new__(trainer_cls)
78-
trainer.model_parts = [model]
79-
trainer.loss_fn = cross_entropy_loss
80-
trainer.parallel_dims = SimpleNamespace(pp_enabled=False, cp_enabled=False)
81-
trainer.train_context = get_train_context(False)
82-
trainer.model_config = model_config
83-
trainer.device = torch.device("cuda")
84-
trainer.tokenizer = HuggingFaceTokenizer(tokenizer_path=_TOKENIZER_PATH)
85-
86-
if trainer_cls is GraphTrainer:
87-
trainer.config = SimpleNamespace(
88-
compile=SimpleNamespace(
89-
mode="aot_fx_trace",
90-
enable_passes=enable_passes,
91-
precompile_artifact_dir="",
92-
)
93-
)
94-
trainer._fwd_bwd_step_module = None
95-
trainer._traced_step = None
96-
97-
return trainer
98-
99-
10064
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
10165
class BitwiseDeterministicBase(unittest.TestCase):
10266
"""Base class for bitwise determinism tests.
@@ -149,8 +113,12 @@ def _run_steps(
149113
# Annotate after deepcopy: annotate_fn wrappers capture bound methods
150114
# that don't rebind correctly through copy.deepcopy.
151115
self.annotate_model(model)
152-
trainer = _build_trainer(
153-
model, self.model_config, trainer_cls, enable_passes=enable_passes
116+
trainer = build_minimal_trainer(
117+
model,
118+
self.model_config,
119+
trainer_cls,
120+
compile_enable_passes=enable_passes,
121+
tokenizer=HuggingFaceTokenizer(tokenizer_path=_TOKENIZER_PATH),
154122
)
155123
global_valid_tokens = torch.tensor(
156124
BATCH_SIZE * SEQ_LEN, dtype=torch.float, device="cuda"
@@ -446,11 +414,6 @@ def test_eager_self_deterministic(self):
446414
"""b3f5b911dea6c9d36f508b08300220d7f39f142a7c34a49b7ef2543abb2065dc""",
447415
)
448416

449-
# TODO: OOMs during flex_attention compilation on A100 GPUs.
450-
# Revisit when GraphTrainer addresses peak memory during compilation.
451-
@unittest.skipUnless(
452-
has_cuda_capability(9, 0), "OOMs during flex_attention compilation on A100"
453-
)
454417
def test_aot_fx_trace_vs_eager(self):
455418
"""aot_fx_trace with passes and eager produce bitwise identical results."""
456419
run_eager = self._run_steps(copy.deepcopy(self.model), Trainer)

0 commit comments

Comments
 (0)