Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / platform / torch / clip_grad.py: 67%

310 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +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"""Distributed-aware gradient clipping for parallel training. 

16 

17Communication is driven by each parameter's DTensorSpec (device_mesh + 

18placements) rather than any specific parallelism strategy, so a single 

19implementation covers FSDP, HSDP, TP+FSDP, and other DTensor-expressed 

20parallelisms. 

21 

22Collective safety aligned with FSDP1; numerical precision aligned with 

23FSDP2's ``_NormPartial`` norm computation path: 

24 

25* Gradient norms from sharded parameters are all-reduced across the 

26 corresponding shard process group. 

27* Non-sharded / replicated norms contribute locally without communication. 

28* **All ranks participate in the same collectives** regardless of local 

29 gradient availability, preventing collective-misalignment deadlocks. 

30 

31The finite p-norm is bit-exact with upstream only for the pure-FSDP, 

32single-dtype case; mixed sharded + replicated is mathematically correct 

33but intentionally diverges (see ``_total_norm_fsdp2_aligned``). 

34 

35Note: PP does not use DTensor layout for gradients today. Cross-stage 

36norm aggregation will require an additional manual all-reduce and is 

37left for future work. 

38""" 

39import functools 

40import math 

41import warnings 

42from collections import defaultdict, namedtuple 

43from typing import Dict, Iterable, List, Optional, Tuple, Union 

44 

45import torch 

46import torch.distributed as dist 

47 

48from hyper_parallel.core.dtensor.dtensor import DTensor 

49from hyper_parallel.core.dtensor.placement_types import Partial 

50 

51try: 

52 from torch.utils._foreach_utils import ( 

53 _device_has_foreach_support, 

54 _group_tensors_by_device_and_dtype, 

55 _has_foreach_support, 

56 ) 

57except ImportError: 

58 _device_has_foreach_support = None # type: ignore[assignment] 

59 _group_tensors_by_device_and_dtype = None # type: ignore[assignment] 

60 _has_foreach_support = None # type: ignore[assignment] 

61 

62__all__: list[str] = ["clip_grad_norm_"] 

63 

64 

65# (id(mesh) or None, shard_dims) -> list of local grads for norm computation 

66_GradGroupKey = Tuple[Optional[int], Tuple[int, ...]] 

67 

68# (mesh_dim_index, dist.ReduceOp, needs_manual_avg) 

69_PartialReduceInfo = Tuple[int, "dist.ReduceOp", bool] 

70 

71# Result of _build_grad_groups; tuple-unpacking compatible with prior 7-tuple. 

72_GradGroups = namedtuple( 

73 "_GradGroups", 

74 "grad_groups all_grads norm_grads key_per_grad mesh_cache device " 

75 "has_dtensor_grad", 

76) 

77 

78 

79# --------------------------------------------------------------------------- 

80# Reduce-op mapping 

81# --------------------------------------------------------------------------- 

82 

83_REDUCE_OP_AVG_SUPPORTED = hasattr(dist.ReduceOp, "AVG") 

84 

85_STR_TO_REDUCE_OP: Dict[str, "dist.ReduceOp"] = { 

86 "sum": dist.ReduceOp.SUM, 

87 "max": dist.ReduceOp.MAX, 

88 "min": dist.ReduceOp.MIN, 

89} 

90if _REDUCE_OP_AVG_SUPPORTED: 

91 _STR_TO_REDUCE_OP["avg"] = dist.ReduceOp.AVG 

92 

93 

94def _str_to_reduce_op(op_str: str) -> Tuple["dist.ReduceOp", bool]: 

95 """Map a ``Partial`` placement's *reduce_op* string to ``dist.ReduceOp``. 

96 

97 Returns ``(reduce_op, needs_manual_avg)`` where *needs_manual_avg* 

98 is ``True`` when ``"avg"`` is requested but the backend does not 

99 support ``dist.ReduceOp.AVG`` — the caller should use SUM and 

100 manually divide by the group size. 

101 """ 

102 lower = op_str.lower() 

103 if lower == "avg" and not _REDUCE_OP_AVG_SUPPORTED: 

104 return dist.ReduceOp.SUM, True 

105 op = _STR_TO_REDUCE_OP.get(lower) 

106 if op is None: 

107 raise ValueError( 

108 f"Unsupported Partial reduce_op: {op_str!r}. " 

109 f"Supported: {sorted(set(list(_STR_TO_REDUCE_OP) + ['avg']))}" 

110 ) 

111 return op, False 

112 

113 

114# --------------------------------------------------------------------------- 

115# Helpers 

116# --------------------------------------------------------------------------- 

117 

118def _normalize_parameters( 

119 parameters: Union["torch.nn.Module", torch.Tensor, Iterable[torch.Tensor]], 

120) -> List[torch.Tensor]: 

121 """Normalize *parameters* to a flat list of tensors. 

122 

123 * ``torch.nn.Module`` -> ``list(module.parameters())`` 

124 * single ``torch.Tensor`` -> ``[tensor]`` 

125 * iterable of tensors -> ``list(iterable)`` 

126 """ 

127 if isinstance(parameters, torch.nn.Module): 

128 return list(parameters.parameters()) 

129 if isinstance(parameters, torch.Tensor): 

130 return [parameters] 

131 return list(parameters) 

132 

133 

134def _param_device(param: torch.Tensor) -> torch.device: 

135 """Return the local device of *param* (unwrap DTensor if needed).""" 

136 if isinstance(param, DTensor): 

137 return param._local_tensor.device # pylint: disable=protected-access 

138 return param.device 

139 

140 

141def _get_grad_obj(param: torch.nn.Parameter) -> Optional[torch.Tensor]: 

142 """Return the gradient object for *param*. 

143 

144 Checks ``param.main_grad`` first (used when 

145 ``MixedPrecisionPolicy.apply_grad_on_fp32_main_grad=True``), 

146 falling back to ``param.grad``. 

147 """ 

148 grad = getattr(param, "main_grad", None) 

149 if grad is not None: 

150 return grad 

151 return param.grad 

152 

153 

154def _get_local_grad(param: torch.nn.Parameter) -> Optional[torch.Tensor]: 

155 """Return the local gradient tensor, or ``None`` if absent. 

156 

157 Supports ``main_grad`` for fp32 mixed-precision training. 

158 """ 

159 if not param.requires_grad: 

160 return None 

161 grad = _get_grad_obj(param) 

162 if grad is None: 

163 return None 

164 if isinstance(grad, DTensor): 

165 return grad._local_tensor # pylint: disable=protected-access 

166 return grad 

167 

168 

169def _get_param_mesh_info( 

170 param: torch.nn.Parameter, 

171) -> Tuple[ 

172 Optional[object], 

173 Tuple[int, ...], 

174 Tuple[_PartialReduceInfo, ...], 

175]: 

176 """Derive DeviceMesh, Shard dims and Partial info from DTensorSpec. 

177 

178 Checks the *gradient's* spec first; falls back to the *parameter's* 

179 spec when the gradient is a plain tensor on a DTensor parameter 

180 (common after FSDP/HSDP backward where ``param.grad`` is stored as 

181 the local shard tensor). 

182 

183 Returns ``(mesh, shard_dims, partial_info)`` where *partial_info* 

184 is a tuple of ``(mesh_dim, dist.ReduceOp, needs_manual_avg)`` 

185 triples that respect the ``Partial`` placement's ``reduce_op`` 

186 attribute. *needs_manual_avg* is ``True`` when ``"avg"`` was 

187 requested but the backend lacks ``dist.ReduceOp.AVG`` support. 

188 """ 

189 grad = _get_grad_obj(param) 

190 # Prefer grad's spec (most accurate); fall back to param's. 

191 spec_source = grad if isinstance(grad, DTensor) else param 

192 if not isinstance(spec_source, DTensor): 

193 return None, (), () 

194 

195 shard_dims = tuple( 

196 i for i, p in enumerate(spec_source.placements) 

197 if p.is_shard() 

198 ) 

199 partial_info = tuple( 

200 (i, *_str_to_reduce_op(p.reduce_op)) 

201 for i, p in enumerate(spec_source.placements) 

202 if isinstance(p, Partial) 

203 ) 

204 return spec_source.device_mesh, shard_dims, partial_info 

205 

206 

207def _sum_p_norms( 

208 dev_grads: List[torch.Tensor], 

209 norm_type: float, 

210 device: torch.device, 

211 total: torch.Tensor, 

212) -> None: 

213 """Accumulate sum-of-p-th-powers for *dev_grads* into *total*.""" 

214 for g in dev_grads: 

215 n = torch.linalg.vector_norm(g, norm_type) 

216 total.add_(n.to(device=device) ** norm_type) 

217 

218 

219def _foreach_p_norms( 

220 grads: List[torch.Tensor], 

221 norm_type: float, 

222 device: torch.device, 

223) -> torch.Tensor: 

224 """Fast path: fuse per-tensor norms via ``_foreach_norm``. 

225 

226 Restricted to float32 tensors to preserve the same numerical 

227 precision as ``vector_norm(dtype=float32)``. Non-float32 tensors 

228 and backends that raise ``RuntimeError`` fall back to per-tensor 

229 ``vector_norm``. 

230 """ 

231 total = torch.tensor(0.0, device=device, dtype=torch.float32) 

232 grouped = _group_tensors_by_device_and_dtype( 

233 [[g.detach() for g in grads]], 

234 ) 

235 for (dev, _), ([dev_grads], _) in grouped.items(): 

236 if ( 

237 dev_grads[0].dtype == torch.float32 

238 and _has_foreach_support(dev_grads, dev) 

239 ): 

240 try: 

241 per_norms = torch._foreach_norm( # pylint: disable=W0212 

242 dev_grads, norm_type, 

243 ) 

244 except RuntimeError: 

245 per_norms = None 

246 if per_norms is not None: 

247 total.add_( 

248 torch.stack([ 

249 n.to(device=device) ** norm_type 

250 for n in per_norms 

251 ]).sum(), 

252 ) 

253 else: 

254 _sum_p_norms(dev_grads, norm_type, device, total) 

255 else: 

256 _sum_p_norms(dev_grads, norm_type, device, total) 

257 return total 

258 

259 

260def _per_tensor_norms( 

261 grads: List[torch.Tensor], 

262 norm_type: float, 

263 device: torch.device, 

264) -> List[torch.Tensor]: 

265 """Return per-tensor norms as a list of scalar tensors on *device*.""" 

266 if not grads: 

267 return [] 

268 

269 if _group_tensors_by_device_and_dtype is None or not hasattr(torch, "_foreach_norm"): 

270 return [ 

271 torch.linalg.vector_norm(g.detach(), norm_type).to(device=device) 

272 for g in grads 

273 ] 

274 

275 norms: List[torch.Tensor] = [] 

276 grouped = _group_tensors_by_device_and_dtype( 

277 [[g.detach() for g in grads]], 

278 ) 

279 for (dev, _), ([dev_grads], _) in grouped.items(): 

280 if dev_grads and _has_foreach_support(dev_grads, dev): 

281 try: 

282 per_norms = torch._foreach_norm( # pylint: disable=W0212 

283 dev_grads, norm_type, 

284 ) 

285 except RuntimeError: 

286 per_norms = None 

287 if per_norms is not None: 

288 norms.extend( 

289 [n.to(device=device) for n in per_norms], 

290 ) 

291 continue 

292 norms.extend([ 

293 torch.linalg.vector_norm(g, norm_type).to(device=device) 

294 for g in dev_grads 

295 ]) 

296 return norms 

297 

298 

299def _compute_local_norm( # pylint: disable=R0911 

300 grads: List[torch.Tensor], 

301 norm_type: float, 

302 device: torch.device, 

303) -> torch.Tensor: 

304 """Compute the combined norm of *grads* locally in FP32. 

305 

306 When *grads* is empty, returns the **identity element** for the 

307 subsequent all-reduce so that this rank contributes a neutral value 

308 (aligned with FSDP1's ``_zero_scalar`` approach): 

309 

310 * ``inf`` -> 0 (neutral for MAX; norms are non-negative) 

311 * ``-inf`` -> +inf (neutral for MIN) 

312 * ``0`` -> 0 (neutral for SUM) 

313 * finite -> 0 (neutral for SUM) 

314 """ 

315 if not grads: 

316 if norm_type == -math.inf: 

317 return torch.tensor( 

318 float("inf"), device=device, dtype=torch.float32, 

319 ) 

320 return torch.tensor(0.0, device=device, dtype=torch.float32) 

321 

322 if norm_type == math.inf: 

323 norms = [ 

324 torch.linalg.vector_norm(g.detach(), math.inf) 

325 for g in grads 

326 ] 

327 return torch.stack(norms).max().to(device) 

328 

329 if norm_type == -math.inf: 

330 norms = [ 

331 torch.linalg.vector_norm(g.detach(), -math.inf) 

332 for g in grads 

333 ] 

334 return torch.stack(norms).min().to(device) 

335 

336 if norm_type == 0: 

337 norms = [ 

338 torch.linalg.vector_norm(g.detach(), 0) 

339 for g in grads 

340 ] 

341 return torch.stack(norms).sum().to(device) 

342 

343 # Finite p-norm: return sum of p-th powers. 

344 if ( 

345 len(grads) > 1 

346 and _group_tensors_by_device_and_dtype is not None 

347 and hasattr(torch, "_foreach_norm") 

348 ): 

349 return _foreach_p_norms(grads, norm_type, device) 

350 

351 # Scalar fallback when foreach utilities are unavailable. 

352 norms = [ 

353 torch.linalg.vector_norm(g.detach(), norm_type) 

354 for g in grads 

355 ] 

356 norm_powers = [n.to(device=device) ** norm_type for n in norms] 

357 return torch.stack(norm_powers).sum() 

358 

359 

360# --------------------------------------------------------------------------- 

361# Total norm aggregation with collectives 

362# --------------------------------------------------------------------------- 

363 

364def _get_total_norm( 

365 grad_groups: Dict[_GradGroupKey, List[torch.Tensor]], 

366 norm_type: float, 

367 mesh_cache: Dict[int, object], 

368 device: torch.device, 

369 norm_grads: List[torch.Tensor], 

370 key_per_grad: List[_GradGroupKey], 

371) -> torch.Tensor: 

372 """Compute total gradient norm with per-group all-reduce. 

373 

374 ``norm_grads`` (parallel to ``key_per_grad``) holds the tensor whose 

375 norm to take per parameter; only the finite p-norm path consumes it. 

376 """ 

377 if norm_type == math.inf: 

378 return _total_norm_inf( 

379 grad_groups, norm_type, mesh_cache, device, 

380 dist.ReduceOp.MAX, 

381 ) 

382 

383 if norm_type == -math.inf: 

384 return _total_norm_inf( 

385 grad_groups, norm_type, mesh_cache, device, 

386 dist.ReduceOp.MIN, 

387 ) 

388 

389 if norm_type == 0: 

390 return _total_norm_sum( 

391 grad_groups, norm_type, mesh_cache, device, 

392 ) 

393 

394 # Finite p-norm: FSDP2-aligned sequence. 

395 total_p = _total_norm_fsdp2_aligned( 

396 grad_groups, norm_type, mesh_cache, device, 

397 norm_grads, key_per_grad, 

398 ) 

399 return total_p ** (1.0 / norm_type) 

400 

401 

402def _total_norm_inf( # pylint: disable=R0913,R0917 

403 grad_groups, norm_type, mesh_cache, device, reduce_op, 

404): 

405 """Shared logic for inf / -inf norms.""" 

406 group_norms: List[torch.Tensor] = [] 

407 for (mesh_id, shard_dims), grads in grad_groups.items(): 

408 local_norm = _compute_local_norm(grads, norm_type, device) 

409 if mesh_id is not None: 

410 mesh = mesh_cache[mesh_id] 

411 for dim in shard_dims: 

412 dist.all_reduce( 

413 local_norm, op=reduce_op, 

414 group=mesh.get_group(dim), 

415 ) 

416 group_norms.append(local_norm) 

417 if not group_norms: 

418 if norm_type == -math.inf: 

419 return torch.tensor(float("inf"), device=device) 

420 return torch.tensor(0.0, device=device) 

421 stacked = torch.stack(group_norms) 

422 return stacked.max() if reduce_op == dist.ReduceOp.MAX else stacked.min() 

423 

424 

425def _total_norm_sum(grad_groups, norm_type, mesh_cache, device): 

426 """Shared logic for finite norms and L0 (all use SUM all-reduce).""" 

427 total = torch.tensor(0.0, device=device) 

428 for (mesh_id, shard_dims), grads in grad_groups.items(): 

429 local_val = _compute_local_norm(grads, norm_type, device) 

430 if mesh_id is not None: 

431 mesh = mesh_cache[mesh_id] 

432 for dim in shard_dims: 

433 dist.all_reduce( 

434 local_val, op=dist.ReduceOp.SUM, 

435 group=mesh.get_group(dim), 

436 ) 

437 total.add_(local_val) 

438 return total 

439 

440 

441def _reduction_signature(grad_groups, mesh_cache): 

442 """Bucket grad-group keys by the *process group(s)* they reduce over. 

443 

444 Two keys that reduce over the same set of process groups (same global 

445 ranks per shard dim) must be **pooled** so their norms accumulate in 

446 one stack -- matching FSDP2's single foreach-norm + single reduce and 

447 keeping the loss bit-exact even when params live on several distinct 

448 ``DeviceMesh`` objects that share the same DP process group (common 

449 for multi-component models). Keys that reduce over *different* 

450 process groups (TP+FSDP heterogeneous sharding, expert parallel) get 

451 separate buckets, each reduced over its own group. 

452 

453 Returns ``(key_to_sig, sig_groups, sig_order)``: 

454 

455 * ``key_to_sig`` -- ``key -> signature`` (hashable; ``()`` = replicate 

456 / no communication). 

457 * ``sig_groups`` -- ``signature -> list[ProcessGroup]`` to all-reduce. 

458 * ``sig_order`` -- signatures in first-seen (parameter) order, so all 

459 ranks issue the same collectives in the same order. 

460 """ 

461 key_to_sig: Dict[_GradGroupKey, Tuple] = {} 

462 sig_groups: Dict[Tuple, List[object]] = {} 

463 sig_order: List[Tuple] = [] 

464 for mesh_id, shard_dims in grad_groups: 

465 if shard_dims and mesh_id is not None: 

466 mesh = mesh_cache[mesh_id] 

467 groups = [mesh.get_group(dim) for dim in shard_dims] 

468 sig = tuple( 

469 tuple(dist.get_process_group_ranks(group)) for group in groups 

470 ) 

471 else: 

472 groups = [] 

473 sig = () 

474 key_to_sig[(mesh_id, shard_dims)] = sig 

475 if sig not in sig_groups: 

476 sig_groups[sig] = groups 

477 sig_order.append(sig) 

478 return key_to_sig, sig_groups, sig_order 

479 

480 

481def _total_norm_fsdp2_aligned(grad_groups, norm_type, mesh_cache, device, 

482 norm_grads, key_per_grad): 

483 """FSDP2-aligned norm for finite p-norms. 

484 

485 Grads are bucketed by the *process group* they reduce over (see 

486 :func:`_reduction_signature`); ``norm_grads`` supplies, in global 

487 parameter order, the Partial-reduced (already-global) view for Partial 

488 grads and the raw local grad otherwise. Each bucket does ONE ``stack`` 

489 → ONE ``vector_norm`` → ``^p`` → ONE ``all_reduce SUM`` per shard dim, 

490 and the buckets are summed locally. 

491 

492 Reduction rules: 

493 

494 * Sharded bucket -- reduce over its own group(s). Same-group params 

495 across distinct ``DeviceMesh`` objects pool into one reduce; distinct 

496 groups (TP+FSDP heterogeneous, expert parallel) stay separate -- the 

497 per-group convention also used by :func:`_total_norm_inf` / 

498 :func:`_total_norm_sum` and by VeOmni / torchtitan. 

499 * Replicate bucket (signature ``()``) -- contribute norm² locally with 

500 NO communication; reducing it over the shard group would over-count 

501 it ``shard_world_size`` times (the FSDP1 convention). 

502 

503 Empty-but-present buckets still issue their all_reduce (identity ``0`` 

504 for SUM) so all ranks run the same collectives. 

505 

506 Bit-exact with upstream ``torch.nn.utils.clip_grad_norm_`` only for the 

507 pure-FSDP, single-bucket, single-dtype case. Mixed shard + replicate is 

508 mathematically correct but intentionally diverges (upstream folds the 

509 replicate norm into one ``_NormPartial`` reduce and over-counts it); 

510 mixed-dtype follows upstream's :func:`_per_tensor_norms` device/dtype 

511 regrouping rather than strict global order. 

512 

513 Returns the global sum of p-th powers (caller takes p-th root). 

514 """ 

515 key_to_sig, sig_groups, sig_order = _reduction_signature( 

516 grad_groups, mesh_cache, 

517 ) 

518 

519 # Bucket norms by reduction signature, preserving global parameter order. 

520 sig_grads: Dict[Tuple, List[torch.Tensor]] = {sig: [] for sig in sig_order} 

521 for grad, key in zip(norm_grads, key_per_grad): 

522 sig_grads[key_to_sig[key]].append(grad) 

523 

524 total_p = torch.tensor(0.0, device=device, dtype=torch.float32) 

525 for sig in sig_order: 

526 grads = sig_grads[sig] 

527 if grads: 

528 norms = _per_tensor_norms(grads, norm_type, device) 

529 local_p = torch.linalg.vector_norm( 

530 torch.stack(norms).to(torch.float32), norm_type, 

531 ) ** norm_type 

532 else: 

533 local_p = torch.tensor(0.0, device=device, dtype=torch.float32) 

534 

535 for group in sig_groups[sig]: 

536 dist.all_reduce(local_p, op=dist.ReduceOp.SUM, group=group) 

537 

538 total_p = total_p + local_p 

539 

540 return total_p 

541 

542 

543def _build_coalesce_buffer( 

544 param_infos: List[Tuple], 

545 indices: List[int], 

546) -> Tuple[List[torch.Tensor], List[int], List[bool], List[int]]: 

547 """Build flat fp32 chunks for one coalesce group. 

548 

549 Returns ``(chunks, chunk_sizes, has_grad, active_indices)``. 

550 Frozen params are skipped; trainable grad-free params contribute 

551 zeros so the collective matches ranks that have a grad. 

552 """ 

553 chunks: List[torch.Tensor] = [] 

554 chunk_sizes: List[int] = [] 

555 has_grad: List[bool] = [] 

556 active_indices: List[int] = [] 

557 

558 for idx in indices: 

559 param = param_infos[idx][0] 

560 local_grad = param_infos[idx][1] 

561 if local_grad is not None: 

562 chunks.append( 

563 local_grad.detach().reshape(-1).to(torch.float32), 

564 ) 

565 chunk_sizes.append(local_grad.numel()) 

566 has_grad.append(True) 

567 active_indices.append(idx) 

568 elif param.requires_grad: 

569 local_p = ( 

570 param._local_tensor # pylint: disable=W0212 

571 if isinstance(param, DTensor) else param.data 

572 ) 

573 numel = local_p.numel() 

574 chunks.append( 

575 torch.zeros( 

576 numel, device=local_p.device, 

577 dtype=torch.float32, 

578 ), 

579 ) 

580 chunk_sizes.append(numel) 

581 has_grad.append(False) 

582 active_indices.append(idx) 

583 

584 return chunks, chunk_sizes, has_grad, active_indices 

585 

586 

587def _coalesce_partial_reduce( # pylint: disable=R0914 

588 param_infos: List[Tuple], 

589 mesh_cache: Dict[int, object], 

590) -> Dict[int, torch.Tensor]: 

591 """Coalesce Partial all-reduces: O(N) collectives → O(G). 

592 

593 Groups parameters sharing the same ``(mesh, partial_info)`` and 

594 flattens their gradients (or zeros for trainable grad-free params) 

595 into a single fp32 buffer. **One** ``all_reduce`` per buffer 

596 replaces the previous per-parameter collective calls. 

597 

598 For TP+FSDP (all params share the same mesh / placements), this 

599 turns ~200 individual all-reduces into 1 — saving 10-20 ms per 

600 training step at typical HCCS/NCCL latencies. 

601 

602 Frozen params (``requires_grad=False``) are consistently grad-free 

603 across all ranks and are excluded from the buffer to avoid wasting 

604 bandwidth. 

605 

606 All buffers use float32 to guarantee dtype consistency across ranks 

607 in mixed-precision training (grad may be fp16/bf16 while param is 

608 fp32). 

609 

610 Returns a dict mapping *param_infos* index → reduced gradient view 

611 (1-D fp32 slice of the coalesced buffer). Only entries for params 

612 with actual gradients are included. 

613 """ 

614 # Group by Partial coalesce key: (mesh_id, partial_info) 

615 coalesce_groups: Dict[ 

616 Tuple, List[int], 

617 ] = defaultdict(list) 

618 for idx, info in enumerate(param_infos): 

619 mesh, partial_info = info[2], info[3] 

620 if partial_info: 

621 if mesh is None: 

622 raise RuntimeError( 

623 "clip_grad_norm_: parameter has Partial placements " 

624 "but no DeviceMesh. This is a DTensor invariant " 

625 "violation." 

626 ) 

627 pck = (id(mesh), partial_info) 

628 coalesce_groups[pck].append(idx) 

629 

630 reduced: Dict[int, torch.Tensor] = {} 

631 

632 for (mesh_id, partial_info), indices in coalesce_groups.items(): 

633 mesh = mesh_cache[mesh_id] 

634 chunks, chunk_sizes, has_grad, active_indices = ( 

635 _build_coalesce_buffer(param_infos, indices) 

636 ) 

637 

638 if not chunks: 

639 continue # all params frozen, no collective needed 

640 

641 # Sanity check: same mesh → same device. Fail fast on 

642 # misconfigured inputs rather than silent NCCL errors. 

643 buf_device = chunks[0].device 

644 for chunk in chunks[1:]: 

645 if chunk.device != buf_device: 

646 raise RuntimeError( 

647 f"clip_grad_norm_: parameters in the same Partial " 

648 f"coalesce group are on different devices " 

649 f"({buf_device} vs {chunk.device}). All parameters " 

650 f"sharing the same DeviceMesh must reside on the " 

651 f"same local device." 

652 ) 

653 

654 buf = torch.cat(chunks) 

655 

656 for pdim, reduce_op, needs_avg in partial_info: 

657 group = mesh.get_group(pdim) 

658 dist.all_reduce(buf, op=reduce_op, group=group) 

659 if needs_avg: 

660 buf /= dist.get_world_size(group=group) 

661 

662 # Extract views for params with actual gradients. 

663 offset = 0 

664 for i, idx in enumerate(active_indices): 

665 numel = chunk_sizes[i] 

666 if has_grad[i]: 

667 reduced[idx] = buf[offset:offset + numel] 

668 offset += numel 

669 

670 return reduced 

671 

672 

673def _build_grad_groups( # pylint: disable=R0914 

674 params: List[torch.Tensor], 

675) -> Tuple[ 

676 Dict[_GradGroupKey, List[torch.Tensor]], 

677 List[torch.Tensor], 

678 List[torch.Tensor], 

679 List[_GradGroupKey], 

680 Dict[int, object], 

681 torch.device, 

682 bool, 

683]: 

684 """Classify parameters into grad groups and pre-reduce Partial grads. 

685 

686 Group structure is derived from *parameter* DTensorSpecs (always 

687 present on every rank) rather than gradients (which may be ``None`` 

688 on some ranks). This ensures every rank enters the same set of 

689 collectives, preventing deadlocks (aligned with FSDP1 where all 

690 ranks unconditionally execute the same all-reduce path). 

691 

692 Partial gradients are reduced via a **coalesced** all-reduce 

693 (see ``_coalesce_partial_reduce``), turning O(N) per-parameter 

694 collectives into O(G) where G is the number of distinct 

695 ``(mesh, partial_info)`` groups (typically 1 for TP+FSDP). 

696 

697 Returns 

698 ``(grad_groups, all_grads, norm_grads, key_per_grad, mesh_cache, 

699 device, has_dtensor_grad)``. ``grad_groups`` maps each 

700 ``(mesh_id, shard_dims)`` key to its grads; ``all_grads`` is the 

701 flat list of raw local grads (global parameter order) scaled 

702 in-place by the clip step; ``norm_grads`` is parallel to 

703 ``all_grads`` but holds the Partial-reduced view for Partial grads 

704 (the value whose norm is taken on the finite-p path); 

705 ``key_per_grad`` is parallel to both and maps each grad back to its 

706 group key. 

707 """ 

708 # --- Phase 1: classify all parameters --- 

709 param_infos: List[Tuple] = [] 

710 mesh_cache: Dict[int, object] = {} 

711 device: Optional[torch.device] = None 

712 

713 for param in params: 

714 mesh, shard_dims, partial_info = _get_param_mesh_info(param) 

715 key: _GradGroupKey = ( 

716 id(mesh) if mesh is not None else None, shard_dims, 

717 ) 

718 if mesh is not None: 

719 mesh_cache[id(mesh)] = mesh 

720 if device is None: 

721 device = _param_device(param) 

722 local_grad = _get_local_grad(param) 

723 param_infos.append( 

724 (param, local_grad, mesh, partial_info, key), 

725 ) 

726 

727 if device is None: 

728 device = torch.device("cpu") 

729 

730 # --- Phase 2: coalesced Partial reduction (O(N) → O(G)) --- 

731 reduced = _coalesce_partial_reduce(param_infos, mesh_cache) 

732 

733 # --- Phase 3: build grad_groups --- 

734 grad_groups: Dict[_GradGroupKey, List[torch.Tensor]] = defaultdict( 

735 list, 

736 ) 

737 all_grads: List[torch.Tensor] = [] 

738 # norm_grads / key_per_grad are parallel to all_grads (see Returns): the 

739 # norm-input view (Partial-reduced where coalesced, else raw local) and 

740 # the group key per grad, kept in global parameter order. 

741 norm_grads: List[torch.Tensor] = [] 

742 key_per_grad: List[_GradGroupKey] = [] 

743 has_dtensor_grad = False 

744 

745 for idx, info in enumerate(param_infos): 

746 param, local_grad, key = info[0], info[1], info[4] 

747 if local_grad is None: 

748 # Ensure the key exists so the Shard norm all-reduce is 

749 # entered even when this rank has no grads for the group. 

750 if key not in grad_groups: 

751 grad_groups[key] = [] 

752 continue 

753 

754 grad_obj = _get_grad_obj(param) 

755 if isinstance(grad_obj, DTensor): 

756 has_dtensor_grad = True 

757 all_grads.append(local_grad) 

758 key_per_grad.append(key) 

759 if idx in reduced: 

760 grad_groups[key].append(reduced[idx]) 

761 norm_grads.append(reduced[idx]) 

762 else: 

763 grad_groups[key].append(local_grad) 

764 norm_grads.append(local_grad) 

765 

766 return _GradGroups( 

767 grad_groups, all_grads, norm_grads, key_per_grad, 

768 mesh_cache, device, has_dtensor_grad, 

769 ) 

770 

771 

772def _clip_grads_with_norm_( 

773 all_grads: List[torch.Tensor], 

774 max_norm: float, 

775 total_norm: torch.Tensor, 

776 foreach: Optional[bool] = None, 

777) -> None: 

778 """Scale gradients in-place so the total norm <= *max_norm*.""" 

779 clip_coef = max_norm / (total_norm + 1e-6) 

780 clip_coef_clamped = torch.clamp(clip_coef, max=1.0) 

781 

782 if _group_tensors_by_device_and_dtype is not None: 

783 grouped_grads = _group_tensors_by_device_and_dtype( 

784 [all_grads], 

785 ) 

786 for (device, dtype), ([device_grads], _) in grouped_grads.items(): 

787 use_foreach = ( 

788 foreach is None and _has_foreach_support(device_grads, device) 

789 ) or (foreach and _device_has_foreach_support(device)) 

790 if use_foreach: 

791 torch._foreach_mul_( # pylint: disable=W0212 

792 device_grads, 

793 clip_coef_clamped.to(device=device, dtype=dtype), 

794 ) 

795 elif foreach: 

796 raise RuntimeError( 

797 f"foreach=True was passed, but can't use the " 

798 f"foreach API on {device.type} tensors" 

799 ) 

800 else: 

801 clip_coef_clamped_cast = clip_coef_clamped.to(device=device, dtype=dtype) 

802 for g in device_grads: 

803 g.mul_(clip_coef_clamped_cast) 

804 else: 

805 # Fallback when _foreach_utils is unavailable. 

806 if foreach: 

807 raise RuntimeError( 

808 "foreach=True was passed, but " 

809 "torch.utils._foreach_utils is not available" 

810 ) 

811 for grad in all_grads: 

812 grad.mul_(clip_coef_clamped.to(grad.device, grad.dtype)) 

813 

814 

815# --------------------------------------------------------------------------- 

816# Public API 

817# --------------------------------------------------------------------------- 

818 

819@torch.no_grad() 

820def clip_grad_norm_( 

821 parameters: Union[ 

822 "torch.nn.Module", torch.Tensor, Iterable[torch.Tensor], 

823 ], 

824 max_norm: float, 

825 norm_type: float = 2.0, 

826 error_if_nonfinite: bool = False, 

827 foreach: Optional[bool] = None, 

828) -> torch.Tensor: 

829 """Compute and clip gradient norm for distributed models. 

830 

831 Drop-in replacement for the standard ``clip_grad_norm_`` that 

832 correctly handles DTensor-sharded parameters by deriving 

833 communication from each parameter's DTensorSpec. 

834 

835 .. warning:: This function uses collective communications. It 

836 **must be called on all ranks** to avoid deadlocks. Aligned 

837 with FSDP1: every rank participates in the same collectives 

838 regardless of local gradient availability. 

839 

840 Communication is derived from each parameter's DTensorSpec: 

841 

842 * ``Shard`` on mesh dim *d* -- all-reduce norm statistics 

843 across ``device_mesh.get_group(d)`` 

844 * ``Partial`` on mesh dim *d* -- all-reduce gradient values 

845 using the placement's ``reduce_op`` before norm computation 

846 * ``Replicate`` / plain tensor -- no communication 

847 

848 This covers FSDP, HSDP, TP+FSDP, and any combination expressible 

849 via DTensor placements. PP cross-stage norm aggregation is not 

850 yet handled (requires manual all-reduce across stages). 

851 

852 Args: 

853 parameters: An ``nn.Module``, a single ``Tensor``, or an 

854 iterable of ``Tensor`` s whose gradients to clip. 

855 max_norm: Maximum allowed gradient norm. 

856 norm_type: Type of the norm (default ``2.0``). 

857 error_if_nonfinite: If ``True``, raise a ``RuntimeError`` 

858 when the total norm is non-finite. Default ``False``. 

859 foreach: Use the faster foreach-based implementation for the 

860 gradient clipping step. If ``None``, use the foreach 

861 implementation for devices that support it and silently 

862 fall back to the per-tensor implementation for others. 

863 Default ``None``. 

864 

865 Returns: 

866 The total (unclipped) gradient norm as a scalar tensor, 

867 cast to the promoted dtype of all gradient tensors. 

868 """ 

869 max_norm = float(max_norm) 

870 norm_type = float(norm_type) 

871 

872 params = _normalize_parameters(parameters) 

873 

874 ( 

875 grad_groups, all_grads, norm_grads, key_per_grad, 

876 mesh_cache, device, has_dtensor_grad, 

877 ) = _build_grad_groups(params) 

878 

879 # -- Norm + clip (all ranks participate) -------------------------------- 

880 # _compute_local_norm returns identity elements for empty groups, 

881 # so the subsequent all-reduce is safe and semantically neutral. 

882 total_norm = _get_total_norm( 

883 grad_groups, norm_type, mesh_cache, device, 

884 norm_grads, key_per_grad, 

885 ) 

886 

887 if error_if_nonfinite and torch.logical_or( 

888 total_norm.isnan(), total_norm.isinf() 

889 ): 

890 raise RuntimeError( 

891 f"The total norm of order {norm_type} for gradients from " 

892 "`parameters` is non-finite, so it cannot be clipped. To " 

893 "disable this error and scale the gradients by the " 

894 "non-finite norm anyway, set " 

895 "`error_if_nonfinite=False`" 

896 ) 

897 

898 if all_grads: 

899 # Disable foreach for dtensor-backed grads to avoid dispatch issues. 

900 effective_foreach = False if has_dtensor_grad and foreach is None else foreach 

901 _clip_grads_with_norm_( 

902 all_grads, max_norm, total_norm, effective_foreach, 

903 ) 

904 

905 # Promote return dtype to match gradient dtypes (FSDP1 convention). 

906 # When this rank has no gradients, return in the default FP32 dtype 

907 # (same as FSDP1's behavior to avoid extra communication). 

908 if not all_grads: 

909 warnings.warn( 

910 "clip_grad_norm_ called on this rank with no gradients -- " 

911 "returning the local norm in the default dtype " 

912 f"{total_norm.dtype}", 

913 stacklevel=2, 

914 ) 

915 return total_norm 

916 

917 total_norm_dtype = functools.reduce( 

918 torch.promote_types, 

919 [g.dtype for g in all_grads], 

920 ) 

921 # Return global all-reduced norm, consistent with torchtitan's 

922 # full_tensor() approach — .item() returns the correct global value. 

923 return total_norm.to(total_norm_dtype)