1use std::sync::Arc;
10
11use nuvec::{
12 Array, DType, Error, Result, add, addmm, divide, matmul, minimum, multiply, reshape, sign,
13 sqrt, subtract, sum,
14};
15
16use crate::Module;
17
18pub trait Optimizer {
20 fn update<M: Module>(&mut self, model: &mut M, gradients: &[Array]) -> Result<()> {
23 let parameters: Vec<Array> = model
24 .trainable_parameters()
25 .into_iter()
26 .map(|(_, a)| a)
27 .collect();
28 if parameters.len() != gradients.len() {
29 return Err(Error::InvalidArgument(format!(
30 "{} gradients for {} parameters",
31 gradients.len(),
32 parameters.len()
33 )));
34 }
35 let updated = self.apply(¶meters, gradients)?;
36 model.update_trainable(&updated)
37 }
38
39 fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>>;
41}
42
43#[derive(Clone)]
45pub enum LearningRate {
46 Constant(f32),
48 Schedule(Arc<dyn Fn(u64) -> f32 + Send + Sync>),
50}
51
52impl LearningRate {
53 pub fn at(&self, step: u64) -> f32 {
55 match self {
56 Self::Constant(lr) => *lr,
57 Self::Schedule(f) => f(step),
58 }
59 }
60}
61
62impl std::fmt::Debug for LearningRate {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::Constant(lr) => write!(f, "Constant({lr})"),
66 Self::Schedule(_) => f.write_str("Schedule"),
67 }
68 }
69}
70
71impl From<f64> for LearningRate {
72 fn from(lr: f64) -> Self {
73 Self::Constant(lr as f32)
74 }
75}
76
77impl From<f32> for LearningRate {
78 fn from(lr: f32) -> Self {
79 Self::Constant(lr)
80 }
81}
82
83pub mod schedules {
85 use std::sync::Arc;
86
87 use super::LearningRate;
88
89 fn schedule(f: impl Fn(u64) -> f32 + Send + Sync + 'static) -> LearningRate {
90 LearningRate::Schedule(Arc::new(f))
91 }
92
93 pub fn exponential_decay(init: f64, decay_rate: f64) -> LearningRate {
95 schedule(move |step| init as f32 * (decay_rate as f32).powf(step as f32))
96 }
97
98 pub fn step_decay(init: f64, decay_rate: f64, step_size: u64) -> LearningRate {
100 let step_size = step_size.max(1);
101 schedule(move |step| init as f32 * (decay_rate as f32).powf((step / step_size) as f32))
102 }
103
104 pub fn cosine_decay(init: f64, decay_steps: u64, end: f64) -> LearningRate {
106 let decay_steps = decay_steps.max(1);
107 let rate = (std::f64::consts::PI / decay_steps as f64) as f32;
108 schedule(move |step| {
109 let s = step.min(decay_steps) as f32;
110 let decay = 0.5f32 * (1.0 + (rate * s).cos());
111 end as f32 + decay * (init - end) as f32
112 })
113 }
114
115 pub fn linear_schedule(init: f64, end: f64, steps: u64) -> LearningRate {
117 let steps = steps.max(1);
118 let slope = ((end - init) / steps as f64) as f32;
119 schedule(move |step| step.min(steps) as f32 * slope + init as f32)
120 }
121
122 pub fn join_schedules(schedules: Vec<LearningRate>, boundaries: Vec<u64>) -> LearningRate {
125 assert_eq!(
126 schedules.len(),
127 boundaries.len() + 1,
128 "join_schedules: one boundary between consecutive schedules"
129 );
130 schedule(move |step| {
131 let mut out = schedules[0].at(step);
132 for (b, s) in boundaries.iter().zip(&schedules[1..]) {
133 if step >= *b {
134 out = s.at(step - b);
135 }
136 }
137 out
138 })
139 }
140}
141
142fn rate(lr: f32, like: &Array) -> Result<Array> {
144 Array::scalar(lr).astype(like.dtype())
145}
146
147fn zeros_like(state: &mut Vec<Array>, parameters: &[Array]) -> Result<()> {
149 if state.len() != parameters.len() {
150 *state = parameters
151 .iter()
152 .map(|p| nuvec::zeros(p.shape(), p.dtype()))
153 .collect::<Result<_>>()?;
154 }
155 Ok(())
156}
157
158#[derive(Debug, Clone)]
161pub struct Sgd {
162 pub learning_rate: LearningRate,
164 pub momentum: f64,
166 pub weight_decay: f64,
168 pub dampening: f64,
170 pub nesterov: bool,
172 step: u64,
173 velocity: Vec<Array>,
174}
175
176impl Sgd {
177 pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
179 Self {
180 learning_rate: learning_rate.into(),
181 momentum: 0.0,
182 weight_decay: 0.0,
183 dampening: 0.0,
184 nesterov: false,
185 step: 0,
186 velocity: Vec::new(),
187 }
188 }
189
190 pub fn momentum(mut self, momentum: f64) -> Self {
192 self.momentum = momentum;
193 self
194 }
195
196 pub fn weight_decay(mut self, weight_decay: f64) -> Self {
198 self.weight_decay = weight_decay;
199 self
200 }
201
202 pub fn nesterov(mut self, nesterov: bool) -> Self {
204 self.nesterov = nesterov;
205 self
206 }
207}
208
209impl Optimizer for Sgd {
210 fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>> {
211 zeros_like(&mut self.velocity, parameters)?;
212 let lr_now = self.learning_rate.at(self.step);
213 self.step += 1;
214 let mut out = Vec::with_capacity(parameters.len());
215 for ((p, g), v) in parameters.iter().zip(gradients).zip(&mut self.velocity) {
216 let mut g = g.clone();
217 if self.weight_decay != 0.0 {
218 g = add(&g, multiply(p, self.weight_decay)?)?;
219 }
220 let lr = rate(lr_now, &g)?;
221 if self.momentum <= 0.0 {
222 out.push(subtract(p, multiply(&lr, &g)?)?);
223 continue;
224 }
225 let mut next = multiply(self.momentum, &*v)?;
226 next = if self.dampening > 0.0 {
227 add(&next, multiply(1.0 - self.dampening, &g)?)?
228 } else {
229 add(&next, &g)?
230 };
231 let update = if self.nesterov {
232 add(&g, multiply(self.momentum, &next)?)?
233 } else {
234 next.clone()
235 };
236 *v = next;
237 out.push(subtract(p, multiply(&lr, &update)?)?);
238 }
239 Ok(out)
240 }
241}
242
243#[derive(Debug, Clone)]
246pub struct Adam {
247 pub learning_rate: LearningRate,
249 pub betas: (f64, f64),
251 pub eps: f64,
253 pub bias_correction: bool,
255 pub weight_decay: f64,
257 step: u64,
258 m: Vec<Array>,
259 v: Vec<Array>,
260}
261
262impl Adam {
263 pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
265 Self {
266 learning_rate: learning_rate.into(),
267 betas: (0.9, 0.999),
268 eps: 1e-8,
269 bias_correction: false,
270 weight_decay: 0.0,
271 step: 0,
272 m: Vec::new(),
273 v: Vec::new(),
274 }
275 }
276
277 pub fn adamw(learning_rate: impl Into<LearningRate>) -> Self {
280 Self::new(learning_rate).weight_decay(0.01)
281 }
282
283 pub fn betas(mut self, b1: f64, b2: f64) -> Self {
285 self.betas = (b1, b2);
286 self
287 }
288
289 pub fn weight_decay(mut self, weight_decay: f64) -> Self {
291 self.weight_decay = weight_decay;
292 self
293 }
294
295 pub fn bias_correction(mut self, enabled: bool) -> Self {
297 self.bias_correction = enabled;
298 self
299 }
300}
301
302impl Optimizer for Adam {
303 fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>> {
304 zeros_like(&mut self.m, parameters)?;
305 zeros_like(&mut self.v, parameters)?;
306 let lr_now = self.learning_rate.at(self.step);
307 self.step += 1;
308 let (b1, b2) = self.betas;
309 let mut out = Vec::with_capacity(parameters.len());
310 for (((p, g), m), v) in parameters
311 .iter()
312 .zip(gradients)
313 .zip(&mut self.m)
314 .zip(&mut self.v)
315 {
316 let lr = rate(lr_now, g)?;
317 let p = if self.weight_decay != 0.0 {
318 multiply(p, subtract(1.0, multiply(&lr, self.weight_decay)?)?)?
320 } else {
321 p.clone()
322 };
323 *m = add(multiply(b1, &*m)?, multiply(1.0 - b1, g)?)?;
324 *v = add(multiply(b2, &*v)?, multiply(1.0 - b2, multiply(g, g)?)?)?;
325 let updated = if self.bias_correction {
326 let step = self.step as f32;
328 let one_minus = |b: f64| Array::scalar(1.0 - (b as f32).powf(step));
329 let c1 = divide(&lr, one_minus(b1))?.astype(g.dtype())?;
330 let c2 = divide(1.0f32, sqrt(&one_minus(b2))?)?.astype(g.dtype())?;
331 let denominator = add(multiply(sqrt(&*v)?, &c2)?, self.eps)?;
332 subtract(&p, divide(multiply(&c1, &*m)?, &denominator)?)?
333 } else {
334 subtract(&p, divide(multiply(&lr, &*m)?, add(sqrt(&*v)?, self.eps)?)?)?
335 };
336 out.push(updated);
337 }
338 Ok(out)
339 }
340}
341
342#[derive(Debug, Clone)]
345pub struct Lion {
346 pub learning_rate: LearningRate,
348 pub betas: (f64, f64),
350 pub weight_decay: f64,
352 step: u64,
353 m: Vec<Array>,
354}
355
356impl Lion {
357 pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
359 Self {
360 learning_rate: learning_rate.into(),
361 betas: (0.9, 0.99),
362 weight_decay: 0.0,
363 step: 0,
364 m: Vec::new(),
365 }
366 }
367
368 pub fn weight_decay(mut self, weight_decay: f64) -> Self {
370 self.weight_decay = weight_decay;
371 self
372 }
373}
374
375impl Optimizer for Lion {
376 fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>> {
377 zeros_like(&mut self.m, parameters)?;
378 let lr_now = self.learning_rate.at(self.step);
379 self.step += 1;
380 let (b1, b2) = self.betas;
381 let mut out = Vec::with_capacity(parameters.len());
382 for ((p, g), m) in parameters.iter().zip(gradients).zip(&mut self.m) {
383 let lr = rate(lr_now, g)?;
384 let c = add(multiply(b1, &*m)?, multiply(1.0 - b1, g)?)?;
385 *m = add(multiply(b2, &*m)?, multiply(1.0 - b2, g)?)?;
386 let p = if self.weight_decay > 0.0 {
387 multiply(subtract(1.0, multiply(&lr, self.weight_decay)?)?, p)?
388 } else {
389 p.clone()
390 };
391 out.push(subtract(&p, multiply(&lr, sign(&c)?)?)?);
392 }
393 Ok(out)
394 }
395}
396
397#[derive(Debug, Clone)]
400pub struct Muon {
401 pub learning_rate: LearningRate,
403 pub momentum: f64,
405 pub weight_decay: f64,
407 pub nesterov: bool,
409 pub ns_steps: usize,
411 step: u64,
412 v: Vec<Array>,
413}
414
415impl Muon {
416 pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
418 Self {
419 learning_rate: learning_rate.into(),
420 momentum: 0.95,
421 weight_decay: 0.01,
422 nesterov: true,
423 ns_steps: 5,
424 step: 0,
425 v: Vec::new(),
426 }
427 }
428}
429
430impl Optimizer for Muon {
431 fn apply(&mut self, parameters: &[Array], gradients: &[Array]) -> Result<Vec<Array>> {
432 zeros_like(&mut self.v, parameters)?;
433 let lr_now = self.learning_rate.at(self.step);
434 self.step += 1;
435 let mut out = Vec::with_capacity(parameters.len());
436 for ((p, g), v) in parameters.iter().zip(gradients).zip(&mut self.v) {
437 let mut g = g.clone();
438 if self.weight_decay != 0.0 {
439 g = add(&g, multiply(self.weight_decay, p)?)?;
440 }
441 *v = add(
442 multiply(self.momentum, &*v)?,
443 multiply(1.0 - self.momentum, &g)?,
444 )?;
445 let mut update = if self.nesterov {
446 add(
447 multiply(&g, 1.0 - self.momentum)?,
448 multiply(&*v, self.momentum)?,
449 )?
450 } else {
451 v.clone()
452 };
453 let mut lr = rate(lr_now, &g)?;
454 if update.ndim() >= 2 {
455 let shape = update.shape().to_vec();
456 let flat = reshape(&update, &[shape[0] as isize, -1])?;
457 let isize_shape: Vec<isize> = shape.iter().map(|&d| d as isize).collect();
458 update = reshape(&orthogonalize(&flat, self.ns_steps)?, &isize_shape)?;
459 let (rows, cols) = (shape[shape.len() - 2], shape[shape.len() - 1]);
460 lr = multiply(&lr, (rows as f64 / cols as f64).max(1.0).sqrt())?;
461 }
462 out.push(subtract(p, multiply(&lr, &update)?)?);
463 }
464 Ok(out)
465 }
466}
467
468fn orthogonalize(x: &Array, steps: usize) -> Result<Array> {
471 let (a, b, c) = (3.4445, -4.775, 2.0315);
472 let transpose = x.shape()[0] > x.shape()[1];
473 let mut x = if transpose { x.t()? } else { x.clone() };
474 let norm = sqrt(&sum(&multiply(&x, &x)?, &[], true)?)?;
475 x = divide(&x, add(&norm, 1e-7)?)?;
476 for _ in 0..steps {
477 let am = matmul(&x, &x.t()?)?;
478 let bm = addmm(&multiply(&am, b)?, &am, &am, c as f32, 1.0)?;
479 x = addmm(&multiply(&x, a)?, &bm, &x, 1.0, 1.0)?;
480 }
481 if transpose { x.t() } else { Ok(x) }
482}
483
484pub fn clip_grad_norm(gradients: &[Array], max_norm: f64) -> Result<(Vec<Array>, Array)> {
487 if max_norm < 0.0 {
488 return Err(Error::InvalidArgument(format!(
489 "clip_grad_norm: max_norm should be >= 0, got {max_norm}"
490 )));
491 }
492 let mut squared: Option<Array> = None;
493 for g in gradients {
494 let g = if g.dtype() == DType::F16 {
495 g.astype(DType::F32)?
496 } else {
497 g.clone()
498 };
499 let s = sum(&multiply(&g, &g)?, &[], false)?;
500 squared = Some(match squared {
501 Some(acc) => add(&acc, &s)?,
502 None => s,
503 });
504 }
505 let total = sqrt(&squared.unwrap_or_else(|| Array::scalar(0.0f32)))?;
506 let normalizer = minimum(divide(max_norm, add(&total, 1e-6)?)?, 1.0)?;
507 let clipped = gradients
508 .iter()
509 .map(|g| {
510 let scaled = multiply(g, &normalizer)?;
511 if g.dtype().category() == nuvec::Category::Float {
512 scaled.astype(g.dtype())
513 } else {
514 Ok(scaled)
515 }
516 })
517 .collect::<Result<_>>()?;
518 Ok((clipped, total))
519}