#![deny(unsafe_code)]
mod common;
mod adafactor;
mod adam;
mod adamw;
mod kron_psgd;
mod lamb;
mod lion;
mod mars;
mod muon;
mod nadamw;
mod qhadamw;
mod radam;
mod sgd;
mod soap;
mod sophia;
mod stiefel;
use std::collections::HashMap;
pub use adafactor::Adafactor;
pub use adam::Adam;
pub use adamw::AdamW;
pub use kron_psgd::KronPsgd;
pub use lamb::Lamb;
pub use lion::Lion;
pub use mars::Mars;
pub use muon::{Muon, newton_schulz_orth};
pub use nadamw::NAdamW;
pub use qhadamw::QHAdamW;
pub use radam::RAdam;
pub use sgd::Sgd;
pub use soap::Soap;
pub use sophia::Sophia;
pub use stiefel::Stiefel;
pub use common::{global_grad_clip_scale, l2_norm};
pub struct OptItem<'a> {
pub name: &'a str,
pub shape: &'a [usize],
pub param: &'a mut [f32],
pub grad: &'a [f32],
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct OptimizerState {
pub step: u64,
pub buffers: Vec<(String, Vec<f32>)>,
}
impl OptimizerState {
pub fn extend_slot(&mut self, slot: &str, map: &HashMap<String, Vec<f32>>) {
let mut names: Vec<&String> = map.keys().collect();
names.sort();
for name in names {
self.buffers
.push((format!("{slot}/{name}"), map[name].clone()));
}
}
pub fn take_slot(&self, slot: &str, map: &mut HashMap<String, Vec<f32>>) {
let prefix = format!("{slot}/");
for (key, values) in &self.buffers {
if let Some(name) = key.strip_prefix(&prefix) {
map.insert(name.to_string(), values.clone());
}
}
}
}
pub trait Optimizer {
fn step(&mut self, name: &str, shape: &[usize], param: &mut [f32], grad: &[f32]);
fn step_batch(&mut self, items: &mut [OptItem<'_>]) {
for it in items.iter_mut() {
self.step(it.name, it.shape, it.param, it.grad);
}
}
fn end_iteration(&mut self) {}
fn set_lr(&mut self, _lr: f32) {}
fn lr_scale(&self, _name: &str) -> f32 {
1.0
}
fn state_dict(&self) -> Option<OptimizerState> {
None
}
fn load_state_dict(&mut self, _state: &OptimizerState) -> bool {
false
}
}
#[cfg(test)]
mod state_dict_tests {
use super::*;
fn resume_matches_uninterrupted<O: Optimizer, F: Fn() -> O>(make: F) {
let grads: [[f32; 4]; 6] = [
[0.5, -0.25, 0.125, 1.0],
[0.4, -0.20, 0.100, 0.9],
[0.3, -0.15, 0.075, 0.8],
[0.2, -0.10, 0.050, 0.7],
[0.1, -0.05, 0.025, 0.6],
[0.05, -0.02, 0.01, 0.5],
];
let shape = [2usize, 2];
let mut reference = make();
let mut p_ref = [1.0f32, 1.0, 1.0, 1.0];
for g in &grads {
reference.step("w", &shape, &mut p_ref, g);
reference.end_iteration();
}
let mut first = make();
let mut p = [1.0f32, 1.0, 1.0, 1.0];
for g in &grads[..3] {
first.step("w", &shape, &mut p, g);
first.end_iteration();
}
let state = first.state_dict().expect("this optimizer opted in");
drop(first);
let mut second = make();
assert!(second.load_state_dict(&state), "load_state_dict refused");
for g in &grads[3..] {
second.step("w", &shape, &mut p, g);
second.end_iteration();
}
for (i, (a, b)) in p_ref.iter().zip(p.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"element {i}: resumed {b} != uninterrupted {a}"
);
}
}
#[test]
fn adam_resumes_bit_exactly() {
resume_matches_uninterrupted(|| Adam::new(0.1));
}
#[test]
fn adamw_resumes_bit_exactly() {
resume_matches_uninterrupted(|| AdamW::new(0.1));
}
#[test]
fn sgd_with_momentum_resumes_bit_exactly() {
resume_matches_uninterrupted(|| {
let mut o = Sgd::new(0.1);
o.momentum = 0.9;
o
});
}
#[test]
fn lion_resumes_bit_exactly() {
resume_matches_uninterrupted(|| Lion::new(0.1));
}
#[test]
fn an_optimizer_without_state_says_so() {
assert!(Soap::new(0.1).state_dict().is_none());
assert!(!Soap::new(0.1).load_state_dict(&OptimizerState::default()));
}
}
#[cfg(test)]
mod hot_loop_tests {
use super::*;
fn spread(n: usize, seed: u32) -> Vec<f32> {
let mut s = seed;
(0..n)
.map(|_| {
s = s.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((s >> 8) as f32 / 8_388_608.0 - 1.0) * 0.1
})
.collect()
}
fn assert_bit_equal(label: &str, got: &[f32], want: &[f32]) {
assert_eq!(got.len(), want.len(), "{label}: length");
for (i, (a, b)) in got.iter().zip(want).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"{label}[{i}]: {a} != {b} (bit-exactness is the contract here — the loops \
were restructured for speed, not for different arithmetic)"
);
}
}
#[test]
fn sgd_matches_its_scalar_reference_bit_exactly() {
const N: usize = 257; let grad = spread(N, 11);
for (momentum, nesterov, weight_decay) in [
(0.0f32, false, 0.0f32),
(0.0, false, 0.05),
(0.9, false, 0.0),
(0.9, false, 0.05),
(0.9, true, 0.0),
(0.9, true, 0.05),
] {
let lr = 0.03f32;
let mut opt = Sgd::new(lr);
opt.momentum = momentum;
opt.nesterov = nesterov;
opt.weight_decay = weight_decay;
let mut param = spread(N, 7);
let mut ref_param = spread(N, 7);
let mut ref_v = vec![0.0f32; N];
for _ in 0..4 {
opt.step("w", &[N], &mut param, &grad);
opt.end_iteration();
for i in 0..N {
let g = grad[i] + weight_decay * ref_param[i];
if momentum == 0.0 {
ref_param[i] -= lr * g;
} else {
ref_v[i] = momentum * ref_v[i] + g;
let update = if nesterov {
g + momentum * ref_v[i]
} else {
ref_v[i]
};
ref_param[i] -= lr * update;
}
}
}
assert_bit_equal(
&format!("sgd(mu={momentum}, nesterov={nesterov}, wd={weight_decay})"),
¶m,
&ref_param,
);
}
}
#[test]
fn adam_f32_and_f64_paths_each_match_their_reference() {
const N: usize = 129;
let grad = spread(N, 11);
let (lr, b1, b2, eps, wd) = (0.03f32, 0.9f32, 0.999f32, 1e-8f32, 0.01f32);
for f32_math in [false, true] {
let mut opt = Adam::new(lr).with_f32_math(f32_math);
opt.beta1 = b1;
opt.beta2 = b2;
opt.eps = eps;
opt.weight_decay = wd;
let mut param = spread(N, 7);
let mut ref_param = spread(N, 7);
let mut ref_m = vec![0.0f32; N];
let mut ref_v = vec![0.0f32; N];
for step in 0..4 {
opt.step("w", &[N], &mut param, &grad);
opt.end_iteration();
if f32_math {
let t = step + 1;
let bc1 = 1.0 - b1.powi(t);
let bc2 = 1.0 - b2.powi(t);
for i in 0..N {
let g = grad[i] + wd * ref_param[i];
ref_m[i] = b1 * ref_m[i] + (1.0 - b1) * g;
ref_v[i] = b2 * ref_v[i] + (1.0 - b2) * (g * g);
let m_hat = ref_m[i] / bc1;
let v_hat = ref_v[i] / bc2;
ref_param[i] -= lr * (m_hat / (v_hat.sqrt() + eps));
}
} else {
let t = (step + 1) as f64;
let (b1, b2) = (b1 as f64, b2 as f64);
let bc1 = 1.0 - b1.powf(t);
let bc2 = 1.0 - b2.powf(t);
let (eps, lr) = (eps as f64, lr as f64);
for i in 0..N {
let g = (grad[i] + wd * ref_param[i]) as f64;
let new_m = b1 * ref_m[i] as f64 + (1.0 - b1) * g;
let new_v = b2 * ref_v[i] as f64 + (1.0 - b2) * g * g;
ref_m[i] = new_m as f32;
ref_v[i] = new_v as f32;
let m_hat = new_m / bc1;
let v_hat = new_v / bc2;
ref_param[i] -= (lr * m_hat / (v_hat.sqrt() + eps)) as f32;
}
}
}
assert_bit_equal(&format!("adam(f32_math={f32_math})"), ¶m, &ref_param);
}
}
#[test]
fn adams_two_precision_paths_are_not_the_same_computation() {
const N: usize = 64;
let grad = spread(N, 11);
let run = |f32_math: bool| {
let mut opt = Adam::new(0.03).with_f32_math(f32_math);
let mut param = spread(N, 7);
for _ in 0..8 {
opt.step("w", &[N], &mut param, &grad);
opt.end_iteration();
}
param
};
let f64_path = run(false);
let f32_path = run(true);
assert!(
f64_path
.iter()
.zip(&f32_path)
.any(|(a, b)| a.to_bits() != b.to_bits()),
"f32_math made no difference, so it is not selecting a different path"
);
for (i, (a, b)) in f64_path.iter().zip(&f32_path).enumerate() {
assert!(
(a - b).abs() <= 1e-5 * a.abs().max(1.0),
"element {i}: f64 path {a} vs f32 path {b}"
);
}
}
#[test]
fn odd_lengths_are_handled_by_the_tail() {
for n in [1usize, 3, 7, 15, 16, 17, 31, 33, 63, 65] {
let grad = spread(n, 11);
let mut param = spread(n, 7);
let before = param.clone();
let mut opt = Sgd::new(0.1);
opt.momentum = 0.9;
opt.step("w", &[n], &mut param, &grad);
assert!(
param.iter().zip(&before).any(|(a, b)| a != b),
"n={n}: nothing moved"
);
assert!(param.iter().all(|x| x.is_finite()), "n={n}: not finite");
}
}
}