Kernels
dongseokmotif Claude Sonnet 4.6 commited on
Commit
573242f
·
1 Parent(s): 33929c0

Replace hardcoded NS coefficients with analytically optimal ones [skip-build]

Browse files

Replaces the five fixed (a, b, c) quintic coefficients in
`_zeropower_via_newtonschulz5` with per-step optimal coefficients computed by
`_optimal_composition` / `_optimal_quintic` via the equioscillation (Chebyshev)
criterion. The precomputed `_coeffs_list` (10 iters, l=1e-3, safety_factor_eps=1e-2,
cushion=0.02) minimizes the worst-case deviation from 1 over the singular-value
interval, and the function now supports arbitrary `steps` by repeating the last
coefficient set. Docstrings updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. torch-ext/optimizer/newton_schulz.py +97 -20
torch-ext/optimizer/newton_schulz.py CHANGED
@@ -1,26 +1,106 @@
1
  import torch
 
 
 
2
 
3
  from .matmul_transpose_triton import matmul_transpose_assign
 
4
 
5
  COMM_DTYPE = torch.bfloat16
6
  DEFAULT_CHUNK_SIZE_RATIO = 4
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- # This code snippet is a modified version adapted from the following GitHub repositories:
10
- # https://github.com/KellerJordan/Muon/blob/master/muon.py
11
- # Muon's Newton–Schulz iteration causes high variance in singular values
12
- # Idea: give each iteration its own 3 coefficients and optimize them via gradient descent.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  @torch.no_grad()
14
- # matmul_transpose_assign from : https://github.com/nil0x9/flash-muon
15
  def _zeropower_via_newtonschulz5(G, steps):
16
  """
17
- Newton-Schulz iteration to compute the zeroth power / orthogonalization of G. We opt to use a
18
- quintic iteration whose coefficients are selected to maximize the slope at zero. For the purpose
19
- of minimizing steps, it turns out to be empirically effective to keep increasing the slope at
20
- zero even beyond the point where the iteration no longer converges all the way to one everywhere
21
- on the interval. This iteration therefore does not produce UV^T but rather something like US'V^T
22
- where S' is diagonal with S_{ii}' ~ Uniform(0.5, 1.5), which turns out not to hurt model
23
- performance at all relative to UV^T, where USV^T = G is the SVD.
 
 
 
 
 
 
 
24
  """
25
  assert len(G.shape) == 2
26
  assert G.dtype == COMM_DTYPE
@@ -28,18 +108,14 @@ def _zeropower_via_newtonschulz5(G, steps):
28
 
29
  if G.size(0) > G.size(1):
30
  X = X.T
31
- # Ensure spectral norm is at most 1
32
  X = X / (X.norm() + 1e-7)
 
 
33
  buf1 = torch.empty(X.size(0), X.size(0), dtype=X.dtype, device=X.device)
34
  buf2 = torch.empty(X.size(0), X.size(0), dtype=X.dtype, device=X.device)
35
  # Perform the NS iterations
36
- for a, b, c in [
37
- (4.0848, -6.8946, 2.9270),
38
- (3.9505, -6.3029, 2.6377),
39
- (3.7418, -5.5913, 2.3037),
40
- (2.8769, -3.1427, 1.2046),
41
- (2.8366, -3.0525, 1.2012),
42
- ]:
43
  matmul_transpose_assign(X, buf1)
44
  matmul_transpose_assign(buf1, buf2)
45
  buf1.mul_(b).add_(buf2, alpha=c)
@@ -47,4 +123,5 @@ def _zeropower_via_newtonschulz5(G, steps):
47
 
48
  if G.size(0) > G.size(1):
49
  X = X.T
 
50
  return X
 
1
  import torch
2
+ from itertools import repeat
3
+ from math import inf, sqrt
4
+ import numpy as np
5
 
6
  from .matmul_transpose_triton import matmul_transpose_assign
7
+ from loguru import logger
8
 
9
  COMM_DTYPE = torch.bfloat16
10
  DEFAULT_CHUNK_SIZE_RATIO = 4
11
 
12
+ def _optimal_quintic(l, u):
13
+ """
14
+ Use the simplified Remez algorithm to find the optimal odd quintic approximant
15
+ to the constant function x -> 1 over the interval [l, u].
16
+
17
+ Returns (a, b, c) for p(x) = ax + bx^3 + cx^5 that minimizes the maximum
18
+ approximation error max_{x in [l,u]} |p(x) - 1|. Iterates by updating the
19
+ two interior equioscillation nodes q, r until convergence. Returns the
20
+ closed-form equioscillating solution when l ≈ u.
21
+ """
22
+ assert 0 <= l <= u
23
+ if 1 - 5e-6 <= l / u:
24
+ return (15/8)/u, (-10/8)/(u**3), (3/8)/(u**5)
25
+ q = (3*l + u) / 4
26
+ r = (l + 3*u) / 4
27
+ E, old_E = inf, None
28
+ while not old_E or abs(old_E - E) > 1e-15:
29
+ old_E = E
30
+ LHS = np.array([
31
+ [l, l**3, l**5, 1],
32
+ [q, q**3, q**5, -1],
33
+ [r, r**3, r**5, 1],
34
+ [u, u**3, u**5, -1],
35
+ ])
36
+ a, b, c, E = np.linalg.solve(LHS, np.ones(4))
37
+ q, r = np.sqrt((-3*b + np.array([-1, 1]) *
38
+ sqrt(9*b**2 - 20*a*c)) / (10*c))
39
+ return float(a), float(b), float(c)
40
 
41
+
42
+ def _optimal_composition(l, num_iters, safety_factor_eps=0, cushion=0):
43
+ """
44
+ Compute the Polar Express coefficient series for `num_iters` quintic iterations.
45
+
46
+ Builds a sequence of per-step optimal odd quintic coefficients (a, b, c) that
47
+ compose to map singular values from [l, 1] toward 1. At each step:
48
+ 1. Solves `_optimal_quintic` on [max(l, cushion*u), u]. The `cushion`
49
+ prevents near-zero singular values from stalling by raising the effective
50
+ lower bound; if it is active (cushion*u > l), the coefficients are
51
+ rescaled so that p(l) and p(u) are centered around 1 w.r.t. the true [l, u].
52
+ 2. Deflates the coefficients by (1 + safety_factor_eps)^degree for all but the
53
+ last iteration, providing numerical headroom at the cost of a slightly slower
54
+ final convergence step.
55
+ 3. Advances the interval: l <- p(l), u <- 2 - p(l) (by symmetry of p around 1).
56
+
57
+ Returns a list of (a, b, c) tuples, one per iteration.
58
+
59
+ Reference: Amsel et al., "The Polar Express: Optimal Matrix Sign Methods and
60
+ Their Application to the Muon Algorithm", https://arxiv.org/abs/2505.16932
61
+ """
62
+ u = 1
63
+ assert 0 <= l <= u
64
+ safety_factor = 1 + safety_factor_eps
65
+ coefficients = []
66
+ for iter in range(num_iters):
67
+ a, b, c = _optimal_quintic(max(l, cushion*u), u)
68
+ if cushion*u > l:
69
+ pl = a*l + b*l**3 + c*l**5
70
+ pu = a*u + b*u**3 + c*u**5
71
+ rescaler = 2/(pl + pu)
72
+ a *= rescaler; b *= rescaler; c *= rescaler
73
+ if iter < num_iters - 1:
74
+ a /= safety_factor; b /= safety_factor**3; c /= safety_factor**5
75
+ coefficients.append((a, b, c))
76
+ l = a*l + b*l**3 + c*l**5
77
+ u = 2 - l
78
+ return coefficients
79
+
80
+
81
+ _coeffs_list = _optimal_composition(l=1e-3, num_iters=10, safety_factor_eps=1e-2, cushion=0.02)
82
+
83
+ # This code is adapted from:
84
+ # KellerJordan/Muon (https://github.com/KellerJordan/Muon/blob/master/muon.py)
85
+ # NoahAmsel/PolarExpress (https://github.com/NoahAmsel/PolarExpress)
86
+ # matmul_transpose_assign kernel from nil0x9/flash-muon (https://github.com/nil0x9/flash-muon)
87
  @torch.no_grad()
 
88
  def _zeropower_via_newtonschulz5(G, steps):
89
  """
90
+ Compute the polar factor of G via the Polar Express method.
91
+
92
+ Applies `steps` quintic iterations X <- aX + bX^3 + cX^5, where (a, b, c)
93
+ are the Polar Express coefficients from `_coeffs_list`. Each step is the
94
+ optimal odd quintic approximant to x -> 1 over the current singular-value
95
+ interval, minimizing the maximum approximation error (Remez / minimax criterion).
96
+ The composition maps singular values from [l, 1] to near 1, producing the
97
+ polar factor (orthogonal factor in the polar decomposition G = UP).
98
+
99
+ `_coeffs_list` is precomputed for 10 iterations (l=1e-3, safety_factor_eps=1e-2,
100
+ cushion=0.02). If `steps` exceeds 10, the final coefficient set is repeated.
101
+
102
+ Reference: Amsel et al., "The Polar Express: Optimal Matrix Sign Methods and
103
+ Their Application to the Muon Algorithm", https://arxiv.org/abs/2505.16932
104
  """
105
  assert len(G.shape) == 2
106
  assert G.dtype == COMM_DTYPE
 
108
 
109
  if G.size(0) > G.size(1):
110
  X = X.T
111
+
112
  X = X / (X.norm() + 1e-7)
113
+ hs = _coeffs_list[:steps] + list(
114
+ repeat(_coeffs_list[-1], steps - len(_coeffs_list)))
115
  buf1 = torch.empty(X.size(0), X.size(0), dtype=X.dtype, device=X.device)
116
  buf2 = torch.empty(X.size(0), X.size(0), dtype=X.dtype, device=X.device)
117
  # Perform the NS iterations
118
+ for a, b, c in hs:
 
 
 
 
 
 
119
  matmul_transpose_assign(X, buf1)
120
  matmul_transpose_assign(buf1, buf2)
121
  buf1.mul_(b).add_(buf2, alpha=c)
 
123
 
124
  if G.size(0) > G.size(1):
125
  X = X.T
126
+
127
  return X