Skip to content

Commit 22831af

Browse files
committed
improve tests
Signed-off-by: Hao Wu <skyw@nvidia.com>
1 parent ba90c5a commit 22831af

2 files changed

Lines changed: 10 additions & 80 deletions

File tree

emerging_optimizers/orthogonalized_optimizers/muown.py

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -40,37 +40,6 @@ class Muown(Muon):
4040
replacement for :class:`~emerging_optimizers.orthogonalized_optimizers.muon.Muon` that splits each 2D
4141
weight into a per-row magnitude and a direction, then optimizes them under their natural geometries:
4242
43-
- **Direction** ``v``: the Muon update (EMA momentum + Newton-Schulz orthogonalization), reusing the
44-
parent's ``scaled_orthogonalize_fn``.
45-
- **Magnitude** ``g`` (one entry per output row): Adam, which realizes the :math:`\\ell_\\infty` duality
46-
map for the diagonal per-neuron gain.
47-
48-
The reparameterization ``W = Diag(g / ||v||_row) v`` is held implicitly inside the optimizer, so the
49-
forward pass is unchanged. At init ``g``, ``v`` are seeded from ``W`` so Muown starts from the same
50-
point as Muon. The row magnitude is the empirical driver of spectral-norm drift under Muon; making it an
51-
explicit, separately-optimized variable controls that drift without the indiscriminate shrinkage of
52-
plain weight decay.
53-
54-
A single ``lr`` drives both halves: the direction step carries the ``0.2 * sqrt(max(m, n))`` scaling
55-
(via ``extra_scale_factor``) that matches Adam's update RMS norm, so no separate magnitude lr is needed.
56-
57-
State per parameter:
58-
- ``step``
59-
- ``g``: per-row magnitude, shape ``[out, 1]``.
60-
- ``v_norm``: cached row norms ``||v||_row`` of the direction, shape ``[out, 1]``.
61-
- ``momentum_buffer``: EMA momentum of the direction gradient (the Muon momentum on ``v``).
62-
- ``m_g``, ``v_g``: Adam first/second moments of the magnitude gradient.
63-
64-
Note:
65-
Weight decay is decoupled and applied to the magnitude ``g`` (which is exactly the spectral-norm
66-
driver). Because ``W`` is recomposed from ``g`` after the decay, the invariant ``||W_row|| == g``
67-
holds without a separate resync.
68-
69-
Warning:
70-
- This optimizer requires that all parameters passed in are 2D.
71-
- It should not be used for the embedding layer, the final fully connected layer, or any 1-D
72-
parameters; those can all be optimized by a standard method (e.g., AdamW).
73-
- This optimizer is experimental and may change in future versions.
7443
7544
Args:
7645
params: Iterable of parameters to optimize or dicts defining parameter groups.
@@ -182,20 +151,16 @@ def step(self, closure: Callable[[], float] | None = None) -> float | None:
182151
grad = p.grad.to(torch.float32)
183152
weight = p.to(torch.float32)
184153

185-
# Decompose grad_W into magnitude and direction gradients via the weight-norm Jacobian.
186-
# u = v / ||v||_row is O(1) per element; reconstruct the direction v from the live weight.
187154
u = weight / g
188155
v = u * v_norm
189156
grad_g = (grad * u).sum(dim=1, keepdim=True)
190157
grad_v = (g / v_norm) * (grad - u * grad_g)
191158

192-
# Direction: Muon update on v (EMA momentum + Newton-Schulz), reusing the parent LMO.
193159
state["momentum_buffer"].lerp_(grad_v, 1 - momentum)
194160
with utils.fp32_matmul_precision(self.fp32_matmul_prec):
195161
direction_update = self.scaled_orthogonalize_fn(state["momentum_buffer"])
196162
v_new = v.add(direction_update, alpha=-lr)
197163

198-
# Magnitude: Adam on g (the l-infinity duality map for the diagonal gain), same lr.
199164
magnitude_update = update_functions.calculate_adam_update(
200165
grad_g,
201166
state["m_g"],
@@ -208,12 +173,8 @@ def step(self, closure: Callable[[], float] | None = None) -> float | None:
208173
)
209174
g.add_(magnitude_update, alpha=-lr)
210175

211-
# Decoupled weight decay on the magnitude (the spectral-norm driver).
212-
if weight_decay != 0.0:
213-
g.add_(g, alpha=-weight_decay * lr)
176+
g.add_(g, alpha=-weight_decay * lr)
214177

215-
# Recompose W = g * v_new / ||v_new||_row and refresh the cached direction norm. Recomposing
216-
# from the decayed g keeps the invariant ||W_row|| == g without a separate resync.
217178
v_norm_new = v_new.norm(dim=1, keepdim=True)
218179
p.copy_(g * (v_new / v_norm_new))
219180
state["v_norm"] = v_norm_new

tests/test_muown.py

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ def setUpModule() -> None:
3131
torch.manual_seed(FLAGS.seed)
3232
if torch.cuda.is_available():
3333
torch.cuda.manual_seed_all(FLAGS.seed)
34-
# Use the most precise fp32 matmul path so the reference (which calls the optimizer's
35-
# orthogonalization outside the fp32_matmul_precision context) computes bit-comparable results.
36-
torch.set_float32_matmul_precision("highest")
3734

3835

3936
class MuownTest(parameterized.TestCase):
@@ -43,7 +40,6 @@ def setUp(self):
4340

4441
@parameterized.product(shape=[(8, 16), (16, 8), (33, 65)])
4542
def test_smoke(self, shape):
46-
"""A few steps run and keep the weight finite."""
4743
p = torch.nn.Parameter(torch.randn(shape, device=self.device))
4844
opt = Muown([p], lr=1e-2, weight_decay=0.01)
4945
for _ in range(3):
@@ -52,7 +48,6 @@ def test_smoke(self, shape):
5248
self.assertTrue(torch.isfinite(p).all())
5349

5450
def test_raises_on_non_2d(self):
55-
"""Muown supports 2D parameters only."""
5651
for shape in [(8,), (2, 3, 4)]:
5752
p = torch.nn.Parameter(torch.randn(shape, device=self.device))
5853
p.grad = torch.randn_like(p)
@@ -61,7 +56,6 @@ def test_raises_on_non_2d(self):
6156
opt.step()
6257

6358
def test_raises_on_closure(self):
64-
"""Closures are not supported."""
6559
p = torch.nn.Parameter(torch.randn((8, 16), device=self.device))
6660
p.grad = torch.randn_like(p)
6761
opt = Muown([p], lr=1e-2)
@@ -70,7 +64,6 @@ def test_raises_on_closure(self):
7064

7165
@parameterized.product(shape=[(8, 16), (16, 8)], weight_decay=[0.0, 0.1])
7266
def test_row_norm_equals_magnitude_state(self, shape, weight_decay):
73-
"""The reparameterization invariant ||W_row|| == g holds after each step."""
7467
p = torch.nn.Parameter(torch.randn(shape, device=self.device))
7568
opt = Muown([p], lr=1e-2, weight_decay=weight_decay)
7669
for _ in range(4):
@@ -80,50 +73,28 @@ def test_row_norm_equals_magnitude_state(self, shape, weight_decay):
8073
torch.testing.assert_close(
8174
row_norm,
8275
opt.state[p]["g"],
83-
atol=1e-5,
76+
atol=0,
8477
rtol=1e-5,
85-
msg=lambda m: f"Row norms of W must equal the magnitude state g.\n\n{m}",
8678
)
8779

88-
def test_weight_decay_shrinks_magnitude(self):
89-
"""Decoupled weight decay shrinks the magnitude g relative to the no-decay run."""
90-
p_wd = torch.nn.Parameter(torch.randn((16, 32), device=self.device))
91-
p_no = torch.nn.Parameter(p_wd.detach().clone())
92-
opt_wd = Muown([p_wd], lr=1e-2, weight_decay=0.1)
93-
opt_no = Muown([p_no], lr=1e-2, weight_decay=0.0)
94-
for _ in range(5):
95-
grad = torch.randn_like(p_wd)
96-
p_wd.grad = grad.clone()
97-
p_no.grad = grad.clone()
98-
opt_wd.step()
99-
opt_no.step()
100-
self.assertLess(opt_wd.state[p_wd]["g"].sum().item(), opt_no.state[p_no]["g"].sum().item())
101-
10280
@parameterized.product(shape=[(8, 16), (16, 8), (33, 65)], momentum=[0.0, 0.95])
103-
def test_agrees_with_reference(self, shape, momentum):
104-
"""Muown matches the authors' reference implementation (no weight decay).
105-
106-
The reference uses classic (heavy-ball) momentum while Muown uses EMA momentum; the two differ by
107-
a constant factor that the scale-invariant Newton-Schulz orthogonalization removes, so the updates
108-
agree. Both use the same injected orthogonalization, so only float rounding (lerp vs mul/add,
109-
Newton-Schulz normalization) separates them — hence a tight tolerance rather than bit-identity.
110-
"""
81+
def test_close_reference(self, shape, momentum):
11182
p = torch.nn.Parameter(torch.randn(shape, device=self.device))
11283
p_ref = torch.nn.Parameter(p.detach().clone())
11384

11485
opt = Muown(
11586
[p],
116-
lr=1e-2,
87+
lr=0.125,
11788
momentum=momentum,
11889
weight_decay=0.0,
119-
extra_scale_factor=0.2,
90+
extra_scale_factor=0.25,
12091
fp32_matmul_prec="highest",
12192
)
12293
# Hold orthogonalization identical: feed the reference Muown's own scaled_orthogonalize_fn.
12394
opt_ref = MuownReference(
12495
[p_ref],
12596
orthogonalize_fn=opt.scaled_orthogonalize_fn,
126-
lr=1e-2,
97+
lr=0.125,
12798
momentum=momentum,
12899
nesterov=False,
129100
weight_decay=0.0,
@@ -139,16 +110,14 @@ def test_agrees_with_reference(self, shape, momentum):
139110
torch.testing.assert_close(
140111
p.detach(),
141112
p_ref.detach(),
142-
atol=1e-5,
143-
rtol=1e-4,
144-
msg=lambda m: f"Muown weight diverged from the reference implementation.\n\n{m}",
113+
atol=1e-6,
114+
rtol=1e-5,
145115
)
146116
torch.testing.assert_close(
147117
opt.state[p]["g"],
148118
opt_ref.state[p_ref]["g"],
149-
atol=1e-5,
150-
rtol=1e-4,
151-
msg=lambda m: f"Muown magnitude g diverged from the reference implementation.\n\n{m}",
119+
atol=1e-7,
120+
rtol=1e-5,
152121
)
153122

154123

0 commit comments

Comments
 (0)