Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / compile / tracer / graph_tracer.py: 93%

263 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15# Tracer reaches into torch internals (torch._dynamo, torch.nn.utils.stateless, 

16# torch.utils._pytree) for joint-graph capture; these have no public API. 

17# pylint: disable=E1136,E1129,W0212 

18""" 

19Graph Tracer - Model Graph Tracer (make_fx + autograd.grad) 

20 

21Traces a model into a joint Forward + Backward FX graph, following 

22the design of torchtitan's ``experiments/graph_trainer/minimal_fx_tracer``: 

23 

241. Module parameters/buffers are extracted from the live model into flat 

25 tensors and threaded through the graph as **static inputs** (leading 

26 placeholders) instead of ``get_attr`` nodes, via 

27 ``torch.nn.utils.stateless._reparametrize_module``. The graph therefore 

28 contains no parameter ``get_attr`` nodes, and passes can split the graph by 

29 reshaping the static-input placeholders. 

30 

312. The forward pass runs ``torch.autograd.grad(loss, params)`` *inside* the 

32 traced function body. For that to be traceable the engine backward must be 

33 patched (``torch.compiler._patch_engine_backward()``) and backward must run 

34 on the calling thread (``torch.autograd.set_multithreading_enabled(False)``), 

35 otherwise the C++ autograd engine dispatches backward to a worker thread 

36 with a fresh ``contextvars.Context`` and the tracing context is lost. 

37 

383. The whole trace runs under ``FakeTensorMode(allow_non_fake_inputs=True)`` 

39 plus a non-strict tracing context, ``preserve_node_meta()`` and 

40 ``_skip_nested_compile()``, mirroring the torchtitan tracer. 

41 

42The joint graph is executed with ``torch.no_grad()`` at runtime: it already 

43contains the explicit backward ops, and a redundant autograd graph on top 

44would keep every forward intermediate alive via ``grad_fn`` references. 

45""" 

46 

47from __future__ import annotations 

48 

49__all__ = [ 

50 "JointGraph", 

51 "trace_model_graph", 

52 "run_traced_graph", 

53 "extract_module_state", 

54] 

55 

56import contextlib 

57import copy 

58import inspect 

59import warnings 

60from collections.abc import Callable, Generator 

61from collections import deque 

62from contextlib import contextmanager 

63from dataclasses import dataclass 

64from typing import Any, Dict, List 

65 

66import torch 

67from torch import nn 

68from torch._decomp import get_decompositions 

69from torch._guards import tracing, TracingContext 

70from torch._subclasses import FakeTensorMode 

71from torch.fx.experimental.proxy_tensor import make_fx 

72from torch.fx.traceback import preserve_node_meta 

73from torch.nn.utils import stateless 

74 

75from .dynamic_shapes import InputGuards, build_symbolic_inputs 

76 

77# Older torch's make_fx predates record_stack_traces / record_module_stack 

78# and rejects unknown kwargs; forward only the kwargs this torch supports. 

79# The joint graph still captures without record_stack_traces -- the fwd->bw 

80# metadata copy skips a missing stack_trace instead of failing. 

81try: 

82 _MAKE_FX_PARAMS = inspect.signature(make_fx).parameters 

83except (ValueError, TypeError): 

84 _MAKE_FX_PARAMS = {} 

85_MAKE_FX_KWARGS: Dict[str, Any] = { 

86 kw: val 

87 for kw, val in (("record_stack_traces", True), ("record_module_stack", True)) 

88 if kw in _MAKE_FX_PARAMS 

89} 

90 

91# Tensors and make_fx-safe primitives are allowed as graph leaves. 

92# Everything else (callables, custom objects) must be captured in the 

93# train_fn closure or registered as pytree constants. 

94_ALLOWED_LEAF_TYPES = (torch.Tensor, int, float, bool, str, type(None)) 

95 

96 

97@contextmanager 

98def _skip_nested_compile() -> Generator[None, None, None]: 

99 """Tell dynamo to inline ``torch.compile``'d functions during make_fx tracing. 

100 

101 make_fx cannot trace through torch.compile'd functions (e.g. compiled 

102 attention kernels). Setting ``error_on_nested_fx_trace`` to False makes 

103 dynamo silently inline the wrapped function instead of raising. 

104 """ 

105 prev = torch._dynamo.config.error_on_nested_fx_trace 

106 torch._dynamo.config.error_on_nested_fx_trace = False 

107 try: 

108 yield 

109 finally: 

110 torch._dynamo.config.error_on_nested_fx_trace = prev 

111 

112 

113@contextmanager 

114def _patch_engine_backward() -> Generator[None, None, None]: 

115 """No-op fallback for torch versions without the patched autograd engine. 

116 

117 ``torch.compiler._patch_engine_backward`` makes ``torch.autograd.grad`` 

118 inside a make_fx-traced function emit backward ops as FX nodes instead of 

119 running a real autograd backward. Present in torchtitan's patched torch; 

120 absent in stock torch, where joint-graph capture of ``autograd.grad`` is 

121 not supported at all. 

122 """ 

123 patcher = getattr(torch.compiler, "_patch_engine_backward", None) 

124 if patcher is None: 

125 warnings.warn( 

126 "torch.compiler._patch_engine_backward not available. " 

127 "Joint-graph capture of autograd.grad may not work correctly. " 

128 "Consider using torchtitan's patched torch or a newer torch version.", 

129 RuntimeWarning, 

130 stacklevel=2, 

131 ) 

132 yield 

133 else: 

134 with patcher(): 

135 yield 

136 

137 

138@contextmanager 

139def _non_strict_tracing_context() -> Generator[None, None, None]: 

140 """Mark this make_fx pass as the non-strict tracing flow. 

141 

142 Required by ``_patch_engine_backward``; no-op fallback on stock torch. 

143 """ 

144 ctx = getattr(torch.compiler, "_non_strict_tracing_context", None) 

145 if ctx is None: 

146 yield 

147 else: 

148 with ctx(): 

149 yield 

150 

151 

152@dataclass 

153class JointGraph: 

154 """ 

155 Joint forward-backward computation graph 

156 

157 Features: 

158 - No parallel information 

159 - Parameters, activations, gradients are all complete tensors 

160 - Forward + Loss + Backward complete joint graph 

161 - Parameters/buffers are static graph inputs (leading placeholders), 

162 not ``get_attr`` nodes 

163 """ 

164 

165 graph_module: torch.fx.GraphModule 

166 # Storage-free descriptors of the tracing-time user inputs (shape/dtype 

167 # for tensors) via ``_input_meta``. Real sample tensors are deliberately 

168 # not kept: they would pin their storage for the whole training run. The 

169 # FakeTensor counterparts live in ``example_inputs``. 

170 inputs: List[Any] 

171 outputs: List[Any] 

172 param_names: List[str] 

173 param_shapes: Dict[str, tuple] 

174 num_layers: int 

175 # Trace-time parameter/buffer values (flat, in graph placeholder order) 

176 # and their FQNs. ``state_fqns`` is the authoritative order for feeding 

177 # runtime parameter state into the graph. 

178 state_fqns: List[str] 

179 example_inputs: tuple 

180 input_guards: InputGuards | None = None 

181 

182 

183def extract_module_state(mod: nn.Module) -> Dict[str, torch.Tensor]: 

184 """Return a merged dict of the module's named parameters and buffers. 

185 

186 Tied tensors (e.g. an ``lm_head.weight`` sharing storage with 

187 ``embed_tokens.weight``) are DEDUPLICATED to a single entry: the graph 

188 threads each unique tensor as one static input, so emitting one entry per 

189 alias would double the placeholder count, duplicate every FSDP collective, 

190 and make ``torch.autograd.grad`` return (and the compiler add) the same 

191 gradient once per alias. The missing aliases are re-materialized only for 

192 tracing, via ``_reparametrize_train_state``'s ``tie_weights=True``. 

193 """ 

194 return { 

195 **dict(mod.named_parameters(remove_duplicate=True)), 

196 **dict(mod.named_buffers(remove_duplicate=True)), 

197 } 

198 

199 

200@contextlib.contextmanager 

201def _reparametrize_train_state( 

202 module: nn.Module, 

203 model_state: Dict[str, torch.Tensor], 

204): 

205 """Rebind ``module``'s parameters/buffers to explicit trace-time tensors. 

206 

207 Inside the traced function this swaps in the fake/static tensors so every 

208 parameter access in the model goes through the graph's static inputs. 

209 

210 ``tie_weights=True`` re-adds, for the duration of the trace, the alias 

211 keys that ``extract_module_state`` deduplicated — mapping each alias to 

212 the SAME static tensor. Both tied uses then reference one ``fx.Node``, so 

213 the traced graph keeps a single placeholder (and a single gradient) for 

214 the shared tensor instead of a baked-in ``get_attr`` constant. 

215 """ 

216 with contextlib.ExitStack() as stack: 

217 stack.enter_context( 

218 stateless._reparametrize_module(module, model_state, tie_weights=True) 

219 ) 

220 yield 

221 

222 

223def _iter_node_args(node: torch.fx.Node): 

224 """Yield every ``fx.Node`` among ``node.args`` (unwrapping nested lists).""" 

225 for a in node.args: 

226 if isinstance(a, torch.fx.Node): 

227 yield a 

228 elif isinstance(a, (tuple, list)): 

229 for x in a: 

230 if isinstance(x, torch.fx.Node): 

231 yield x 

232 

233 

234def _output_args(fx_g: torch.fx.GraphModule) -> List[torch.fx.Node]: 

235 """Return the flattened forward arg list of the graph's ``output`` node.""" 

236 out_node = next((n for n in fx_g.graph.nodes if n.op == "output"), None) 

237 if out_node is None: 

238 return [] 

239 args: List[torch.fx.Node] = [] 

240 for a in out_node.args: 

241 if isinstance(a, torch.fx.Node): 

242 args.append(a) 

243 elif isinstance(a, (tuple, list)): 

244 for x in a: 

245 if isinstance(x, torch.fx.Node): 

246 args.append(x) 

247 return args 

248 

249 

250def _ancestors_of(node: torch.fx.Node) -> set: 

251 """Return the transitive producers of ``node`` (excluding ``node``).""" 

252 ancestors = set() 

253 dq = deque([node]) 

254 while dq: 

255 cur = dq.popleft() 

256 for a in _iter_node_args(cur): 

257 if a.op == "placeholder" or a in ancestors: 

258 continue 

259 ancestors.add(a) 

260 dq.append(a) 

261 return ancestors 

262 

263 

264def _has_module_stack(node: torch.fx.Node) -> bool: 

265 """True if the node carries any ``nn_module_stack`` entry. 

266 

267 make_fx leaves stack-less nodes (e.g. autograd engine output) with an 

268 *empty* ``OrderedDict`` rather than ``None``, so a truthiness check is 

269 the robust test. 

270 """ 

271 return bool(node.meta.get("nn_module_stack")) 

272 

273 

274def _annotate_autograd_backward(fx_g: torch.fx.GraphModule) -> None: 

275 """Tag backward nodes with ``meta['autograd_backward'] = True``. 

276 

277 Stock torch's make_fx (without the torchtitan ``_patch_engine_backward`` 

278 hook) never attaches the autograd engine's ``autograd_backward`` tag, so 

279 passes that split the joint graph into fwd/bwd halves get nothing to key 

280 on. This reconstructs the tag structurally. 

281 

282 The joint graph's ``output`` returns ``[loss, grad_0, ... grad_n]``. The 

283 loss and the nodes feeding it (logits, loss reduction, ``getitem`` of the 

284 loss value) are forward even though they live outside any module and so 

285 carry no ``nn_module_stack``. Every other output is a parameter gradient 

286 produced by ``torch.autograd.grad``. 

287 

288 Two pre-computed sets separate the halves: 

289 

290 - ``fwd_loss_nodes``: the transitive producers of ``loss`` — the logits, 

291 the shift/reshape that makes the loss target, and the loss reduction 

292 itself. These are forward (they are the loss *source*); backward is 

293 computed *from* them. 

294 - ``bwd_nodes``: ``call_function`` nodes reachable from the parameter 

295 gradients (``output[1:]``) that carry no ``nn_module_stack`` and are 

296 not in ``fwd_loss_nodes``. The module stack is only recorded while a 

297 traced module ``forward`` runs, so autograd-engine nodes have none. 

298 Walking backward and stopping at forward boundaries (stack-bearing 

299 nodes or the loss source) spans exactly the backward half. 

300 """ 

301 output = _output_args(fx_g) 

302 if not output: 

303 return 

304 loss_node = output[0] 

305 grad_seeds = output[1:] 

306 

307 # Forward loss chain: everything that flows INTO the loss. Backward is 

308 # computed from the loss, so these are forward regardless of whether any 

309 # module recorded them (they may sit in ``train_fn``, outside modules). 

310 fwd_loss_nodes = _ancestors_of(loss_node) 

311 fwd_loss_nodes.add(loss_node) 

312 

313 # BFS backward over dataflow from the gradient outputs, marking every 

314 # stack-less, non-loss-source node as backward. Stack-bearing nodes are 

315 # forward (module boundaries); loss-source nodes are forward (the loss is 

316 # the backward's *source*, not part of it). 

317 bwd_nodes = set() 

318 dq = deque() 

319 for seed in grad_seeds: 

320 if ( 

321 not _has_module_stack(seed) 

322 and seed.op != "placeholder" 

323 and seed not in fwd_loss_nodes 

324 ): 

325 bwd_nodes.add(seed) 

326 dq.append(seed) 

327 while dq: 

328 n = dq.popleft() 

329 for a in _iter_node_args(n): 

330 if a.op == "placeholder" or a in bwd_nodes: 

331 continue 

332 if a in fwd_loss_nodes: 

333 continue # forward loss source; do not expand 

334 if _has_module_stack(a): 

335 continue # forward module boundary; do not expand 

336 bwd_nodes.add(a) 

337 dq.append(a) 

338 

339 for node in bwd_nodes: 

340 node.meta["autograd_backward"] = True 

341 

342 

343def _copy_fwd_metadata_to_bw_nodes(fx_g: torch.fx.GraphModule) -> None: 

344 """Copy forward metadata to backward nodes across all nested FX subgraphs. 

345 

346 Uses a two-pass approach over all submodule graphs (including HOP 

347 subgraphs). Pass 1 collects forward nodes by ``seq_nr``; pass 2 copies 

348 ``custom``/``nn_module_stack``/``stack_trace`` from the matching forward 

349 node to each backward node. Backward nodes are identified by the autograd 

350 engine's ``autograd_backward`` tag on ``node.meta``. 

351 """ 

352 seq_nr_to_fwd_node: Dict[int, torch.fx.Node] = {} 

353 for node in _iter_seq_nr_nodes(fx_g, backward=False): 

354 seq_nr_to_fwd_node.setdefault(node.meta["seq_nr"], node) 

355 

356 for node in _iter_seq_nr_nodes(fx_g, backward=True): 

357 fwd_node = seq_nr_to_fwd_node.get(node.meta["seq_nr"]) 

358 if fwd_node is None or fwd_node is node: 

359 continue 

360 _copy_fwd_meta(fwd_node, node) 

361 

362 

363def _iter_seq_nr_nodes(fx_g: torch.fx.GraphModule, backward: bool): 

364 """Yield ``seq_nr``-bearing call/get_attr nodes across all subgraphs. 

365 

366 ``backward=False`` yields forward nodes, ``backward=True`` backward ones 

367 (tagged by the autograd engine's ``autograd_backward`` meta). 

368 """ 

369 for submod in fx_g.modules(): 

370 if not isinstance(submod, torch.fx.GraphModule): 

371 continue 

372 for node in submod.graph.nodes: 

373 if ( 

374 node.op not in ("call_function", "get_attr") 

375 or "seq_nr" not in node.meta 

376 or node.meta.get("autograd_backward", False) != backward 

377 ): 

378 continue 

379 yield node 

380 

381 

382def _copy_fwd_meta(fwd_node: torch.fx.Node, node: torch.fx.Node) -> None: 

383 """Copy ``custom``/``nn_module_stack``/``stack_trace`` from fwd to bwd.""" 

384 custom = fwd_node.meta.get("custom") 

385 if custom: 

386 node.meta.setdefault("custom", {}).update(copy.deepcopy(custom)) 

387 nn_module_stack = fwd_node.meta.get("nn_module_stack") 

388 if nn_module_stack is not None: 

389 node.meta["nn_module_stack"] = nn_module_stack.copy() 

390 stack_trace = fwd_node.meta.get("stack_trace") 

391 if stack_trace is not None: 

392 node.meta["stack_trace"] = stack_trace 

393 

394 

395def _fakeify_input(fake_mode: FakeTensorMode, x: Any) -> Any: 

396 """Convert a real tensor input into its fake counterpart.""" 

397 if not isinstance(x, torch.Tensor): 

398 return x 

399 return fake_mode.from_tensor(x, static_shapes=False) 

400 

401 

402def _input_meta(x: Any) -> Any: 

403 """Return a storage-free descriptor of a traced user input. 

404 

405 Tensors become shape/dtype metadata; primitives pass through; pytree 

406 containers (list/tuple/dict) are converted recursively. Used for 

407 ``JointGraph.inputs`` so the joint graph never pins the real sample 

408 tensors' storage for the lifetime of training. 

409 """ 

410 if isinstance(x, torch.Tensor): 

411 return {"shape": tuple(x.shape), "dtype": str(x.dtype)} 

412 if isinstance(x, tuple): 

413 return tuple(_input_meta(v) for v in x) 

414 if isinstance(x, list): 

415 return [_input_meta(v) for v in x] 

416 if isinstance(x, dict): 

417 return {k: _input_meta(v) for k, v in x.items()} 

418 return x 

419 

420 

421def _validate_trace_leaves(leaves: list[Any]) -> None: 

422 """Reject state or input objects that cannot become explicit FX inputs.""" 

423 for leaf in leaves: 

424 if isinstance(leaf, nn.Module): 

425 raise ValueError( 

426 "trace_model_graph requires explicit tensor state, not " 

427 "nn.Module instances. Capture nn.Modules in train_fn's closure." 

428 ) 

429 if not isinstance(leaf, _ALLOWED_LEAF_TYPES): 

430 raise ValueError( 

431 "trace_model_graph requires all pytree leaves in " 

432 f"state/args to be tensors or primitives (int/float/bool/str), " 

433 f"got {type(leaf).__name__}." 

434 ) 

435 

436 

437def _make_fwd_bwd_fn( 

438 model: nn.Module, 

439 train_fn: Callable, 

440 state_fqns: list[str], 

441 state_is_param: list[bool], 

442 state_spec: Any, 

443 user_inputs_spec: Any, 

444) -> tuple[Callable, list[str]]: 

445 """Build the joint callable and its named-loss output metadata.""" 

446 num_state_inputs = len(state_fqns) 

447 # ``_fwd_bwd_fn`` communicates the traced ``loss_dict`` key order to the 

448 # enclosing scope through this box (plain closure writes cannot escape 

449 # the nested ``def``); the keys are attached to the GraphModule below. 

450 loss_dict_keys_box: list[str] = [] 

451 

452 def _fwd_bwd_fn(*plain_args): 

453 state_wrapped = plain_args[:num_state_inputs] 

454 user_wrapped = plain_args[num_state_inputs:] 

455 state_t = torch.utils._pytree.tree_unflatten(list(state_wrapped), state_spec) 

456 user_args = torch.utils._pytree.tree_unflatten( 

457 list(user_wrapped), user_inputs_spec 

458 ) 

459 

460 with ( 

461 _reparametrize_train_state(model, state_t["model"]), 

462 _patch_engine_backward(), 

463 ): 

464 out = train_fn(model, **user_args) 

465 

466 # ``train_fn`` may return either a bare loss tensor or the 

467 # ``(loss, loss_dict)`` pair produced by the trainer's 

468 # ``postforward``. The named loss_dict values are emitted as 

469 # graph outputs AFTER the gradients so passes that assume 

470 # ``output[0] == loss`` and ``output[1:] == grads`` keep working 

471 # once they trim the trailing loss_dict outputs. 

472 if ( 

473 isinstance(out, tuple) 

474 and len(out) == 2 

475 and isinstance(out[1], dict) 

476 ): 

477 loss, loss_dict = out 

478 loss_dict_keys = list(loss_dict.keys()) 

479 loss_dict_keys_box.extend(loss_dict_keys) 

480 loss_dict_values = [loss_dict[key] for key in loss_dict_keys] 

481 else: 

482 loss, loss_dict_keys, loss_dict_values = out, [], [] 

483 

484 # Read params from state_t["model"] so the exact trace-time 

485 # tensors (not the live module's) drive autograd.grad. 

486 params = [] 

487 for idx, fqn in enumerate(state_fqns): 

488 if state_is_param[idx]: 

489 param = state_t["model"][fqn] 

490 if param.requires_grad: 

491 params.append(param) 

492 

493 grads = torch.autograd.grad(loss, params, allow_unused=True) 

494 

495 # Replace None grads with zeros so the output arity is stable. 

496 processed_grads = [] 

497 for grad, param in zip(grads, params): 

498 if grad is None: 

499 processed_grads.append(torch.zeros_like(param)) 

500 else: 

501 processed_grads.append(grad) 

502 

503 return [loss] + processed_grads + loss_dict_values 

504 

505 return _fwd_bwd_fn, loss_dict_keys_box 

506 

507 

508def trace_model_graph( # pylint: disable=too-many-locals 

509 model: torch.nn.Module, 

510 train_fn: Callable, 

511 inputs: Dict[str, Any], 

512 *, 

513 dynamic: bool = False, 

514 dynamic_arg_dims: dict[str, int | list[int]] | None = None, 

515) -> JointGraph: 

516 """ 

517 Trace model to generate complete forward + backward graph 

518 

519 Args: 

520 model: Model (no parallel wrapping) 

521 train_fn: Training function signature: 

522 ``train_fn(model, **inputs) -> loss`` 

523 inputs: Model inputs, forwarded to ``train_fn`` as keyword arguments 

524 dynamic: Symbolize all user tensor dimensions when no explicit mapping is supplied. 

525 dynamic_arg_dims: Dotted input paths to dynamic dimensions (supports negative indices). 

526 Providing a mapping enables selective symbolic tracing, even when dynamic is False. 

527 Parameters and buffers always remain static. 

528 

529 Returns: 

530 JointGraph: Joint forward-backward computation graph 

531 

532 Note: 

533 Static tracing remains the default. Dynamic traces retain shape guards; 

534 incompatible shapes or shape-dependent branches raise before execution. 

535 """ 

536 # Extract module state (parameters/buffers) into flat tensors threaded 

537 # through the graph as static inputs (leading placeholders). 

538 model_state = extract_module_state(model) 

539 state_fqns = list(model_state.keys()) 

540 # Which leading state inputs are parameters (vs buffers). FSDP shards 

541 # parameters only: buffers -- e.g. the non-persistent RoPE ``cache`` -- 

542 # are full-rank by nature and must flow through as plain inputs, never 

543 # all-gathered. ``state_is_param`` is indexed by state position, aligned 

544 # with ``state_fqns``. 

545 param_fqns = set(dict(model.named_parameters(remove_duplicate=False)).keys()) 

546 state_is_param = [fqn in param_fqns for fqn in state_fqns] 

547 state_flat, state_spec = torch.utils._pytree.tree_flatten({"model": model_state}) 

548 

549 # user_inputs is a plain dict so the traced closure unpacks it back into 

550 # train_fn's keyword arguments. 

551 user_inputs = inputs 

552 user_inputs_flat, user_inputs_spec = torch.utils._pytree.tree_flatten(user_inputs) 

553 

554 _validate_trace_leaves([*state_flat, *user_inputs_flat]) 

555 

556 full_args = list(state_flat) + list(user_inputs_flat) 

557 

558 if not isinstance(dynamic, bool): 

559 raise ValueError("dynamic must be a bool") 

560 symbolic = dynamic or dynamic_arg_dims is not None 

561 if symbolic: 

562 fake_mode, fake_args = build_symbolic_inputs(state_flat, inputs, dynamic_arg_dims) 

563 else: 

564 fake_mode = FakeTensorMode(allow_non_fake_inputs=True) 

565 fake_args = tuple(_fakeify_input(fake_mode, value) for value in full_args) 

566 num_state_inputs = len(state_flat) 

567 

568 _fwd_bwd_fn, loss_dict_keys_box = _make_fwd_bwd_fn( # pylint: disable=C0103 

569 model, train_fn, state_fqns, state_is_param, state_spec, user_inputs_spec 

570 ) 

571 

572 # make_fx only records ``nn_module_stack`` when the traced callable 

573 # carries ``_orig_mod``: its ``_init_modes_from_inputs`` then installs a 

574 # ``_ModuleStackTracer`` that captures which module's ``forward`` was 

575 # executing as each node is created. Forward nodes thereby gain the 

576 # module-stack metadata ``PpPass._stage_from_stack`` needs to map a node 

577 # to its pipeline stage. Backward nodes are annotated separately (see 

578 # ``_annotate_autograd_backward``), after tracing. 

579 _fwd_bwd_fn._orig_mod = model # type: ignore[attr-defined] 

580 

581 ctx = TracingContext(fake_mode) 

582 with ( 

583 fake_mode, 

584 tracing(ctx), 

585 preserve_node_meta(), 

586 _skip_nested_compile(), 

587 torch.autograd.set_multithreading_enabled(False), 

588 _non_strict_tracing_context(), 

589 ): 

590 # Native constant padding can specialize SymInt sizes (notably on 

591 # NPU). Its upstream decomposition preserves causal-LM label lengths. 

592 decompositions = get_decompositions([torch.ops.aten.constant_pad_nd.default]) if symbolic else None 

593 traced_graph = make_fx( 

594 _fwd_bwd_fn, decomposition_table=decompositions, **_MAKE_FX_KWARGS 

595 )(*fake_args) 

596 

597 # Stock torch's make_fx does not tag backward nodes (no torchtitan 

598 # ``_patch_engine_backward`` hook), so the joint graph splits into 

599 # fwd/bwd halves with no ``autograd_backward`` meta to key on. 

600 # ``_annotate_autograd_backward`` reconstructs the tag structurally so 

601 # ``PpPass`` can classify both halves. 

602 _annotate_autograd_backward(traced_graph) 

603 

604 # Copy forward metadata (nn_module_stack/stack_trace) to backward nodes so 

605 # passes can match nodes by module FQN on both halves of the joint graph. 

606 # On torchtitan's patched torch this is the primary source (the engine 

607 # already tagged backward nodes); on stock torch it is a no-op because 

608 # backward nodes carry no ``seq_nr`` that collides with a forward one. 

609 _copy_fwd_metadata_to_bw_nodes(traced_graph) 

610 

611 # Expose the state layout on the GraphModule so downstream passes can 

612 # identify which leading placeholders are parameters/buffers, and the 

613 # trainer can feed runtime state in the same order. 

614 traced_graph.state_fqns = state_fqns 

615 traced_graph.state_is_param = state_is_param 

616 traced_graph.num_state_inputs = num_state_inputs 

617 # Kept for ``run_traced_graph`` to validate the runtime inputs' pytree 

618 # structure against the traced one. 

619 traced_graph.user_inputs_spec = user_inputs_spec 

620 # Key order of the traced ``loss_dict``: its values are emitted as 

621 # graph outputs AFTER the gradients, so ``run_traced_graph`` can split 

622 # (loss, grads, loss_dict) and passes can trim the trailing loss_dict 

623 # outputs off the gradient range. 

624 traced_graph.loss_dict_keys = list(loss_dict_keys_box) 

625 

626 param_names = [name for name, _ in model.named_parameters() if _.requires_grad] 

627 param_shapes = { 

628 name: tuple(param.shape) for name, param in model.named_parameters() 

629 } 

630 

631 num_layers = 0 

632 if hasattr(model, "layers"): 

633 num_layers = len(model.layers) 

634 

635 return JointGraph( 

636 graph_module=traced_graph, 

637 inputs=[_input_meta(v) for v in inputs.values()], 

638 outputs=[], 

639 param_names=param_names, 

640 param_shapes=param_shapes, 

641 num_layers=num_layers, 

642 state_fqns=state_fqns, 

643 example_inputs=fake_args, 

644 input_guards=( 

645 InputGuards(user_inputs_flat, fake_args[num_state_inputs:], fake_mode.shape_env) 

646 if symbolic else None 

647 ), 

648 ) 

649 

650 

651def run_traced_graph( 

652 joint_graph: JointGraph, 

653 model: torch.nn.Module, 

654 inputs: Dict[str, Any], 

655) -> tuple: 

656 """ 

657 Execute a traced joint graph against the live model state. 

658 

659 Parameters/buffers are sampled from ``model`` at call time and fed to the 

660 graph as static inputs (in ``joint_graph.state_fqns`` order). ``inputs`` 

661 is flattened with the same pytree structure used at trace time (checked 

662 against the stored spec) and appended after the state. Runs under 

663 ``torch.no_grad()`` because the graph already contains the explicit 

664 backward ops traced by ``torch.autograd.grad``. 

665 

666 FSDP is invisible here: FSDPPass shards ``model``'s parameters in place, 

667 so ``model.parameters()`` yields the shards the graph expects as its 

668 leading static inputs (the graph re-gathers them via AllGather each step). 

669 

670 Returns: 

671 tuple: (loss, grads, loss_dict) where ``grads`` aligns with the 

672 model's trainable parameter list and ``loss_dict`` maps the traced 

673 loss names to their graph outputs (empty when ``train_fn`` returned 

674 a bare loss tensor). 

675 """ 

676 model_state = extract_module_state(model) 

677 if list(model_state.keys()) != joint_graph.state_fqns: 

678 raise ValueError( 

679 "model has different parameter/buffer names than during tracing.\n" 

680 f" Traced: {joint_graph.state_fqns}\n" 

681 f" Got: {list(model_state.keys())}" 

682 ) 

683 

684 user_flat, user_spec = torch.utils._pytree.tree_flatten(inputs) 

685 traced_spec = getattr(joint_graph.graph_module, "user_inputs_spec", None) 

686 if traced_spec is not None and user_spec != traced_spec: 

687 raise ValueError( 

688 "model inputs have a different pytree structure than during " 

689 "tracing (keys, nesting, or leaf types changed).\n" 

690 f" Traced spec: {traced_spec}\n" 

691 f" Got spec: {user_spec}" 

692 ) 

693 

694 state_flat, _ = torch.utils._pytree.tree_flatten({"model": model_state}) 

695 

696 if joint_graph.input_guards is not None: 

697 joint_graph.input_guards.validate(user_flat) 

698 

699 flat_inputs = list(state_flat) + list(user_flat) 

700 

701 with torch.no_grad(): 

702 outputs = joint_graph.graph_module(*flat_inputs) 

703 

704 loss_dict_keys = getattr(joint_graph.graph_module, "loss_dict_keys", []) 

705 num_loss_outputs = len(loss_dict_keys) 

706 if isinstance(outputs, (list, tuple)): 

707 loss = outputs[0] 

708 loss_dict = ( 

709 dict(zip(loss_dict_keys, outputs[len(outputs) - num_loss_outputs:])) 

710 if num_loss_outputs 

711 else {} 

712 ) 

713 grads = ( 

714 list(outputs[1:len(outputs) - num_loss_outputs]) 

715 if num_loss_outputs 

716 else list(outputs[1:]) 

717 ) 

718 else: 

719 loss, grads, loss_dict = outputs, [], {} 

720 return loss, grads, loss_dict