Skip to content

Commit f74b869

Browse files
committed
ai add muown
Signed-off-by: Hao Wu <skyw@nvidia.com>
1 parent 93376d9 commit f74b869

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

emerging_optimizers/orthogonalized_optimizers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from emerging_optimizers.orthogonalized_optimizers.mop import *
1717
from emerging_optimizers.orthogonalized_optimizers.muon import *
1818
from emerging_optimizers.orthogonalized_optimizers.muon_hyperball import *
19+
from emerging_optimizers.orthogonalized_optimizers.muown import *
1920
from emerging_optimizers.orthogonalized_optimizers.orthogonalized_optimizer import *
2021
from emerging_optimizers.orthogonalized_optimizers.polargrad import *
2122
from emerging_optimizers.orthogonalized_optimizers.scion import *
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from typing import TYPE_CHECKING, Callable, override
17+
18+
19+
if TYPE_CHECKING:
20+
from typing import overload
21+
22+
import torch
23+
from torch.optim.optimizer import ParamsT
24+
25+
from emerging_optimizers import registry, utils
26+
from emerging_optimizers.orthogonalized_optimizers.muon import Muon, MuonScaleT
27+
from emerging_optimizers.orthogonalized_optimizers.muon_utils import NSCoeffT
28+
from emerging_optimizers.scalar_optimizers import update_functions
29+
from emerging_optimizers.utils import FP32MatmulPrecT
30+
31+
32+
__all__ = ["Muown"]
33+
34+
35+
@registry.register_optimizer("muown")
36+
class Muown(Muon):
37+
"""Muown: Muon with internal weight normalization (row-norm control).
38+
39+
Muown (Lion et al., *Muown: Row-Norm Control for Muon Optimization*, arXiv:2605.10797) is a drop-in
40+
replacement for :class:`~emerging_optimizers.orthogonalized_optimizers.muon.Muon` that splits each 2D
41+
weight into a per-row magnitude and a direction, then optimizes them under their natural geometries:
42+
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.
74+
75+
Args:
76+
params: Iterable of parameters to optimize or dicts defining parameter groups.
77+
lr: Learning rate shared by the direction and magnitude updates.
78+
momentum: EMA momentum for the direction (Muon) update.
79+
betas: Adam ``(beta1, beta2)`` for the magnitude update.
80+
adam_eps: Adam epsilon for the magnitude update.
81+
weight_decay: Decoupled weight decay coefficient, applied to the magnitude ``g``.
82+
fp32_matmul_prec: Precision for the orthogonalization GEMM operations.
83+
coefficient_type: Newton-Schulz coefficient set (see :class:`Muon`).
84+
num_ns_steps: Number of Newton-Schulz iteration steps.
85+
scale_mode: Update scale mode (see :func:`~emerging_optimizers.orthogonalized_optimizers.muon.get_muon_scale_factor`).
86+
extra_scale_factor: Extra scale on the direction update; ``0.2`` matches Adam's update RMS norm.
87+
use_syrk: Whether to use the Triton SYRK kernel for Newton-Schulz.
88+
"""
89+
90+
def __init__(
91+
self,
92+
params: ParamsT,
93+
lr: float = 3e-4,
94+
momentum: float = 0.95,
95+
weight_decay: float = 0.0,
96+
*,
97+
betas: tuple[float, float] = (0.9, 0.95),
98+
adam_eps: float = 1e-8,
99+
fp32_matmul_prec: FP32MatmulPrecT = "medium",
100+
coefficient_type: NSCoeffT = "quintic",
101+
num_ns_steps: int = 5,
102+
scale_mode: MuonScaleT = "spectral",
103+
extra_scale_factor: float = 1.0,
104+
use_syrk: bool = False,
105+
) -> None:
106+
if not 0.0 <= betas[0] < 1.0:
107+
raise ValueError(f"Invalid beta1: {betas[0]}")
108+
if not 0.0 <= betas[1] < 1.0:
109+
raise ValueError(f"Invalid beta2: {betas[1]}")
110+
111+
self.betas = betas
112+
self.adam_eps = adam_eps
113+
114+
super().__init__(
115+
params,
116+
lr,
117+
momentum,
118+
weight_decay,
119+
nesterov=False,
120+
weight_decay_method="decoupled",
121+
fp32_matmul_prec=fp32_matmul_prec,
122+
coefficient_type=coefficient_type,
123+
num_ns_steps=num_ns_steps,
124+
scale_mode=scale_mode,
125+
extra_scale_factor=extra_scale_factor,
126+
use_syrk=use_syrk,
127+
)
128+
129+
@torch.no_grad() # type: ignore[misc]
130+
@override
131+
def _init_group(self, group: dict, skip_non_grad_params: bool = True) -> None:
132+
for p in group["params"]:
133+
if skip_non_grad_params and p.grad is None:
134+
continue
135+
if p.dim() != 2:
136+
raise TypeError("Muown is only supported for 2D parameters")
137+
state = self.state[p]
138+
if len(state) == 0:
139+
# Seed g, v from the current weight so Muown starts from the same point as Muon.
140+
row_norm = p.norm(dim=1, keepdim=True).to(torch.float32)
141+
state["step"] = 0
142+
state["g"] = row_norm.clone()
143+
state["v_norm"] = row_norm.clone()
144+
state["momentum_buffer"] = torch.zeros_like(p, dtype=torch.float32)
145+
state["m_g"] = torch.zeros_like(row_norm)
146+
state["v_g"] = torch.zeros_like(row_norm)
147+
148+
if TYPE_CHECKING:
149+
150+
@overload
151+
def step(self, closure: None = ...) -> None: ...
152+
153+
@overload
154+
def step(self, closure: Callable[[], float]) -> float: ...
155+
156+
@torch.no_grad() # type: ignore[misc]
157+
@override
158+
def step(self, closure: Callable[[], float] | None = None) -> float | None:
159+
"""Performs a single optimization step.
160+
161+
Args:
162+
closure: Unsupported; must be ``None``.
163+
"""
164+
if closure is not None:
165+
raise ValueError("closure is not supported")
166+
167+
for group in self.param_groups:
168+
self._init_group(group)
169+
170+
lr = group["lr"]
171+
momentum = group["momentum"]
172+
weight_decay = group["weight_decay"]
173+
for p in group["params"]:
174+
if p.grad is None:
175+
continue # pragma: no cover
176+
177+
state = self.state[p]
178+
state["step"] += 1
179+
g = state["g"]
180+
v_norm = state["v_norm"]
181+
182+
grad = p.grad.to(torch.float32)
183+
weight = p.to(torch.float32)
184+
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.
187+
u = weight / g
188+
v = u * v_norm
189+
grad_g = (grad * u).sum(dim=1, keepdim=True)
190+
grad_v = (g / v_norm) * (grad - u * grad_g)
191+
192+
# Direction: Muon update on v (EMA momentum + Newton-Schulz), reusing the parent LMO.
193+
state["momentum_buffer"].lerp_(grad_v, 1 - momentum)
194+
with utils.fp32_matmul_precision(self.fp32_matmul_prec):
195+
direction_update = self.scaled_orthogonalize_fn(state["momentum_buffer"])
196+
v_new = v.add(direction_update, alpha=-lr)
197+
198+
# Magnitude: Adam on g (the l-infinity duality map for the diagonal gain), same lr.
199+
magnitude_update = update_functions.calculate_adam_update(
200+
grad_g,
201+
state["m_g"],
202+
state["v_g"],
203+
betas=self.betas,
204+
eps=self.adam_eps,
205+
correct_bias=True,
206+
nesterov=False,
207+
step=state["step"],
208+
)
209+
g.add_(magnitude_update, alpha=-lr)
210+
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)
214+
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.
217+
v_norm_new = v_new.norm(dim=1, keepdim=True)
218+
p.copy_(g * (v_new / v_norm_new))
219+
state["v_norm"] = v_norm_new
220+
221+
return None

0 commit comments

Comments
 (0)