Agron / agron_nets /optimizers /__init__.py
Njap-png
Add Agron neural-network zoo, optimisers, algorithms, schedulers and unified trainer
a311e94
Raw History Blame Contribute Delete
43.9 kB
"""Advanced optimisers for the Agron zoo.
Every optimiser is a standard ``torch.optim.Optimizer`` subclass, so it can be
used directly with PyTorch or routed through :func:`build_optimizer`.
Registered optimisers
---------------------
``adam``, ``adamw``, ``adamax``, ``adagrad``, ``rmsprop``, ``sgd``, ``nadam``,
``lion``, ``adafactor``, ``adan``, ``lamb``, ``muon``, ``shampoo``,
``schedulefree``, ``kahan``, ``sophia``
"""
from __future__ import annotations
import math
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
import torch
from torch.optim import Optimizer
# --------------------------------------------------------------------------- #
# Registry
# --------------------------------------------------------------------------- #
OPTIMIZERS: Dict[str, Dict[str, Any]] = {}
def _register(name: str, cls, description: str, family: str, defaults: Optional[Dict[str, Any]] = None) -> None:
OPTIMIZERS[name] = {
"name": name,
"class": cls,
"description": description,
"family": family,
"defaults": defaults or {},
}
def optimizer_names() -> List[str]:
return sorted(OPTIMIZERS.keys())
def get_optimizer(name: str) -> Dict[str, Any]:
key = (name or "adamw").strip().lower()
if key not in OPTIMIZERS:
raise KeyError(f"unknown optimizer '{name}'. available: {', '.join(optimizer_names())}")
return OPTIMIZERS[key]
def build_optimizer(name: str, params, **overrides):
"""Instantiate the named optimiser for ``params``."""
entry = get_optimizer(name)
defaults = dict(entry.get("defaults", {}))
defaults.update(overrides)
return entry["class"](params, **defaults)
def optimizer_report() -> List[Dict[str, Any]]:
return [
{"name": k, "family": v["family"], "description": v["description"], "defaults": v["defaults"]}
for k, v in sorted(OPTIMIZERS.items())
]
def _check_lr(lr: float, eps: float = 0.0) -> None:
if lr < 0.0:
raise ValueError(f"Invalid learning rate: {lr}")
if eps < 0.0:
raise ValueError(f"Invalid epsilon value: {eps}")
def _check_beta(beta: float, index: int) -> None:
if not 0.0 <= beta < 1.0:
raise ValueError(f"Invalid beta parameter at index {index}: {beta}")
# --------------------------------------------------------------------------- #
# Adam family
# --------------------------------------------------------------------------- #
class Adam(Optimizer):
"""Vanilla Adam with optional AMSGrad."""
def __init__(self, params, lr: float = 1e-3, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-8, weight_decay: float = 0.0, amsgrad: bool = False):
_check_lr(lr, eps)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay, amsgrad=amsgrad))
@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:
beta1, beta2 = group["betas"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("Adam does not support sparse gradients")
if group["weight_decay"] != 0:
grad = grad.add(p, alpha=group["weight_decay"])
state = self.state[p]
if not state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["v"] = torch.zeros_like(p)
if group["amsgrad"]:
state["max_v"] = torch.zeros_like(p)
state["step"] += 1
m, v = state["m"], state["v"]
m.lerp_(grad, 1 - beta1)
v.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
bc1 = 1 - beta1 ** state["step"]
bc2 = 1 - beta2 ** state["step"]
if group["amsgrad"]:
max_v = state["max_v"]
max_v.copy_(torch.maximum(max_v, v))
denom = (max_v.sqrt() / math.sqrt(bc2)) + group["eps"]
else:
denom = (v.sqrt() / math.sqrt(bc2)) + group["eps"]
p.addcdiv_(m, denom, value=-(group["lr"] / bc1))
return loss
class AdamW(Optimizer):
"""AdamW with decoupled weight decay and optional AMSGrad."""
def __init__(self, params, lr: float = 1e-3, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-8, weight_decay: float = 0.01, amsgrad: bool = False):
_check_lr(lr, eps)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay, amsgrad=amsgrad))
@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:
beta1, beta2 = group["betas"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("AdamW does not support sparse gradients")
state = self.state[p]
if not state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["v"] = torch.zeros_like(p)
if group["amsgrad"]:
state["max_v"] = torch.zeros_like(p)
state["step"] += 1
m, v = state["m"], state["v"]
if group["weight_decay"] != 0:
p.mul_(1 - group["lr"] * group["weight_decay"])
m.lerp_(grad, 1 - beta1)
v.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
bc1 = 1 - beta1 ** state["step"]
bc2 = 1 - beta2 ** state["step"]
if group["amsgrad"]:
max_v = state["max_v"]
max_v.copy_(torch.maximum(max_v, v))
denom = (max_v.sqrt() / math.sqrt(bc2)) + group["eps"]
else:
denom = (v.sqrt() / math.sqrt(bc2)) + group["eps"]
p.addcdiv_(m, denom, value=-(group["lr"] / bc1))
return loss
class NAdam(Optimizer):
"""Nesterov-accelerated Adam."""
def __init__(self, params, lr: float = 2e-3, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-8, weight_decay: float = 0.0, momentum_decay: float = 4e-3):
_check_lr(lr, eps)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay,
momentum_decay=momentum_decay))
@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:
beta1, beta2 = group["betas"]
mu = group["momentum_decay"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("NAdam does not support sparse gradients")
if group["weight_decay"] != 0:
grad = grad.add(p, alpha=group["weight_decay"])
state = self.state[p]
if not state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["v"] = torch.zeros_like(p)
state["mu_product"] = 1.0
state["step"] += 1
m, v = state["m"], state["v"]
mu_product = state["mu_product"] * (1 - 0.5 * 0.96 ** (state["step"] * mu))
mu_product_next = mu_product * (1 - 0.5 * 0.96 ** ((state["step"] + 1) * mu))
state["mu_product"] = mu_product_next
m.mul_(beta1).add_(grad, alpha=1 - beta1)
v.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
m_hat = beta1 * m / (1 - mu_product_next) + (1 - beta1) * grad / (1 - mu_product)
v_hat = v / (1 - beta2 ** state["step"])
p.addcdiv_(m_hat, v_hat.sqrt().add_(group["eps"]), value=-group["lr"])
return loss
class Adamax(Optimizer):
"""Adamax (infinity-norm variant of Adam)."""
def __init__(self, params, lr: float = 2e-3, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-8, weight_decay: float = 0.0):
_check_lr(lr, eps)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
if weight_decay < 0.0:
raise ValueError(f"Invalid weight_decay value: {weight_decay}")
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay))
@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:
beta1, beta2 = group["betas"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("Adamax does not support sparse gradients")
if group["weight_decay"] != 0:
grad = grad.add(p, alpha=group["weight_decay"])
state = self.state[p]
if not state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["u"] = torch.zeros_like(p)
state["step"] += 1
m, u = state["m"], state["u"]
m.mul_(beta1).add_(grad, alpha=1 - beta1)
u.mul_(beta2).clamp_(min=0).add_(grad.abs().add_(group["eps"]), alpha=1 - beta2)
bias_c = 1 - beta1 ** state["step"]
p.addcdiv_(m, u, value=-(group["lr"] / bias_c))
return loss
class Adagrad(Optimizer):
"""Adagrad with optional decoupled weight decay."""
def __init__(self, params, lr: float = 1e-2, lr_decay: float = 0.0, weight_decay: float = 0.0,
initial_accumulator_value: float = 0.0, eps: float = 1e-10):
_check_lr(lr, eps)
if lr_decay < 0.0:
raise ValueError(f"Invalid lr_decay value: {lr_decay}")
if weight_decay < 0.0:
raise ValueError(f"Invalid weight_decay value: {weight_decay}")
if initial_accumulator_value < 0.0:
raise ValueError("initial_accumulator_value must be non-negative")
super().__init__(params, dict(lr=lr, lr_decay=lr_decay, weight_decay=weight_decay,
initial_accumulator_value=initial_accumulator_value, eps=eps))
@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:
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("Adagrad does not support sparse gradients")
state = self.state[p]
if not state:
state["step"] = 0
state["sum"] = torch.full_like(p, group["initial_accumulator_value"])
state["step"] += 1
if group["weight_decay"] != 0:
grad = grad.add(p, alpha=group["weight_decay"])
clr = group["lr"] / (1 + (state["step"] - 1) * group["lr_decay"])
state["sum"].addcmul_(grad, grad, value=1)
p.addcdiv_(grad, state["sum"].sqrt().add_(group["eps"]), value=-clr)
return loss
class RMSprop(Optimizer):
"""RMSprop with momentum and centered variants."""
def __init__(self, params, lr: float = 1e-2, alpha: float = 0.99, eps: float = 1e-8,
weight_decay: float = 0.0, momentum: float = 0.0, centered: bool = False):
_check_lr(lr, eps)
if momentum < 0.0:
raise ValueError(f"Invalid momentum value: {momentum}")
if weight_decay < 0.0:
raise ValueError(f"Invalid weight_decay value: {weight_decay}")
if not 0.0 <= alpha < 1.0:
raise ValueError(f"Invalid alpha value: {alpha}")
super().__init__(params, dict(lr=lr, alpha=alpha, eps=eps, weight_decay=weight_decay,
momentum=momentum, centered=centered))
@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:
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("RMSprop does not support sparse gradients")
state = self.state[p]
if not state:
state["step"] = 0
state["square_avg"] = torch.zeros_like(p)
if group["momentum"] > 0:
state["momentum_buffer"] = torch.zeros_like(p)
if group["centered"]:
state["grad_avg"] = torch.zeros_like(p)
state["step"] += 1
alpha = group["alpha"]
square_avg = state["square_avg"]
square_avg.mul_(alpha).addcmul_(grad, grad, value=1 - alpha)
if group["centered"]:
grad_avg = state["grad_avg"]
grad_avg.mul_(alpha).add_(grad, alpha=1 - alpha)
avg = square_avg.addcmul_(grad_avg, grad_avg, value=-1).sqrt().add_(group["eps"])
else:
avg = square_avg.sqrt().add_(group["eps"])
if group["weight_decay"] != 0:
grad = grad.add(p, alpha=group["weight_decay"])
if group["momentum"] > 0:
buf = state["momentum_buffer"]
buf.mul_(group["momentum"]).addcdiv_(grad, avg)
p.add_(buf, alpha=-group["lr"])
else:
p.addcdiv_(grad, avg, value=-group["lr"])
return loss
class SGD(Optimizer):
"""SGD with momentum, dampening, weight decay and Nesterov."""
def __init__(self, params, lr: float = 1e-2, momentum: float = 0.0, dampening: float = 0.0,
weight_decay: float = 0.0, nesterov: bool = False):
_check_lr(lr)
if momentum < 0.0:
raise ValueError(f"Invalid momentum value: {momentum}")
if weight_decay < 0.0:
raise ValueError(f"Invalid weight_decay value: {weight_decay}")
if nesterov and (momentum <= 0 or dampening != 0):
raise ValueError("Nesterov momentum requires momentum > 0 and dampening == 0")
super().__init__(params, dict(lr=lr, momentum=momentum, dampening=dampening,
weight_decay=weight_decay, nesterov=nesterov))
@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:
for p in group["params"]:
if p.grad is None:
continue
d_p = p.grad
if group["weight_decay"] != 0:
d_p = d_p.add(p, alpha=group["weight_decay"])
if group["momentum"] != 0:
buf = self.state[p].get("momentum_buffer")
if buf is None:
buf = self.state[p]["momentum_buffer"] = torch.clone(d_p).detach()
else:
buf.mul_(group["momentum"]).add_(d_p, alpha=1 - group["dampening"])
d_p = d_p.add(buf, alpha=group["momentum"]) if group["nesterov"] else buf
p.add_(d_p, alpha=-group["lr"])
return loss
# --------------------------------------------------------------------------- #
# Sign / momentum based
# --------------------------------------------------------------------------- #
class Lion(Optimizer):
"""Evolved Sign Momentum (Chen et al., 2023)."""
def __init__(self, params, lr: float = 1e-4, betas: Tuple[float, float] = (0.9, 0.99),
weight_decay: float = 0.01):
_check_lr(lr)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
super().__init__(params, dict(lr=lr, betas=betas, weight_decay=weight_decay))
@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:
beta1, beta2 = group["betas"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("Lion does not support sparse gradients")
state = self.state[p]
if not state:
state["m"] = torch.zeros_like(p)
m = state["m"]
if group["weight_decay"] != 0:
p.mul_(1 - group["lr"] * group["weight_decay"])
update = m.mul(beta1).add(grad, alpha=1 - beta1)
p.add_(torch.sign(update), alpha=-group["lr"])
m.mul_(beta2).add_(grad, alpha=1 - beta2)
state["m"] = m
return loss
# --------------------------------------------------------------------------- #
# Factored / memory efficient
# --------------------------------------------------------------------------- #
class Adafactor(Optimizer):
"""Adafactor with factored second moments and update clipping."""
def __init__(self, params, lr: float = 1e-3, beta2: float = 0.999,
eps: Tuple[float, float] = (1e-30, 1e-3), clip_threshold: float = 1.0,
weight_decay: float = 0.0, min_dim_size_to_factor: int = 128):
_check_lr(lr)
_check_beta(beta2, 1)
super().__init__(params, dict(lr=lr, beta2=beta2, eps=eps, clip_threshold=clip_threshold,
weight_decay=weight_decay,
min_dim_size_to_factor=min_dim_size_to_factor))
@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:
beta2 = group["beta2"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("Adafactor does not support sparse gradients")
state = self.state[p]
factored = (
grad.dim() >= 2
and min(grad.shape[-1], grad.shape[-2]) >= group["min_dim_size_to_factor"]
)
if not state:
state["step"] = 0
if factored:
state["row"] = torch.zeros(grad.shape[:-1], device=grad.device)
state["col"] = torch.zeros(grad.shape[:-2] + grad.shape[-1:], device=grad.device)
else:
state["v"] = torch.zeros_like(grad)
state["step"] += 1
step = state["step"]
if factored:
row, col = state["row"], state["col"]
row.mul_(beta2).add_(grad.pow(2).mean(dim=-1), alpha=1 - beta2)
col.mul_(beta2).add_(grad.pow(2).mean(dim=-2), alpha=1 - beta2)
row_mean = row.mean(dim=-1, keepdim=True).clamp_min(1e-30)
col_mean = col.mean(dim=-2, keepdim=True).clamp_min(1e-30)
v = (row / row_mean).unsqueeze(-1) * (col / col_mean).unsqueeze(-2)
else:
v = state["v"]
v.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
update = grad / (v.sqrt() + group["eps"][1])
update_norm = update.norm()
if update_norm > 0:
update = update / max(1.0, (update_norm / group["clip_threshold"]).item())
rel_step = group["lr"] * math.sqrt(max(1e-38, 1 - beta2 ** step))
p.add_(update, alpha=-rel_step)
if group["weight_decay"] != 0:
p.mul_(1 - group["lr"] * group["weight_decay"])
return loss
class Adan(Optimizer):
"""Adan - adaptive Nesterov momentum optimiser (Xie et al., 2022)."""
def __init__(self, params, lr: float = 1e-3, betas: Tuple[float, float, float] = (0.98, 0.92, 0.99),
eps: float = 1e-8, weight_decay: float = 0.01):
_check_lr(lr, eps)
for i, b in enumerate(betas):
_check_beta(b, i)
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay))
@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:
beta1, beta2, beta3 = group["betas"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("Adan does not support sparse gradients")
state = self.state[p]
if not state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["v"] = torch.zeros_like(p)
state["n"] = torch.zeros_like(p)
state["prev_grad"] = torch.zeros_like(p)
state["step"] += 1
m, v, n, prev_grad = state["m"], state["v"], state["n"], state["prev_grad"]
if group["weight_decay"] != 0:
p.mul_(1 - group["lr"] * group["weight_decay"])
grad_diff = grad - prev_grad
m.mul_(beta1).add_(grad, alpha=1 - beta1)
v.mul_(beta2).add_(grad_diff, alpha=1 - beta2)
n.mul_(beta3).addcmul_(grad + (1 - beta2) * grad_diff,
grad + (1 - beta2) * grad_diff, value=1 - beta3)
prev_grad.copy_(grad)
bc1 = 1 - beta1 ** state["step"]
bc2 = 1 - beta2 ** state["step"]
bc3 = 1 - beta3 ** state["step"]
denom = (n.sqrt() / math.sqrt(bc3)) + group["eps"]
update = (m + (1 - beta2) * v) / denom
p.add_(update, alpha=-(group["lr"] / bc1))
return loss
class LAMB(Optimizer):
"""LAMB - layer-wise adaptive moments for batched training."""
def __init__(self, params, lr: float = 1e-3, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-6, weight_decay: float = 0.01):
_check_lr(lr, eps)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay))
@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:
beta1, beta2 = group["betas"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("LAMB does not support sparse gradients")
state = self.state[p]
if not state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["v"] = torch.zeros_like(p)
state["step"] += 1
m, v = state["m"], state["v"]
m.mul_(beta1).add_(grad, alpha=1 - beta1)
v.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
bc1 = 1 - beta1 ** state["step"]
bc2 = 1 - beta2 ** state["step"]
update = (m / bc1) / ((v / bc2).sqrt() + group["eps"])
if group["weight_decay"] != 0:
update = update.add(p, alpha=group["weight_decay"])
param_norm = p.norm()
update_norm = update.norm()
trust_ratio = 1.0
if param_norm > 0 and update_norm > 0:
trust_ratio = (param_norm / update_norm).item()
p.add_(update, alpha=-group["lr"] * trust_ratio)
return loss
# --------------------------------------------------------------------------- #
# Second-order inspired
# --------------------------------------------------------------------------- #
def _zeropower_newton_schulz(matrix: torch.Tensor, steps: int = 5) -> torch.Tensor:
"""Approximate the orthogonal polar factor via Newton-Schulz iteration."""
a, b, c = 3.4445, -4.7750, 2.0315
x = matrix.float()
transposed = x.size(0) > x.size(1)
if transposed:
x = x.T
x = x / (x.norm() + 1e-7)
for _ in range(steps):
gram = x @ x.T
x = a * x + (b * gram + c * (gram @ gram)) @ x
if transposed:
x = x.T
return x.to(matrix.dtype)
class Muon(Optimizer):
"""Muon - momentum orthogonalised by Newton-Schulz iterations.
Two-dimensional parameters are orthogonalised; one-dimensional parameters
fall back to Adam-style updates.
"""
def __init__(self, params, lr: float = 0.02, momentum: float = 0.95,
nesterov: bool = True, weight_decay: float = 0.0,
ns_steps: int = 5, eps: float = 1e-8,
adam_betas: Tuple[float, float] = (0.9, 0.999)):
_check_lr(lr, eps)
super().__init__(params, dict(lr=lr, momentum=momentum, nesterov=nesterov,
weight_decay=weight_decay, ns_steps=ns_steps,
eps=eps, adam_betas=adam_betas,
ns_coefficients=(3.4445, -4.7750, 2.0315)))
@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:
params = [p for p in group["params"] if p.grad is not None]
if not params:
continue
grads = [p.grad for p in params]
if group["weight_decay"] != 0:
for p in params:
p.mul_(1 - group["lr"] * group["weight_decay"])
# 2D parameters: orthogonalised momentum
ortho = [(p, g) for p, g in zip(params, grads) if p.dim() >= 2]
flat = [(p, g) for p, g in zip(params, grads) if p.dim() < 2]
for p, g in ortho:
state = self.state[p]
if "momentum" not in state:
state["momentum"] = torch.zeros_like(p)
buf = state["momentum"]
buf.mul_(group["momentum"]).add_(g)
update = g.add(buf, alpha=group["momentum"]) if group["nesterov"] else buf
o = _zeropower_newton_schulz(update, group["ns_steps"])
scale = max(1.0, p.size(0) / p.size(1)) ** 0.5
p.add_(o, alpha=-group["lr"] * scale)
beta1, beta2 = group["adam_betas"]
for p, g in flat:
state = self.state[p]
if "step" not in state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["v"] = torch.zeros_like(p)
state["step"] += 1
m, v = state["m"], state["v"]
m.lerp_(g, 1 - beta1)
v.mul_(beta2).addcmul_(g, g, value=1 - beta2)
bc1 = 1 - beta1 ** state["step"]
bc2 = 1 - beta2 ** state["step"]
denom = (v.sqrt() / math.sqrt(bc2)) + group["eps"]
p.addcdiv_(m, denom, value=-(group["lr"] / bc1))
return loss
def _matrix_inverse_root(mat: torch.Tensor, root: int = 2, eps: float = 1e-8) -> torch.Tensor:
"""``mat ** (-1/root)`` via a symmetric eigendecomposition."""
mat = mat.float() + eps * torch.eye(mat.size(0), device=mat.device)
eigvals, eigvecs = torch.linalg.eigh(mat)
eigvals = eigvals.clamp_min(1e-12).pow(-1.0 / root)
return (eigvecs * eigvals.unsqueeze(0)) @ eigvecs.T
class Shampoo(Optimizer):
"""Shampoo - full-matrix preconditioning of gradients.
Preconditioners are refreshed every ``precondition_frequency`` steps and
fall back to a diagonal preconditioner for very large dimensions.
"""
def __init__(self, params, lr: float = 1e-2, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-8, weight_decay: float = 0.0,
precondition_frequency: int = 25, max_dim: int = 2048):
_check_lr(lr, eps)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay,
precondition_frequency=precondition_frequency,
max_dim=max_dim))
@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:
beta1, beta2 = group["betas"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("Shampoo does not support sparse gradients")
state = self.state[p]
if not state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["precond"] = None
state["factors"] = {}
state["step"] += 1
m = state["m"]
m.mul_(beta1).add_(grad, alpha=1 - beta1)
update = m
if group["weight_decay"] != 0:
p.mul_(1 - group["lr"] * group["weight_decay"])
if grad.dim() == 2:
factors = state["factors"]
if "L" not in factors:
factors["L"] = torch.zeros_like(grad @ grad.T)
factors["R"] = torch.zeros_like(grad.T @ grad)
factors["L"].mul_(beta2).add_(grad @ grad.T, alpha=1 - beta2)
factors["R"].mul_(beta2).add_(grad.T @ grad, alpha=1 - beta2)
if state["step"] % group["precondition_frequency"] == 0 or state["precond"] is None:
if max(grad.shape) > group["max_dim"]:
# diagonal fallback for very large layers
diagonal = factors["L"].diagonal().clamp_min(1e-12).pow(-0.25)
l_inv = torch.diag(diagonal)
r_diag = factors["R"].diagonal().clamp_min(1e-12).pow(-0.25)
r_inv = torch.diag(r_diag)
else:
l_inv = _matrix_inverse_root(factors["L"], 4)
r_inv = _matrix_inverse_root(factors["R"], 4)
state["precond"] = (l_inv, r_inv)
l_inv, r_inv = state["precond"]
try:
update = l_inv @ update @ r_inv
except Exception:
update = update
elif grad.dim() > 2:
update = update / (grad.pow(2).mean(dim=tuple(range(1, grad.dim())), keepdim=True).sqrt() + group["eps"])
p.add_(update, alpha=-group["lr"])
return loss
class Sophia(Optimizer):
"""Sophia - second-order clipped stochastic optimisation.
The diagonal Hessian estimate is updated by :meth:`update_hessian` using
the Gauss-Newton-Bartlett (GNB) estimator with a Hutchinson probe.
"""
def __init__(self, params, lr: float = 1e-3, betas: Tuple[float, float] = (0.965, 0.99),
eps: float = 1e-15, weight_decay: float = 0.01, rho: float = 0.04):
_check_lr(lr, eps)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay, rho=rho))
@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:
beta1, beta2 = group["betas"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
state = self.state[p]
if not state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["h"] = torch.zeros_like(p)
state["step"] += 1
m, h = state["m"], state["h"]
if group["weight_decay"] != 0:
p.mul_(1 - group["lr"] * group["weight_decay"])
m.mul_(beta1).add_(grad, alpha=1 - beta1)
ratio = (m / (group["rho"] * h + group["eps"])).clamp(-1.0, 1.0)
p.add_(ratio, alpha=-group["lr"])
return loss
def update_hessian(self, loss: torch.Tensor, seed: Optional[int] = None) -> None:
"""Refresh the diagonal Hessian using a Hutchinson/GNB probe."""
params = [p for group in self.param_groups for p in group["params"] if p.requires_grad]
if not params:
return
generator = torch.Generator(device=params[0].device)
if seed is not None:
generator.manual_seed(seed)
grads = torch.autograd.grad(loss, params, create_graph=True, retain_graph=True, allow_unused=True)
probes = [torch.randint_like(p, 0, 2, generator=generator).float() * 2 - 1 for p in params]
dot = sum((g * v).sum() for g, v in zip(grads, probes) if g is not None and g.requires_grad)
if not isinstance(dot, torch.Tensor) or not dot.requires_grad:
return
second = torch.autograd.grad(dot, params, retain_graph=False, allow_unused=True)
for group in self.param_groups:
beta2 = group["betas"][1]
for p, hv in zip(params, second):
if p not in self.state:
continue
if hv is None:
continue
state = self.state[p]
h = state["h"]
probe_sq = (hv * hv).abs()
h.mul_(beta2).add_(probe_sq, alpha=1 - beta2)
# --------------------------------------------------------------------------- #
# Schedule-free and compensated
# --------------------------------------------------------------------------- #
class ScheduleFreeAdamW(Optimizer):
"""Schedule-Free AdamW (Defazio et al., 2024).
Implements Algorithm 1 of the paper: ``z`` is the fast iterate, ``x`` the
averaged eval point and ``y = (1-b1) z + b1 x`` the gradient point.
Call :meth:`train` before the forward pass and :meth:`eval` before
validation / inference.
"""
def __init__(self, params, lr: float = 2.5e-3, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-8, weight_decay: float = 0.0, warmup_steps: int = 0):
_check_lr(lr, eps)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay,
warmup_steps=warmup_steps))
def _state(self, p):
state = self.state[p]
if not state:
state["step"] = 0
state["z"] = p.detach().clone()
state["x"] = p.detach().clone()
state["v"] = torch.zeros_like(p)
return state
@torch.no_grad()
def train(self):
"""Set the parameters to the gradient point ``y``."""
for group in self.param_groups:
beta1 = group["betas"][0]
for p in group["params"]:
state = self._state(p)
p.copy_((1 - beta1) * state["z"] + beta1 * state["x"])
@torch.no_grad()
def eval(self):
"""Set the parameters to the averaged eval point ``x``."""
for group in self.param_groups:
for p in group["params"]:
state = self.state[p]
if "x" in state:
p.copy_(state["x"])
@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:
beta1, beta2 = group["betas"]
lr = group["lr"]
for p in group["params"]:
if p.grad is None:
continue
state = self._state(p)
state["step"] += 1
t = state["step"]
z, x, v = state["z"], state["x"], state["v"]
grad = p.grad
v.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
if t == 1:
v.copy_(grad.pow(2))
denom = v.sqrt().add_(group["eps"])
if group["weight_decay"] != 0:
z.mul_(1 - lr * group["weight_decay"])
z.addcdiv_(grad, denom, value=-lr)
warmup = group["warmup_steps"]
ckp1 = t / warmup if (warmup > 0 and t < warmup) else 1.0 / (t + 1)
x.lerp_(z, ckp1)
# prepare the next gradient point
p.copy_((1 - beta1) * z + beta1 * x)
return loss
class KahanAdamW(Optimizer):
"""AdamW with Kahan compensated summation on the parameter update.
Compensated summation removes the low-precision drift that accumulates
when training long runs in bf16/fp16.
"""
def __init__(self, params, lr: float = 1e-3, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-8, weight_decay: float = 0.01):
_check_lr(lr, eps)
_check_beta(betas[0], 0)
_check_beta(betas[1], 1)
super().__init__(params, dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay))
@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:
beta1, beta2 = group["betas"]
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if group["weight_decay"] != 0:
grad = grad.add(p, alpha=group["weight_decay"])
state = self.state[p]
if not state:
state["step"] = 0
state["m"] = torch.zeros_like(p)
state["v"] = torch.zeros_like(p)
state["comp"] = torch.zeros_like(p, dtype=torch.float32)
state["step"] += 1
m, v = state["m"], state["v"]
m.lerp_(grad, 1 - beta1)
v.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
bc1 = 1 - beta1 ** state["step"]
bc2 = 1 - beta2 ** state["step"]
denom = (v.sqrt() / math.sqrt(bc2)) + group["eps"]
delta = -(group["lr"] / bc1) * (m / denom)
# Kahan compensated update
comp = state["comp"]
y = delta.float() - comp
t = p.float() + y
comp = (t - p.float()) - y
state["comp"] = comp
p.copy_(t.to(p.dtype))
return loss
# --------------------------------------------------------------------------- #
# Register everything
# --------------------------------------------------------------------------- #
_register("adam", Adam, "Vanilla Adam with optional AMSGrad", "adaptive",
{"lr": 1e-3, "betas": (0.9, 0.999)})
_register("adamw", AdamW, "AdamW with decoupled weight decay", "adaptive",
{"lr": 1e-3, "betas": (0.9, 0.999), "weight_decay": 0.01})
_register("nadam", NAdam, "Nesterov-accelerated Adam", "adaptive",
{"lr": 2e-3, "betas": (0.9, 0.999)})
_register("adamax", Adamax, "Adam with an infinity-norm second moment", "adaptive",
{"lr": 2e-3, "betas": (0.9, 0.999)})
_register("adagrad", Adagrad, "Cumulative squared-gradient preconditioning", "adaptive",
{"lr": 1e-2})
_register("rmsprop", RMSprop, "RMSprop with momentum / centering", "adaptive",
{"lr": 1e-2, "alpha": 0.99})
_register("sgd", SGD, "SGD with momentum and Nesterov", "momentum",
{"lr": 1e-2, "momentum": 0.9})
_register("lion", Lion, "Evolved Sign Momentum (Lion)", "sign-momentum",
{"lr": 1e-4, "betas": (0.9, 0.99), "weight_decay": 0.01})
_register("adafactor", Adafactor, "Memory-efficient factored second moments", "factored",
{"lr": 1e-3})
_register("adan", Adan, "Adaptive Nesterov momentum (Adan)", "adaptive",
{"lr": 1e-3, "betas": (0.98, 0.92, 0.99), "weight_decay": 0.01})
_register("lamb", LAMB, "Layer-wise adaptive moments (LAMB)", "layer-adaptive",
{"lr": 1e-3, "betas": (0.9, 0.999), "weight_decay": 0.01})
_register("muon", Muon, "Momentum orthogonalised with Newton-Schulz", "orthogonalised",
{"lr": 0.02, "momentum": 0.95})
_register("shampoo", Shampoo, "Full-matrix gradient preconditioning", "second-order",
{"lr": 1e-2, "precondition_frequency": 25})
_register("sophia", Sophia, "Second-order clipped optimisation with GNB Hessian", "second-order",
{"lr": 1e-3, "betas": (0.965, 0.99)})
_register("schedulefree", ScheduleFreeAdamW, "Schedule-Free AdamW", "schedule-free",
{"lr": 2.5e-3, "betas": (0.9, 0.999)})
_register("kahan", KahanAdamW, "AdamW with Kahan compensated updates", "compensated",
{"lr": 1e-3, "betas": (0.9, 0.999), "weight_decay": 0.01})
__all__ = [
"OPTIMIZERS",
"Adam",
"AdamW",
"NAdam",
"Adamax",
"Adagrad",
"RMSprop",
"SGD",
"Lion",
"Adafactor",
"Adan",
"LAMB",
"Muon",
"Shampoo",
"Sophia",
"ScheduleFreeAdamW",
"KahanAdamW",
"build_optimizer",
"get_optimizer",
"optimizer_names",
"optimizer_report",
]