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

267 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +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.experimental.symbolic_shapes import ShapeEnv 

73from torch.fx.passes.runtime_assert import insert_deferred_runtime_asserts 

74from torch.fx.traceback import preserve_node_meta 

75from torch.nn.utils import stateless 

76 

77from .dynamic_shapes import InputGuards, build_symbolic_inputs 

78 

79# Older torch's make_fx predates record_stack_traces / record_module_stack 

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

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

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

83try: 

84 _MAKE_FX_PARAMS = inspect.signature(make_fx).parameters 

85except (ValueError, TypeError): 

86 _MAKE_FX_PARAMS = {} 

87_MAKE_FX_KWARGS: Dict[str, Any] = { 

88 kw: val 

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

90 if kw in _MAKE_FX_PARAMS 

91} 

92 

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

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

95# train_fn closure or registered as pytree constants. 

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

97 

98 

99@contextmanager 

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

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

102 

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

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

105 dynamo silently inline the wrapped function instead of raising. 

106 """ 

107 prev = torch._dynamo.config.error_on_nested_fx_trace 

108 torch._dynamo.config.error_on_nested_fx_trace = False 

109 try: 

110 yield 

111 finally: 

112 torch._dynamo.config.error_on_nested_fx_trace = prev 

113 

114 

115@contextmanager 

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

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

118 

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

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

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

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

123 not supported at all. 

124 """ 

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

126 if patcher is None: 

127 warnings.warn( 

128 "torch.compiler._patch_engine_backward not available. " 

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

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

131 RuntimeWarning, 

132 stacklevel=2, 

133 ) 

134 yield 

135 else: 

136 with patcher(): 

137 yield 

138 

139 

140@contextmanager 

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

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

143 

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

145 """ 

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

147 if ctx is None: 

148 yield 

149 else: 

150 with ctx(): 

151 yield 

152 

153 

154@dataclass 

155class JointGraph: 

156 """ 

157 Joint forward-backward computation graph 

158 

159 Features: 

160 - No parallel information 

161 - Parameters, activations, gradients are all complete tensors 

162 - Forward + Loss + Backward complete joint graph 

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

164 not ``get_attr`` nodes 

165 """ 

166 

167 graph_module: torch.fx.GraphModule 

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

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

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

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

172 inputs: List[Any] 

173 outputs: List[Any] 

174 param_names: List[str] 

175 param_shapes: Dict[str, tuple] 

176 num_layers: int 

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

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

179 # runtime parameter state into the graph. 

180 state_fqns: List[str] 

181 example_inputs: tuple 

182 input_guards: InputGuards | None = None 

183 

184 

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

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

187 

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

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

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

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

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

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

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

195 """ 

196 return { 

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

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

199 } 

200 

201 

202@contextlib.contextmanager 

203def _reparametrize_train_state( 

204 module: nn.Module, 

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

206): 

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

208 

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

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

211 

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

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

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

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

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

217 """ 

218 with contextlib.ExitStack() as stack: 

219 stack.enter_context( 

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

221 ) 

222 yield 

223 

224 

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

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

227 for a in node.args: 

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

229 yield a 

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

231 for x in a: 

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

233 yield x 

234 

235 

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

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

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

239 if out_node is None: 

240 return [] 

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

242 for a in out_node.args: 

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

244 args.append(a) 

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

246 for x in a: 

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

248 args.append(x) 

249 return args 

250 

251 

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

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

254 ancestors = set() 

255 dq = deque([node]) 

256 while dq: 

257 cur = dq.popleft() 

258 for a in _iter_node_args(cur): 

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

260 continue 

261 ancestors.add(a) 

262 dq.append(a) 

263 return ancestors 

264 

265 

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

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

268 

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

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

271 the robust test. 

272 """ 

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

274 

275 

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

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

278 

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

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

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

282 on. This reconstructs the tag structurally. 

283 

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

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

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

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

288 produced by ``torch.autograd.grad``. 

289 

290 Two pre-computed sets separate the halves: 

291 

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

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

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

295 computed *from* them. 

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

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

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

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

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

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

302 """ 

303 output = _output_args(fx_g) 

304 if not output: 

305 return 

306 loss_node = output[0] 

307 grad_seeds = output[1:] 

308 

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

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

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

312 fwd_loss_nodes = _ancestors_of(loss_node) 

313 fwd_loss_nodes.add(loss_node) 

314 

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

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

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

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

319 bwd_nodes = set() 

320 dq = deque() 

321 for seed in grad_seeds: 

322 if ( 

323 not _has_module_stack(seed) 

324 and seed.op != "placeholder" 

325 and seed not in fwd_loss_nodes 

326 ): 

327 bwd_nodes.add(seed) 

328 dq.append(seed) 

329 while dq: 

330 n = dq.popleft() 

331 for a in _iter_node_args(n): 

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

333 continue 

334 if a in fwd_loss_nodes: 

335 continue # forward loss source; do not expand 

336 if _has_module_stack(a): 

337 continue # forward module boundary; do not expand 

338 bwd_nodes.add(a) 

339 dq.append(a) 

340 

341 for node in bwd_nodes: 

342 node.meta["autograd_backward"] = True 

343 

344 

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

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

347 

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

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

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

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

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

353 """ 

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

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

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

357 

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

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

360 if fwd_node is None or fwd_node is node: 

361 continue 

362 _copy_fwd_meta(fwd_node, node) 

363 

364 

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

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

367 

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

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

370 """ 

371 for submod in fx_g.modules(): 

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

373 continue 

374 for node in submod.graph.nodes: 

375 if ( 

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

377 or "seq_nr" not in node.meta 

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

379 ): 

380 continue 

381 yield node 

382 

383 

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

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

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

387 if custom: 

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

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

390 if nn_module_stack is not None: 

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

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

393 if stack_trace is not None: 

394 node.meta["stack_trace"] = stack_trace 

395 

396 

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

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

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

400 return x 

401 return fake_mode.from_tensor(x, static_shapes=True) 

402 

403 

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

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

406 

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

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

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

410 tensors' storage for the lifetime of training. 

411 """ 

412 if isinstance(x, torch.Tensor): 

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

414 if isinstance(x, tuple): 

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

416 if isinstance(x, list): 

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

418 if isinstance(x, dict): 

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

420 return x 

421 

422 

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

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

425 for leaf in leaves: 

426 if isinstance(leaf, nn.Module): 

427 raise ValueError( 

428 "trace_model_graph requires explicit tensor state, not " 

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

430 ) 

431 if not isinstance(leaf, _ALLOWED_LEAF_TYPES): 

432 raise ValueError( 

433 "trace_model_graph requires all pytree leaves in " 

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

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

436 ) 

437 

438 

439def _make_fwd_bwd_fn( 

440 model: nn.Module, 

441 train_fn: Callable, 

442 state_fqns: list[str], 

443 state_is_param: list[bool], 

444 state_spec: Any, 

445 user_inputs_spec: Any, 

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

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

448 num_state_inputs = len(state_fqns) 

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

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

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

452 loss_dict_keys_box: list[str] = [] 

453 

454 def _fwd_bwd_fn(*plain_args): 

455 state_wrapped = plain_args[:num_state_inputs] 

456 user_wrapped = plain_args[num_state_inputs:] 

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

458 user_args = torch.utils._pytree.tree_unflatten( 

459 list(user_wrapped), user_inputs_spec 

460 ) 

461 

462 with ( 

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

464 _patch_engine_backward(), 

465 ): 

466 out = train_fn(model, **user_args) 

467 

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

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

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

471 # graph outputs AFTER the gradients so passes that assume 

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

473 # once they trim the trailing loss_dict outputs. 

474 if ( 

475 isinstance(out, tuple) 

476 and len(out) == 2 

477 and isinstance(out[1], dict) 

478 ): 

479 loss, loss_dict = out 

480 loss_dict_keys = list(loss_dict.keys()) 

481 loss_dict_keys_box.extend(loss_dict_keys) 

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

483 else: 

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

485 

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

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

488 params = [] 

489 for idx, fqn in enumerate(state_fqns): 

490 if state_is_param[idx]: 

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

492 if param.requires_grad: 

493 params.append(param) 

494 

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

496 

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

498 processed_grads = [] 

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

500 if grad is None: 

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

502 else: 

503 processed_grads.append(grad) 

504 

505 return [loss] + processed_grads + loss_dict_values 

506 

507 return _fwd_bwd_fn, loss_dict_keys_box 

508 

509 

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

511 model: torch.nn.Module, 

512 train_fn: Callable, 

513 inputs: Dict[str, Any], 

514 *, 

515 dynamic: bool = False, 

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

517) -> JointGraph: 

518 """ 

519 Trace model to generate complete forward + backward graph 

520 

521 Args: 

522 model: Model (no parallel wrapping) 

523 train_fn: Training function signature: 

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

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

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

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

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

529 Parameters and buffers always remain static. 

530 

531 Returns: 

532 JointGraph: Joint forward-backward computation graph 

533 

534 Note: 

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

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

537 """ 

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

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

540 model_state = extract_module_state(model) 

541 state_fqns = list(model_state.keys()) 

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

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

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

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

546 # with ``state_fqns``. 

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

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

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

550 

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

552 # train_fn's keyword arguments. 

553 user_inputs = inputs 

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

555 

556 _validate_trace_leaves([*state_flat, *user_inputs_flat]) 

557 

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

559 

560 if not isinstance(dynamic, bool): 

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

562 symbolic = dynamic or dynamic_arg_dims is not None 

563 if symbolic: 

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

565 else: 

566 fake_mode = FakeTensorMode(allow_non_fake_inputs=True, shape_env=ShapeEnv(), static_shapes=True) 

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

568 num_state_inputs = len(state_flat) 

569 

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

571 model, train_fn, state_fqns, state_is_param, state_spec, user_inputs_spec 

572 ) 

573 

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

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

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

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

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

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

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

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

582 

583 ctx = TracingContext(fake_mode) 

584 with ( 

585 fake_mode, 

586 tracing(ctx), 

587 preserve_node_meta(), 

588 _skip_nested_compile(), 

589 torch.autograd.set_multithreading_enabled(False), 

590 _non_strict_tracing_context(), 

591 ): 

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

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

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

595 traced_graph = make_fx( 

596 _fwd_bwd_fn, decomposition_table=decompositions, **_MAKE_FX_KWARGS 

597 )(*fake_args) 

598 

599 # EP receive lengths depend on routing values even when input shapes are fixed. 

600 insert_deferred_runtime_asserts(traced_graph, fake_mode.shape_env, "joint_graph") 

601 traced_graph.recompile() 

602 

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

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

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

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

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

608 _annotate_autograd_backward(traced_graph) 

609 

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

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

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

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

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

615 _copy_fwd_metadata_to_bw_nodes(traced_graph) 

616 

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

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

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

620 traced_graph.state_fqns = state_fqns 

621 traced_graph.state_is_param = state_is_param 

622 traced_graph.num_state_inputs = num_state_inputs 

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

624 # structure against the traced one. 

625 traced_graph.user_inputs_spec = user_inputs_spec 

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

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

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

629 # outputs off the gradient range. 

630 traced_graph.loss_dict_keys = list(loss_dict_keys_box) 

631 

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

633 param_shapes = { 

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

635 } 

636 

637 num_layers = 0 

638 if hasattr(model, "layers"): 

639 num_layers = len(model.layers) 

640 

641 return JointGraph( 

642 graph_module=traced_graph, 

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

644 outputs=[], 

645 param_names=param_names, 

646 param_shapes=param_shapes, 

647 num_layers=num_layers, 

648 state_fqns=state_fqns, 

649 example_inputs=fake_args, 

650 input_guards=( 

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

652 if symbolic else None 

653 ), 

654 ) 

655 

656 

657def run_traced_graph( 

658 joint_graph: JointGraph, 

659 model: torch.nn.Module, 

660 inputs: Dict[str, Any], 

661) -> tuple: 

662 """ 

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

664 

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

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

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

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

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

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

671 

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

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

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

675 

676 Returns: 

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

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

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

680 a bare loss tensor). 

681 """ 

682 model_state = extract_module_state(model) 

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

684 raise ValueError( 

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

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

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

688 ) 

689 

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

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

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

693 raise ValueError( 

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

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

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

697 f" Got spec: {user_spec}" 

698 ) 

699 

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

701 

702 if joint_graph.input_guards is not None: 

703 joint_graph.input_guards.validate(user_flat) 

704 

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

706 

707 with torch.no_grad(): 

708 outputs = joint_graph.graph_module(*flat_inputs) 

709 

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

711 num_loss_outputs = len(loss_dict_keys) 

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

713 loss = outputs[0] 

714 loss_dict = ( 

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

716 if num_loss_outputs 

717 else {} 

718 ) 

719 grads = ( 

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

721 if num_loss_outputs 

722 else list(outputs[1:]) 

723 ) 

724 else: 

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

726 return loss, grads, loss_dict