Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / compile / passes / parallel / fsdp_pass.py: 95%

635 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""" 

16FSDP / DDP / HSDP Pass - Data Parallel Partitioning Pass 

17 

18Operates on the joint fwd+bwd graph produced by the tracer, where 

19parameters/buffers are **static inputs** (leading placeholders), not get_attr 

20nodes. The data-parallel mode comes from ``PassConfig.dp_mode`` and mirrors 

21simplefsdp's ``data_parallel`` modes: 

22 

23- ``"fsdp"`` (fully_shard): parameters sharded on dim 0; forward all_gather, 

24 backward reduce_scatter. 

25- ``"ddp"`` (replicate): parameters stay replicated; no all_gather, gradients 

26 are all-reduced on the ``dp_replicate`` axis. 

27- ``"hsdp"`` (hybrid_shard): parameters replicated on ``dp_replicate`` and 

28 sharded on ``fsdp``; forward all_gather on ``fsdp``, backward reduce_scatter 

29 on ``fsdp`` then all_reduce on ``dp_replicate``. 

30 

31Responsibilities: 

321. Identify parameter placeholders belonging to DP-wrapped modules 

33 (via GraphParallelPlan, exact FQN or pattern) 

342. Insert AllGather after each such placeholder (Shard -> Replicate), so the 

35 computation body operates on full parameters while the graph input stays 

36 sharded (skipped for ``"ddp"``) 

373. Insert gradient reduction on the gradient outputs of DP parameters — 

38 reduce_scatter (FSDP/HSDP) and/or all_reduce (DDP/HSDP) 

394. Physically shard the *live model's* parameters in place (dim 0, by FSDP 

40 rank) for FSDP/HSDP, so ``model.parameters()`` already holds the local 

41 shard and the trainer / optimizer need no DP awareness at all 

425. Reshard (``fsdp_reshard_after_forward``): sink each AllGather to its first 

43 forward use, free the replicated parameter after its last forward read, 

44 and, for parameters the backward still needs, re-gather plus rematerialize 

45 the saved view chain just before the first backward consumer. Peak memory 

46 tracks the forward working set instead of every replicated parameter. 

47 

48All DP logic (which parameters, the collectives, and the sharding itself) 

49lives in this pass; the trainer simply feeds ``model.parameters()``. 

50""" 

51 

52__all__ = ["FSDPPass"] 

53 

54import logging 

55import operator 

56from contextlib import contextmanager 

57from dataclasses import dataclass 

58from typing import Any, Dict, Iterator, List, Optional, Set, Tuple 

59 

60import torch 

61import torch.distributed as dist 

62from torch import fx, nn 

63from torch._subclasses import FakeTensor 

64from torch.distributed.distributed_c10d import _resolve_process_group 

65from torch.fx.node import _side_effectful_functions 

66from torch.ops import _c10d_functional 

67 

68from ...graph_parallel_plan import GraphParallelPlan 

69from ...pass_config import GradientReduction, PassConfig, normalize_dp_mode 

70from ..base import GraphPass 

71from ..overlap.utils import rs_layout_preparation 

72 

73_LOG = logging.getLogger(__name__) 

74 

75_GETITEM = operator.getitem 

76_DEFAULT_GRADIENT_REDUCTION = GradientReduction() 

77 

78 

79def tag_gradient_identity(wait: fx.Node, param_name: str, state_idx: int) -> None: 

80 """Attach the known output-to-parameter identity to a reduction chain. 

81 

82 Args: 

83 wait: Final waited gradient returned by FSDPPass. 

84 param_name: Canonical traced parameter FQN. 

85 state_idx: Position in the traced state inputs. 

86 """ 

87 node = wait 

88 while node.op == "call_function" and str(node.target).startswith( 

89 "_c10d_functional." 

90 ): 

91 node.meta.update(param_name=param_name, state_idx=state_idx) 

92 if not node.args or not isinstance(node.args[0], fx.Node): 

93 break 

94 node = node.args[0] 

95 

96 

97def _annotate_rs_spec(node: fx.Node, degree: int) -> None: 

98 """Attach packed input/output shapes to one reduce-scatter node.""" 

99 source = node.args[0] 

100 preparation = rs_layout_preparation(node) 

101 if preparation: 

102 # Contiguous changes strides, not the padded shape/dtype/device. 

103 source = preparation[0].args[0] 

104 value = source.meta.get("val") 

105 spec = source.meta.get("fsdp_padded_input_spec") 

106 if spec is None and value is not None and hasattr(value, "shape"): 

107 spec = { 

108 "shape": tuple(value.shape), 

109 "dtype": value.dtype, 

110 "device": str(value.device), 

111 } 

112 if spec is None: 

113 return 

114 shape = spec["shape"] 

115 static = shape and all( 

116 isinstance(dim, int) and not isinstance(dim, bool) and dim > 0 for dim in shape 

117 ) 

118 if not static or shape[0] % degree: 

119 return 

120 node.meta["fsdp_bucket_spec"] = { 

121 "input_shape": tuple(shape), 

122 "output_shape": (shape[0] // degree, *shape[1:]), 

123 "dtype": spec["dtype"], 

124 "device": spec["device"], 

125 } 

126 

127 

128def _annotate_ag_spec(node: fx.Node, degree: int, params: dict) -> None: 

129 """Attach shard/full shapes of one all-gather node from its parameter.""" 

130 param = params.get(node.meta.get("param_name")) 

131 if param is None: 

132 return 

133 shape = tuple(param.shape) 

134 if not shape or any( 

135 not isinstance(dim, int) or isinstance(dim, bool) or dim <= 0 for dim in shape 

136 ): 

137 return 

138 node.meta["fsdp_bucket_spec"] = { 

139 "input_shape": shape, 

140 "output_shape": (shape[0] * degree, *shape[1:]), 

141 "dtype": param.dtype, 

142 "device": str(param.device), 

143 } 

144 

145 

146def annotate_fsdp_bucket_specs(graph_module: fx.GraphModule, model: nn.Module) -> None: 

147 """Record actual padded shard shapes after the live model has been sharded. 

148 

149 Args: 

150 graph_module: Graph transformed by FSDPPass. 

151 model: Live model whose parameters now have their rank-local shard shapes. 

152 """ 

153 params = dict(model.named_parameters()) 

154 for node in graph_module.graph.nodes: 

155 kind = node.meta.get("comm_type") 

156 if kind not in ( 

157 "fsdp_all_gather", 

158 "fsdp_all_gather_backward", 

159 "fsdp_reduce_scatter", 

160 ): 

161 continue 

162 degree = node.meta.get("fsdp_degree") 

163 if (not isinstance(degree, int) or isinstance(degree, bool)) or degree <= 1: 

164 continue 

165 if kind == "fsdp_reduce_scatter": 

166 _annotate_rs_spec(node, degree) 

167 else: 

168 _annotate_ag_spec(node, degree, params) 

169 

170 

171def _free_tensor_storage(tensor: torch.Tensor) -> None: 

172 """Release ``tensor``'s storage in place (FSDP reshard free step). 

173 

174 Inserted as a side-effecting FX node between an unsharded parameter's last 

175 forward reader and its backward re-gather, so the replicated full 

176 parameter (and every view aliasing it) does not stay resident for the 

177 whole joint graph. Returns ``None``; the node exists only for its effect. 

178 Mirrors ``fully_shard``'s ``free_unsharded_param`` storage release. 

179 """ 

180 # FakeTensorProp (InductorPass shape propagation) executes this node on 

181 # fake values whose storage cannot be resized; there is nothing to free. 

182 if isinstance(tensor, FakeTensor): 

183 return 

184 if isinstance(tensor, torch.Tensor) and tensor.untyped_storage().size() > 0: 

185 tensor.untyped_storage().resize_(0) 

186 

187 

188# FX dead-code elimination drops call_function nodes it judges pure; our free 

189# op is pure to FX but has a real side effect. Register it so a later 

190# ``eliminate_dead_code`` (a future pass, or a user) cannot delete the free. 

191_side_effectful_functions.add(_free_tensor_storage) 

192 

193 

194@dataclass 

195class _UnshardedParam: 

196 """Forward all_gather that materializes one FSDP parameter. 

197 

198 ``param_node`` is the sharded placeholder (graph input); ``ag_node`` / 

199 ``wait_node`` produce and expose the replicated (unsharded) full parameter 

200 the forward — and, after a reshard re-gather, the backward — read. 

201 """ 

202 

203 param_node: fx.Node 

204 ag_node: fx.Node 

205 wait_node: fx.Node 

206 

207 

208@dataclass 

209class _DpParamClassification: 

210 """How DP handles each trainable parameter's gradient. 

211 

212 Produced by ``_classify_dp_params``: 

213 

214 - ``sharded_nodes``: marked, trainable parameter placeholders that get an 

215 AllGather (Shard -> Replicate) and a reduce_scatter gradient, and are 

216 physically sharded in the live model. Dim-0-divisible params are chunked 

217 directly; non-divisible ones are pad-sharded (see ``_PaddedParam``). 

218 - ``replicated_indices``: state indices of parameters kept replicated (never 

219 chunked) whose gradient is averaged over the DP group with ``all_reduce`` 

220 instead of reduce-scattered — a marked 0-dim scalar (cannot be sharded), or 

221 a partial-plan leftover that falls back to DDP. 

222 - ``unmarked_trainable``: ``(state_idx, fqn)`` for the partial-plan leftovers 

223 (a subset of ``replicated_indices``), kept only so the caller can log that 

224 the plan did not cover them. 

225 """ 

226 

227 sharded_nodes: List[fx.Node] 

228 replicated_indices: Set[int] 

229 unmarked_trainable: List[Tuple[int, str]] 

230 

231 

232@dataclass 

233class _PaddedParam: 

234 """A marked parameter that cannot be sharded evenly, pad-sharded instead. 

235 

236 Mirrors eager ``fully_shard`` (``core/fully_shard/hsdp_param.py``): dim 0 of 

237 ``n`` elements is zero-padded to ``n_pad = ceil(n / degree) * degree`` so 

238 every rank owns a uniform ``n_pad / degree``-element shard. The graph 

239 narrows the gathered ``[n_pad, ...]`` back to ``[n, ...]`` for the compute, 

240 and the backward pads the ``[n, ...]`` gradient back to ``[n_pad, ...]`` 

241 before reduce_scatter, so the returned local gradient matches the padded 

242 shard (the padding region gets an all-zero gradient, hence never moves). 

243 """ 

244 

245 n: int 

246 n_pad: int 

247 dim0_pad: int 

248 ndim: int 

249 

250 

251@dataclass(frozen=True) 

252class _GradReductionInfo: 

253 """A parameter's selected mesh view and independent normalization policy.""" 

254 

255 group_name: str 

256 degree: int 

257 reduction: GradientReduction 

258 

259 

260def _iter_arg_nodes(node: fx.Node) -> Iterator[fx.Node]: 

261 """Yield every ``fx.Node`` in ``node.args`` / ``node.kwargs`` (nested).""" 

262 stack: List[Any] = list(node.args) + list(node.kwargs.values()) 

263 while stack: 

264 item = stack.pop() 

265 if isinstance(item, fx.Node): 

266 yield item 

267 elif isinstance(item, (tuple, list)): 

268 stack.extend(item) 

269 elif isinstance(item, dict): 

270 stack.extend(item.values()) 

271 

272 

273@contextmanager 

274def _insertion_point( 

275 graph: fx.Graph, anchor: Optional[fx.Node], fallback: fx.Node 

276) -> Iterator[None]: 

277 """Yield a graph insertion context. 

278 

279 Owns the ``anchor``-vs-``fallback`` choice so the gather-insertion body 

280 stays a single code path: when ``anchor`` is given, new nodes land right 

281 before it; otherwise they land right after ``fallback``. 

282 """ 

283 if anchor is None: 

284 with graph.inserting_after(fallback): 

285 yield 

286 else: 

287 with graph.inserting_before(anchor): 

288 yield 

289 

290 

291class FSDPPass(GraphPass): 

292 """ 

293 FSDP Partitioning Pass (Module-Level, static-input graph) 

294 

295 Responsibilities: 

296 1. Only shard parameters in modules marked for FSDP wrapping 

297 2. Insert AllGather after each FSDP parameter placeholder 

298 (Shard -> Replicate) 

299 3. Insert ReduceScatter after Backward (Replicate -> Shard) 

300 4. Physically shard the live model's parameters in place so the 

301 trainer / optimizer stay FSDP-agnostic 

302 

303 Key Difference from Old Implementation: 

304 - Old: shard all parameters via get_attr nodes and physically rewrite the 

305 GraphModule's stored tensors 

306 - New: parameters are static graph inputs; the pass inserts collectives 

307 into the graph and shards the live model's parameters in place 

308 """ 

309 

310 name = "fsdp_parallel" 

311 

312 def __init__( 

313 self, 

314 fsdp_group_name: Optional[str] = None, 

315 parallel_plan: Optional[GraphParallelPlan] = None, 

316 ) -> None: 

317 """Initialize FSDP pass state. 

318 

319 Args: 

320 fsdp_group_name: Process-group name registered for FSDP 

321 collectives. Defaults to ``"fsdp"``. May be overridden 

322 per-run via the ``fsdp_group_name`` kwarg in ``run``. 

323 parallel_plan: Declarative plan identifying which modules to 

324 shard. When ``None``, all parameters are sharded. 

325 """ 

326 super().__init__() 

327 self._fsdp_group_name = fsdp_group_name or "fsdp" 

328 self._parallel_plan = parallel_plan 

329 # Resolved from ``pass_config.fsdp_degree`` (or world_size) at 

330 # ``run`` entry; left as ``None`` here so a stray access before 

331 # ``run`` fails loudly instead of silently using a wrong default. 

332 self._fsdp_degree: Optional[int] = None 

333 self._expert_param_names: Set[str] = set() 

334 self._expert_fsdp_group_name = "fsdp_expert" 

335 self._expert_fsdp_degree: Optional[int] = None 

336 self._gradient_reductions: Dict[str, GradientReduction] = {} 

337 self._source_gradient_groups: Dict[str, str] = {} 

338 self._parameter_groups: Dict[str, tuple[str, int]] = {} 

339 # Data-parallel mode ("fsdp" | "ddp" | "hsdp") and the replicate-axis 

340 # group/degree used by DDP / HSDP gradient reduction. 

341 self._dp_mode = "fsdp" 

342 self._dp_replicate_group_name = "dp_replicate" 

343 self._dp_replicate_degree: Optional[int] = None 

344 self._processed_params: Set[str] = set() 

345 self._fsdp_modules: Set[str] = set() 

346 # Per-param forward all_gather record (the unsharded parameter), 

347 # populated by ``_insert_all_gather_for_params`` and consumed by the 

348 # reshard step. 

349 self._unsharded_params: Dict[str, _UnshardedParam] = {} 

350 # Non-divisible marked params keyed by FQN; populated by 

351 # ``_classify_dp_params`` and consumed by the all_gather / grad-reduce / 

352 # live-shard steps so they are pad-sharded like eager ``fully_shard``. 

353 self._padded_params: Dict[str, _PaddedParam] = {} 

354 

355 def run( 

356 self, 

357 graph_module: fx.GraphModule, 

358 pass_config: PassConfig, 

359 **kwargs: Any, 

360 ) -> fx.GraphModule: 

361 """Insert AllGather/ReduceScatter and shard the live model. 

362 

363 Args: 

364 graph_module: Joint fwd+bwd FX graph from the tracer. 

365 pass_config: Parallel configuration. ``fsdp_degree`` is 

366 read directly; ``None`` falls back to ``world_size`` (the 

367 FSDP-only path). A ``TypeError`` here means a non-Protocol 

368 config was passed — fix at the caller, do not paper over. 

369 **kwargs: Must include ``model`` (the live ``nn.Module``) and 

370 ``fsdp_group_name`` / ``parallel_plan`` as needed. 

371 

372 Returns: 

373 The transformed graph module. 

374 """ 

375 if not dist.is_initialized() or dist.get_world_size() == 1: 

376 _LOG.info("Skipped: distributed not initialized or world_size=1") 

377 return graph_module 

378 

379 model = self._setup_dp_config(pass_config, kwargs) 

380 

381 # Identify DP parameter placeholders. The joint graph's 

382 # parameters/buffers are static inputs (leading placeholders, not 

383 # get_attr nodes): locate them by position via the state layout the 

384 # tracer attached, then keep only those in DP-marked modules. For 

385 # FSDP/HSDP a marked param whose leading dim is not divisible by the 

386 # shard degree is pad-sharded (like eager fully_shard) so it is still 

387 # sharded and gradient-reduced; scalars stay replicated. DDP has no 

388 # shard axis so every marked parameter is reduced. 

389 state_fqns = getattr(graph_module, "state_fqns", []) 

390 num_state_inputs = getattr(graph_module, "num_state_inputs", 0) 

391 # Param-vs-buffer flag per leading state input. Absent on graphs 

392 # traced before this flag existed; default to all-params so old 

393 # traces keep the previous (parameter-only) behaviour. 

394 state_is_param = getattr(graph_module, "state_is_param", None) 

395 

396 self._reset_run_state() 

397 classification = self._classify_dp_params( 

398 graph_module, state_fqns, num_state_inputs, state_is_param, model 

399 ) 

400 param_nodes = classification.sharded_nodes 

401 graph_module.fsdp_sharded_param_names = frozenset( 

402 node.meta["param_name"] for node in param_nodes if self._dp_mode != "ddp" 

403 ) 

404 

405 _LOG.info( 

406 "Identified %d sharded (incl. %d pad-sharded) + %d replicated DP " 

407 "parameter nodes out of %d total state inputs", 

408 len(param_nodes), 

409 len(self._padded_params), 

410 len(classification.replicated_indices), 

411 num_state_inputs, 

412 ) 

413 

414 # A partial plan that leaves trainable params unmarked keeps their local 

415 # gradient (never reduced) — a silent divergence source across DP ranks, 

416 # so warn loudly before continuing. 

417 if classification.unmarked_trainable: 

418 self._warn_unmarked_trainable(classification.unmarked_trainable) 

419 

420 if not param_nodes and not classification.replicated_indices and not self._gradient_reductions: 

421 _LOG.warning( 

422 "No DP parameters found, check GraphParallelPlan or model structure" 

423 ) 

424 return graph_module 

425 

426 graph_module = self._apply_dp_transforms( 

427 graph_module, 

428 model, 

429 classification, 

430 pass_config, 

431 state_fqns, 

432 num_state_inputs, 

433 state_is_param, 

434 ) 

435 

436 _LOG.info( 

437 "Completed dp_mode=%s, processed %d parameters", 

438 self._dp_mode, 

439 len(param_nodes), 

440 ) 

441 

442 graph_module.recompile() 

443 return graph_module 

444 

445 def _setup_dp_config( 

446 self, pass_config: PassConfig, kwargs: Dict[str, Any] 

447 ) -> nn.Module: 

448 """Resolve run-time DP config, stash it on ``self``, return the model. 

449 

450 Reads ``dp_mode`` / ``fsdp_degree`` / ``dp_replicate_degree`` off 

451 ``pass_config`` (the last two fall back to ``world_size`` when unset) 

452 and the group names / ``parallel_plan`` / ``model`` off ``kwargs``. 

453 

454 Returns: 

455 The live ``nn.Module`` to physically shard. 

456 

457 Raises: 

458 ValueError: If ``model`` is missing from ``kwargs``. 

459 """ 

460 self._dp_mode = normalize_dp_mode(getattr(pass_config, "dp_mode", "fsdp")) 

461 # FSDP shard-axis group size: use the explicitly configured degree when 

462 # present (required for TP+FSDP / HSDP, where the FSDP group is a 

463 # proper sub-group of the world — using world_size would over-shard 

464 # along the TP axis). 

465 configured = pass_config.fsdp_degree 

466 self._fsdp_degree = configured if configured else dist.get_world_size() 

467 self._fsdp_group_name = kwargs.get("fsdp_group_name", self._fsdp_group_name) 

468 self._expert_param_names = set(kwargs.get("expert_param_names", ())) 

469 self._gradient_reductions = dict(kwargs.get("gradient_reductions", {})) 

470 self._source_gradient_groups = dict(kwargs.get("source_gradient_groups", {})) 

471 self._parameter_groups = dict(kwargs.get("parameter_groups", {})) 

472 if not self._expert_param_names.issubset(self._gradient_reductions): 

473 raise ValueError("Expert parameters require explicit gradient reduction policies") 

474 self._expert_fsdp_group_name = kwargs.get( 

475 "expert_fsdp_group_name", self._expert_fsdp_group_name 

476 ) 

477 self._expert_fsdp_degree = ( 

478 dist.get_world_size(_resolve_process_group(self._expert_fsdp_group_name)) 

479 if self._expert_param_names 

480 else None 

481 ) 

482 self._dp_replicate_group_name = kwargs.get( 

483 "dp_replicate_group_name", self._dp_replicate_group_name 

484 ) 

485 # Replicate-axis degree used by DDP / HSDP gradient reduction. 

486 replicate_degree = getattr(pass_config, "dp_replicate_degree", None) 

487 if self._dp_mode == "ddp": 

488 self._dp_replicate_degree = replicate_degree or dist.get_world_size() 

489 elif self._dp_mode == "hsdp": 

490 self._dp_replicate_degree = replicate_degree or ( 

491 dist.get_world_size() // self._fsdp_degree 

492 ) 

493 else: 

494 self._dp_replicate_degree = replicate_degree 

495 self._parallel_plan = kwargs.get("parallel_plan", self._parallel_plan) 

496 model = kwargs.get("model") 

497 if model is None: 

498 raise ValueError( 

499 "FSDPPass requires the live model via kwargs (model=...) so it " 

500 "can physically shard parameters; the trainer passes it through " 

501 "in compile()" 

502 ) 

503 

504 _LOG.info( 

505 "Running dp_mode=%s fsdp_degree=%s dp_replicate_degree=%s world_size=%s", 

506 self._dp_mode, 

507 self._fsdp_degree, 

508 self._dp_replicate_degree, 

509 dist.get_world_size(), 

510 ) 

511 return model 

512 

513 def _reset_run_state(self) -> None: 

514 """Clear per-run state so a reused pass does not carry names across graphs.""" 

515 self._processed_params = set() 

516 self._fsdp_modules = set() 

517 self._unsharded_params = {} 

518 self._padded_params = {} 

519 

520 def _apply_dp_transforms( 

521 self, 

522 graph_module: fx.GraphModule, 

523 model: nn.Module, 

524 classification: _DpParamClassification, 

525 pass_config: PassConfig, 

526 state_fqns: List[str], 

527 num_state_inputs: int, 

528 state_is_param: Optional[List[bool]], 

529 ) -> fx.GraphModule: 

530 """Insert gather / grad-reduce / reshard and shard the live model. 

531 

532 DDP replicates parameters: no all_gather, no live-model sharding and no 

533 reshard — only the gradient all-reduce path runs. FSDP/HSDP also 

534 all_gather each parameter for the forward and pad/shard the live model 

535 in place. 

536 """ 

537 param_nodes = classification.sharded_nodes 

538 if self._dp_mode != "ddp": 

539 graph_module = self._insert_all_gather_for_params( 

540 graph_module, param_nodes, pass_config 

541 ) 

542 

543 sharded_param_indices = frozenset( 

544 node.meta["state_idx"] for node in param_nodes 

545 ) 

546 graph_module = self._insert_grad_reduction( 

547 graph_module, 

548 sharded_param_indices, 

549 classification.replicated_indices, 

550 state_fqns, 

551 num_state_inputs, 

552 state_is_param, 

553 model, 

554 ) 

555 

556 if self._dp_mode != "ddp": 

557 # Reshard: free each replicated parameter once forward is done and 

558 # re-gather + rematerialize it for the backward. Runs after the 

559 # grad reduction so the grad path is untouched; no-op when disabled. 

560 graph_module = self._insert_reshard_logic( 

561 graph_module, param_nodes, pass_config 

562 ) 

563 

564 # Shard the live model's parameters in place (dim 0, by this rank's 

565 # index in the FSDP group). ``model.parameters()`` then yields the 

566 # shards, so the trainer's optimizer / grad accumulation stay 

567 # DP-agnostic; the graph re-gathers each step. 

568 self._shard_live_model_params(model) 

569 annotate_fsdp_bucket_specs(graph_module, model) 

570 

571 return graph_module 

572 

573 def _param_group(self, name: str) -> tuple[str, int]: 

574 """Choose the expert or dense shard axis for one parameter.""" 

575 if name in self._parameter_groups: 

576 return self._parameter_groups[name] 

577 if name in self._expert_param_names: 

578 return self._expert_fsdp_group_name, self._expert_fsdp_degree 

579 return self._fsdp_group_name, self._fsdp_degree 

580 

581 def _classify_dp_params( 

582 self, 

583 graph_module: fx.GraphModule, 

584 state_fqns: List[str], 

585 num_state_inputs: int, 

586 state_is_param: Optional[List[bool]] = None, 

587 model: Optional[nn.Module] = None, 

588 ) -> _DpParamClassification: 

589 """Classify parameter placeholders by how DP handles their gradient. 

590 

591 Parameters are the leading ``num_state_inputs`` placeholders of the 

592 joint graph, in ``state_fqns`` order. A parameter is DP-marked when any 

593 ancestor module FQN (e.g. ``layers.0.attention.wq`` for 

594 ``layers.0.attention.wq.weight``) is marked in the GraphParallelPlan — 

595 either exactly or via pattern (e.g. ``layers.*``). 

596 

597 Buffers (e.g. RoPE's non-persistent ``cache``) and frozen parameters 

598 are skipped: the tracer omits them from the gradient list and the 

599 live-model sharder leaves them full-size. The pad-shard and unmarked- 

600 fallback decisions are delegated to ``_marked_param_stays_replicated`` 

601 and ``_unmarked_disposition``. 

602 

603 Returns: 

604 A ``_DpParamClassification`` (see its docstring). 

605 """ 

606 sharded_nodes: List[fx.Node] = [] 

607 replicated_indices: Set[int] = set() 

608 unmarked_trainable: List[Tuple[int, str]] = [] 

609 

610 placeholders = [n for n in graph_module.graph.nodes if n.op == "placeholder"] 

611 if len(placeholders) < num_state_inputs: 

612 return _DpParamClassification( 

613 sharded_nodes, replicated_indices, unmarked_trainable 

614 ) 

615 

616 # Build a FQN -> Parameter lookup so the divisibility gate uses the 

617 # same source of truth as ``_shard_live_model_params`` (which iterates 

618 # ``model.named_parameters``). ``state_fqns`` covers both parameters 

619 # and buffers; only parameters are reachable here because buffers are 

620 # filtered out by ``state_is_param`` above. 

621 param_lookup: Dict[str, nn.Parameter] = ( 

622 dict(model.named_parameters(remove_duplicate=False)) 

623 if model is not None 

624 else {} 

625 ) 

626 

627 for idx in range(num_state_inputs): 

628 fqn, param, skip = self._resolve_dp_param( 

629 idx, state_fqns, state_is_param, param_lookup, model 

630 ) 

631 if skip: 

632 continue 

633 

634 _, degree = self._param_group(fqn) 

635 if fqn in self._expert_param_names and degree == 1: 

636 continue 

637 

638 fallback = self._unmarked_disposition(fqn, param, model) 

639 if fallback is not None: 

640 if fallback: 

641 replicated_indices.add(idx) 

642 unmarked_trainable.append((idx, fqn)) 

643 continue 

644 

645 if self._marked_param_stays_replicated(fqn, param): 

646 replicated_indices.add(idx) 

647 continue 

648 

649 sharded_nodes.append( 

650 self._mark_sharded_param(idx, fqn, degree, placeholders[idx], param) 

651 ) 

652 

653 return _DpParamClassification( 

654 sharded_nodes, replicated_indices, unmarked_trainable 

655 ) 

656 

657 def _resolve_dp_param( 

658 self, 

659 idx: int, 

660 state_fqns: List[str], 

661 state_is_param: Optional[List[bool]], 

662 param_lookup: Dict[str, nn.Parameter], 

663 model: Optional[nn.Module], 

664 ) -> Tuple[Optional[str], Optional[nn.Parameter], bool]: 

665 """Resolve one state placeholder to ``(fqn, param, skip)``. 

666 

667 Buffers and frozen parameters are skipped: the tracer omits them from 

668 the gradient list and the live-model sharder leaves them full-size. 

669 """ 

670 if state_is_param is not None and not state_is_param[idx]: 

671 # Buffer, not a parameter -> never FSDP-sharded. 

672 return None, None, True 

673 

674 fqn = state_fqns[idx] 

675 param = param_lookup.get(fqn) 

676 if model is not None and param is None: 

677 return None, None, True 

678 

679 if param is not None and not param.requires_grad: 

680 _LOG.info("Skip %s: requires_grad=False (frozen)", fqn) 

681 return None, None, True 

682 return fqn, param, False 

683 

684 def _mark_sharded_param( 

685 self, 

686 idx: int, 

687 fqn: str, 

688 degree: int, 

689 node: fx.Node, 

690 param: Optional[nn.Parameter], 

691 ) -> fx.Node: 

692 """Tag a DP-sharded placeholder and record its live-model shard size.""" 

693 node.meta["state_idx"] = idx 

694 node.meta["param_name"] = fqn 

695 node.meta["fsdp_degree"] = degree 

696 node.meta["is_param"] = True 

697 # Record the TP-local, unsharded size before live-model FSDP slicing. 

698 # Newly inserted collective nodes have no shape-propagated ``val``. 

699 value = param if param is not None else node.meta.get("val") 

700 if isinstance(value, torch.Tensor) and isinstance(value.numel(), int): 

701 node.meta["fsdp_unsharded_nbytes"] = value.numel() * value.element_size() 

702 self._fsdp_modules.add(self._get_parent_module_fqn(fqn)) 

703 return node 

704 

705 def _unmarked_disposition( 

706 self, 

707 fqn: str, 

708 param: Optional[nn.Parameter], 

709 model: Optional[nn.Module], 

710 ) -> Optional[bool]: 

711 """Decide how a trainable param a partial plan left unmarked is handled. 

712 

713 Rather than leave it with a local, unreduced gradient (silent 

714 divergence across DP ranks), fall it back to DDP: keep it replicated 

715 and average its gradient over the DP group. 

716 

717 Returns: 

718 ``True`` to replicate it and warn the caller, ``False`` to skip it 

719 silently (``param is None`` with a live model means a buffer, only 

720 reachable when an old trace lacks ``state_is_param``), or ``None`` 

721 when the param is marked and the caller should shard it normally. 

722 """ 

723 if self._parallel_plan is None or self._param_belongs_to_fsdp_module(fqn): 

724 return None 

725 return model is None or param is not None 

726 

727 def _marked_param_stays_replicated( 

728 self, fqn: str, param: Optional[nn.Parameter] 

729 ) -> bool: 

730 """Decide whether a marked param is kept replicated instead of sharded. 

731 

732 A marked param whose dim 0 is not divisible by ``fsdp_degree`` cannot be 

733 chunked evenly. A 0-dim scalar cannot be padded along dim 0, so it stays 

734 replicated and its gradient is averaged over the shard axis. Every 

735 other non-divisible param is recorded for pad-sharding (mirroring eager 

736 ``fully_shard``): dim 0 is zero-padded to a multiple of the degree, so 

737 it is still sharded and gradient-reduced. DDP has no shard axis, so 

738 every marked param is handled the same way regardless of divisibility. 

739 """ 

740 if self._dp_mode == "ddp" or param is None: 

741 return False 

742 _, degree = self._param_group(fqn) 

743 shape = param.shape 

744 if fqn in self._expert_param_names and (not shape or shape[0] % degree): 

745 raise ValueError( 

746 f"Graph FSDP+EP expert parameter {fqn} with shape {tuple(shape)} " 

747 f"cannot shard across edp_shard degree {degree}" 

748 ) 

749 if shape and shape[0] % degree == 0: 

750 return False 

751 if not shape: 

752 _LOG.info( 

753 "Scalar param %s stays replicated: 0-dim cannot be " 

754 "pad-sharded; its gradient will be all-reduced", 

755 fqn, 

756 ) 

757 return True 

758 n = int(shape[0]) 

759 n_pad = -(-n // degree) * degree 

760 self._padded_params[fqn] = _PaddedParam( 

761 n=n, n_pad=n_pad, dim0_pad=n_pad - n, ndim=param.ndim 

762 ) 

763 _LOG.info( 

764 "Param %s pad-sharded: dim 0 %d -> %d (fsdp_degree %d)", 

765 fqn, 

766 n, 

767 n_pad, 

768 degree, 

769 ) 

770 return False 

771 

772 @staticmethod 

773 def _warn_unmarked_trainable(entries: List[Tuple[int, str]]) -> None: 

774 """Report trainable params a partial plan left unmarked. 

775 

776 They are not FSDP-sharded, so they fall back to DDP: kept replicated and 

777 their gradient averaged over the DP group. The warning is informational — 

778 the plan simply did not cover them, and they cost no sharded memory but a 

779 replicated gradient all-reduce. 

780 """ 

781 shown = [fqn for _, fqn in entries[:10]] 

782 remaining = len(entries) - len(shown) 

783 _LOG.warning( 

784 "%d trainable parameter(s) are not marked for DP; falling back to " 

785 "DDP (kept replicated, gradient all-reduced over the DP group): " 

786 "%s%s. Mark them via GraphParallelPlan.fsdp_mark()/" 

787 "fsdp_mark_pattern() to FSDP-shard them instead.", 

788 len(entries), 

789 ", ".join(shown), 

790 f" (+{remaining} more)" if remaining > 0 else "", 

791 ) 

792 

793 def _shard_live_model_params(self, model: nn.Module) -> None: 

794 """ 

795 Physically shard the live model's parameters in place (dim 0). 

796 

797 After this, model.parameters() yields the local shards. 

798 """ 

799 # Use the FSDP group's local rank (NOT the global rank) as the 

800 # chunk index — when fsdp_degree < world_size (TP+FSDP), the global 

801 # rank exceeds the chunk count and causes IndexError. 

802 fsdp_pg = _resolve_process_group(self._fsdp_group_name) 

803 group_ranks = {self._fsdp_group_name: dist.get_rank(group=fsdp_pg)} 

804 sharded_count = 0 

805 

806 for name, param in model.named_parameters(): 

807 if not param.requires_grad: 

808 continue 

809 

810 group_name, degree = self._param_group(name) 

811 if name in self._expert_param_names and degree == 1: 

812 continue 

813 

814 # Same plan gate as ``_classify_dp_params``: only parameters inside 

815 # FSDP-marked modules are sharded. Skipping the gate here would 

816 # shard params the graph still expects full-rank 

817 # (no AllGather inserted), causing a shape mismatch at 

818 # ``run_traced_graph`` time on any non-``*`` plan. 

819 if ( 

820 self._parallel_plan is not None 

821 and not self._param_belongs_to_fsdp_module(name) 

822 ): 

823 _LOG.info( 

824 "Skip %s: not in an FSDP-wrapped module", 

825 name, 

826 ) 

827 continue 

828 

829 if group_name not in group_ranks: 

830 group_ranks[group_name] = dist.get_rank( 

831 group=_resolve_process_group(group_name) 

832 ) 

833 rank = group_ranks[group_name] 

834 

835 # Pad-shard a non-divisible param (recorded by 

836 # ``_classify_dp_params``): zero-pad dim 0 to a multiple of the 

837 # degree, then take this rank's equal-size chunk — exactly what 

838 # eager ``fully_shard`` does. A 0-dim scalar cannot be padded and 

839 # stays replicated. 

840 pad_info = self._padded_params.get(name) 

841 if pad_info is not None: 

842 original_shape = param.shape 

843 full = param.detach() 

844 if pad_info.dim0_pad: 

845 full = torch.cat( 

846 [full, full.new_zeros((pad_info.dim0_pad, *full.shape[1:]))], 

847 dim=0, 

848 ) 

849 param.data = full.chunk(degree, dim=0)[rank].clone() 

850 sharded_count += 1 

851 _LOG.info( 

852 "Pad-sharded %s: %s -> %s", 

853 name, 

854 list(original_shape), 

855 list(param.shape), 

856 ) 

857 continue 

858 

859 # Mirror ``_classify_dp_params``: a 0-dim scalar stays replicated. 

860 if not param.shape or param.shape[0] % degree != 0: 

861 _LOG.info( 

862 "Skip %s: dim 0 (%s) not divisible by fsdp_degree (%s)", 

863 name, 

864 param.shape[0] if param.shape else "scalar", 

865 degree, 

866 ) 

867 continue 

868 

869 original_shape = param.shape 

870 param.data = param.detach().chunk(degree, dim=0)[rank].clone() 

871 sharded_count += 1 

872 _LOG.info( 

873 "Sharded %s: %s -> %s", 

874 name, 

875 list(original_shape), 

876 list(param.shape), 

877 ) 

878 

879 _LOG.info("Total sharded parameters: %d", sharded_count) 

880 

881 def _param_belongs_to_fsdp_module(self, param_fqn: str) -> bool: 

882 """Check if a parameter FQN belongs to an FSDP-wrapped module. 

883 

884 The parameter's own module FQN (``layers.0.attention.wq.weight`` -> 

885 ``layers.0.attention.wq``) plus every ancestor (``layers.0``, ...) is 

886 tested, so both ``fsdp_mark("layers.0.attention.wq")`` and 

887 ``fsdp_mark_pattern("layers.*")`` match. 

888 

889 A parameter directly on the root (``weight``) has no module ancestor; 

890 its own FQN is still tested first so it can be marked by its name 

891 (``fsdp_mark("weight")``) or by a ``*`` pattern. 

892 """ 

893 if self._parallel_plan.is_marked_for_fsdp(param_fqn): 

894 return True 

895 

896 parts = param_fqn.split(".") 

897 for i in range(len(parts) - 1, 0, -1): 

898 module_fqn = ".".join(parts[:i]) 

899 if self._parallel_plan.is_marked_for_fsdp(module_fqn): 

900 return True 

901 return False 

902 

903 def _get_parent_module_fqn(self, param_fqn: str) -> str: 

904 """ 

905 Get parent module FQN from parameter FQN. 

906 

907 e.g., "layers.0.attention.wq.weight" -> "layers.0.attention.wq" 

908 """ 

909 parts = param_fqn.split(".") 

910 return ".".join(parts[:-1]) if len(parts) > 1 else param_fqn 

911 

912 def _insert_all_gather_for_params( 

913 self, 

914 graph_module: fx.GraphModule, 

915 param_nodes: List[fx.Node], 

916 pass_config: PassConfig, 

917 ) -> fx.GraphModule: 

918 """ 

919 Insert AllGather for each FSDP parameter placeholder. 

920 

921 Parameter state: Shard -> Replicate. All subsequent uses of the 

922 placeholder are rewired to the unsharded (replicated) tensor, so the 

923 computation body keeps operating on full parameters while the graph 

924 input stays sharded. The gather is placed after the placeholder, or 

925 sunk to just before the first consumer when reshard is enabled. 

926 

927 When reshard is enabled the gather is **sunk** to just before the 

928 parameter's first consumer instead of sitting next to the placeholder. 

929 All state placeholders lead the graph, so an unsunk gather would 

930 materialize every full parameter before any compute runs, leaving the 

931 reshard free step nothing to reclaim; sinking is what makes peak 

932 memory track the forward working set. 

933 """ 

934 graph = graph_module.graph 

935 

936 for param_node in param_nodes: 

937 if param_node.name in self._processed_params: 

938 continue 

939 

940 sink = pass_config.fsdp_reshard_after_forward 

941 if sink and not param_node.users: 

942 # Dead parameter: a gather would materialize a full replica 

943 # nobody reads, and reshard would never free it. Leave the 

944 # placeholder sharded. 

945 self._processed_params.add(param_node.name) 

946 continue 

947 anchor = self._first_user(param_node) if sink else None 

948 

949 group_name, degree = self._param_group( 

950 param_node.meta.get("param_name", "") 

951 ) 

952 

953 # AllGather + immediate wait for correctness; FsdpOverlapPass may 

954 # sink the wait past independent compute later. 

955 with _insertion_point(graph, anchor, param_node): 

956 ag_node = graph.call_function( 

957 _c10d_functional.all_gather_into_tensor, 

958 args=(param_node, degree, group_name), 

959 ) 

960 ag_node.meta["comm_type"] = "fsdp_all_gather" 

961 ag_node.meta["comm_group"] = group_name 

962 ag_node.meta["param_node"] = param_node.name 

963 ag_node.meta["param_name"] = param_node.meta.get("param_name") 

964 ag_node.meta["state_idx"] = param_node.meta.get("state_idx") 

965 ag_node.meta["fsdp_degree"] = degree 

966 ag_node.meta["fsdp_unsharded_nbytes"] = param_node.meta.get( 

967 "fsdp_unsharded_nbytes" 

968 ) 

969 

970 # Insert the wait in its own block: a wait placed in the same block 

971 # as the gather would land *before* the gather it depends on (the 

972 # first insert inside ``inserting_after(X)`` lands right after 

973 # ``X``), and codegen would emit 

974 # ``wait_tensor(all_gather_into_tensor)`` before that variable is 

975 # assigned. 

976 with graph.inserting_after(ag_node): 

977 wait_node = graph.call_function( 

978 _c10d_functional.wait_tensor, 

979 args=(ag_node,), 

980 ) 

981 wait_node.meta["wait_for"] = ag_node.name 

982 

983 param_node.meta["fsdp_sharded"] = True 

984 param_node.meta["fsdp_ag_node"] = ag_node.name 

985 self._unsharded_params[param_node.name] = _UnshardedParam( 

986 param_node=param_node, ag_node=ag_node, wait_node=wait_node 

987 ) 

988 

989 # Pad-sharded parameter: the gather produced the padded full tensor 

990 # ``[n_pad, ...]``; narrow it back to the real ``[n, ...]`` and route 

991 # every consumer to that view. Use the ``aten`` overload (not the 

992 # ``torch.narrow`` builtin) so its schema's ``alias_info`` marks it a 

993 # view: ``_propagates_alias`` keys off that schema, so the reshard 

994 # alias walk (free / rematerialize) tracks the narrow and does not 

995 # free the padded gather out from under its forward consumers. 

996 replacement = wait_node 

997 pad_info = self._padded_params.get(param_node.meta.get("param_name")) 

998 if pad_info is not None: 

999 with graph.inserting_after(wait_node): 

1000 replacement = graph.call_function( 

1001 torch.ops.aten.narrow.default, 

1002 args=(wait_node, 0, 0, pad_info.n), 

1003 ) 

1004 replacement.meta["param_name"] = param_node.meta.get("param_name") 

1005 replacement.meta["state_idx"] = param_node.meta.get("state_idx") 

1006 

1007 for user in list(param_node.users.keys()): 

1008 if user not in (ag_node, wait_node): 

1009 user.replace_input_with(param_node, replacement) 

1010 

1011 self._processed_params.add(param_node.name) 

1012 

1013 return graph_module 

1014 

1015 @staticmethod 

1016 def _first_user(param_node: fx.Node) -> Optional[fx.Node]: 

1017 """Earliest (in graph order) consumer of ``param_node``. 

1018 

1019 Placeholders are all at the top; their consumers are spread through 

1020 the forward. Returning the earliest consumer lets the gather sink 

1021 next to the first real use. ``None`` when the placeholder is unused 

1022 (then the gather stays next to the placeholder). 

1023 """ 

1024 users = [u for u in param_node.users if u.op != "output"] 

1025 if not users: 

1026 return None 

1027 forward_users = [u for u in users if not u.meta.get("autograd_backward", False)] 

1028 candidates = forward_users or users 

1029 order = {n: i for i, n in enumerate(param_node.graph.nodes)} 

1030 return min(candidates, key=lambda u: order.get(u, len(order))) 

1031 

1032 def _insert_grad_reduction( # pylint: disable=too-many-locals 

1033 self, 

1034 graph_module: fx.GraphModule, 

1035 sharded_param_indices: Set[int], 

1036 replicated_param_indices: Set[int], 

1037 state_fqns: List[str], 

1038 num_state_inputs: int, 

1039 state_is_param: Optional[List[bool]] = None, 

1040 model: Optional[nn.Module] = None, 

1041 ) -> fx.GraphModule: 

1042 """Insert gradient reduction on the gradient outputs of DP parameters. 

1043 

1044 The joint graph returns ``[loss, grad0, grad1, ...]`` from the fwd+bwd 

1045 function; gradient ``i`` (output index ``i+1``) corresponds to the 

1046 ``i``-th trainable parameter (``state_fqns`` order, buffers and frozen 

1047 params skipped, mirrored by ``_build_trainable_state_indices``). For a 

1048 parameter in ``sharded_param_indices`` (a real shard axis) the mode 

1049 decides the collective chain: ``fsdp`` reduce_scatters on the ``fsdp`` 

1050 group (Replicate -> Shard); ``ddp`` all_reduces on ``dp_replicate``; 

1051 ``hsdp`` reduce_scatters on ``fsdp`` then all_reduces on 

1052 ``dp_replicate`` (shard axis first, so the cross-axis message is 

1053 already sharded). Pad-sharded params (``_padded_params``) zero-pad their 

1054 ``[n, ...]`` gradient back to ``[n_pad, ...]`` first so the local 

1055 gradient matches the padded shard. 

1056 

1057 For a parameter in ``replicated_param_indices`` (a marked 0-dim scalar, 

1058 or a partial-plan leftover that fell back to DDP) no shard axis exists, 

1059 so the gradient is reduced with all_reduce instead. Ordinary parameters 

1060 average over the DP axes; source-layout parameters use their explicit 

1061 reduction policy. Gradients outside the DP modules stay full. 

1062 """ 

1063 graph = graph_module.graph 

1064 

1065 output_node = next((n for n in graph.nodes if n.op == "output"), None) 

1066 if output_node is None: 

1067 return graph_module 

1068 

1069 returned = output_node.args[0] 

1070 if not isinstance(returned, (list, tuple)): 

1071 # Model returned a single value (e.g. just loss) -> no grads. 

1072 return graph_module 

1073 

1074 trainable_state_indices = self._build_trainable_state_indices( 

1075 state_fqns, num_state_inputs, state_is_param, model 

1076 ) 

1077 

1078 new_returned = list(returned) 

1079 # ``train_fn`` may append the traced ``loss_dict`` outputs AFTER the 

1080 # gradients (see ``trace_model_graph``); those are not gradients and 

1081 # must stay outside the reduce_scatter range. 

1082 num_loss_outputs = len(getattr(graph_module, "loss_dict_keys", [])) 

1083 num_grads = len(new_returned) - 1 - num_loss_outputs # index 0 is the loss 

1084 if num_grads != len(trainable_state_indices): 

1085 raise ValueError( 

1086 f"Gradient count ({num_grads}) does not match trainable " 

1087 f"parameter count ({len(trainable_state_indices)}). The " 

1088 f"traced graph and the live model disagree on which " 

1089 f"parameters are trainable; refusing to insert gradient " 

1090 f"reduction to avoid silent gradient/state misalignment." 

1091 ) 

1092 

1093 # Index 0 is the loss; gradients start at index 1. Gradient i+1 

1094 # corresponds to trainable parameter i, whose state_idx is 

1095 # trainable_state_indices[i]. The trailing loss_dict outputs are 

1096 # named loss values, not gradients. 

1097 for i in range(1, len(new_returned) - num_loss_outputs): 

1098 grad_node = new_returned[i] 

1099 if not isinstance(grad_node, fx.Node): 

1100 continue 

1101 

1102 state_idx = trainable_state_indices[i - 1] 

1103 fqn = state_fqns[state_idx] 

1104 group_name, degree = self._param_group(fqn) 

1105 if state_idx in sharded_param_indices: 

1106 replicated = False 

1107 pad_info = self._padded_params.get(fqn) 

1108 elif state_idx in replicated_param_indices: 

1109 replicated = True 

1110 pad_info = None 

1111 elif degree == 1 and fqn in self._gradient_reductions: 

1112 replicated = True 

1113 pad_info = None 

1114 else: 

1115 continue 

1116 

1117 reduction_info = _GradReductionInfo( 

1118 group_name, degree, self._gradient_reductions.get(fqn, _DEFAULT_GRADIENT_REDUCTION) 

1119 ) 

1120 with graph.inserting_before(output_node): 

1121 new_returned[i] = self._build_grad_reduction_nodes( 

1122 graph, 

1123 grad_node, 

1124 reduction_info, 

1125 replicated=replicated, 

1126 pad_info=pad_info, 

1127 ) 

1128 tag_gradient_identity(new_returned[i], fqn, state_idx) 

1129 source_group = self._source_gradient_groups.get(fqn) 

1130 if source_group is not None: 

1131 new_returned[i] = self._insert_source_gradient_reduction( 

1132 graph, new_returned[i], source_group 

1133 ) 

1134 tag_gradient_identity(new_returned[i], state_fqns[state_idx], state_idx) 

1135 

1136 output_node.args = (type(returned)(new_returned),) + tuple(output_node.args[1:]) 

1137 

1138 return graph_module 

1139 

1140 @staticmethod 

1141 def _insert_source_gradient_reduction( 

1142 graph: fx.Graph, gradient: fx.Node, group_name: str 

1143 ) -> fx.Node: 

1144 """Sum replicated source-layout contributions after completed DP reduction.""" 

1145 contiguous = graph.call_method("contiguous", args=(gradient,)) 

1146 reduced = graph.call_function( 

1147 _c10d_functional.all_reduce, args=(contiguous, "sum", group_name) 

1148 ) 

1149 reduced.meta.update(comm_type="source_all_reduce", comm_group=group_name, autograd_backward=True) 

1150 return FSDPPass._insert_wait(graph, reduced) 

1151 

1152 def _build_grad_reduction_nodes( 

1153 self, 

1154 graph: fx.Graph, 

1155 grad_node: fx.Node, 

1156 info: _GradReductionInfo, 

1157 replicated: bool = False, 

1158 pad_info: Optional[_PaddedParam] = None, 

1159 ) -> fx.Node: 

1160 """Build and return the waited gradient-reduction node for ``grad_node``. 

1161 

1162 The returned (wait) node replaces ``grad_node`` in the graph output. 

1163 Emits the mode-specific chain (see ``_insert_grad_reduction``). When 

1164 ``pad_info`` is given the parameter is pad-sharded: the real ``[n, ...]`` 

1165 gradient is zero-padded on dim 0 to ``[n_pad, ...]`` before the 

1166 reduce_scatter, so the scattered local gradient matches the padded live 

1167 shard (mirrors eager ``fully_shard``'s padded grad buffer). When 

1168 ``replicated`` is True the parameter is a 0-dim scalar that could not be 

1169 sharded, so its gradient is reduced over the shard group with 

1170 ``all_reduce`` instead of reduce-scattered — the graph and the live 

1171 model both keep the parameter full, so a reduce_scatter would change the 

1172 shape and desync them. 

1173 

1174 Every collective is waited on before its result is consumed. A chained 

1175 collective runs on a different process-group stream (HSDP's 

1176 reduce_scatter is on ``fsdp`` while the all_reduce is on 

1177 ``dp_replicate``), so without the intermediate wait the all_reduce could 

1178 read a partially written shard. This mirrors eager ``fully_shard``, 

1179 which orders the all_reduce stream after the reduce_scatter stream. 

1180 """ 

1181 group_name, degree = info.group_name, info.degree 

1182 reduced = grad_node 

1183 if info.reduction.scale is not None: 

1184 reduced = graph.call_function(torch.mul, args=(reduced, info.reduction.scale)) 

1185 if degree == 1 and self._dp_mode == "fsdp": 

1186 return reduced 

1187 if pad_info is not None: 

1188 # constant_pad_nd's pad tuple runs last-dim-first: pad dim 0 on the 

1189 # right by ``dim0_pad`` and leave every other dim untouched. 

1190 pad_tuple = tuple([0, 0] * (pad_info.ndim - 1) + [0, pad_info.dim0_pad]) 

1191 reduced = graph.call_function( 

1192 torch.constant_pad_nd, args=(reduced, pad_tuple) 

1193 ) 

1194 reduced.meta["comm_type"] = "fsdp_grad_pad" 

1195 value = grad_node.meta.get("val") 

1196 if value is not None and hasattr(value, "shape"): 

1197 reduced.meta["fsdp_padded_input_spec"] = { 

1198 "shape": (pad_info.n_pad, *value.shape[1:]), 

1199 "dtype": value.dtype, 

1200 "device": str(value.device), 

1201 } 

1202 if replicated: 

1203 # Replicated parameter (a 0-dim scalar, or a partial-plan leftover 

1204 # that fell back to DDP): average the full gradient over the shard 

1205 # group so every replica takes the same optimizer step. DDP mode has 

1206 # no shard group — the replicate all_reduce below handles it. 

1207 if self._dp_mode in ("fsdp", "hsdp"): 

1208 ar_node = graph.call_function( 

1209 _c10d_functional.all_reduce, 

1210 args=(reduced, info.reduction.reduce_op, group_name), 

1211 ) 

1212 ar_node.meta["comm_type"] = "fsdp_all_reduce" 

1213 ar_node.meta["comm_group"] = group_name 

1214 ar_node.meta["fsdp_degree"] = degree 

1215 reduced = ar_node 

1216 # HSDP chains the replicate all_reduce (a different process-group 

1217 # stream) after the shard all_reduce, so wait first. 

1218 if self._dp_mode == "hsdp": 

1219 reduced = self._insert_wait(graph, reduced) 

1220 elif self._dp_mode in ("fsdp", "hsdp"): 

1221 # Matmul backward can return a transposed view on NPU, including dense router weights. 

1222 reduced = graph.call_method("contiguous", args=(reduced,)) 

1223 rs_node = graph.call_function( 

1224 _c10d_functional.reduce_scatter_tensor, 

1225 args=( 

1226 reduced, 

1227 info.reduction.reduce_op, 

1228 degree, 

1229 group_name, 

1230 ), 

1231 ) 

1232 rs_node.meta["comm_type"] = "fsdp_reduce_scatter" 

1233 rs_node.meta["comm_group"] = group_name 

1234 rs_node.meta["fsdp_degree"] = degree 

1235 rs_node.meta["autograd_backward"] = True 

1236 reduced = rs_node 

1237 # HSDP chains all_reduce (a different process-group stream) after 

1238 # the reduce_scatter, so wait first; plain FSDP waits on the final 

1239 # node only. 

1240 if self._dp_mode == "hsdp": 

1241 reduced = self._insert_wait(graph, reduced) 

1242 

1243 if self._dp_mode in ("ddp", "hsdp"): 

1244 ar_node = graph.call_function( 

1245 _c10d_functional.all_reduce, 

1246 args=(reduced, info.reduction.reduce_op, self._dp_replicate_group_name), 

1247 ) 

1248 ar_node.meta["comm_type"] = "dp_all_reduce" 

1249 ar_node.meta["comm_group"] = self._dp_replicate_group_name 

1250 ar_node.meta["dp_replicate_degree"] = self._dp_replicate_degree 

1251 ar_node.meta["autograd_backward"] = True 

1252 reduced = ar_node 

1253 

1254 return self._insert_wait(graph, reduced) 

1255 

1256 @staticmethod 

1257 def _insert_wait(graph: fx.Graph, tensor_node: fx.Node) -> fx.Node: 

1258 """Append ``wait_tensor(tensor_node)`` and return the waited node.""" 

1259 wait_node = graph.call_function( 

1260 _c10d_functional.wait_tensor, 

1261 args=(tensor_node,), 

1262 ) 

1263 wait_node.meta["wait_for"] = tensor_node.name 

1264 wait_node.meta["autograd_backward"] = True 

1265 return wait_node 

1266 

1267 def _build_trainable_state_indices( 

1268 self, 

1269 state_fqns: List[str], 

1270 num_state_inputs: int, 

1271 state_is_param: Optional[List[bool]], 

1272 model: Optional[nn.Module], 

1273 ) -> List[int]: 

1274 """ 

1275 Return state indices of trainable parameters, in ``state_fqns`` order. 

1276 

1277 Mirrors the tracer's ``params`` list construction (skip buffers via 

1278 ``state_is_param``, skip frozen params via ``requires_grad``) so 

1279 gradient ``i`` maps to ``trainable_state_indices[i]``. Reproducing this 

1280 filter here is what keeps reduce_scatter aligned for models with 

1281 buffers or frozen params. 

1282 """ 

1283 if model is None: 

1284 # Without the model we cannot inspect requires_grad; fall back to 

1285 # the all-trainable assumption. The grad-count check in the caller 

1286 # raises if that assumption is wrong, so misalignment is not silent. 

1287 return list(range(num_state_inputs)) 

1288 

1289 param_lookup = dict(model.named_parameters(remove_duplicate=False)) 

1290 trainable: List[int] = [] 

1291 for idx in range(num_state_inputs): 

1292 if state_is_param is not None and not state_is_param[idx]: 

1293 continue 

1294 param = param_lookup.get(state_fqns[idx]) 

1295 if param is not None and param.requires_grad: 

1296 trainable.append(idx) 

1297 return trainable 

1298 

1299 def _insert_reshard_logic( 

1300 self, 

1301 graph_module: fx.GraphModule, 

1302 param_nodes: List[fx.Node], 

1303 pass_config: PassConfig, 

1304 ) -> fx.GraphModule: 

1305 """Reshard each FSDP parameter after forward (FSDP release). 

1306 

1307 For every FSDP parameter whose all_gather was inserted by 

1308 ``_insert_all_gather_for_params``: 

1309 

1310 1. Free the replicated full parameter once its last forward reader — 

1311 including forward views aliasing it — has run. 

1312 2. If the backward still needs the parameter (directly or through a 

1313 saved forward view), insert a fresh all_gather just before the 

1314 first backward consumer and rematerialize the saved view chain, so 

1315 the backward computes the exact same values from the re-gathered 

1316 (unsharded) parameter. 

1317 3. Free that backward re-gather once its last backward reader runs. 

1318 

1319 Steps 1-3 keep only the forward working set resident during forward and 

1320 only the active layer's parameter resident during backward, instead of 

1321 every replicated parameter for the whole joint graph. 

1322 

1323 No-op when ``fsdp_reshard_after_forward`` is disabled. 

1324 """ 

1325 if not pass_config.fsdp_reshard_after_forward: 

1326 return graph_module 

1327 

1328 for param_node in param_nodes: 

1329 record = self._unsharded_params.get(param_node.name) 

1330 if record is None: 

1331 continue 

1332 self._reshard_param(graph_module.graph, record) 

1333 return graph_module 

1334 

1335 def _reshard_param(self, graph: fx.Graph, record: _UnshardedParam) -> None: 

1336 """Reshard one FSDP parameter (free + backward re-gather/remat).""" 

1337 wait = record.wait_node 

1338 alias_nodes = self._alias_descendants(wait) 

1339 order = {n: i for i, n in enumerate(graph.nodes)} 

1340 

1341 # Alias (view) descendants the backward reads must be rematerialized 

1342 # from the re-gathered parameter; the unsharded tensor itself may be 

1343 # consumed directly by the backward in some models. 

1344 needed = [ 

1345 n for n in [wait, *alias_nodes] if self._feeds_backward(n, alias_nodes) 

1346 ] 

1347 

1348 # Remat plan first: the forward free is only safe once every backward 

1349 # consumer has been rewired onto fresh (re-gathered) storage. If the 

1350 # view chain cannot be cloned, keep the previous resident-for-the-graph 

1351 # behavior — a missed free costs memory, a misordered one is unsound. 

1352 if self._rematerialize_for_backward(graph, record, needed, order): 

1353 # 1. Free after the last forward data read of the unsharded storage. 

1354 self._insert_reshard_free(graph, wait, record, order, backward=False) 

1355 

1356 def _rematerialize_for_backward( 

1357 self, 

1358 graph: fx.Graph, 

1359 record: _UnshardedParam, 

1360 needed: List[fx.Node], 

1361 order: Dict[fx.Node, int], 

1362 ) -> bool: 

1363 """Re-gather ahead of the first backward consumer and remat views. 

1364 

1365 The saved forward view chain is cloned against the re-gathered 

1366 parameter so the backward reads identical values from a fresh, 

1367 correctly-sized tensor. 

1368 

1369 Returns: 

1370 ``True`` when the free may proceed — no backward consumer exists, 

1371 or every one of them was rewired onto rematerialized storage. 

1372 ``False`` when a needed view cannot be cloned; the caller must 

1373 keep the unsharded parameter resident. 

1374 """ 

1375 backward_users = [ 

1376 u for n in needed for u in n.users if u.meta.get("autograd_backward", False) 

1377 ] 

1378 if not backward_users: 

1379 return True 

1380 

1381 wait = record.wait_node 

1382 uncloneable = [n for n in needed if n is not wait and not self._cloneable(n)] 

1383 if uncloneable: 

1384 _LOG.warning( 

1385 "FSDP reshard: cannot rematerialize %s for '%s'; keeping the " 

1386 "unsharded parameter resident", 

1387 [n.format_node() for n in uncloneable], 

1388 record.param_node.meta.get("param_name"), 

1389 ) 

1390 return False 

1391 

1392 anchor = min(backward_users, key=lambda u: order.get(u, len(order))) 

1393 wait2 = self._insert_backward_gather(graph, record, anchor) 

1394 

1395 recreate: Dict[fx.Node, fx.Node] = {wait: wait2} 

1396 last_inserted = wait2 

1397 for node in sorted( 

1398 (n for n in needed if n is not wait), key=lambda n: order[n] 

1399 ): 

1400 last_inserted = self._clone_with_remap( 

1401 graph, node, recreate, wait, wait2, last_inserted 

1402 ) 

1403 

1404 self._rewire_backward_users(recreate) 

1405 self._insert_reshard_free(graph, wait2, record, order, backward=True) 

1406 return True 

1407 

1408 @staticmethod 

1409 def _cloneable(node: fx.Node) -> bool: 

1410 """Whether ``node`` can be cloned as a rematerialized view op.""" 

1411 return node.op == "call_function" and callable(node.target) 

1412 

1413 def _insert_backward_gather( 

1414 self, graph: fx.Graph, record: _UnshardedParam, anchor: fx.Node 

1415 ) -> fx.Node: 

1416 """Insert the backward all_gather + wait pair before ``anchor``.""" 

1417 group_name, degree = self._param_group( 

1418 record.param_node.meta.get("param_name", "") 

1419 ) 

1420 with graph.inserting_before(anchor): 

1421 ag_node = graph.call_function( 

1422 _c10d_functional.all_gather_into_tensor, 

1423 args=(record.param_node, degree, group_name), 

1424 ) 

1425 ag_node.meta["comm_type"] = "fsdp_all_gather_backward" 

1426 ag_node.meta["comm_group"] = group_name 

1427 ag_node.meta["param_node"] = record.param_node.name 

1428 ag_node.meta["param_name"] = record.param_node.meta.get("param_name") 

1429 ag_node.meta["state_idx"] = record.param_node.meta.get("state_idx") 

1430 ag_node.meta["fsdp_degree"] = degree 

1431 ag_node.meta["fsdp_unsharded_nbytes"] = record.param_node.meta.get( 

1432 "fsdp_unsharded_nbytes" 

1433 ) 

1434 # This gather lives in the backward half; tag it so downstream 

1435 # passes (e.g. PpPass phase classification) do not mistake it and 

1436 # its remat clones for forward nodes — their only arg is a forward 

1437 # state placeholder, which would otherwise imply the forward phase. 

1438 ag_node.meta["autograd_backward"] = True 

1439 with graph.inserting_before(anchor): 

1440 wait_node = graph.call_function( 

1441 _c10d_functional.wait_tensor, args=(ag_node,) 

1442 ) 

1443 wait_node.meta["wait_for"] = ag_node.name 

1444 wait_node.meta["autograd_backward"] = True 

1445 return wait_node 

1446 

1447 def _clone_with_remap( 

1448 self, 

1449 graph: fx.Graph, 

1450 node: fx.Node, 

1451 recreate: Dict[fx.Node, fx.Node], 

1452 wait: fx.Node, 

1453 wait2: fx.Node, 

1454 insert_after: fx.Node, 

1455 ) -> fx.Node: 

1456 """Clone ``node`` after ``insert_after``, remapping args to re-gathers.""" 

1457 args = tuple(self._remap_arg(a, recreate, wait, wait2) for a in node.args) 

1458 kwargs = { 

1459 k: self._remap_arg(v, recreate, wait, wait2) for k, v in node.kwargs.items() 

1460 } 

1461 with graph.inserting_after(insert_after): 

1462 clone = graph.call_function(node.target, args=args, kwargs=kwargs) 

1463 self._copy_recreated_meta(node, clone) 

1464 # Rematerialized views serve the backward half, so they carry the 

1465 # backward phase (see ``_insert_backward_gather``). 

1466 clone.meta["autograd_backward"] = True 

1467 recreate[node] = clone 

1468 return clone 

1469 

1470 @staticmethod 

1471 def _rewire_backward_users(recreate: Dict[fx.Node, fx.Node]) -> None: 

1472 """Point every backward consumer of an original node at its clone.""" 

1473 for original, clone in recreate.items(): 

1474 for user in list(original.users): 

1475 if user.meta.get("autograd_backward", False): 

1476 user.replace_input_with(original, clone) 

1477 

1478 def _insert_reshard_free( 

1479 self, 

1480 graph: fx.Graph, 

1481 tensor: fx.Node, 

1482 record: _UnshardedParam, 

1483 order: Dict[fx.Node, int], 

1484 backward: bool, 

1485 ) -> None: 

1486 """Free ``tensor``'s storage after its last reader in the phase.""" 

1487 last_reader = self._last_data_reader(tensor, order, backward=backward) 

1488 if last_reader is None: 

1489 return 

1490 comm_type = "fsdp_reshard_free_backward" if backward else "fsdp_reshard_free" 

1491 with graph.inserting_after(last_reader): 

1492 free_node = graph.call_function(_free_tensor_storage, args=(tensor,)) 

1493 free_node.meta["comm_type"] = comm_type 

1494 free_node.meta["param_name"] = record.param_node.meta.get("param_name") 

1495 if backward: 

1496 free_node.meta["autograd_backward"] = True 

1497 

1498 def _alias_descendants(self, root: fx.Node) -> Set[fx.Node]: 

1499 """Forward view descendants of ``root`` (nodes aliasing its storage). 

1500 

1501 Only aliasing ops propagate: an op that allocates a new tensor 

1502 (``mm``, ``addmm``, ``relu``, ...) copies the data out, so freeing 

1503 ``root``'s storage does not invalidate it and it need not be 

1504 rematerialized. Backward nodes terminate the walk. 

1505 """ 

1506 alias: Set[fx.Node] = set() 

1507 stack = [root] 

1508 while stack: 

1509 current = stack.pop() 

1510 for user in current.users: 

1511 if user is root or user in alias: 

1512 continue 

1513 if user.meta.get("autograd_backward", False): 

1514 continue 

1515 if current not in set(_iter_arg_nodes(user)): 

1516 continue 

1517 if not self._propagates_alias(user): 

1518 continue 

1519 alias.add(user) 

1520 stack.append(user) 

1521 return alias 

1522 

1523 @staticmethod 

1524 def _propagates_alias(node: fx.Node) -> bool: 

1525 """Whether ``node``'s output aliases its already-aliasing input. 

1526 

1527 Two forms propagate. Schema-declared view returns (``aten.t``, 

1528 ``aten.slice``, ...), and ``operator.getitem`` picking one output of a 

1529 multi-output view op such as ``aten.chunk`` / ``aten.split`` / 

1530 ``aten.unbind`` — getitem itself has no schema, so the alias must be 

1531 inherited from the indexed producer (which the caller has already 

1532 established is in the alias closure). 

1533 """ 

1534 if node.op != "call_function": 

1535 return False 

1536 if node.target is _GETITEM: 

1537 return True 

1538 schema = getattr(node.target, "_schema", None) 

1539 if schema is None: 

1540 return False 

1541 return any(ret.alias_info is not None for ret in schema.returns) 

1542 

1543 def _feeds_backward(self, node: fx.Node, alias_nodes: Set[fx.Node]) -> bool: 

1544 """Whether ``node`` (or an aliasing descendant) is read by backward.""" 

1545 memo: Dict[fx.Node, bool] = {} 

1546 

1547 def visit(current: fx.Node) -> bool: 

1548 """Whether ``current`` or an aliasing descendant reaches backward. 

1549 

1550 Args: 

1551 current: Node to test. 

1552 

1553 Returns: 

1554 True when a backward node consumes the node or its aliases. 

1555 """ 

1556 if current in memo: 

1557 return memo[current] 

1558 memo[current] = False # cycle guard (aliasing is a DAG) 

1559 result = False 

1560 for user in current.users: 

1561 if user.meta.get("autograd_backward", False): 

1562 result = True 

1563 break 

1564 if user in alias_nodes and visit(user): 

1565 result = True 

1566 break 

1567 memo[current] = result 

1568 return result 

1569 

1570 return visit(node) 

1571 

1572 def _last_data_reader( 

1573 self, root: fx.Node, order: Dict[fx.Node, int], backward: bool 

1574 ) -> Optional[fx.Node]: 

1575 """Latest, in graph order, op that reads ``root``'s storage data. 

1576 

1577 Walks the alias (view) chain so a free is not placed after a view 

1578 *creation* but before the non-view op that actually reads it: freeing 

1579 between ``t_2 = t(w)`` and ``mm(grad, t_2)`` would invalidate ``t_2``. 

1580 Multi-output views reach their consumers through ``getitem``, so the 

1581 walk continues through those picks instead of mistaking them for the 

1582 final reader. ``backward=False`` picks the last forward read (free 

1583 point for the unsharded parameter); ``backward=True`` the last 

1584 backward read (free point for the backward re-gather). 

1585 """ 

1586 last: Optional[fx.Node] = None 

1587 last_pos = -1 

1588 seen: Set[fx.Node] = set() 

1589 stack = [root] 

1590 while stack: 

1591 current = stack.pop() 

1592 if current in seen: 

1593 continue 

1594 seen.add(current) 

1595 for user in current.users: 

1596 if user.op == "output": 

1597 continue 

1598 if self._propagates_alias(user) and current in set( 

1599 _iter_arg_nodes(user) 

1600 ): 

1601 stack.append(user) 

1602 continue 

1603 if bool(user.meta.get("autograd_backward", False)) != backward: 

1604 continue 

1605 pos = order.get(user, -1) 

1606 if pos > last_pos: 

1607 last_pos = pos 

1608 last = user 

1609 return last 

1610 

1611 @classmethod 

1612 def _remap_arg( 

1613 cls, 

1614 arg: Any, 

1615 recreate: Dict[fx.Node, fx.Node], 

1616 wait: fx.Node, 

1617 wait2: fx.Node, 

1618 ) -> Any: 

1619 """Rewrite an op arg for a rematerialized clone (nested containers).""" 

1620 if arg is wait: 

1621 return wait2 

1622 if isinstance(arg, fx.Node): 

1623 return recreate.get(arg, arg) 

1624 if isinstance(arg, tuple): 

1625 return tuple(cls._remap_arg(a, recreate, wait, wait2) for a in arg) 

1626 if isinstance(arg, list): 

1627 return [cls._remap_arg(a, recreate, wait, wait2) for a in arg] 

1628 if isinstance(arg, dict): 

1629 return {k: cls._remap_arg(v, recreate, wait, wait2) for k, v in arg.items()} 

1630 return arg 

1631 

1632 @staticmethod 

1633 def _copy_recreated_meta(original: fx.Node, clone: fx.Node) -> None: 

1634 """Copy the module/stack/shape metadata a remat clone needs.""" 

1635 for key in ( 

1636 "nn_module_stack", 

1637 "custom", 

1638 "stack_trace", 

1639 "tensor_meta", 

1640 "val", 

1641 ): 

1642 value = original.meta.get(key) 

1643 if value is not None: 

1644 clone.meta[key] = value