GUIDO_test_200M / vathos /optimizers.py
Paerle's picture
Initial upload: GUIDO-small 200M bugfix ckpt + Vathos + modeling + README
d8c733f verified
Raw
History Blame Contribute Delete
18.8 kB
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")
# ---------------------------------------------------------------------------
# Shared update primitives
# ---------------------------------------------------------------------------
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) # buf ← β·buf + (1-β)·grad
return grad.lerp_(buf, beta) # returns (1-β)·grad + β·buf [Nesterov]
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
)
# Normalise: √(m·n) / √n = √m → same F-norm as the Muon branch
s = s / (u.size(-1) ** 0.5)
return s
# ---------------------------------------------------------------------------
# Single-device variant
# ---------------------------------------------------------------------------
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, {})
# ------------------------------------------------------------------
# Scheduling API
# ------------------------------------------------------------------
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
# ------------------------------------------------------------------
# Step
# ------------------------------------------------------------------
@torch.no_grad()
def step(self, closure=None):
loss = None
if closure is not None:
with torch.enable_grad():
loss = closure()
# One global Bernoulli draw: all 2-D params follow the same branch this step.
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)
# Shared momentum step (identical regardless of branch)
pre = _nesterov_momentum(p.grad, state["momentum_buffer"], beta)
# Branch selection
if use_sign:
update = _sign_branch(pre, rand_zero=self.rand_zero)
else:
update = _muon_branch(pre, ns_steps=self.ns_steps)
# Weight decay at base lr; parameter update at effective lr
p.mul_(1.0 - base_lr * group["weight_decay"])
p.add_(update.reshape(p.shape), alpha=-effective_lr)
else: # AdamW
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
# ---------------------------------------------------------------------------
# Distributed variant
# ---------------------------------------------------------------------------
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, {})
# ------------------------------------------------------------------
# Scheduling API
# ------------------------------------------------------------------
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
# ------------------------------------------------------------------
# Step
# ------------------------------------------------------------------
@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()
# Broadcast the Bernoulli draw from rank 0 so every GPU takes the same branch.
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)
# Weight decay at base lr; parameter update at effective lr
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: # AdamW
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