use std::sync::Arc;
use nuvec::{
Array, DType, Error, Result, add, addmm, divide, matmul, minimum, multiply, reshape, sign,
sqrt, subtract, sum,
};
use crate::Module;
pub trait Optimizer {
fn update<M: Module>(&mut self, model: &mut M, gradients: &[Array]) -> Result<()> {
let parameters: Vec<Array> = model
.trainable_parameters()
.into_iter()
.map(|(_, a)| a)
.collect();
if parameters.len() != gradients.len() {
return Err(Error::InvalidArgument(format!(
"{} gradients for {} parameters",
gradients.len(),
parameters.len()
)));
}
let updated = self.apply(¶meters, gradients)?;
model.update_trainable(&updated)
}
fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>>;
}
#[derive(Clone)]
pub enum LearningRate {
Constant(f32),
Schedule(Arc<dyn Fn(u64) -> f32 + Send + Sync>),
}
impl LearningRate {
pub fn at(&self, step: u64) -> f32 {
match self {
Self::Constant(lr) => *lr,
Self::Schedule(f) => f(step),
}
}
}
impl std::fmt::Debug for LearningRate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Constant(lr) => write!(f, "Constant({lr})"),
Self::Schedule(_) => f.write_str("Schedule"),
}
}
}
impl From<f64> for LearningRate {
fn from(lr: f64) -> Self {
Self::Constant(lr as f32)
}
}
impl From<f32> for LearningRate {
fn from(lr: f32) -> Self {
Self::Constant(lr)
}
}
pub mod schedules {
use std::sync::Arc;
use super::LearningRate;
fn schedule(f: impl Fn(u64) -> f32 + Send + Sync + 'static) -> LearningRate {
LearningRate::Schedule(Arc::new(f))
}
pub fn exponential_decay(init: f64, decay_rate: f64) -> LearningRate {
schedule(move |step| init as f32 * (decay_rate as f32).powf(step as f32))
}
pub fn step_decay(init: f64, decay_rate: f64, step_size: u64) -> LearningRate {
let step_size = step_size.max(1);
schedule(move |step| init as f32 * (decay_rate as f32).powf((step / step_size) as f32))
}
pub fn cosine_decay(init: f64, decay_steps: u64, end: f64) -> LearningRate {
let decay_steps = decay_steps.max(1);
let rate = (std::f64::consts::PI / decay_steps as f64) as f32;
schedule(move |step| {
let s = step.min(decay_steps) as f32;
let decay = 0.5f32 * (1.0 + (rate * s).cos());
end as f32 + decay * (init - end) as f32
})
}
pub fn linear_schedule(init: f64, end: f64, steps: u64) -> LearningRate {
let steps = steps.max(1);
let slope = ((end - init) / steps as f64) as f32;
schedule(move |step| step.min(steps) as f32 * slope + init as f32)
}
pub fn join_schedules(schedules: Vec<LearningRate>, boundaries: Vec<u64>) -> LearningRate {
assert_eq!(
schedules.len(),
boundaries.len() + 1,
"join_schedules: one boundary between consecutive schedules"
);
schedule(move |step| {
let mut out = schedules[0].at(step);
for (b, s) in boundaries.iter().zip(&schedules[1..]) {
if step >= *b {
out = s.at(step - b);
}
}
out
})
}
}
fn rate(lr: f32, like: &Array) -> Result<Array> {
Array::scalar(lr).astype(like.dtype())
}
fn zeros_like(state: &mut Vec<Array>, parameters: &[Array]) -> Result<()> {
if state.len() != parameters.len() {
*state = parameters
.iter()
.map(|p| nuvec::zeros(p.shape(), p.dtype()))
.collect::<Result<_>>()?;
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct Sgd {
pub learning_rate: LearningRate,
pub momentum: f64,
pub weight_decay: f64,
pub dampening: f64,
pub nesterov: bool,
step: u64,
velocity: Vec<Array>,
}
impl Sgd {
pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
Self {
learning_rate: learning_rate.into(),
momentum: 0.0,
weight_decay: 0.0,
dampening: 0.0,
nesterov: false,
step: 0,
velocity: Vec::new(),
}
}
pub fn momentum(mut self, momentum: f64) -> Self {
self.momentum = momentum;
self
}
pub fn weight_decay(mut self, weight_decay: f64) -> Self {
self.weight_decay = weight_decay;
self
}
pub fn nesterov(mut self, nesterov: bool) -> Self {
self.nesterov = nesterov;
self
}
}
impl Optimizer for Sgd {
fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>> {
zeros_like(&mut self.velocity, parameters)?;
let lr_now = self.learning_rate.at(self.step);
self.step += 1;
let mut out = Vec::with_capacity(parameters.len());
for ((p, g), v) in parameters.iter().zip(gradients).zip(&mut self.velocity) {
let mut g = g.clone();
if self.weight_decay != 0.0 {
g = add(&g, multiply(p, self.weight_decay)?)?;
}
let lr = rate(lr_now, &g)?;
if self.momentum <= 0.0 {
out.push(subtract(p, multiply(&lr, &g)?)?);
continue;
}
let mut next = multiply(self.momentum, &*v)?;
next = if self.dampening > 0.0 {
add(&next, multiply(1.0 - self.dampening, &g)?)?
} else {
add(&next, &g)?
};
let update = if self.nesterov {
add(&g, multiply(self.momentum, &next)?)?
} else {
next.clone()
};
*v = next;
out.push(subtract(p, multiply(&lr, &update)?)?);
}
Ok(out)
}
}
#[derive(Debug, Clone)]
pub struct Adam {
pub learning_rate: LearningRate,
pub betas: (f64, f64),
pub eps: f64,
pub bias_correction: bool,
pub weight_decay: f64,
step: u64,
m: Vec<Array>,
v: Vec<Array>,
}
impl Adam {
pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
Self {
learning_rate: learning_rate.into(),
betas: (0.9, 0.999),
eps: 1e-8,
bias_correction: false,
weight_decay: 0.0,
step: 0,
m: Vec::new(),
v: Vec::new(),
}
}
pub fn adamw(learning_rate: impl Into<LearningRate>) -> Self {
Self::new(learning_rate).weight_decay(0.01)
}
pub fn betas(mut self, b1: f64, b2: f64) -> Self {
self.betas = (b1, b2);
self
}
pub fn weight_decay(mut self, weight_decay: f64) -> Self {
self.weight_decay = weight_decay;
self
}
pub fn bias_correction(mut self, enabled: bool) -> Self {
self.bias_correction = enabled;
self
}
}
impl Optimizer for Adam {
fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>> {
zeros_like(&mut self.m, parameters)?;
zeros_like(&mut self.v, parameters)?;
let lr_now = self.learning_rate.at(self.step);
self.step += 1;
let (b1, b2) = self.betas;
let mut out = Vec::with_capacity(parameters.len());
for (((p, g), m), v) in parameters
.iter()
.zip(gradients)
.zip(&mut self.m)
.zip(&mut self.v)
{
let lr = rate(lr_now, g)?;
let p = if self.weight_decay != 0.0 {
multiply(p, subtract(1.0, multiply(&lr, self.weight_decay)?)?)?
} else {
p.clone()
};
*m = add(multiply(b1, &*m)?, multiply(1.0 - b1, g)?)?;
*v = add(multiply(b2, &*v)?, multiply(1.0 - b2, multiply(g, g)?)?)?;
let updated = if self.bias_correction {
let step = self.step as f32;
let one_minus = |b: f64| Array::scalar(1.0 - (b as f32).powf(step));
let c1 = divide(&lr, one_minus(b1))?.astype(g.dtype())?;
let c2 = divide(1.0f32, sqrt(&one_minus(b2))?)?.astype(g.dtype())?;
let denominator = add(multiply(sqrt(&*v)?, &c2)?, self.eps)?;
subtract(&p, divide(multiply(&c1, &*m)?, &denominator)?)?
} else {
subtract(&p, divide(multiply(&lr, &*m)?, add(sqrt(&*v)?, self.eps)?)?)?
};
out.push(updated);
}
Ok(out)
}
}
#[derive(Debug, Clone)]
pub struct Lion {
pub learning_rate: LearningRate,
pub betas: (f64, f64),
pub weight_decay: f64,
step: u64,
m: Vec<Array>,
}
impl Lion {
pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
Self {
learning_rate: learning_rate.into(),
betas: (0.9, 0.99),
weight_decay: 0.0,
step: 0,
m: Vec::new(),
}
}
pub fn weight_decay(mut self, weight_decay: f64) -> Self {
self.weight_decay = weight_decay;
self
}
}
impl Optimizer for Lion {
fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>> {
zeros_like(&mut self.m, parameters)?;
let lr_now = self.learning_rate.at(self.step);
self.step += 1;
let (b1, b2) = self.betas;
let mut out = Vec::with_capacity(parameters.len());
for ((p, g), m) in parameters.iter().zip(gradients).zip(&mut self.m) {
let lr = rate(lr_now, g)?;
let c = add(multiply(b1, &*m)?, multiply(1.0 - b1, g)?)?;
*m = add(multiply(b2, &*m)?, multiply(1.0 - b2, g)?)?;
let p = if self.weight_decay > 0.0 {
multiply(subtract(1.0, multiply(&lr, self.weight_decay)?)?, p)?
} else {
p.clone()
};
out.push(subtract(&p, multiply(&lr, sign(&c)?)?)?);
}
Ok(out)
}
}
#[derive(Debug, Clone)]
pub struct Muon {
pub learning_rate: LearningRate,
pub momentum: f64,
pub weight_decay: f64,
pub nesterov: bool,
pub ns_steps: usize,
step: u64,
v: Vec<Array>,
}
impl Muon {
pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
Self {
learning_rate: learning_rate.into(),
momentum: 0.95,
weight_decay: 0.01,
nesterov: true,
ns_steps: 5,
step: 0,
v: Vec::new(),
}
}
}
impl Optimizer for Muon {
fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>> {
zeros_like(&mut self.v, parameters)?;
let lr_now = self.learning_rate.at(self.step);
self.step += 1;
let mut out = Vec::with_capacity(parameters.len());
for ((p, g), v) in parameters.iter().zip(gradients).zip(&mut self.v) {
let mut g = g.clone();
if self.weight_decay != 0.0 {
g = add(&g, multiply(self.weight_decay, p)?)?;
}
*v = add(
multiply(self.momentum, &*v)?,
multiply(1.0 - self.momentum, &g)?,
)?;
let mut update = if self.nesterov {
add(
multiply(&g, 1.0 - self.momentum)?,
multiply(&*v, self.momentum)?,
)?
} else {
v.clone()
};
let mut lr = rate(lr_now, &g)?;
if update.ndim() >= 2 {
let shape = update.shape().to_vec();
let flat = reshape(&update, &[shape[0] as isize, -1])?;
let isize_shape: Vec<isize> = shape.iter().map(|&d| d as isize).collect();
update = reshape(&orthogonalize(&flat, self.ns_steps)?, &isize_shape)?;
let (rows, cols) = (shape[shape.len() - 2], shape[shape.len() - 1]);
lr = multiply(&lr, (rows as f64 / cols as f64).max(1.0).sqrt())?;
}
out.push(subtract(p, multiply(&lr, &update)?)?);
}
Ok(out)
}
}
fn orthogonalize(x: &Array, steps: usize) -> Result<Array> {
let (a, b, c) = (3.4445, -4.775, 2.0315);
let transpose = x.shape()[0] > x.shape()[1];
let mut x = if transpose { x.t()? } else { x.clone() };
let norm = sqrt(&sum(&multiply(&x, &x)?, &[], true)?)?;
x = divide(&x, add(&norm, 1e-7)?)?;
for _ in 0..steps {
let am = matmul(&x, &x.t()?)?;
let bm = addmm(&multiply(&am, b)?, &am, &am, c as f32, 1.0)?;
x = addmm(&multiply(&x, a)?, &bm, &x, 1.0, 1.0)?;
}
if transpose { x.t() } else { Ok(x) }
}
pub fn clip_grad_norm(gradients: &[Array], max_norm: f64) -> Result<(Vec<Array>, Array)> {
if max_norm < 0.0 {
return Err(Error::InvalidArgument(format!(
"clip_grad_norm: max_norm should be >= 0, got {max_norm}"
)));
}
let mut squared: Option<Array> = None;
for g in gradients {
let g = if g.dtype() == DType::F16 {
g.astype(DType::F32)?
} else {
g.clone()
};
let s = sum(&multiply(&g, &g)?, &[], false)?;
squared = Some(match squared {
Some(acc) => add(&acc, &s)?,
None => s,
});
}
let total = sqrt(&squared.unwrap_or_else(|| Array::scalar(0.0f32)))?;
let normalizer = minimum(divide(max_norm, add(&total, 1e-6)?)?, 1.0)?;
let clipped = gradients
.iter()
.map(|g| {
let scaled = multiply(g, &normalizer)?;
if g.dtype().category() == nuvec::Category::Float {
scaled.astype(g.dtype())
} else {
Ok(scaled)
}
})
.collect::<Result<_>>()?;
Ok((clipped, total))
}