Skip to content

Commit 8fa4baf

Browse files
committed
Change to use annotate_module_components
1 parent e89da8f commit 8fa4baf

18 files changed

Lines changed: 599 additions & 91 deletions

File tree

.github/workflows/integration_test_8gpu_graph_trainer.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ jobs:
7171
# Run the graph passes unit tests
7272
torchrun --nproc-per-node=8 -m pytest torchtitan/experiments/graph_trainer/tests/test_passes.py::TestReassignToPgPass -v
7373
pytest torchtitan/experiments/graph_trainer/tests/test_passes.py::TestApplySACPass -v
74+
pytest torchtitan/experiments/graph_trainer/tests/test_passes.py::TestAnnotateModuleFqns -v
75+
76+
# Run kernel annotation profiler tests (skips if cuda-compat < 13.1)
77+
pytest torchtitan/experiments/graph_trainer/tests/profiler_tests.py -v
7478
7579
# Run the make_fx tracer unit tests
7680
pytest torchtitan/experiments/graph_trainer/tests/test_trace_module.py -v -k "TestTraceModule or TestTraceDTensor or TestMetadataPropagation"

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,3 @@ CLAUDE.local.md
4949
/debug_*.py
5050
CLAUDE_CONTEXT/
5151
/.claude/settings.local.json
52-
agent_space/

torchtitan/experiments/graph_trainer/.claude/CLAUDE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,18 @@ This verifies that the aot_fx_trace path produces bitwise identical losses
109109
and gradients across runs, and matches eager numerics exactly. Any change
110110
that breaks this test must be investigated and fixed before proceeding with
111111
other tests.
112+
113+
### CUDA Graph Kernel Annotations
114+
115+
The `insert_kernel_annotations_pass` labels CUDA graph kernels with their
116+
originating `nn.Module` path in profiler traces. It runs automatically in the
117+
`aot_fx_trace` path (bundled with the cudagraph pass). The profiler
118+
post-processes the trace automatically via `_maybe_annotate_trace` in
119+
`torchtitan/tools/profiling.py` — no manual post-processing is needed.
120+
121+
Requirements: `cuda-python` package and CUDA toolkit/driver >= 13.1
122+
(or `cuda-compat >= 13.1` on `LD_LIBRARY_PATH`). The pass is a no-op when
123+
these are unavailable.
124+
125+
To view annotated traces, open the exported JSON in https://ui.perfetto.dev.
126+
Kernel events will have `module_fqn` fields like `layers.0.attention.wq`.

torchtitan/experiments/graph_trainer/common_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@
1818
from torchtitan.tools.logging import logger
1919

2020
_AC_REGION_ID = "ac_region_id"
21+
_MODULE_FQN = "module_fqn"
22+
23+
24+
def annotate_module_fqns(model: nn.Module) -> None:
25+
"""Annotate all modules' forward with their fully-qualified names.
26+
27+
Every named submodule (excluding the root) gets its forward method wrapped
28+
with annotate_fn so that FX nodes carry the module_fqn metadata.
29+
"""
30+
for fqn, submodule in model.named_modules():
31+
if fqn: # skip root module
32+
submodule.forward = annotate_fn({_MODULE_FQN: fqn})(submodule.forward)
2133

2234

2335
def annotate_ac_regions(model: nn.Module) -> None:

torchtitan/experiments/graph_trainer/compile.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,6 @@ def apply_compile(
240240
return model
241241

242242
torch._inductor.config.reorder_for_peak_memory = False
243-
torch._inductor.config.triton.cudagraph_kernel_annotations = True
244243
torch._dynamo.config.capture_scalar_outputs = True
245244

246245
fsdp_reshard_after_forward = get_fsdp_reshard_after_forward_policy(

torchtitan/experiments/graph_trainer/cudagraph.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ def __init__(self) -> None:
3131
self._initialized = False
3232
self._cudagraph_wrappers: list["CUDAGraphWrapper"] = []
3333
self._teardown_called = False
34+
# toolsId (graph_id << 32 | node_id) -> list of annotation dicts
35+
# (e.g. [{"module_fqn": "layers.0.attention.wq"}]).
3436
self.all_annotations: dict[int, list] = {}
37+
self.enable_annotations: bool = False
3538

3639
def maybe_initialize(self) -> None:
3740
if self._initialized:
@@ -106,6 +109,56 @@ def get_cudagraph_annotations() -> dict[int, list]:
106109
return _cg_manager.all_annotations
107110

108111

112+
def enable_cudagraph_annotations() -> None:
113+
"""Enable kernel annotation capture on subsequent CUDA graph recordings.
114+
115+
Also registers a profiler trace post-processor that merges the captured
116+
annotations into exported traces automatically.
117+
"""
118+
_cg_manager.enable_annotations = True
119+
_register_trace_post_processor()
120+
121+
122+
def _annotate_trace_file(trace_path: str) -> None:
123+
"""Post-process a profiler trace with CUDA graph kernel annotations."""
124+
annotations = _cg_manager.all_annotations
125+
if not annotations:
126+
return
127+
128+
try:
129+
from torch.cuda._annotate_cuda_graph_trace import ( # pyrefly: ignore[missing-import]
130+
annotate_trace,
131+
)
132+
except ImportError:
133+
logger.warning(
134+
"torch.cuda._annotate_cuda_graph_trace not available. "
135+
"Upgrade PyTorch to enable trace CUDA graph kernel annotation."
136+
)
137+
return
138+
139+
import json
140+
141+
with open(trace_path) as f:
142+
trace = json.load(f)
143+
144+
count = annotate_trace(trace, annotations)
145+
if count > 0:
146+
with open(trace_path, "w") as f:
147+
json.dump(trace, f)
148+
logger.info(f"Annotated {count} CUDAGraph kernel event(s) in profiler trace")
149+
150+
151+
def _register_trace_post_processor() -> None:
152+
"""Register the annotation post-processor with the core profiler (once)."""
153+
from torchtitan.tools.profiling import (
154+
_trace_post_processors,
155+
register_trace_post_processor,
156+
)
157+
158+
if _annotate_trace_file not in _trace_post_processors:
159+
register_trace_post_processor(_annotate_trace_file)
160+
161+
109162
class CUDAGraphWrapper:
110163
"""Wraps a callable with cudagraph. It warms up the callable, records cudagraph,
111164
and replays cudagraph during runtime. It also handles static input tensors, which
@@ -212,15 +265,15 @@ def __call__(self, *args):
212265
self._cudagraph,
213266
pool=_cg_manager.graph_pool,
214267
stream=_cg_manager.stream,
215-
enable_annotations=True,
268+
enable_annotations=_cg_manager.enable_annotations,
216269
):
217270
# `output` is managed by pytorch's cudagraph pool
218271
self._output = self._runnable(*args)
219272

220-
# Save kernel annotations for trace post-processing.
221-
from torch.cuda._graph_annotations import get_kernel_annotations
273+
if _cg_manager.enable_annotations:
274+
from torch.cuda._graph_annotations import get_kernel_annotations
222275

223-
_cg_manager.all_annotations.update(get_kernel_annotations())
276+
_cg_manager.all_annotations.update(get_kernel_annotations())
224277

225278
if self._should_check_address:
226279
self._check_static_inputs_address()

torchtitan/experiments/graph_trainer/deepseek_v3/parallelize.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from torchtitan.distributed.tensor_parallel import maybe_enable_async_tp
2020
from torchtitan.experiments.graph_trainer.common_utils import (
2121
annotate_ac_regions,
22+
annotate_module_fqns,
2223
apply_graph_ac,
2324
)
2425
from torchtitan.experiments.graph_trainer.compile import apply_compile
@@ -42,6 +43,9 @@ def annotate_deepseekv3(model: GraphTrainerDeepSeekV3Model) -> None:
4243
- AC region annotation: Tags each transformer block's forward with a unique
4344
ac_region_id so that apply_sac_pass can assign per-block ac_graph_id
4445
boundaries for the min-cut partitioner.
46+
- Module FQN annotation: Tags each submodule's forward with its
47+
fully-qualified name so that insert_kernel_annotations_pass can label
48+
CUDA graph kernels with their originating module.
4549
4650
"""
4751
from torchtitan.distributed.expert_parallel import ExpertParallel
@@ -56,6 +60,7 @@ def annotate_deepseekv3(model: GraphTrainerDeepSeekV3Model) -> None:
5660
MoE.forward = annotate_fn({"EP": "compute"})(MoE.forward)
5761

5862
annotate_ac_regions(model)
63+
annotate_module_fqns(model)
5964

6065

6166
# Adapted from llama4/infra/parallelize.py

torchtitan/experiments/graph_trainer/graph_utils.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -519,11 +519,6 @@ def get_compiler_passes_from_config(
519519
)
520520
)
521521
else:
522-
# Auto-insert annotation pass before cudagraph capture.
523-
if pass_name == "cudagraph":
524-
compiler_passes.append(
525-
AVAILABLE_COMPILER_PASSES["insert_kernel_annotations"]
526-
)
527522
compiler_passes.append(AVAILABLE_COMPILER_PASSES[pass_name])
528523

529524
if pass_names:

torchtitan/experiments/graph_trainer/llama3/parallelize.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from torchtitan.distributed.tensor_parallel import maybe_enable_async_tp
1717
from torchtitan.experiments.graph_trainer.common_utils import (
1818
annotate_ac_regions,
19+
annotate_module_fqns,
1920
apply_graph_ac,
2021
)
2122
from torchtitan.experiments.graph_trainer.compile import apply_compile
@@ -35,8 +36,12 @@ def annotate_llama(model: GraphTrainerLlama3Model) -> None:
3536
- AC region annotation: Tags each transformer block's forward with a unique
3637
ac_region_id so that apply_sac_pass can assign per-block ac_graph_id
3738
boundaries for the min-cut partitioner.
39+
- Module FQN annotation: Tags each submodule's forward with its
40+
fully-qualified name so that insert_kernel_annotations_pass can label
41+
CUDA graph kernels with their originating module.
3842
"""
3943
annotate_ac_regions(model)
44+
annotate_module_fqns(model)
4045

4146

4247
def parallelize_llama(

torchtitan/experiments/graph_trainer/passes.py

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ def construct_default_graph_passes(
8282
regional_inductor_pass,
8383
]
8484

85-
# Insert kernel annotation markers before cudagraph capture.
86-
passes.append(insert_kernel_annotations_pass)
87-
88-
# cudagraph should be the last pass.
85+
# cudagraph should be the last pass. insert_kernel_annotations runs
86+
# right before it to inject mark_kernels enter/exit calls that execute
87+
# during CUDA graph capture. Both are no-ops when requirements aren't met.
8988
from torchtitan.experiments.graph_trainer.cudagraph import is_cudagraph_compatible
9089

9190
if is_cudagraph_compatible(traced_result.gm):
91+
passes.append(insert_kernel_annotations_pass)
9292
static_input_indices = list(range(traced_result.num_static_inputs))
9393
passes.append(
9494
functools.partial(
@@ -263,67 +263,89 @@ def _get_fake_mode_from_gm(gm: torch.fx.GraphModule):
263263
return gm
264264

265265

266-
def _mark_kernels_enter(annotation: dict) -> object:
267-
"""Helper called from FX graph to enter a mark_kernels scope."""
268-
from torch.cuda._graph_annotations import mark_kernels
269-
270-
ctx = mark_kernels(annotation)
271-
ctx.__enter__()
272-
return ctx
273-
274-
275-
def _mark_kernels_exit(ctx: object) -> None:
276-
"""Helper called from FX graph to exit a mark_kernels scope."""
277-
ctx.__exit__(None, None, None) # type: ignore[union-attr]
278-
279-
280266
def insert_kernel_annotations_pass(
281267
gm: torch.fx.GraphModule,
282268
example_inputs: tuple | None = None,
283269
) -> torch.fx.GraphModule:
284-
"""Insert mark_kernels() calls at component boundaries in the FX graph.
270+
"""Insert mark_kernels() calls at module boundaries in the FX graph.
285271
286-
Reads ``node.meta["custom"]["component"]`` (set via
287-
``torch.fx.traceback.annotate``) and inserts enter/exit calls so that
272+
Reads ``node.meta["custom"]["module_fqn"]`` (set via
273+
``annotate_module_fqns``) and inserts enter/exit calls so that
288274
CUDA graph capture records the annotations.
275+
276+
Requires ``cuda-python`` package and CUDA toolkit/driver >= 13.1
277+
(or cuda-compat >= 13.1). Returns the graph unchanged when unavailable.
278+
279+
Also enables annotation capture on :class:`CUDAGraphWrapper` so that
280+
``enable_annotations=True`` is passed to ``torch.cuda.graph()``.
281+
282+
Alternative approaches:
283+
284+
1. **fx.Interpreter**: During cudagraph capture, run the graph via an
285+
``fx.Interpreter`` subclass that reads ``module_fqn`` metadata and
286+
calls ``mark_kernels`` enter/exit around each node — avoids mutating
287+
the graph.
288+
2. **Custom CodeGen**: Use a custom ``torch.fx.graph.CodeGen`` to emit
289+
enter/exit lines (or ``with`` blocks) directly in the generated
290+
Python code.
291+
292+
The current graph-pass approach is the least invasive.
289293
"""
294+
from torch.cuda._graph_annotations import _is_tools_id_unavailable
295+
296+
from torchtitan.experiments.graph_trainer.common_utils import _MODULE_FQN
297+
from torchtitan.experiments.graph_trainer.cudagraph import (
298+
enable_cudagraph_annotations,
299+
)
300+
301+
def _enter(annotation: dict) -> object:
302+
from torch.cuda._graph_annotations import mark_kernels
303+
304+
ctx = mark_kernels(annotation)
305+
ctx.__enter__()
306+
return ctx
307+
308+
def _exit(ctx: object) -> None:
309+
ctx.__exit__(None, None, None) # type: ignore[union-attr]
310+
311+
if _is_tools_id_unavailable():
312+
return gm
313+
314+
enable_cudagraph_annotations()
315+
290316
graph = gm.graph
291-
current_component: str | None = None
317+
current_fqn: str | None = None
292318
current_ctx_node = None
293319

294320
for node in list(graph.nodes):
295-
component = (node.meta.get("custom") or {}).get("component")
321+
fqn = (node.meta.get("custom") or {}).get(_MODULE_FQN)
296322

297-
if component != current_component:
323+
if fqn != current_fqn:
298324
# Close previous scope
299325
if current_ctx_node is not None:
300326
with graph.inserting_before(node):
301-
exit_node = graph.call_function(
302-
_mark_kernels_exit, (current_ctx_node,)
303-
)
327+
exit_node = graph.call_function(_exit, (current_ctx_node,))
304328
exit_node.meta["custom"] = {}
305329
current_ctx_node = None
306330

307331
# Open new scope
308-
if component is not None:
332+
if fqn is not None:
309333
with graph.inserting_before(node):
310334
enter_node = graph.call_function(
311-
_mark_kernels_enter,
312-
({"component": component},),
335+
_enter,
336+
({_MODULE_FQN: fqn},),
313337
)
314338
enter_node.meta["custom"] = {}
315339
current_ctx_node = enter_node
316340

317-
current_component = component
341+
current_fqn = fqn
318342

319343
# Close any trailing scope (before output/return)
320344
if current_ctx_node is not None:
321345
output_nodes = [n for n in graph.nodes if n.op == "output"]
322346
if output_nodes:
323347
with graph.inserting_before(output_nodes[0]):
324-
exit_node = graph.call_function(
325-
_mark_kernels_exit, (current_ctx_node,)
326-
)
348+
exit_node = graph.call_function(_exit, (current_ctx_node,))
327349
exit_node.meta["custom"] = {}
328350

329351
graph.lint()
@@ -751,7 +773,6 @@ def tlparse_log_graph_pass(
751773
"auto_bucketing": autobucketing_reordering_pass,
752774
"transformer_block_bucketing": transformer_block_bucketing_reordering_pass,
753775
"regional_inductor": regional_inductor_pass,
754-
"insert_kernel_annotations": insert_kernel_annotations_pass,
755776
"cudagraph": cudagraph_pass,
756777
"full_inductor_compilation": full_inductor_compilation_pass,
757778
}

0 commit comments

Comments
 (0)