33
44"""Gradient clipping and NaN cleaning."""
55
6- from collections import defaultdict
7-
86import torch
7+ import torch .distributed .fsdp as FSDP1
98import torch .nn as nn
10- from torch .distributed .fsdp import FSDPModule
11- import torch .distributed as dist
129from torch .distributed .tensor import DTensor
1310
1411
15- def _local_gradient (gradient : torch .Tensor ) -> torch .Tensor :
16- """Return the local tensor backing an FSDP2 DTensor gradient."""
17- if isinstance (gradient , DTensor ):
18- return gradient ._local_tensor
19- return gradient
20-
21-
22- def _local_gradient_groups (model : nn .Module ) -> list [list [torch .Tensor ]]:
23- """Group mutable local gradients by device and dtype."""
24- # The global L2 norm is the sum of each gradient's squared norm, and one
25- # clip coefficient scales every gradient. Compatible tensors can therefore
26- # share foreach kernels without changing the result; grouping by device and
27- # dtype satisfies foreach constraints while reducing per-tensor launches.
28- groups = defaultdict (list )
29- for parameter in model .parameters ():
30- if parameter .grad is not None :
31- gradient = _local_gradient (parameter .grad )
32- groups [(gradient .device , gradient .dtype )].append (gradient )
33- return list (groups .values ())
34-
35-
36- def _local_norm_sq (
37- gradient_groups : list [list [torch .Tensor ]],
38- device : torch .device ,
39- ) -> torch .Tensor :
40- """Compute the local squared L2 norm in float32."""
41- total_norm_sq = torch .zeros ((), device = device )
42- for gradients in gradient_groups :
43- norms = torch ._foreach_norm (gradients , 2.0 , dtype = torch .float32 )
44- total_norm_sq += torch .stack (norms ).pow (2 ).sum ()
45- return total_norm_sq
46-
47-
4812def get_grad_norm (model : nn .Module ) -> float :
49- """Compute global gradient norm, accounting for FSDP sharding.
50-
51- For FSDP models, gradients are sharded across ranks. Each rank computes
52- its local norm squared, then all-reduce sums them to get the global norm.
53- For non-FSDP models, computes the norm directly.
54-
55- Args:
56- model: The model whose gradients to analyze. Can be a vanilla PyTorch
57- module, FSDP-wrapped module, or module with FSDP sub-modules.
58-
59- Returns:
60- The L2 norm of all model gradients (global norm for distributed).
13+ """Return the total gradient L2 norm.
14+
15+ FSDP1 delegates to ``FSDP.clip_grad_norm_`` with an infinite threshold so
16+ that FSDP aggregates parameter shards over its process group. Although this
17+ does not clip finite gradients, the PyTorch API may still perform an
18+ in-place multiplication by the clamped coefficient.
19+
20+ The non-FSDP1 branch handles FSDP2 and DDP models with ``get_total_norm``.
21+ FSDP2 gradients remain DTensors, allowing the operation to dispatch the
22+ required collectives from their device mesh and placements; DDP gradients
23+ are already synchronized and can be treated as regular tensors. Unwrapped
24+ models use the same path. A DTensor result is materialized as a replicated
25+ scalar before conversion to ``float``.
6126 """
27+ if isinstance (model , FSDP1 .FullyShardedDataParallel ):
28+ total_norm = model .clip_grad_norm_ (max_norm = float ("inf" ), norm_type = 2.0 )
29+ return float (total_norm )
6230
63- is_fsdp = isinstance (model , FSDPModule )
64- gradient_groups = _local_gradient_groups (model )
65- total_norm_sq = _local_norm_sq (
66- gradient_groups ,
67- next (model .parameters ()).device ,
68- )
69-
70- if is_fsdp and dist .is_initialized ():
71- dist .all_reduce (total_norm_sq , op = dist .ReduceOp .SUM )
72- return total_norm_sq .sqrt ().item ()
31+ else :
32+ gradients = [
33+ parameter .grad
34+ for parameter in model .parameters ()
35+ if parameter .grad is not None
36+ ]
37+ if not gradients :
38+ return 0.0
39+
40+ total_norm = torch .nn .utils .get_total_norm (gradients , norm_type = 2.0 )
41+ if isinstance (total_norm , DTensor ):
42+ total_norm = total_norm .full_tensor ()
43+ return float (total_norm )
7344
7445
7546def clip_gradients (model : nn .Module , max_norm : float ) -> float :
76- """Gradient clipping for FSDP with mixed-dtype gradients (fp32 + bf16).
77-
78- FSDP shards parameters across ranks, so each rank only holds a shard of
79- gradients. We compute local norm in float32, all-reduce to get global norm,
80- then clip.
81-
82- Returns:
83- The global gradient L2 norm computed *before* clipping. Reuse this for
84- logging instead of recomputing the (post-clip) norm separately.
47+ """Clip gradients in place and return their pre-clipping total L2 norm.
48+
49+ FSDP1 uses ``FSDP.clip_grad_norm_`` so local parameter shards are aggregated
50+ over the FSDP process group before one global clipping coefficient is
51+ applied. The non-FSDP1 branch handles FSDP2 and DDP models with
52+ ``torch.nn.utils.clip_grad_norm_``. FSDP2 derives the required collectives
53+ from each gradient's DTensor mesh and placements, while DDP gradients are
54+ already synchronized before clipping. Unwrapped models use the same path.
8555 """
8656
87- is_fsdp = isinstance (model , FSDPModule )
88- if not is_fsdp :
89- # clip_grad_norm_ returns the total norm *before* clipping. Under DDP
90- # grads are already all-reduced, so the local norm equals the global one.
57+ if isinstance (model , FSDP1 . FullyShardedDataParallel ):
58+ total_norm = model . clip_grad_norm_ ( max_norm )
59+ return float ( total_norm )
60+ else :
9161 total_norm = torch .nn .utils .clip_grad_norm_ (model .parameters (), max_norm )
9262 return float (total_norm )
9363
94- # Compute local sharded norm in float32 (handles mixed dtype)
95- gradient_groups = _local_gradient_groups (model )
96- local_norm_sq = _local_norm_sq (
97- gradient_groups ,
98- next (model .parameters ()).device ,
99- )
100-
101- # All-reduce to get global norm across all ranks
102- if dist .is_initialized ():
103- torch .distributed .all_reduce (local_norm_sq )
104- total_norm = local_norm_sq .sqrt ()
105-
106- clip_coef = max_norm / (total_norm + 1e-6 )
107- clip_coef = torch .clamp (clip_coef , max = 1.0 )
108- for gradients in gradient_groups :
109- torch ._foreach_mul_ (gradients , clip_coef )
110-
111- return total_norm .item ()
112-
11364
11465def clean_nan_gradients (model : nn .Module ):
115- """Replace NaN/Inf gradients with 0."""
66+ """Replace NaN/Inf gradients with 0.
67+
68+ Args:
69+ model: Model after backward. Gradients are rewritten in place (``out=``),
70+ so DTensor gradients are cleaned through their local shard and no
71+ tensor is reallocated.
72+
73+ Note:
74+ Returns ``None`` and reports nothing: there is no signal about how much was
75+ replaced, which makes this a silent mask over divergence. A run that needs
76+ this every step is broken somewhere upstream (learning rate, loss scaling,
77+ bad batch) and zeroing gradients only postpones the diagnosis.
78+
79+ Both ``+inf`` and ``-inf`` become 0.0 rather than a large finite value —
80+ the intent is to drop the offending contribution, not to clip it.
81+
82+ Purely local, no collectives, so ranks may clean different amounts. That is
83+ consistent for sharded gradients (each rank owns a distinct shard), but it
84+ means a replicated gradient could in principle be cleaned on one rank only;
85+ DDP has already all-reduced by this point, so in practice the ranks see the
86+ same values.
87+
88+ Order matters: cleaning before ``clip_gradients`` keeps a single NaN from
89+ poisoning the whole model through the shared clip coefficient, while
90+ cleaning after leaves the coefficient already NaN and the gradients all
91+ zero.
92+ """
11693 for param in model .parameters ():
11794 if param .grad is not None :
118- grad = _local_gradient (param .grad )
119- torch .nan_to_num (grad , nan = 0.0 , posinf = 0.0 , neginf = 0.0 , out = grad )
95+ grad = (
96+ param .grad .to_local ()
97+ if isinstance (param .grad , DTensor )
98+ else param .grad
99+ )
100+ torch .nan_to_num (grad , nan = 0.0 , posinf = 0.0 , neginf = 0.0 , out = grad )
0 commit comments