| import random |
| import torch |
| import torch.optim as optim |
| from Vathos.functions import flag |
| from torch.optim import Optimizer, AdamW, SGD |
|
|
| try: |
| from muon import Muon |
| except ImportError: |
| flag("Muon library is not installed, usage of Muon dependent optimizer will not be possible", 2) |
|
|
|
|
| class ValueScheduler: |
| def __init__(self): |
| self.step = 0 |
| self.value = 0 |
| self.functions = {} |
| self.values = [] |
| self.compiled = False |
|
|
| def set(self, f, l=0, u=float('inf')): |
| assert l < u |
| self.functions[(l, u)] = f |
|
|
| def compile(self, l, m): |
| if self.compiled: |
| flag("ValueScheduler Already Compiled, recompiling...") |
| self.values = [] |
| for l, u in self.functions: |
| i = l |
| f = self.functions[(l, u)] |
| while i <= u: |
| self.values.append(f(i)) |
|
|
| def get(self, step): |
| assert self.compiled |
| return self.values[step] |
|
|
|
|
| class SignSGD(optim.Optimizer): |
|
|
| def __init__(self, params, lr=0.01, rand_zero=True): |
| defaults = dict(lr=lr, rand_zero=rand_zero) |
| super().__init__(params, defaults) |
|
|
| @torch.no_grad() |
| def step(self, closure=None): |
| loss = None |
| if closure is not None: |
| with torch.enable_grad(): |
| loss = closure() |
|
|
| for group in self.param_groups: |
| lr = group['lr'] |
| rand_zero = group['rand_zero'] |
|
|
| for p in group['params']: |
| if p.grad is None: |
| continue |
|
|
| grad = torch.sign(p.grad) |
|
|
| if rand_zero: |
| zero_mask = (grad == 0) |
| if zero_mask.any(): |
| grad[zero_mask] = torch.randint( |
| 0, 2, (zero_mask.sum().item(),), |
| dtype=grad.dtype, |
| device=grad.device |
| ) * 2 - 1 |
|
|
| p.add_(grad, alpha=-lr) |
|
|
| return loss |
|
|
|
|
| """ |
| stochastic_sign_muon.py |
| ======================= |
| StochasticSignMuon: per-step stochastic interpolation between Muon and SignSGD |
| for 2-D hidden weight matrices, with AdamW for everything else. |
| |
| Design principle |
| ---------------- |
| Both Muon and SignSGD operate on the *same* Nesterov momentum buffer: |
| |
| buf ← β·buf + (1-β)·grad # EMA (identical for both) |
| pre = (1-β)·grad + β·buf # Nesterov blend (identical for both) |
| |
| Muon branch : update = NS(pre) · scale # orthogonalise |
| Sign branch : update = sign(pre) / √n # sign-normalised |
| |
| Because the momentum path is fully shared, τ can be freely scheduled — or even |
| changed every step — without resetting or corrupting any state. |
| |
| Frobenius-norm alignment |
| ------------------------ |
| Muon's NS output has F-norm ≈ √m for an m×n matrix (m = rows). |
| sign(pre) has F-norm = √(m·n). |
| Dividing by √n makes the Sign branch produce the same F-norm as Muon, |
| so the learning rate carries the same meaning in both branches. |
| |
| Usage (single device) |
| --------------------- |
| from stochastic_sign_muon import SingleDeviceStochasticSignMuon |
| |
| hidden_weights = [p for p in model.body.parameters() if p.ndim >= 2] |
| rest = ( |
| [p for p in model.body.parameters() if p.ndim < 2] |
| + list(model.head.parameters()) |
| + list(model.embed.parameters()) |
| ) |
| |
| optimizer = SingleDeviceStochasticSignMuon( |
| [ |
| dict(params=hidden_weights, use_muon=True, lr=0.02, weight_decay=0.01), |
| dict(params=rest, use_muon=False, lr=3e-4, betas=(0.9, 0.95), |
| weight_decay=0.01), |
| ], |
| tau=0.0, # start as pure Muon; schedule upward to inject Sign noise |
| ) |
| |
| # Inside the training loop you can change tau at any time: |
| optimizer.set_tau(0.3) # 30 % of steps will now be Sign steps |
| """ |
|
|
| import torch |
| import torch.distributed as dist |
| try: |
| from muon import zeropower_via_newtonschulz5, adam_update |
| except ImportError: |
| flag("Unable to import muon, consider downloading it via pip install git+https://github.com/KellerJordan/Muon.git to use Muon backed optimizers") |
|
|
|
|
| |
| |
| |
|
|
| def _nesterov_momentum( |
| grad: torch.Tensor, |
| buf: torch.Tensor, |
| beta: float, |
| ) -> torch.Tensor: |
| """ |
| EMA update followed by a Nesterov combination. |
| |
| Modifies `buf` and `grad` in-place (safe inside @torch.no_grad). |
| Returns the Nesterov pre-update vector. |
| """ |
| buf.lerp_(grad, 1.0 - beta) |
| return grad.lerp_(buf, beta) |
|
|
|
|
| def _muon_branch(pre: torch.Tensor, ns_steps: int = 5) -> torch.Tensor: |
| """ |
| Newton-Schulz orthogonalisation + anisotropic scaling. |
| Output F-norm ≈ √m for an m×n pre-update. |
| Returns a tensor of the same shape as `pre`. |
| """ |
| u = pre.view(len(pre), -1) if pre.ndim == 4 else pre |
| u = zeropower_via_newtonschulz5(u, steps=ns_steps) |
| u = u * max(1.0, u.size(-2) / u.size(-1)) ** 0.5 |
| return u |
|
|
|
|
| def _sign_branch(pre: torch.Tensor, rand_zero: bool = True) -> torch.Tensor: |
| """ |
| Signed update normalised to match Muon's output F-norm. |
| |
| sign(pre) has F-norm √(m·n). Dividing by √n yields √m, matching Muon. |
| Zero entries are randomised to ±1 when rand_zero=True. |
| Returns a tensor of the same shape as `pre`. |
| """ |
| u = pre.view(len(pre), -1) if pre.ndim == 4 else pre |
|
|
| s = torch.sign(u) |
|
|
| if rand_zero: |
| zero_mask = s == 0 |
| if zero_mask.any(): |
| s[zero_mask] = ( |
| torch.randint( |
| 0, 2, |
| (zero_mask.sum().item(),), |
| dtype=u.dtype, |
| device=u.device, |
| ) * 2 - 1 |
| ) |
|
|
| |
| s = s / (u.size(-1) ** 0.5) |
| return s |
|
|
|
|
| |
| |
| |
|
|
| class SingleDeviceStochasticSignMuon(torch.optim.Optimizer): |
| """ |
| Non-distributed stochastic Muon ↔ SignSGD interpolation with AdamW aux. |
| |
| Parameters |
| ---------- |
| param_groups : list[dict] |
| Each group must contain ``use_muon`` (bool). |
| |
| use_muon=True groups recognise: lr, momentum, weight_decay |
| use_muon=False groups recognise: lr, betas, eps, weight_decay |
| |
| tau : float |
| Probability of taking a Sign step instead of a Muon step. [0, 1]. |
| 0.0 → pure Muon (default). 1.0 → pure SignSGD. |
| Schedule via :meth:`set_tau`. No state reset required. |
| |
| sign_lr_scale : float |
| Multiplicative downscale applied to lr when the Sign branch fires. |
| Compensates for Sign's lower signal-to-noise ratio vs Muon at equal |
| F-norm. Default 0.1; tune in [0.05, 0.2]. |
| Schedule via :meth:`set_sign_lr_scale`. No state reset required. |
| Weight decay is always applied at the base group lr regardless. |
| |
| rand_zero : bool |
| Randomise zero-gradient sign entries in the Sign branch (default True). |
| |
| nesterov : bool |
| Use Nesterov momentum (default True, matching canonical Muon). |
| |
| ns_steps : int |
| Newton-Schulz iteration count for the Muon branch (default 5). |
| """ |
|
|
| def __init__( |
| self, |
| param_groups, |
| tau: float = 0.0, |
| sign_lr_scale: float = 0.1, |
| rand_zero: bool = True, |
| nesterov: bool = True, |
| ns_steps: int = 5, |
| ): |
| assert 0.0 <= tau <= 1.0, f"tau must be in [0, 1], got {tau}" |
| assert sign_lr_scale > 0.0, f"sign_lr_scale must be > 0, got {sign_lr_scale}" |
|
|
| self.tau = tau |
| self.sign_lr_scale = sign_lr_scale |
| self.rand_zero = rand_zero |
| self.nesterov = nesterov |
| self.ns_steps = ns_steps |
|
|
| for group in param_groups: |
| assert "use_muon" in group, "Every param group must have a 'use_muon' key." |
| if group["use_muon"]: |
| group.setdefault("lr", 0.02) |
| group.setdefault("momentum", 0.95) |
| group.setdefault("weight_decay", 0.0) |
| allowed = {"params", "lr", "momentum", "weight_decay", "use_muon"} |
| assert set(group.keys()) == allowed, ( |
| f"Unexpected keys in Muon group: {set(group.keys()) - allowed}" |
| ) |
| else: |
| group.setdefault("lr", 3e-4) |
| group.setdefault("betas", (0.9, 0.95)) |
| group.setdefault("eps", 1e-10) |
| group.setdefault("weight_decay", 0.0) |
| allowed = {"params", "lr", "betas", "eps", "weight_decay", "use_muon"} |
| assert set(group.keys()) == allowed, ( |
| f"Unexpected keys in Adam group: {set(group.keys()) - allowed}" |
| ) |
|
|
| super().__init__(param_groups, {}) |
|
|
| |
| |
| |
|
|
| def set_tau(self, tau: float) -> None: |
| """ |
| Set the Muon ↔ Sign mixing probability. |
| Safe to call at any point; does not reset any optimizer state. |
| """ |
| assert 0.0 <= tau <= 1.0, f"tau must be in [0, 1], got {tau}" |
| self.tau = tau |
|
|
| def set_sign_lr_scale(self, scale: float) -> None: |
| """ |
| Set the lr multiplier for Sign steps. |
| Safe to call at any point; does not reset any optimizer state. |
| """ |
| assert scale > 0.0, f"sign_lr_scale must be > 0, got {scale}" |
| self.sign_lr_scale = scale |
|
|
| |
| |
| |
|
|
| @torch.no_grad() |
| def step(self, closure=None): |
| loss = None |
| if closure is not None: |
| with torch.enable_grad(): |
| loss = closure() |
|
|
| |
| use_sign: bool = (torch.rand(1).item() < self.tau) |
|
|
| for group in self.param_groups: |
|
|
| if group["use_muon"]: |
| beta = group["momentum"] |
| base_lr = group["lr"] |
| effective_lr = base_lr * self.sign_lr_scale if use_sign else base_lr |
|
|
| for p in group["params"]: |
| if p.grad is None: |
| p.grad = torch.zeros_like(p) |
|
|
| state = self.state[p] |
| if not state: |
| state["momentum_buffer"] = torch.zeros_like(p) |
|
|
| |
| pre = _nesterov_momentum(p.grad, state["momentum_buffer"], beta) |
|
|
| |
| if use_sign: |
| update = _sign_branch(pre, rand_zero=self.rand_zero) |
| else: |
| update = _muon_branch(pre, ns_steps=self.ns_steps) |
|
|
| |
| p.mul_(1.0 - base_lr * group["weight_decay"]) |
| p.add_(update.reshape(p.shape), alpha=-effective_lr) |
|
|
| else: |
| for p in group["params"]: |
| if p.grad is None: |
| p.grad = torch.zeros_like(p) |
|
|
| state = self.state[p] |
| if not state: |
| state["exp_avg"] = torch.zeros_like(p) |
| state["exp_avg_sq"] = torch.zeros_like(p) |
| state["step"] = 0 |
|
|
| state["step"] += 1 |
| update = adam_update( |
| p.grad, |
| state["exp_avg"], |
| state["exp_avg_sq"], |
| state["step"], |
| group["betas"], |
| group["eps"], |
| ) |
| p.mul_(1.0 - group["lr"] * group["weight_decay"]) |
| p.add_(update, alpha=-group["lr"]) |
|
|
| return loss |
|
|
|
|
| |
| |
| |
|
|
| class StochasticSignMuon(torch.optim.Optimizer): |
| """ |
| Distributed stochastic Muon ↔ SignSGD interpolation with AdamW aux. |
| |
| Requires an initialised ``torch.distributed`` process group. |
| Parameters are sorted by size and sharded across ranks exactly as in the |
| original ``MuonWithAuxAdam``. |
| |
| The Bernoulli draw is made on rank 0 and broadcast to all ranks so that |
| every GPU takes the same branch on every step. |
| |
| See ``SingleDeviceStochasticSignMuon`` for the full parameter docstring. |
| """ |
|
|
| def __init__( |
| self, |
| param_groups, |
| tau: float = 0.0, |
| sign_lr_scale: float = 0.1, |
| rand_zero: bool = True, |
| nesterov: bool = True, |
| ns_steps: int = 5, |
| ): |
| assert 0.0 <= tau <= 1.0, f"tau must be in [0, 1], got {tau}" |
| assert sign_lr_scale > 0.0, f"sign_lr_scale must be > 0, got {sign_lr_scale}" |
|
|
| self.tau = tau |
| self.sign_lr_scale = sign_lr_scale |
| self.rand_zero = rand_zero |
| self.nesterov = nesterov |
| self.ns_steps = ns_steps |
|
|
| for group in param_groups: |
| assert "use_muon" in group, "Every param group must have a 'use_muon' key." |
| if group["use_muon"]: |
| group["params"] = sorted( |
| group["params"], key=lambda x: x.size(), reverse=True |
| ) |
| group.setdefault("lr", 0.02) |
| group.setdefault("momentum", 0.95) |
| group.setdefault("weight_decay", 0.0) |
| allowed = {"params", "lr", "momentum", "weight_decay", "use_muon"} |
| assert set(group.keys()) == allowed |
| else: |
| group.setdefault("lr", 3e-4) |
| group.setdefault("betas", (0.9, 0.95)) |
| group.setdefault("eps", 1e-10) |
| group.setdefault("weight_decay", 0.0) |
| allowed = {"params", "lr", "betas", "eps", "weight_decay", "use_muon"} |
| assert set(group.keys()) == allowed |
|
|
| super().__init__(param_groups, {}) |
|
|
| |
| |
| |
|
|
| def set_tau(self, tau: float) -> None: |
| """Schedule the Muon ↔ Sign mixing probability. Safe from any rank.""" |
| assert 0.0 <= tau <= 1.0, f"tau must be in [0, 1], got {tau}" |
| self.tau = tau |
|
|
| def set_sign_lr_scale(self, scale: float) -> None: |
| """Schedule the lr multiplier for Sign steps. Safe from any rank.""" |
| assert scale > 0.0, f"sign_lr_scale must be > 0, got {scale}" |
| self.sign_lr_scale = scale |
|
|
| |
| |
| |
|
|
| @torch.no_grad() |
| def step(self, closure=None): |
| loss = None |
| if closure is not None: |
| with torch.enable_grad(): |
| loss = closure() |
|
|
| world_size = dist.get_world_size() |
| rank = dist.get_rank() |
|
|
| |
| use_sign_t = torch.zeros(1, dtype=torch.float32, |
| device=torch.device("cuda", rank)) |
| if rank == 0: |
| use_sign_t[0] = 1.0 if torch.rand(1).item() < self.tau else 0.0 |
| dist.broadcast(use_sign_t, src=0) |
| use_sign: bool = use_sign_t.item() > 0.5 |
|
|
| for group in self.param_groups: |
|
|
| if group["use_muon"]: |
| beta = group["momentum"] |
| base_lr = group["lr"] |
| effective_lr = base_lr * self.sign_lr_scale if use_sign else base_lr |
|
|
| params = group["params"] |
| params_pad = params + [torch.empty_like(params[-1])] * ( |
| world_size - len(params) % world_size |
| ) |
|
|
| for base_i in range(0, len(params), world_size): |
| local_idx = base_i + rank |
| if local_idx < len(params): |
| p = params[local_idx] |
| if p.grad is None: |
| p.grad = torch.zeros_like(p) |
|
|
| state = self.state[p] |
| if not state: |
| state["momentum_buffer"] = torch.zeros_like(p) |
|
|
| pre = _nesterov_momentum( |
| p.grad, state["momentum_buffer"], beta |
| ) |
|
|
| if use_sign: |
| update = _sign_branch(pre, rand_zero=self.rand_zero) |
| else: |
| update = _muon_branch(pre, ns_steps=self.ns_steps) |
|
|
| |
| p.mul_(1.0 - base_lr * group["weight_decay"]) |
| p.add_(update.reshape(p.shape), alpha=-effective_lr) |
|
|
| dist.all_gather( |
| params_pad[base_i: base_i + world_size], |
| params_pad[base_i + rank], |
| ) |
|
|
| else: |
| for p in group["params"]: |
| if p.grad is None: |
| p.grad = torch.zeros_like(p) |
|
|
| state = self.state[p] |
| if not state: |
| state["exp_avg"] = torch.zeros_like(p) |
| state["exp_avg_sq"] = torch.zeros_like(p) |
| state["step"] = 0 |
|
|
| state["step"] += 1 |
| update = adam_update( |
| p.grad, |
| state["exp_avg"], |
| state["exp_avg_sq"], |
| state["step"], |
| group["betas"], |
| group["eps"], |
| ) |
| p.mul_(1.0 - group["lr"] * group["weight_decay"]) |
| p.add_(update, alpha=-group["lr"]) |
|
|
| return loss |
|
|