Skip to content

Commit ee6034c

Browse files
[graph_trainer] Add remat pass and torch.no_grad() execution to minimal_fx_tracer
- Annotate backward FX nodes with {"remat_pass_tag": "is_backward"} during _patch_engine_run_backward so remat_using_tags_for_fwd_loss_bwd_graph can identify the forward/backward boundary. - Apply remat_using_tags_for_fwd_loss_bwd_graph as a default post-trace pass. Nodes tagged PREFER_RECOMPUTE (from selective AC) are duplicated before backward and the forward copies are DCE'd, reducing peak memory. - Execute traced graph under torch.no_grad() since the graph already contains explicit backward ops. Without this, PyTorch builds a redundant autograd graph keeping all forward intermediates alive via grad_fn references. - Add test_llama_1b_peak_memory: verifies traced+AC peak memory is within 20% of eager+AC on Llama 1B (BS=2, seq=2048, bf16). ghstack-source-id: b038c22 Pull Request resolved: #2766
1 parent 5a5215f commit ee6034c

2 files changed

Lines changed: 112 additions & 3 deletions

File tree

torchtitan/experiments/graph_trainer/make_fx_tracer.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import torch
1313
import torch.nn as nn
1414
import torch.utils._pytree as pytree
15+
from torch._functorch._activation_checkpointing.remat_using_tags_for_fwd_loss_bwd_graph_pass import (
16+
remat_using_tags_for_fwd_loss_bwd_graph,
17+
)
1518
from torch._functorch._aot_autograd.logging_utils import (
1619
setup_stacktrace_preservation_hooks,
1720
)
@@ -202,7 +205,8 @@ def _remove_cpu_shadow_chains(gm: torch.fx.GraphModule) -> None:
202205

203206
@contextmanager
204207
def _patch_engine_run_backward() -> Generator[None, None, None]:
205-
"""Patch _engine_run_backward to install stacktrace preservation hooks.
208+
"""Patch _engine_run_backward to install stacktrace preservation hooks and
209+
annotate backward nodes for the activation checkpointing remat pass.
206210
207211
Why this is needed:
208212
When make_fx traces a function that calls loss.backward(), the backward
@@ -215,6 +219,11 @@ def _patch_engine_run_backward() -> Generator[None, None, None]:
215219
nodes back to their forward counterparts (needed by
216220
``_copy_fwd_metadata_to_bw_nodes``).
217221
222+
Additionally, we annotate all backward nodes with
223+
``{"remat_pass_tag": "is_backward"}`` so that
224+
``remat_using_tags_for_fwd_loss_bwd_graph`` can identify the backward
225+
region boundary for activation checkpointing rematerialization.
226+
218227
We must patch the name in both modules since ``torch.autograd.__init__``
219228
imports it via ``from .graph import``.
220229
"""
@@ -231,7 +240,8 @@ def _patched(t_outputs, *args, **kwargs): # type: ignore[no-untyped-def]
231240
]
232241
if roots:
233242
setup_stacktrace_preservation_hooks(roots)
234-
return _orig_fn(t_outputs, *args, **kwargs)
243+
with torch.fx.traceback.annotate({"remat_pass_tag": "is_backward"}):
244+
return _orig_fn(t_outputs, *args, **kwargs)
235245

236246
torch.autograd.graph._engine_run_backward = _patched # type: ignore[assignment]
237247
torch.autograd._engine_run_backward = _patched # type: ignore[assignment]
@@ -327,6 +337,11 @@ def __call__(self, *args: Any) -> Any:
327337
328338
The module must be the first argument (position 0), matching the
329339
convention enforced by :func:`minimal_fx_tracer`.
340+
341+
Runs under ``torch.no_grad()`` because the graph already contains
342+
explicit backward ops (from ``torch.autograd.grad`` traced by make_fx).
343+
Without this, PyTorch would build a redundant autograd graph on top,
344+
keeping all forward intermediates alive via ``grad_fn`` references.
330345
"""
331346
mod = args[0]
332347
params_dict = _get_params_and_buffers(mod)
@@ -348,7 +363,8 @@ def __call__(self, *args: Any) -> Any:
348363
all_args = params_flat + list(user_args_flat)
349364
flat_inputs, _ = _unwrap_subclasses(all_args)
350365

351-
flat_outputs = self.gm(*flat_inputs)
366+
with torch.no_grad():
367+
flat_outputs = self.gm(*flat_inputs)
352368
wrapped = _wrap_subclasses(
353369
flat_outputs, self.num_flat_outputs, self.output_subclass_layouts
354370
)
@@ -478,6 +494,11 @@ def fn_with_subclass_handling(*plain_args: Any) -> list:
478494

479495
_remove_cpu_shadow_chains(traced)
480496

497+
# Rematerialize activations tagged PREFER_RECOMPUTE by selective AC.
498+
# Duplicates recomputable forward ops before the backward region and
499+
# DCEs the original copies, reducing peak memory.
500+
traced = remat_using_tags_for_fwd_loss_bwd_graph(traced)
501+
481502
assert output_spec is not None
482503
return TracedResult(
483504
gm=traced,

torchtitan/experiments/graph_trainer/tests/test_trace_module.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,94 @@ def forward(model, tokens, attn_masks):
665665
f"{node.name} missing ac_region_id annotation",
666666
)
667667

668+
def test_llama_1b_peak_memory(self):
669+
"""Traced+AC peak memory within 10% of eager+AC, with bitwise-identical numerics."""
670+
from torchtitan.config import ActivationCheckpointConfig
671+
from torchtitan.distributed.activation_checkpoint import apply_ac
672+
from torchtitan.models.llama3 import llama3_configs, Llama3Model
673+
674+
config = llama3_configs["1B"]
675+
batch_size, seq_len = 2, 2048
676+
dtype = torch.bfloat16
677+
num_steps = 3
678+
lr = 1e-3
679+
680+
tokens = torch.randint(
681+
0, config.vocab_size, (batch_size, seq_len), device=self.DEVICE
682+
)
683+
labels = torch.randint(
684+
0, config.vocab_size, (batch_size, seq_len), device=self.DEVICE
685+
)
686+
687+
def make_model():
688+
model = Llama3Model(config).to(device=self.DEVICE, dtype=dtype)
689+
with torch.no_grad():
690+
model.init_weights(buffer_device=torch.device(self.DEVICE))
691+
apply_ac(model, ActivationCheckpointConfig(mode="selective"))
692+
return model
693+
694+
def run(mode):
695+
model = make_model()
696+
model.load_state_dict(state)
697+
if mode == "traced":
698+
train_step = make_train_step(get_loss)
699+
traced = minimal_fx_tracer(train_step, (model, tokens, labels))
700+
opt = torch.optim.Adam(model.parameters(), lr=lr)
701+
step_results = []
702+
torch.cuda.reset_peak_memory_stats()
703+
for _ in range(num_steps):
704+
if mode == "traced":
705+
result = traced(model, tokens, labels)
706+
loss, grads = result[0], result[1:]
707+
for p, g in zip(model.parameters(), grads, strict=True):
708+
p.grad = g
709+
else:
710+
logits = model(tokens)
711+
loss = get_loss(logits, labels)
712+
loss.backward()
713+
grads = [p.grad for p in model.parameters()]
714+
step_results.append((
715+
loss.detach().cpu(),
716+
[g.clone().cpu() for g in grads],
717+
))
718+
opt.step()
719+
opt.zero_grad()
720+
peak = torch.cuda.max_memory_allocated() / 1e9
721+
del model, opt
722+
torch.cuda.empty_cache()
723+
return step_results, peak
724+
725+
ref = make_model()
726+
state = ref.state_dict()
727+
del ref
728+
torch.cuda.empty_cache()
729+
730+
eager_results, peak_eager = run("eager")
731+
traced_results, peak_traced = run("traced")
732+
733+
# Bitwise-identical loss and grads across all steps.
734+
for step, ((el, eg), (tl, tg)) in enumerate(
735+
zip(eager_results, traced_results, strict=True)
736+
):
737+
self.assertTrue(
738+
torch.equal(el, tl),
739+
f"Step {step}: loss mismatch — eager={el.item():.6f} vs traced={tl.item():.6f}",
740+
)
741+
for i, (ge, gt) in enumerate(zip(eg, tg, strict=True)):
742+
self.assertTrue(
743+
torch.equal(ge, gt),
744+
f"Step {step}: grad[{i}] mismatch — max_diff={( ge - gt).abs().max().item():.2e}",
745+
)
746+
747+
# Peak memory within 10%.
748+
ratio = peak_traced / peak_eager
749+
self.assertLess(
750+
ratio,
751+
1.1,
752+
f"Traced+AC peak memory ({peak_traced:.2f} GB) is more than "
753+
f"10% above eager+AC ({peak_eager:.2f} GB), ratio={ratio:.2f}",
754+
)
755+
668756

669757
class TestTraceFSDP(FSDPTest):
670758
@property

0 commit comments

Comments
 (0)