use std::collections::HashMap;
use crate::Optimizer;
use crate::common::zeros_entry;
#[derive(Debug, Clone)]
pub struct Muon {
pub lr: f32,
pub momentum: f32,
pub nesterov: bool,
pub weight_decay: f32,
pub ns_steps: u32,
pub ns_coeffs: (f32, f32, f32),
m: HashMap<String, Vec<f32>>,
}
impl Muon {
pub fn new(lr: f32) -> Self {
Self {
lr,
momentum: 0.95,
nesterov: true,
weight_decay: 0.0,
ns_steps: 5,
ns_coeffs: (3.4445, -4.7750, 2.0315),
m: HashMap::new(),
}
}
pub fn with_momentum(mut self, mu: f32) -> Self {
self.momentum = mu;
self
}
pub fn with_weight_decay(mut self, wd: f32) -> Self {
self.weight_decay = wd;
self
}
pub fn with_ns_steps(mut self, n: u32) -> Self {
self.ns_steps = n;
self
}
}
impl Optimizer for Muon {
fn set_lr(&mut self, lr: f32) {
self.lr = lr;
}
fn step(&mut self, name: &str, shape: &[usize], param: &mut [f32], grad: &[f32]) {
debug_assert_eq!(param.len(), grad.len());
let mu = self.momentum;
let wd = self.weight_decay;
let lr = self.lr;
let m = zeros_entry(&mut self.m, name, param.len());
for i in 0..param.len() {
m[i] = mu * m[i] + grad[i];
}
if shape.len() != 2 {
for i in 0..param.len() {
let g = if self.nesterov {
grad[i] + mu * m[i]
} else {
m[i]
};
param[i] -= lr * (g + wd * param[i]);
}
return;
}
let (rows, cols) = (shape[0], shape[1]);
debug_assert_eq!(rows * cols, param.len());
let mut g_mat = vec![0.0f32; rows * cols];
if self.nesterov {
for i in 0..rows * cols {
g_mat[i] = grad[i] + mu * m[i];
}
} else {
g_mat.copy_from_slice(m);
}
let ortho = newton_schulz_orth(&g_mat, rows, cols, self.ns_steps, self.ns_coeffs);
let s = (rows.max(cols) as f32).sqrt();
for i in 0..param.len() {
param[i] -= lr * (s * ortho[i] + wd * param[i]);
}
}
}
#[allow(dead_code)]
fn par_rows<F: Fn(usize, &mut [f32]) + Sync>(out: &mut [f32], cols: usize, f: F) {
let n = out.len() / cols;
if n < 64 {
for (i, row) in out.chunks_mut(cols).enumerate() {
f(i, row);
}
return;
}
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
out.par_chunks_mut(cols)
.enumerate()
.for_each(|(i, row)| f(i, row));
}
#[cfg(not(feature = "parallel"))]
{
let threads = std::thread::available_parallelism()
.map(|x| x.get())
.unwrap_or(1)
.min(n);
let per = n.div_ceil(threads);
std::thread::scope(|s| {
for (t, chunk) in out.chunks_mut(per * cols).enumerate() {
let f = &f;
s.spawn(move || {
let base = t * per;
for (ii, row) in chunk.chunks_mut(cols).enumerate() {
f(base + ii, row);
}
});
}
});
}
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
mod accel {
unsafe extern "C" {
#[link_name = "cblas_sgemm"]
fn cblas_sgemm(
order: i32,
transa: i32,
transb: i32,
m: i32,
n: i32,
k: i32,
alpha: f32,
a: *const f32,
lda: i32,
b: *const f32,
ldb: i32,
beta: f32,
c: *mut f32,
ldc: i32,
);
}
const ROW_MAJOR: i32 = 101;
const NO_TRANS: i32 = 111;
const TRANS: i32 = 112;
#[inline]
pub fn gemm_nn(a: &[f32], b: &[f32], c: &mut [f32], m: usize, k: usize, n: usize) {
unsafe {
cblas_sgemm(
ROW_MAJOR,
NO_TRANS,
NO_TRANS,
m as i32,
n as i32,
k as i32,
1.0,
a.as_ptr(),
k as i32,
b.as_ptr(),
n as i32,
0.0,
c.as_mut_ptr(),
n as i32,
);
}
}
#[inline]
pub fn gemm_nt(a: &[f32], b: &[f32], c: &mut [f32], m: usize, k: usize, n: usize) {
unsafe {
cblas_sgemm(
ROW_MAJOR,
NO_TRANS,
TRANS,
m as i32,
n as i32,
k as i32,
1.0,
a.as_ptr(),
k as i32,
b.as_ptr(),
k as i32,
0.0,
c.as_mut_ptr(),
n as i32,
);
}
}
}
pub fn newton_schulz_orth(
g: &[f32],
rows: usize,
cols: usize,
steps: u32,
c: (f32, f32, f32),
) -> Vec<f32> {
let mut x = g.to_vec();
let mut fro = 0.0f64;
for &xi in &x {
fro += xi as f64 * xi as f64;
}
let fro = (fro.sqrt() as f32).max(1e-12);
for xi in &mut x {
*xi /= fro;
}
let (mut x_mat, r, k, transposed) = if rows > cols {
let mut t = vec![0.0f32; rows * cols];
for i in 0..rows {
for j in 0..cols {
t[j * rows + i] = x[i * cols + j];
}
}
(t, cols, rows, true)
} else {
(x, rows, cols, false)
};
let (a, b, cc) = c;
let mut tmp = vec![0.0f32; r * k]; let mut a_mat = vec![0.0f32; r * r];
let mut a2 = vec![0.0f32; r * r];
for _ in 0..steps {
#[cfg(target_os = "macos")]
{
accel::gemm_nt(&x_mat, &x_mat, &mut a_mat, r, k, r); accel::gemm_nn(&a_mat, &a_mat, &mut a2, r, r, r); for i in 0..r * r {
a2[i] = b * a_mat[i] + cc * a2[i]; }
accel::gemm_nn(&a2, &x_mat, &mut tmp, r, r, k); for i in 0..r * k {
tmp[i] += a * x_mat[i]; }
}
#[cfg(not(target_os = "macos"))]
{
par_rows(&mut a_mat, r, |i, arow| {
let xi = &x_mat[i * k..i * k + k];
for (j, aij) in arow.iter_mut().enumerate() {
let xj = &x_mat[j * k..j * k + k];
let mut s = 0.0f32;
for p in 0..k {
s += xi[p] * xj[p];
}
*aij = s;
}
});
par_rows(&mut a2, r, |i, a2row| {
a2row.fill(0.0);
let ai = &a_mat[i * r..i * r + r];
for p in 0..r {
let aip = ai[p];
let ap = &a_mat[p * r..p * r + r];
for j in 0..r {
a2row[j] += aip * ap[j];
}
}
});
par_rows(&mut tmp, k, |i, trow| {
let xi = &x_mat[i * k..i * k + k];
for j in 0..k {
trow[j] = a * xi[j];
}
let ai = &a_mat[i * r..i * r + r];
let a2i = &a2[i * r..i * r + r];
for p in 0..r {
let coef = b * ai[p] + cc * a2i[p];
let xp = &x_mat[p * k..p * k + k];
for j in 0..k {
trow[j] += coef * xp[j];
}
}
});
}
std::mem::swap(&mut x_mat, &mut tmp);
}
if transposed {
let mut out = vec![0.0f32; rows * cols];
for i in 0..r {
for j in 0..k {
out[j * r + i] = x_mat[i * k + j];
}
}
out
} else {
x_mat
}
}