@@ -61,6 +61,8 @@ def _is_backward_node(node: torch.fx.Node) -> bool:
6161
6262def compile_time_passes (
6363 traced_result : "TracedResult" ,
64+ * ,
65+ cudagraph_compatible : bool = False ,
6466) -> list [Callable ]:
6567 """Cleanup, FlexAttention annotation, and regional_inductor passes.
6668
@@ -76,7 +78,7 @@ def compile_time_passes(
7678 # )
7779 from torchtitan .models .common .attention import FlexAttention
7880
79- return [
81+ passes : list [ Callable ] = [
8082 remove_detach_pass ,
8183 remove_identity_view_pass ,
8284 remove_identity_slice_pass ,
@@ -95,17 +97,23 @@ def compile_time_passes(
9597 flex_compile_config = FlexAttention .inductor_configs ,
9698 ),
9799 regional_inductor_pass ,
98- # TODO: Switch to upstream PyTorch implementation when
99- # https://github.com/pytorch/pytorch/pull/178246 lands.
100- # custom_codegen_pass saves the FX graph to disk for:
101- # 1. Debugging: inspect the generated graph code directly
102- # 2. Profiling provenance: dual-path codegen with _RecordFunctionFast
103- # gives fine-grained operator-level attribution in profiler traces
104- # 3. User-editable codegen: users can directly modify the generated
105- # program on disk for fine-grain scheduling optimizations, with
106- # hot-reload picking up changes at runtime
107- custom_codegen_pass ,
108100 ]
101+ if cudagraph_compatible :
102+ # Must run before custom_codegen_pass (last in pre_passes)
103+ # which replaces the GraphModule's forward().
104+ # Also must run before cudagraph_pass.
105+ passes .append (insert_kernel_annotations_pass )
106+ # TODO: Switch to upstream PyTorch implementation when
107+ # https://github.com/pytorch/pytorch/pull/178246 lands.
108+ # custom_codegen_pass saves the FX graph to disk for:
109+ # 1. Debugging: inspect the generated graph code directly
110+ # 2. Profiling provenance: dual-path codegen with _RecordFunctionFast
111+ # gives fine-grained operator-level attribution in profiler traces
112+ # 3. User-editable codegen: users can directly modify the generated
113+ # program on disk for fine-grain scheduling optimizations, with
114+ # hot-reload picking up changes at runtime
115+ passes .append (custom_codegen_pass )
116+ return passes
109117
110118
111119def construct_default_graph_passes (
@@ -124,11 +132,14 @@ def construct_default_graph_passes(
124132 from torchtitan .experiments .graph_trainer .cudagraph import is_cudagraph_compatible
125133
126134 passes : list [Callable ] = []
135+ cudagraph_compatible = is_cudagraph_compatible (traced_result .gm )
136+
127137 if not precompiled :
128- passes .extend (compile_time_passes (traced_result ))
138+ passes .extend (
139+ compile_time_passes (traced_result , cudagraph_compatible = cudagraph_compatible )
140+ )
129141
130- # cudagraph should be the last pass.
131- if is_cudagraph_compatible (traced_result .gm ):
142+ if cudagraph_compatible :
132143 static_input_indices = list (range (traced_result .num_static_inputs ))
133144 passes .append (
134145 functools .partial (
@@ -380,6 +391,96 @@ def _get_fake_mode_from_gm(gm: torch.fx.GraphModule):
380391 return gm
381392
382393
394+ def insert_kernel_annotations_pass (
395+ gm : torch .fx .GraphModule ,
396+ example_inputs : tuple | None = None ,
397+ ) -> torch .fx .GraphModule :
398+ """Insert mark_kernels() calls at module boundaries in the FX graph.
399+
400+ Reads ``node.meta["custom"]["module_fqn"]`` (set via
401+ ``annotate_module_fqns``) and inserts enter/exit calls so that
402+ CUDA graph capture records the annotations.
403+
404+ Requires ``cuda-python`` package and CUDA toolkit/driver >= 13.1
405+ (or cuda-compat >= 13.1). Returns the graph unchanged when unavailable.
406+
407+ Also enables annotation capture on :class:`CUDAGraphWrapper` so that
408+ ``enable_annotations=True`` is passed to ``torch.cuda.graph()``.
409+
410+ Alternative approaches:
411+
412+ 1. **fx.Interpreter**: During cudagraph capture, run the graph via an
413+ ``fx.Interpreter`` subclass that reads ``module_fqn`` metadata and
414+ calls ``mark_kernels`` enter/exit around each node — avoids mutating
415+ the graph.
416+ 2. **Custom CodeGen**: Use a custom ``torch.fx.graph.CodeGen`` to emit
417+ enter/exit lines (or ``with`` blocks) directly in the generated
418+ Python code.
419+
420+ The current graph-pass approach is the least invasive.
421+ """
422+ from torch .cuda ._graph_annotations import _is_tools_id_unavailable
423+
424+ from torchtitan .experiments .graph_trainer .common_utils import _MODULE_FQN
425+ from torchtitan .experiments .graph_trainer .cudagraph import (
426+ enable_cudagraph_annotations ,
427+ )
428+
429+ def _enter (annotation : dict ) -> object :
430+ from torch .cuda ._graph_annotations import mark_kernels
431+
432+ ctx = mark_kernels (annotation )
433+ ctx .__enter__ ()
434+ return ctx
435+
436+ def _exit (ctx : object ) -> None :
437+ ctx .__exit__ (None , None , None ) # type: ignore[union-attr]
438+
439+ if _is_tools_id_unavailable ():
440+ return gm
441+
442+ enable_cudagraph_annotations ()
443+
444+ graph = gm .graph
445+ current_fqn : str | None = None
446+ current_ctx_node = None
447+
448+ for node in list (graph .nodes ):
449+ fqn = (node .meta .get ("custom" ) or {}).get (_MODULE_FQN )
450+
451+ if fqn != current_fqn :
452+ # Close previous scope
453+ if current_ctx_node is not None :
454+ with graph .inserting_before (node ):
455+ exit_node = graph .call_function (_exit , (current_ctx_node ,))
456+ exit_node .meta ["custom" ] = {}
457+ current_ctx_node = None
458+
459+ # Open new scope
460+ if fqn is not None :
461+ with graph .inserting_before (node ):
462+ enter_node = graph .call_function (
463+ _enter ,
464+ ({_MODULE_FQN : fqn },),
465+ )
466+ enter_node .meta ["custom" ] = {}
467+ current_ctx_node = enter_node
468+
469+ current_fqn = fqn
470+
471+ # Close any trailing scope (before output/return)
472+ if current_ctx_node is not None :
473+ output_nodes = [n for n in graph .nodes if n .op == "output" ]
474+ if output_nodes :
475+ with graph .inserting_before (output_nodes [0 ]):
476+ exit_node = graph .call_function (_exit , (current_ctx_node ,))
477+ exit_node .meta ["custom" ] = {}
478+
479+ graph .lint ()
480+ gm .recompile ()
481+ return gm
482+
483+
383484def cudagraph_pass (
384485 gm : torch .fx .GraphModule ,
385486 example_inputs : tuple ,
0 commit comments