Source code for emerging_optimizers.orthogonalized_optimizers.muon_hyperball
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, override
import torch
from emerging_optimizers import registry
from emerging_optimizers.orthogonalized_optimizers import muon
__all__ = ["MuonHyperball"]
[docs]
@registry.register_optimizer("muon_hyperball")
class MuonHyperball(muon.Muon):
"""Muon optimizer with hyperball-style norm-preserving weight updates.
This optimizer extends Muon by performing gradient descent on the sphere manifold
while preserving the weight norm. The update rule is:
.. math::
W_{t+1} = R \\cdot \\text{normalize}(W_t - \\text{lr} \\cdot R \\cdot \\text{normalize}(\\text{update}))
where :math:`R` is the user-specified Frobenius norm. This keeps the weight matrix at
constant scale while updating.
Warning:
This optimizer is experimental and may change in future versions.
See :class:`~emerging_optimizers.orthogonalized_optimizers.muon.Muon` for full documentation
of the base Muon optimizer.
Args:
*args: Arguments passed to Muon.
hyperball_radius: Fixed radius for the hyperball. All parameters must
already have this Frobenius norm at construction time.
hyperball_eps: Epsilon for numerical stability in normalization.
**kwargs: Keyword arguments passed to Muon.
Raises:
ValueError: If any parameter has zero norm, or if a parameter's
Frobenius norm does not match ``hyperball_radius``.
"""
def __init__(
self,
*args: Any,
hyperball_radius: float,
hyperball_eps: float = 1e-15,
**kwargs: Any,
) -> None:
self.hyperball_eps = hyperball_eps
self.hyperball_radius = hyperball_radius
super().__init__(*args, **kwargs)
with torch.no_grad():
for group in self.param_groups:
for p in group["params"]:
p_norm = p.norm()
if p_norm <= hyperball_eps: # p_norm is non-negative, abs() is not needed
raise ValueError(
"MuonHyperball requires all parameters to have non-zero norm. "
"Found parameter with almost zero norm."
)
if not torch.isclose(
p_norm,
torch.tensor(self.hyperball_radius, dtype=p_norm.dtype, device=p_norm.device),
atol=0,
rtol=1e-5,
):
raise ValueError(
f"hyperball_radius={self.hyperball_radius} was specified but a parameter "
f"has Frobenius norm {p_norm.item()}. Rescale your model parameters to the "
f"desired radius before constructing the optimizer."
)
[docs]
@override
def pre_weight_update_fn_inplace(self, p: torch.Tensor, update: torch.Tensor) -> None:
"""Normalize the update using Frobenius norm, scaled by R.
Args:
p: The parameter tensor.
update: The orthogonalized gradient tensor.
"""
update_norm = update.norm().clamp_min(self.hyperball_eps)
update.mul_(self.hyperball_radius / update_norm)
[docs]
@override
def post_weight_update_fn_inplace(self, p: torch.Tensor) -> None:
"""Normalize the updated weights and scale back to original norm using Frobenius norm.
Args:
p: The parameter tensor (already updated).
"""
# Normalize the result and scale back by R: p = R * (p / ||p||_F) using Frobenius norm.
p_norm = p.norm().clamp_min(self.hyperball_eps)
p.mul_(self.hyperball_radius / p_norm)