@@ -129,11 +129,21 @@ def get_coefficient_iterator(
129129 return islice (base , steps )
130130
131131
132- def distributed_normalize_p2 (x : torch .Tensor , eps : float , group : torch .distributed .ProcessGroup ) -> torch .Tensor :
133- """Normalize a tensor in a distributed way."""
134- x_sq_sum = (x * x ).sum ()
132+ def distributed_normalize_p2 (
133+ x : torch .Tensor , eps : float , group : torch .distributed .ProcessGroup , normalize_in_double : bool = False
134+ ) -> torch .Tensor :
135+ """Normalize a tensor by its distributed Frobenius norm.
136+
137+ When ``normalize_in_double`` is set, the squared sum is accumulated in float64 so that tiny
138+ entries do not underflow to zero when squared in float32.
139+ """
140+ x_sq = x .double () if normalize_in_double else x
141+ x_sq_sum = (x_sq * x_sq ).sum ()
135142 torch .distributed .all_reduce (x_sq_sum , op = torch .distributed .ReduceOp .SUM , group = group )
136- return x / torch .sqrt (x_sq_sum ).clamp_min (eps )
143+ norm = torch .sqrt (x_sq_sum ).to (x .dtype )
144+ if not normalize_in_double :
145+ norm .clamp_min_ (eps )
146+ return x / norm
137147
138148
139149def newton_schulz (
@@ -145,6 +155,7 @@ def newton_schulz(
145155 transpose : bool | None = None ,
146156 tp_group : torch .distributed .ProcessGroup | None = None ,
147157 use_syrk : bool = False ,
158+ normalize_in_double : bool = False ,
148159) -> torch .Tensor :
149160 """Use Newton-Schulz iteration to compute the zeroth power / orthogonalization of x.
150161
@@ -177,6 +188,10 @@ def newton_schulz(
177188 If None, will be determined based on the size of the tensor.
178189 tp_group: The process group for communication if input is distributed.
179190 use_syrk: Whether to use the Triton kernel for the Newton-Schulz iteration.
191+ normalize_in_double: Whether to reduce the Frobenius norm in float64. This keeps the squared
192+ sum out of float32 underflow for inputs with very small entries, at the cost of a float64
193+ reduction. Without customized kernels, manually handle scaling without triggering a device to host
194+ sync are usually more expensive than using double.
180195
181196 Returns:
182197 The orthogonalization of x.
@@ -192,13 +207,17 @@ def newton_schulz(
192207 if transpose :
193208 x = x .mT
194209
195- # Ensure spectral norm is at most 1.
196- # NOTE: ``eps`` is a divide-by-zero guard; it must stay well below any realistic ``||x||_F``
197- # yet remain fp32-safe when squared. See issue #229.
210+ # Ensure spectral norm is at most 1 by normalizing with the Frobenius norm. Reducing in float64
211+ # (``normalize_in_double``) keeps the squared sum out of float32 underflow for tiny-norm inputs.
198212 if tp_group is not None :
199- X = distributed_normalize_p2 (x , eps , tp_group )
213+ X = distributed_normalize_p2 (x , eps , tp_group , normalize_in_double )
200214 else :
201- X = torch .nn .functional .normalize (x , p = 2 , dim = (- 2 , - 1 ), eps = eps ) # type: ignore[arg-type]
215+ if not normalize_in_double :
216+ X = torch .nn .functional .normalize (x , p = 2 , dim = (- 2 , - 1 ), eps = eps ) # type: ignore[arg-type]
217+ else :
218+ # eps is ignored when normalize in double.
219+ norm = torch .linalg .vector_norm (x , dim = (- 2 , - 1 ), keepdim = True , dtype = torch .float64 ).to (x .dtype )
220+ X = x / norm
202221
203222 if coefficient_type in _COEFFICIENT_SETS :
204223 coefficient_sets = _COEFFICIENT_SETS [coefficient_type ]
0 commit comments