|
| 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 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 | +@torch.compile |
| 36 | +def _weight_norm_decompose( |
| 37 | + weight: torch.Tensor, |
| 38 | + grad: torch.Tensor, |
| 39 | + g: torch.Tensor, |
| 40 | + v_norm: torch.Tensor, |
| 41 | +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: |
| 42 | + r"""Reconstructs the direction and splits the gradient under the weight-norm reparameterization. |
| 43 | +
|
| 44 | + Args: |
| 45 | + weight: The current 2D weight ``W``. |
| 46 | + grad: The gradient ``grad_W`` with respect to ``W``. |
| 47 | + g: Per-row magnitude, shape ``[rows, 1]``. |
| 48 | + v_norm: Cached row norms ``||v||_row`` of the direction, shape ``[rows, 1]``. |
| 49 | +
|
| 50 | + Returns: |
| 51 | + ``(v, grad_g, grad_v)``: the reconstructed direction and the magnitude and direction gradients. |
| 52 | + """ |
| 53 | + u = weight / g |
| 54 | + v = u * v_norm |
| 55 | + grad_g = (grad * u).sum(dim=1, keepdim=True) |
| 56 | + grad_v = (g / v_norm) * (grad - u * grad_g) |
| 57 | + return v, grad_g, grad_v |
| 58 | + |
| 59 | + |
| 60 | +class Muown(Muon): |
| 61 | + """Muown: Muon with internal weight normalization (row-norm control). |
| 62 | +
|
| 63 | + Muown (Lion et al., *Muown: Row-Norm Control for Muon Optimization*, arXiv:2605.10797) is a drop-in |
| 64 | + replacement for :class:`~emerging_optimizers.orthogonalized_optimizers.muon.Muon` that splits each 2D |
| 65 | + weight into a per-row magnitude and a direction, then optimizes them under their natural geometries: |
| 66 | +
|
| 67 | +
|
| 68 | + Args: |
| 69 | + params: Iterable of parameters to optimize or dicts defining parameter groups. |
| 70 | + lr: Learning rate shared by the direction and magnitude updates. |
| 71 | + momentum: EMA momentum for the direction (Muon) update. |
| 72 | + betas: Adam ``(beta1, beta2)`` for the magnitude update. |
| 73 | + adam_eps: Adam epsilon for the magnitude update. |
| 74 | + weight_decay: Decoupled weight decay coefficient, applied to the magnitude ``g``. |
| 75 | + fp32_matmul_prec: Precision for the orthogonalization GEMM operations. |
| 76 | + coefficient_type: Newton-Schulz coefficient set (see :class:`Muon`). |
| 77 | + num_ns_steps: Number of Newton-Schulz iteration steps. |
| 78 | + scale_mode: Update scale mode (see :func:`~emerging_optimizers.orthogonalized_optimizers.muon.get_muon_scale_factor`). |
| 79 | + extra_scale_factor: Extra scale on the direction update; ``0.2`` matches Adam's update RMS norm. |
| 80 | + use_syrk: Whether to use the Triton SYRK kernel for Newton-Schulz. |
| 81 | + """ |
| 82 | + |
| 83 | + def __init__( |
| 84 | + self, |
| 85 | + params: ParamsT, |
| 86 | + lr: float = 3e-4, |
| 87 | + momentum: float = 0.95, |
| 88 | + weight_decay: float = 0.0, |
| 89 | + *, |
| 90 | + betas: tuple[float, float] = (0.9, 0.95), |
| 91 | + adam_eps: float = 1e-8, |
| 92 | + fp32_matmul_prec: FP32MatmulPrecT = "medium", |
| 93 | + coefficient_type: NSCoeffT = "quintic", |
| 94 | + num_ns_steps: int = 5, |
| 95 | + scale_mode: MuonScaleT = "spectral", |
| 96 | + extra_scale_factor: float = 1.0, |
| 97 | + use_syrk: bool = False, |
| 98 | + ) -> None: |
| 99 | + self.betas = betas |
| 100 | + self.adam_eps = adam_eps |
| 101 | + |
| 102 | + super().__init__( |
| 103 | + params, |
| 104 | + lr, |
| 105 | + momentum, |
| 106 | + weight_decay, |
| 107 | + nesterov=False, |
| 108 | + weight_decay_method="decoupled", |
| 109 | + fp32_matmul_prec=fp32_matmul_prec, |
| 110 | + coefficient_type=coefficient_type, |
| 111 | + num_ns_steps=num_ns_steps, |
| 112 | + scale_mode=scale_mode, |
| 113 | + extra_scale_factor=extra_scale_factor, |
| 114 | + use_syrk=use_syrk, |
| 115 | + ) |
| 116 | + |
| 117 | + @torch.no_grad() # type: ignore[misc] |
| 118 | + @override |
| 119 | + def _init_group(self, group: dict, skip_non_grad_params: bool = True) -> None: |
| 120 | + for p in group["params"]: |
| 121 | + if skip_non_grad_params and p.grad is None: |
| 122 | + continue # pragma: no cover |
| 123 | + if p.dim() != 2: |
| 124 | + raise TypeError("Muown is only supported for 2D parameters") |
| 125 | + state = self.state[p] |
| 126 | + if len(state) == 0: |
| 127 | + # Seed g, v from the current weight so Muown starts from the same point as Muon. Floor the |
| 128 | + # row norm so an all-zero weight row does not give g=0, which would make u = weight / g a |
| 129 | + # 0/0 NaN on the first step. |
| 130 | + row_norm = p.norm(dim=1, keepdim=True).to(torch.float32).clamp_min(1e-12) |
| 131 | + state["step"] = 0 |
| 132 | + state["g"] = row_norm.clone() |
| 133 | + state["v_norm"] = row_norm.clone() |
| 134 | + state["momentum_buffer"] = torch.zeros_like(p, dtype=torch.float32) |
| 135 | + state["m_g"] = torch.zeros_like(row_norm) |
| 136 | + state["v_g"] = torch.zeros_like(row_norm) |
| 137 | + |
| 138 | + if TYPE_CHECKING: |
| 139 | + |
| 140 | + @overload |
| 141 | + def step(self, closure: None = ...) -> None: ... |
| 142 | + |
| 143 | + @overload |
| 144 | + def step(self, closure: Callable[[], float]) -> float: ... |
| 145 | + |
| 146 | + @torch.no_grad() # type: ignore[misc] |
| 147 | + @override |
| 148 | + def step(self, closure: Callable[[], float] | None = None) -> float | None: |
| 149 | + """Performs a single optimization step. |
| 150 | +
|
| 151 | + Args: |
| 152 | + closure: Unsupported; must be ``None``. |
| 153 | + """ |
| 154 | + if closure is not None: |
| 155 | + raise ValueError("closure is not supported") |
| 156 | + |
| 157 | + for group in self.param_groups: |
| 158 | + self._init_group(group) |
| 159 | + |
| 160 | + lr = group["lr"] |
| 161 | + momentum = group["momentum"] |
| 162 | + weight_decay = group["weight_decay"] |
| 163 | + for p in group["params"]: |
| 164 | + if p.grad is None: |
| 165 | + continue # pragma: no cover |
| 166 | + |
| 167 | + state = self.state[p] |
| 168 | + curr_iter_1_based = state["step"] + 1 |
| 169 | + g = state["g"] |
| 170 | + v_norm = state["v_norm"] |
| 171 | + |
| 172 | + v, grad_g, grad_v = _weight_norm_decompose(p.to(torch.float32), p.grad.to(torch.float32), g, v_norm) |
| 173 | + |
| 174 | + state["momentum_buffer"].lerp_(grad_v, 1 - momentum) |
| 175 | + with utils.fp32_matmul_precision(self.fp32_matmul_prec): |
| 176 | + direction_update = self.scaled_orthogonalize_fn(state["momentum_buffer"]) |
| 177 | + v_new = v.add(direction_update, alpha=-lr) |
| 178 | + |
| 179 | + magnitude_update = update_functions.calculate_adam_update( |
| 180 | + grad_g, |
| 181 | + state["m_g"], |
| 182 | + state["v_g"], |
| 183 | + betas=self.betas, |
| 184 | + eps=self.adam_eps, |
| 185 | + correct_bias=True, |
| 186 | + nesterov=False, |
| 187 | + step=curr_iter_1_based, # 1-based iteration index is used for bias correction |
| 188 | + ) |
| 189 | + g.add_(magnitude_update, alpha=-lr) |
| 190 | + |
| 191 | + # Decoupled weight decay on the magnitude (the spectral-norm driver). |
| 192 | + self._apply_weight_decay_inplace(g, grad_g, lr, weight_decay) |
| 193 | + |
| 194 | + v_norm_new = v_new.norm(dim=1, keepdim=True) |
| 195 | + p.copy_(g * (v_new / v_norm_new)) |
| 196 | + state["v_norm"] = v_norm_new |
| 197 | + |
| 198 | + state["step"] += 1 |
| 199 | + |
| 200 | + return None |
0 commit comments