Skip to content

Commit 87b7abc

Browse files
Update on "[graph_trainer] Add torch.no_grad() and graph-based SAC to traced execution"
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 <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 <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" /> For Llama3 8B, from the step-30 rank0 memory snapshots: - No SAC: - end-of-step memory_allocated() = 14.99 GiB GiB - end-of-step memory_reserved() = 22.75 GiB - SAC: - end-of-step memory_allocated() = 14.99 GiB GiB - end-of-step memory_reserved() = 16.6 GiB 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" /> [ghstack-poisoned]
2 parents 1267aae + 64bf2c7 commit 87b7abc

8 files changed

Lines changed: 20 additions & 132 deletions

File tree

.github/workflows/integration_test_8gpu_graph_trainer.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
sudo mkdir -p "$RUNNER_TEMP/artifacts-to-be-uploaded"
6262
sudo chown -R $(id -u):$(id -g) "$RUNNER_TEMP/artifacts-to-be-uploaded"
6363
64-
python -m torchtitan.experiments.graph_trainer.tests.integration_tests --test_suite graph_trainer_default --gpu_arch_type cuda $RUNNER_TEMP/artifacts-to-be-uploaded --ngpu 8 --collect_peak_memory
64+
python -m torchtitan.experiments.graph_trainer.tests.integration_tests --test_suite graph_trainer_default --gpu_arch_type cuda $RUNNER_TEMP/artifacts-to-be-uploaded --ngpu 8
6565
6666
6767
# Run the numerics unit tests (dense models only; MoE tests run in the H100 workflow)

tests/integration_tests/run_tests.py

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55
# LICENSE file in the root directory of this source tree.
66

77
import argparse
8-
import json
9-
import logging
108
import os
119
import subprocess
1210
import time
13-
from pathlib import Path
1411

1512
from torchtitan.tools.logging import logger
1613

@@ -26,9 +23,6 @@
2623
"h100": build_h100_tests_list,
2724
}
2825

29-
TB_MAX_RESERVED_TAG = "memory/max_reserved(GiB)"
30-
TB_MAX_ACTIVE_TAG = "memory/max_active(GiB)"
31-
3226

3327
def _run_cmd(cmd, timeout=None):
3428
return subprocess.run(
@@ -41,60 +35,11 @@ def _run_cmd(cmd, timeout=None):
4135
)
4236

4337

44-
def _read_peak_memory(tb_root: Path) -> dict[str, float | int]:
45-
try:
46-
from tensorboard.backend.event_processing.event_accumulator import (
47-
EventAccumulator,
48-
)
49-
except ModuleNotFoundError as exc:
50-
raise RuntimeError(
51-
"Peak memory collection requires tensorboard to be installed."
52-
) from exc
53-
54-
if not tb_root.exists():
55-
raise FileNotFoundError(f"TensorBoard directory not found: {tb_root}")
56-
run_dirs = [path for path in tb_root.iterdir() if path.is_dir()]
57-
if len(run_dirs) != 1:
58-
raise RuntimeError(
59-
f"Expected exactly one TensorBoard run directory under {tb_root}, found {run_dirs}"
60-
)
61-
62-
tensorboard_logger = logging.getLogger("tensorboard")
63-
original_level = tensorboard_logger.level
64-
tensorboard_logger.setLevel(logging.WARNING)
65-
event_accumulator = EventAccumulator(str(run_dirs[0]))
66-
try:
67-
event_accumulator.Reload()
68-
finally:
69-
tensorboard_logger.setLevel(original_level)
70-
71-
scalar_tags = event_accumulator.Tags().get("scalars", [])
72-
73-
def _peak_for_tag(tag: str) -> tuple[float, int]:
74-
if tag not in scalar_tags:
75-
raise KeyError(
76-
f"Scalar tag {tag!r} not found in {run_dirs[0]}: {scalar_tags}"
77-
)
78-
peak = max(event_accumulator.Scalars(tag), key=lambda scalar: scalar.value)
79-
return peak.value, peak.step
80-
81-
reserved_gib, reserved_step = _peak_for_tag(TB_MAX_RESERVED_TAG)
82-
active_gib, active_step = _peak_for_tag(TB_MAX_ACTIVE_TAG)
83-
return {
84-
"max_reserved_gib": reserved_gib,
85-
"max_reserved_step": reserved_step,
86-
"max_active_gib": active_gib,
87-
"max_active_step": active_step,
88-
}
89-
90-
9138
def run_single_test(
9239
test_flavor: OverrideDefinitions,
9340
output_dir: str,
9441
module: str | None = None,
9542
config: str | None = None,
96-
*,
97-
collect_peak_memory: bool = False,
9843
):
9944
# run_test supports sequence of tests.
10045
test_name = test_flavor.test_name
@@ -103,7 +48,6 @@ def run_single_test(
10348
all_ranks = ",".join(map(str, range(test_flavor.ngpu)))
10449

10550
for idx, override_arg in enumerate(test_flavor.override_args):
106-
tb_folder = f"tb_{idx}"
10751
cmd = ""
10852
if module is not None:
10953
cmd += f"MODULE={module} "
@@ -115,11 +59,6 @@ def run_single_test(
11559
cmd = f'TORCH_TRACE="{output_dir}/{test_name}/compile_trace" ' + cmd
11660

11761
cmd += " " + dump_folder_arg
118-
if collect_peak_memory:
119-
cmd += (
120-
f" --metrics.enable_tensorboard --metrics.log_freq=1 "
121-
f"--metrics.save_tb_folder={tb_folder}"
122-
)
12362
if override_arg:
12463
cmd += " " + " ".join(override_arg)
12564
logger.info(
@@ -150,18 +89,6 @@ def run_single_test(
15089
f"Command: {cmd}\n"
15190
f"stderr: {result.stderr}\n"
15291
)
153-
if collect_peak_memory:
154-
tb_root = Path(output_dir) / test_name / tb_folder
155-
peak_memory = _read_peak_memory(tb_root)
156-
summary_path = Path(output_dir) / test_name / f"peak_memory_{idx}.json"
157-
summary_path.write_text(json.dumps(peak_memory, indent=2))
158-
logger.info(
159-
f"Peak memory for {test_name}[{idx}]: "
160-
f"reserved={peak_memory['max_reserved_gib']:.3f} GiB "
161-
f"at step {peak_memory['max_reserved_step']}, "
162-
f"active={peak_memory['max_active_gib']:.3f} GiB "
163-
f"at step {peak_memory['max_active_step']}"
164-
)
16592

16693

16794
def run_tests(
@@ -171,8 +98,6 @@ def run_tests(
17198
config=None,
17299
):
173100
"""Run all integration tests to test the core features of TorchTitan"""
174-
collect_peak_memory = getattr(args, "collect_peak_memory", False)
175-
176101
exclude_set = set()
177102
if hasattr(args, "exclude") and args.exclude:
178103
exclude_set = {name.strip() for name in args.exclude.split(",")}
@@ -207,7 +132,6 @@ def run_tests(
207132
args.output_dir,
208133
module,
209134
config,
210-
collect_peak_memory=collect_peak_memory,
211135
)
212136
except Exception as e:
213137
logger.error(str(e))
@@ -278,12 +202,6 @@ def main():
278202
default=None,
279203
help="Comma-separated list of test names to skip",
280204
)
281-
parser.add_argument(
282-
"--collect_peak_memory",
283-
default=False,
284-
action="store_true",
285-
help="Collect peak reserved/active CUDA memory from TensorBoard logs.",
286-
)
287205
args = parser.parse_args()
288206

289207
if not os.path.exists(args.output_dir):

tests/utils.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@
1111
from torch.distributed.tensor import DTensor
1212

1313

14-
def _canonicalize_tensor_name(name: str) -> str:
15-
"""Normalize transparent wrapper segments so hashes reflect tensor contents."""
16-
return name.replace("._checkpoint_wrapped_module", "")
17-
18-
1914
def _hash_model_impl(
2015
model: torch.nn.Module,
2116
algo: str,
@@ -34,7 +29,6 @@ def _hash_model_impl(
3429

3530
def hash_named_tensor(name: str, obj) -> None:
3631
if isinstance(obj, torch.Tensor):
37-
name = _canonicalize_tensor_name(name)
3832
if isinstance(obj, DTensor):
3933
t = obj.to_local().cpu().contiguous()
4034
else:

torchtitan/experiments/graph_trainer/common_utils.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,29 +155,20 @@ def convert_modules_to_fqns(modules, module_to_fqn_mapping):
155155
return module_fqns
156156

157157

158-
def enable_graph_ac_for_mode(ac_mode: str) -> bool:
159-
if ac_mode == "none":
160-
return False
161-
if ac_mode == "selective":
162-
return True
163-
raise ValueError(
164-
"graph_trainer only supports activation_checkpoint.mode "
165-
f"'selective' or 'none', got {ac_mode!r}. Use 'selective' for "
166-
"graph-based SAC."
167-
)
168-
169-
170158
def apply_graph_ac(
171159
compile_config: CompileConfig,
172160
ac_config: "ActivationCheckpointConfig",
173161
) -> None:
174162
"""Add apply_sac to compile joint passes for graph-based selective AC.
175163
176-
``selective`` enables graph SAC, ``none`` is a no-op, and other modes
177-
raise ValueError.
164+
Must be called only when ac_config.mode != "none". Only "selective" mode
165+
is supported; other modes raise ValueError.
178166
"""
179-
if not enable_graph_ac_for_mode(ac_config.mode):
180-
return
167+
if ac_config.mode != "selective":
168+
raise ValueError(
169+
f"graph_trainer only supports activation_checkpoint.mode 'selective' or "
170+
f"'none', got {ac_config.mode!r}. Use 'selective' for graph-based SAC."
171+
)
181172

182173
joint_pass_names = getattr(compile_config, "joint_passes", [])
183174
if "apply_sac" not in joint_pass_names:

torchtitan/experiments/graph_trainer/make_fx_tracer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,11 @@ def fn_with_subclass_handling(*plain_args: Any) -> list:
361361

362362
ctx = TracingContext(fake_mode)
363363
# preserve_node_meta propagates fx.traceback.annotate metadata to traced nodes
364+
# _skip_nested_compile lets this tracer run when an outer dynamo trace
365+
# reaches torch.compile'd FlexAttention kernels.
366+
# _non_strict_tracing_context is required by _patch_autograd_grad() and
367+
# marks this make_fx pass as the non-strict tracing flow, distinct from
368+
# other make_fx-based entry points such as non-strict export.
364369
with (
365370
fake_mode,
366371
tracing(ctx),

torchtitan/experiments/graph_trainer/passes.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def _is_backward_node(node: torch.fx.Node) -> bool:
187187

188188

189189
def construct_default_graph_passes(
190-
traced_result: TracedResult,
190+
traced_result: "TracedResult",
191191
) -> list[Callable]:
192192
"""Build the default pass list for the aot_fx_trace compile path.
193193
@@ -205,6 +205,7 @@ def construct_default_graph_passes(
205205

206206
passes: list[Callable] = [
207207
functools.partial(tlparse_log_graph_pass, graph_name="make_fx_graph_traced"),
208+
apply_ac_on_fwd_bwd_graph,
208209
remove_detach_pass,
209210
remove_identity_view_pass,
210211
remove_identity_slice_pass,
@@ -253,13 +254,6 @@ def apply_graph_passes(
253254
return gm
254255

255256

256-
def graph_ac_pass(
257-
gm: torch.fx.GraphModule, example_inputs: tuple | None = None
258-
) -> torch.fx.GraphModule:
259-
"""Apply graph-based SAC to a traced fwd+loss+bwd graph."""
260-
return apply_ac_on_fwd_bwd_graph(gm)
261-
262-
263257
def autobucketing_reordering_pass(
264258
gm: torch.fx.GraphModule, example_inputs: tuple | None = None
265259
) -> torch.fx.GraphModule:
@@ -553,8 +547,6 @@ def apply_sac_pass(
553547
if node.op != "call_function":
554548
continue
555549

556-
custom_meta = node.meta.get("custom", {})
557-
558550
# Skip backward nodes — they must not carry recompute tags,
559551
# otherwise the remat pass would try to duplicate backward ops.
560552
if _is_backward_node(node):
@@ -577,6 +569,7 @@ def apply_sac_pass(
577569
node.meta["recompute"] = parent.meta["recompute"]
578570
node.meta["ac_graph_id"] = parent.meta.get("ac_graph_id", 0)
579571
continue
572+
custom_meta = node.meta.get("custom", {})
580573
ac_region_id = custom_meta.get(_AC_REGION_ID, 0)
581574
node.meta["ac_graph_id"] = ac_region_id
582575

@@ -610,7 +603,9 @@ def apply_sac_pass(
610603
return gm
611604

612605

613-
def apply_ac_on_fwd_bwd_graph(gm: torch.fx.GraphModule) -> torch.fx.GraphModule:
606+
def apply_ac_on_fwd_bwd_graph(
607+
gm: torch.fx.GraphModule, example_inputs: tuple | None = None
608+
) -> torch.fx.GraphModule:
614609
"""Apply graph-based SAC to a traced fwd+loss+bwd graph.
615610
616611
Tags forward nodes with recompute policy via apply_sac_pass (backward

torchtitan/experiments/graph_trainer/tests/integration_tests.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -547,11 +547,6 @@ def main():
547547
help="test to run, acceptable values: `test_name` in `build_test_list` (default: all)",
548548
)
549549
parser.add_argument("--ngpu", default=8, type=int)
550-
parser.add_argument(
551-
"--collect_peak_memory",
552-
action="store_true",
553-
help="Collect peak reserved/active CUDA memory from TensorBoard logs.",
554-
)
555550
args = parser.parse_args()
556551

557552
if not os.path.exists(args.output_dir):

torchtitan/experiments/graph_trainer/trainer.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
import torch.nn as nn
1212

1313
from torchtitan.experiments.graph_trainer.common_utils import (
14-
annotate_ac_regions,
15-
enable_graph_ac_for_mode,
1614
maybe_register_blockmask_pytree_node,
1715
)
1816
from torchtitan.experiments.graph_trainer.configs import GraphTrainerCompileConfig
@@ -25,7 +23,6 @@
2523
from torchtitan.experiments.graph_trainer.passes import (
2624
apply_graph_passes,
2725
construct_default_graph_passes,
28-
graph_ac_pass,
2926
)
3027
from torchtitan.trainer import Trainer
3128

@@ -119,10 +116,6 @@ def _make_fx_forward_backward_step(
119116
) -> torch.Tensor:
120117
if self._traced_step is None:
121118
fwd_bwd_fn = make_fwd_bwd_step(self.loss_fn)
122-
ac_mode = self.config.activation_checkpoint.mode
123-
enable_graph_ac = enable_graph_ac_for_mode(ac_mode)
124-
if enable_graph_ac:
125-
annotate_ac_regions(model)
126119
maybe_register_blockmask_pytree_node()
127120
with self.train_context():
128121
self._traced_step = trace_train_step(fwd_bwd_fn)(
@@ -135,10 +128,7 @@ def _make_fx_forward_backward_step(
135128
)
136129

137130
if self.config.compile.enable_passes:
138-
passes = []
139-
if enable_graph_ac:
140-
passes.append(graph_ac_pass)
141-
passes.extend(construct_default_graph_passes(self._traced_step))
131+
passes = construct_default_graph_passes(self._traced_step)
142132
self._traced_step.gm = apply_graph_passes(
143133
self._traced_step.gm,
144134
self._traced_step.example_inputs,

0 commit comments

Comments
 (0)