1use std::collections::{BTreeMap, HashMap, HashSet};
19use std::sync::Arc;
20
21use nuvec::tree::{Leaves, Visit, VisitMut};
22use nuvec::{Array, Category, DType, Error, OpErrorKind, Result, Tree, ops};
23
24use crate::Module;
25
26pub trait Optimizer {
32 fn apply(&mut self, params: &[(&str, &Array)], grads: &[&Array]) -> Result<Vec<Array>>;
35
36 fn init_params(&mut self, params: &[(&str, &Array)]);
38
39 fn visit_state(&self, v: &mut Visit<'_>);
41
42 fn visit_state_mut(&mut self, v: &mut VisitMut<'_>);
44
45 fn learning_rate(&self) -> Array;
47
48 fn set_learning_rate(&mut self, lr: f32);
50
51 fn clone_box(&self) -> Box<dyn Optimizer>;
53
54 fn init<M: Tree>(&mut self, model: &M)
58 where
59 Self: Sized,
60 {
61 let params = model.trainable_parameters();
62 let refs: Vec<(&str, &Array)> = params.iter().map(|(p, a)| (p.as_str(), a)).collect();
63 self.init_params(&refs);
64 }
65
66 fn update<M: Tree>(&mut self, model: &mut M, grads: &M) -> Result<()>
70 where
71 Self: Sized,
72 {
73 let (params, gradients) = trainable_with_grads(model, grads)?;
74 let p: Vec<(&str, &Array)> = params.iter().map(|(n, a)| (n.as_str(), a)).collect();
75 let g: Vec<&Array> = gradients.iter().collect();
76 let updated = self.apply(&p, &g)?;
77 let mut updated: HashMap<&str, Array> = params
78 .iter()
79 .map(|(n, _)| n.as_str())
80 .zip(updated)
81 .collect();
82 *model = model.map(|path, a| updated.remove(path).unwrap_or_else(|| a.clone()));
83 Ok(())
84 }
85}
86
87type Trainable = (Vec<(String, Array)>, Vec<Array>);
89
90fn trainable_with_grads<M: Tree>(model: &M, grads: &M) -> Result<Trainable> {
93 let (paths, gpaths) = (model.paths(), grads.paths());
94 if paths != gpaths {
95 let first = paths.iter().zip(&gpaths).find(|(a, b)| a != b).map_or_else(
96 || "a different number of leaves".to_owned(),
97 |(a, b)| format!("{b:?} where the model has {a:?}"),
98 );
99 return Err(Error::InvalidArgument(format!(
100 "update: the gradients do not have the model's paths: {first}"
101 )));
102 }
103 let trainable: HashSet<String> = model
104 .trainable_parameters()
105 .into_iter()
106 .map(|(p, _)| p)
107 .collect();
108 let mut params = Vec::new();
109 let mut gs = Vec::new();
110 for ((path, a), g) in model.flatten().into_iter().zip(grads.leaves()) {
111 if trainable.contains(&path) {
112 params.push((path, a));
113 gs.push(g);
114 }
115 }
116 Ok((params, gs))
117}
118
119impl Optimizer for Box<dyn Optimizer> {
120 fn apply(&mut self, params: &[(&str, &Array)], grads: &[&Array]) -> Result<Vec<Array>> {
121 (**self).apply(params, grads)
122 }
123 fn init_params(&mut self, params: &[(&str, &Array)]) {
124 (**self).init_params(params);
125 }
126 fn visit_state(&self, v: &mut Visit<'_>) {
127 (**self).visit_state(v);
128 }
129 fn visit_state_mut(&mut self, v: &mut VisitMut<'_>) {
130 (**self).visit_state_mut(v);
131 }
132 fn learning_rate(&self) -> Array {
133 (**self).learning_rate()
134 }
135 fn set_learning_rate(&mut self, lr: f32) {
136 (**self).set_learning_rate(lr);
137 }
138 fn clone_box(&self) -> Box<dyn Optimizer> {
139 (**self).clone_box()
140 }
141}
142
143impl Clone for Box<dyn Optimizer> {
144 fn clone(&self) -> Self {
145 (**self).clone_box()
146 }
147}
148
149impl Leaves for Box<dyn Optimizer> {
152 fn visit(&self, v: &mut Visit<'_>) {
153 (**self).visit_state(v);
154 }
155}
156
157impl Tree for Box<dyn Optimizer> {
158 fn visit_mut(&mut self, v: &mut VisitMut<'_>) {
159 (**self).visit_state_mut(v);
160 }
161}
162
163impl std::fmt::Debug for dyn Optimizer {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 f.write_str("dyn Optimizer")
166 }
167}
168
169type Boxed = Box<dyn Optimizer>;
171
172#[derive(Debug, Clone, Tree)]
175pub struct OptimizerState<S: Tree> {
176 pub step: Array,
178 pub learning_rate: Array,
180 pub slots: BTreeMap<String, S>,
182}
183
184impl<S: Tree> OptimizerState<S> {
185 fn new(lr: f32) -> Self {
186 Self {
187 step: Array::from(0u64),
188 learning_rate: Array::from(lr),
189 slots: BTreeMap::new(),
190 }
191 }
192}
193
194#[derive(Clone)]
197pub enum LearningRate {
198 Constant(f32),
200 Schedule(Arc<dyn Fn(&Array) -> Array + Send + Sync>),
202}
203
204impl LearningRate {
205 pub fn at(&self, step: &Array) -> Array {
207 match self {
208 Self::Constant(lr) => Array::from(*lr),
209 Self::Schedule(f) => f(step).astype(DType::F32),
210 }
211 }
212
213 fn initial(&self) -> Array {
214 self.at(&Array::from(0u64))
215 }
216}
217
218impl std::fmt::Debug for LearningRate {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 match self {
221 Self::Constant(lr) => write!(f, "Constant({lr})"),
222 Self::Schedule(_) => f.write_str("Schedule"),
223 }
224 }
225}
226
227impl From<f64> for LearningRate {
228 fn from(lr: f64) -> Self {
229 Self::Constant(lr as f32)
230 }
231}
232
233impl From<f32> for LearningRate {
234 fn from(lr: f32) -> Self {
235 Self::Constant(lr)
236 }
237}
238
239pub mod schedules {
242 use std::sync::Arc;
243
244 use nuvec::{Array, DType};
245
246 use super::LearningRate;
247
248 fn schedule(f: impl Fn(&Array) -> Array + Send + Sync + 'static) -> LearningRate {
249 LearningRate::Schedule(Arc::new(f))
250 }
251
252 fn steps(step: &Array) -> Array {
254 step.astype(DType::F32)
255 }
256
257 fn power(b: f64, t: &Array) -> Array {
260 t.multiply(f64::from(b as f32).ln()).exp()
261 }
262
263 pub fn exponential_decay(init: f64, decay_rate: f64) -> LearningRate {
265 schedule(move |step| power(decay_rate, &steps(step)).multiply(init))
266 }
267
268 pub fn step_decay(init: f64, decay_rate: f64, step_size: u64) -> LearningRate {
270 let size = step_size.max(1) as f64;
271 schedule(move |step| {
272 let k = steps(step)
273 .divide(size)
274 .astype(DType::U32)
275 .astype(DType::F32);
276 power(decay_rate, &k).multiply(init)
277 })
278 }
279
280 pub fn cosine_decay(init: f64, decay_steps: u64, end: f64) -> LearningRate {
282 let decay_steps = decay_steps.max(1) as f64;
283 let rate = std::f64::consts::PI / decay_steps;
284 schedule(move |step| {
285 let s = steps(step).minimum(decay_steps);
286 let decay = s.multiply(rate).cos().add(1.0).multiply(0.5);
287 decay.multiply(init - end).add(end)
288 })
289 }
290
291 pub fn linear_schedule(init: f64, end: f64, steps_total: u64) -> LearningRate {
293 let n = steps_total.max(1) as f64;
294 let slope = (end - init) / n;
295 schedule(move |step| steps(step).minimum(n).multiply(slope).add(init))
296 }
297
298 pub fn join_schedules(
302 schedules: Vec<LearningRate>,
303 boundaries: Vec<u64>,
304 ) -> nuvec::Result<LearningRate> {
305 if schedules.len() != boundaries.len() + 1 {
306 return Err(nuvec::Error::InvalidArgument(format!(
307 "join_schedules: {} schedules need {} boundaries, got {}",
308 schedules.len(),
309 schedules.len().saturating_sub(1),
310 boundaries.len()
311 )));
312 }
313 Ok(schedule(move |step| {
314 let t = steps(step);
315 let mut out = schedules[0].at(&t);
316 for (b, s) in boundaries.iter().zip(&schedules[1..]) {
317 let b = *b as f64;
318 let later = s.at(&t.subtract(b).maximum(0.0));
319 out = t.less(b).select(&out, &later);
320 }
321 out
322 }))
323 }
324}
325
326fn rate(lr: &Array, like: &Array) -> Array {
328 lr.astype(like.dtype())
329}
330
331fn step<S: Tree>(
335 state: &mut OptimizerState<S>,
336 schedule: &LearningRate,
337 params: &[(&str, &Array)],
338 grads: &[&Array],
339 new_slot: impl Fn(&Array) -> S,
340 mut f: impl FnMut(&Array, &Array, &mut S, &Array, &Array) -> Array,
341) -> Result<Vec<Array>> {
342 if params.len() != grads.len() {
343 return Err(Error::InvalidArgument(format!(
344 "{} gradients for {} parameters",
345 grads.len(),
346 params.len()
347 )));
348 }
349 let mut next = state.clone();
350 if let LearningRate::Schedule(s) = schedule {
351 next.learning_rate = s(&next.step).astype(DType::F32);
352 }
353 next.step = next.step.add(1u64);
354 let mut seen = HashSet::new();
355 let mut out = Vec::with_capacity(params.len());
356 for (&(path, p), &g) in params.iter().zip(grads) {
357 if !seen.insert(path) {
358 return Err(Error::InvalidArgument(format!(
359 "parameter {path:?} given twice"
360 )));
361 }
362 let slot = next
363 .slots
364 .entry(path.to_owned())
365 .or_insert_with(|| new_slot(p));
366 let mut mismatch = false;
367 slot.for_each(|_, s| mismatch |= s.shape() != p.shape());
368 if mismatch {
369 return Err(Error::InvalidArgument(format!(
370 "parameter {path:?} changed its shape to {:?}",
371 p.shape()
372 )));
373 }
374 out.push(f(p, g, slot, &next.learning_rate, &next.step).check()?);
375 }
376 *state = next;
377 Ok(out)
378}
379
380macro_rules! state_access {
382 ($slot:ty) => {
383 fn init_params(&mut self, params: &[(&str, &Array)]) {
384 for &(path, p) in params {
385 self.state
386 .slots
387 .entry(path.to_owned())
388 .or_insert_with(|| <$slot>::zeros(p));
389 }
390 }
391 fn visit_state(&self, v: &mut Visit<'_>) {
392 v.static_field("optimizer", &std::any::type_name::<Self>());
393 Leaves::visit(self, v);
394 }
395 fn visit_state_mut(&mut self, v: &mut VisitMut<'_>) {
396 Tree::visit_mut(self, v);
397 }
398 fn learning_rate(&self) -> Array {
399 self.state.learning_rate.clone()
400 }
401 fn set_learning_rate(&mut self, lr: f32) {
402 self.state.learning_rate = Array::from(lr);
403 }
404 fn clone_box(&self) -> Box<dyn Optimizer> {
405 Box::new(self.clone())
406 }
407 };
408}
409
410fn zeros(p: &Array) -> Array {
412 ops::zeros(p.shape(), p.dtype())
413}
414
415#[derive(Debug, Clone, Tree)]
417pub struct SgdSlot {
418 pub v: Array,
420}
421
422impl SgdSlot {
423 fn zeros(p: &Array) -> Self {
424 Self { v: zeros(p) }
425 }
426}
427
428#[derive(Debug, Clone, Tree)]
431pub struct Sgd {
432 #[tree(skip, static_unchecked)]
433 schedule: LearningRate,
434 #[tree(skip)]
435 momentum: f64,
436 #[tree(skip)]
437 weight_decay: f64,
438 #[tree(skip)]
439 dampening: f64,
440 #[tree(skip)]
441 nesterov: bool,
442 #[tree(rename = "")]
443 state: OptimizerState<SgdSlot>,
444}
445
446fn fresh<S: Tree>(schedule: &LearningRate) -> OptimizerState<S> {
448 let mut state = OptimizerState::new(0.0);
449 state.learning_rate = schedule.initial();
450 state
451}
452
453impl Sgd {
454 pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
456 let schedule = learning_rate.into();
457 Self {
458 state: fresh(&schedule),
459 schedule,
460 momentum: 0.0,
461 weight_decay: 0.0,
462 dampening: 0.0,
463 nesterov: false,
464 }
465 }
466
467 pub fn momentum(mut self, momentum: f64) -> Self {
469 self.momentum = momentum;
470 self
471 }
472
473 pub fn weight_decay(mut self, weight_decay: f64) -> Self {
475 self.weight_decay = weight_decay;
476 self
477 }
478
479 pub fn dampening(mut self, dampening: f64) -> Self {
481 self.dampening = dampening;
482 self
483 }
484
485 pub fn nesterov(mut self) -> Self {
487 self.nesterov = true;
488 self
489 }
490}
491
492impl Optimizer for Sgd {
493 fn apply(&mut self, params: &[(&str, &Array)], grads: &[&Array]) -> Result<Vec<Array>> {
494 let (momentum, wd, damp, nesterov) = (
495 self.momentum,
496 self.weight_decay,
497 self.dampening,
498 self.nesterov,
499 );
500 let sgd = |p: &Array, g: &Array, s: &mut SgdSlot, lr: &Array, _: &Array| {
501 let mut g = g.clone();
502 if wd != 0.0 {
503 g = g.add(p.multiply(wd));
504 }
505 let lr = rate(lr, &g);
506 if momentum <= 0.0 {
507 return p.subtract(lr.multiply(&g));
508 }
509 let mut next = s.v.multiply(momentum);
510 next = if damp > 0.0 {
511 next.add(g.multiply(1.0 - damp))
512 } else {
513 next.add(&g)
514 };
515 let update = if nesterov {
516 g.add(next.multiply(momentum))
517 } else {
518 next.clone()
519 };
520 s.v = next;
521 p.subtract(lr.multiply(&update))
522 };
523 step(
524 &mut self.state,
525 &self.schedule,
526 params,
527 grads,
528 SgdSlot::zeros,
529 sgd,
530 )
531 }
532
533 state_access!(SgdSlot);
534}
535
536#[derive(Debug, Clone, Tree)]
538pub struct AdamSlot {
539 pub m: Array,
541 pub v: Array,
543}
544
545impl AdamSlot {
546 fn zeros(p: &Array) -> Self {
547 Self {
548 m: zeros(p),
549 v: zeros(p),
550 }
551 }
552}
553
554fn one_minus_power(b: f64, step: &Array) -> Array {
559 let ln_b = f64::from(b as f32).ln();
560 let x = step.astype(DType::F32).multiply(ln_b);
561 let u = x.exp();
562 let kahan = u.subtract(1.0).multiply(&x).divide(u.log());
563 u.equal(1.0).select(&x, &kahan).negative()
564}
565
566#[derive(Debug, Clone, Copy)]
568struct AdamParams {
569 betas: (f64, f64),
570 eps: f64,
571 bias_correction: bool,
572 decay: f64,
574}
575
576fn adam_step(
578 p: &Array,
579 g: &Array,
580 s: &mut AdamSlot,
581 lr: &Array,
582 step: &Array,
583 h: AdamParams,
584) -> Array {
585 let (b1, b2) = h.betas;
586 let lr = rate(lr, g);
587 let p = if h.decay != 0.0 {
588 p.multiply(ops::subtract(1.0, lr.multiply(h.decay)))
590 } else {
591 p.clone()
592 };
593 s.m = s.m.multiply(b1).add(g.multiply(1.0 - b1));
594 s.v = s.v.multiply(b2).add(g.multiply(g).multiply(1.0 - b2));
595 if h.bias_correction {
596 let c1 = lr.divide(one_minus_power(b1, step)).astype(g.dtype());
598 let c2 =
599 ops::divide(Array::from(1.0f32), one_minus_power(b2, step).sqrt()).astype(g.dtype());
600 let denominator = s.v.sqrt().multiply(&c2).add(h.eps);
601 p.subtract(c1.multiply(&s.m).divide(&denominator))
602 } else {
603 p.subtract(lr.multiply(&s.m).divide(s.v.sqrt().add(h.eps)))
604 }
605}
606
607#[derive(Debug, Clone, Tree)]
610pub struct Adam {
611 #[tree(skip, static_unchecked)]
612 schedule: LearningRate,
613 #[tree(skip)]
614 betas: (f64, f64),
615 #[tree(skip)]
616 eps: f64,
617 #[tree(skip)]
618 bias_correction: bool,
619 #[tree(rename = "")]
620 state: OptimizerState<AdamSlot>,
621}
622
623impl Adam {
624 pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
626 let schedule = learning_rate.into();
627 Self {
628 state: fresh(&schedule),
629 schedule,
630 betas: (0.9, 0.999),
631 eps: 1e-8,
632 bias_correction: false,
633 }
634 }
635
636 pub fn betas(mut self, b1: f64, b2: f64) -> Self {
638 self.betas = (b1, b2);
639 self
640 }
641
642 pub fn eps(mut self, eps: f64) -> Self {
644 self.eps = eps;
645 self
646 }
647
648 pub fn bias_correction(mut self) -> Self {
650 self.bias_correction = true;
651 self
652 }
653}
654
655impl Optimizer for Adam {
656 fn apply(&mut self, params: &[(&str, &Array)], grads: &[&Array]) -> Result<Vec<Array>> {
657 let h = AdamParams {
658 betas: self.betas,
659 eps: self.eps,
660 bias_correction: self.bias_correction,
661 decay: 0.0,
662 };
663 let f = |p: &Array, g: &Array, s: &mut AdamSlot, lr: &Array, t: &Array| {
664 adam_step(p, g, s, lr, t, h)
665 };
666 step(
667 &mut self.state,
668 &self.schedule,
669 params,
670 grads,
671 AdamSlot::zeros,
672 f,
673 )
674 }
675
676 state_access!(AdamSlot);
677}
678
679#[derive(Debug, Clone, Tree)]
682pub struct AdamW {
683 #[tree(skip, static_unchecked)]
684 schedule: LearningRate,
685 #[tree(skip)]
686 betas: (f64, f64),
687 #[tree(skip)]
688 eps: f64,
689 #[tree(skip)]
690 bias_correction: bool,
691 #[tree(skip)]
692 weight_decay: f64,
693 #[tree(rename = "")]
694 state: OptimizerState<AdamSlot>,
695}
696
697impl AdamW {
698 pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
700 let schedule = learning_rate.into();
701 Self {
702 state: fresh(&schedule),
703 schedule,
704 betas: (0.9, 0.999),
705 eps: 1e-8,
706 bias_correction: false,
707 weight_decay: 0.01,
708 }
709 }
710
711 pub fn betas(mut self, b1: f64, b2: f64) -> Self {
713 self.betas = (b1, b2);
714 self
715 }
716
717 pub fn eps(mut self, eps: f64) -> Self {
719 self.eps = eps;
720 self
721 }
722
723 pub fn weight_decay(mut self, weight_decay: f64) -> Self {
725 self.weight_decay = weight_decay;
726 self
727 }
728
729 pub fn bias_correction(mut self) -> Self {
731 self.bias_correction = true;
732 self
733 }
734}
735
736impl Optimizer for AdamW {
737 fn apply(&mut self, params: &[(&str, &Array)], grads: &[&Array]) -> Result<Vec<Array>> {
738 let h = AdamParams {
739 betas: self.betas,
740 eps: self.eps,
741 bias_correction: self.bias_correction,
742 decay: self.weight_decay,
743 };
744 let f = |p: &Array, g: &Array, s: &mut AdamSlot, lr: &Array, t: &Array| {
745 adam_step(p, g, s, lr, t, h)
746 };
747 step(
748 &mut self.state,
749 &self.schedule,
750 params,
751 grads,
752 AdamSlot::zeros,
753 f,
754 )
755 }
756
757 state_access!(AdamSlot);
758}
759
760#[derive(Debug, Clone, Tree)]
762pub struct MomentumSlot {
763 pub m: Array,
765}
766
767impl MomentumSlot {
768 fn zeros(p: &Array) -> Self {
769 Self { m: zeros(p) }
770 }
771}
772
773#[derive(Debug, Clone, Tree)]
776pub struct Lion {
777 #[tree(skip, static_unchecked)]
778 schedule: LearningRate,
779 #[tree(skip)]
780 betas: (f64, f64),
781 #[tree(skip)]
782 weight_decay: f64,
783 #[tree(rename = "")]
784 state: OptimizerState<MomentumSlot>,
785}
786
787impl Lion {
788 pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
790 let schedule = learning_rate.into();
791 Self {
792 state: fresh(&schedule),
793 schedule,
794 betas: (0.9, 0.99),
795 weight_decay: 0.0,
796 }
797 }
798
799 pub fn betas(mut self, b1: f64, b2: f64) -> Self {
801 self.betas = (b1, b2);
802 self
803 }
804
805 pub fn weight_decay(mut self, weight_decay: f64) -> Self {
807 self.weight_decay = weight_decay;
808 self
809 }
810}
811
812impl Optimizer for Lion {
813 fn apply(&mut self, params: &[(&str, &Array)], grads: &[&Array]) -> Result<Vec<Array>> {
814 let ((b1, b2), wd) = (self.betas, self.weight_decay);
815 let lion = |p: &Array, g: &Array, s: &mut MomentumSlot, lr: &Array, _: &Array| {
816 let lr = rate(lr, g);
817 let c = s.m.multiply(b1).add(g.multiply(1.0 - b1));
818 s.m = s.m.multiply(b2).add(g.multiply(1.0 - b2));
819 let p = if wd > 0.0 {
820 ops::subtract(1.0, lr.multiply(wd)).multiply(p)
821 } else {
822 p.clone()
823 };
824 p.subtract(lr.multiply(c.sign()))
825 };
826 step(
827 &mut self.state,
828 &self.schedule,
829 params,
830 grads,
831 MomentumSlot::zeros,
832 lion,
833 )
834 }
835
836 state_access!(MomentumSlot);
837}
838
839#[derive(Debug, Clone, Tree)]
842pub struct Muon {
843 #[tree(skip, static_unchecked)]
844 schedule: LearningRate,
845 #[tree(skip)]
846 momentum: f64,
847 #[tree(skip)]
848 weight_decay: f64,
849 #[tree(skip)]
850 nesterov: bool,
851 #[tree(skip)]
852 ns_steps: usize,
853 #[tree(rename = "")]
854 state: OptimizerState<MomentumSlot>,
855}
856
857impl Muon {
858 pub fn new(learning_rate: impl Into<LearningRate>) -> Self {
860 let schedule = learning_rate.into();
861 Self {
862 state: fresh(&schedule),
863 schedule,
864 momentum: 0.95,
865 weight_decay: 0.01,
866 nesterov: true,
867 ns_steps: 5,
868 }
869 }
870
871 pub fn momentum(mut self, momentum: f64) -> Self {
873 self.momentum = momentum;
874 self
875 }
876
877 pub fn weight_decay(mut self, weight_decay: f64) -> Self {
879 self.weight_decay = weight_decay;
880 self
881 }
882
883 pub fn without_nesterov(mut self) -> Self {
885 self.nesterov = false;
886 self
887 }
888
889 pub fn ns_steps(mut self, n: usize) -> Self {
891 self.ns_steps = n;
892 self
893 }
894}
895
896impl Optimizer for Muon {
897 fn apply(&mut self, params: &[(&str, &Array)], grads: &[&Array]) -> Result<Vec<Array>> {
898 let (momentum, wd, nesterov, ns) = (
899 self.momentum,
900 self.weight_decay,
901 self.nesterov,
902 self.ns_steps,
903 );
904 let muon = |p: &Array, g: &Array, s: &mut MomentumSlot, lr: &Array, _: &Array| {
905 let mut g = g.clone();
906 if wd != 0.0 {
907 g = g.add(p.multiply(wd));
908 }
909 s.m = s.m.multiply(momentum).add(g.multiply(1.0 - momentum));
910 let mut update = if nesterov {
911 g.multiply(1.0 - momentum).add(s.m.multiply(momentum))
912 } else {
913 s.m.clone()
914 };
915 let mut lr = rate(lr, &g);
916 if update.ndim() >= 2 {
917 let shape = update.shape().to_vec();
918 let flat = update.reshape([shape[0], -1]);
919 update = orthogonalize(&flat, ns).reshape(&shape[..]);
920 let (rows, cols) = (shape[shape.len() - 2], shape[shape.len() - 1]);
921 lr = lr.multiply((f64::from(rows) / f64::from(cols)).max(1.0).sqrt());
922 }
923 p.subtract(lr.multiply(&update))
924 };
925 step(
926 &mut self.state,
927 &self.schedule,
928 params,
929 grads,
930 MomentumSlot::zeros,
931 muon,
932 )
933 }
934
935 state_access!(MomentumSlot);
936}
937
938fn orthogonalize(x: &Array, steps: usize) -> Array {
941 let (a, b, c) = (3.4445, -4.775, 2.0315);
942 let transpose = x.shape()[0] > x.shape()[1];
943 let mut x = if transpose { x.t() } else { x.clone() };
944 let norm = x.multiply(&x).sum_keepdims(..).sqrt();
945 x = x.divide(norm.add(1e-7));
946 for _ in 0..steps {
947 let am = x.matmul(&x.t());
948 let bm = am.multiply(b).addmm(&am, &am, c as f32, 1.0);
949 x = x.multiply(a).addmm(&bm, &x, 1.0, 1.0);
950 }
951 if transpose { x.t() } else { x }
952}
953
954#[derive(Debug, Clone, Tree)]
967pub struct MultiOptimizer {
968 groups: Vec<Boxed>,
969 default: Boxed,
970 #[tree(skip, static_unchecked)]
971 filters: Vec<Filter>,
972}
973
974#[derive(Clone)]
976struct Filter(Arc<dyn Fn(&str) -> bool + Send + Sync>);
977
978impl std::fmt::Debug for Filter {
979 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
980 f.write_str("Filter")
981 }
982}
983
984impl MultiOptimizer {
985 pub fn new(default: impl Optimizer + 'static) -> Self {
987 Self {
988 groups: Vec::new(),
989 default: Box::new(default),
990 filters: Vec::new(),
991 }
992 }
993
994 pub fn group(
997 mut self,
998 filter: impl Fn(&str) -> bool + Send + Sync + 'static,
999 opt: impl Optimizer + 'static,
1000 ) -> Self {
1001 self.groups.push(Box::new(opt));
1002 self.filters.push(Filter(Arc::new(filter)));
1003 self
1004 }
1005
1006 fn group_of(&self, path: &str) -> usize {
1008 self.filters
1009 .iter()
1010 .position(|f| (f.0)(path))
1011 .unwrap_or(self.groups.len())
1012 }
1013}
1014
1015impl Optimizer for MultiOptimizer {
1016 fn apply(&mut self, params: &[(&str, &Array)], grads: &[&Array]) -> Result<Vec<Array>> {
1017 if params.len() != grads.len() {
1018 return Err(Error::InvalidArgument(format!(
1019 "{} gradients for {} parameters",
1020 grads.len(),
1021 params.len()
1022 )));
1023 }
1024 let n = self.groups.len() + 1;
1025 let mut members: Vec<Vec<usize>> = vec![Vec::new(); n];
1026 for (i, (path, _)) in params.iter().enumerate() {
1027 members[self.group_of(path)].push(i);
1028 }
1029 let mut next = self.clone();
1030 let mut out: Vec<Option<Array>> = vec![None; params.len()];
1031 for (k, idx) in members.iter().enumerate() {
1032 let p: Vec<(&str, &Array)> = idx.iter().map(|&i| params[i]).collect();
1033 let g: Vec<&Array> = idx.iter().map(|&i| grads[i]).collect();
1034 let opt = if k < next.groups.len() {
1035 &mut next.groups[k]
1036 } else {
1037 &mut next.default
1038 };
1039 for (&i, a) in idx.iter().zip(opt.apply(&p, &g)?) {
1040 out[i] = Some(a);
1041 }
1042 }
1043 *self = next;
1044 Ok(out
1045 .into_iter()
1046 .map(|a| a.expect("every parameter has a group"))
1047 .collect())
1048 }
1049
1050 fn init_params(&mut self, params: &[(&str, &Array)]) {
1051 let n = self.groups.len();
1052 for k in 0..=n {
1053 let p: Vec<(&str, &Array)> = params
1054 .iter()
1055 .copied()
1056 .filter(|(path, _)| self.group_of(path) == k)
1057 .collect();
1058 if k < n {
1059 self.groups[k].init_params(&p);
1060 } else {
1061 self.default.init_params(&p);
1062 }
1063 }
1064 }
1065
1066 fn visit_state(&self, v: &mut Visit<'_>) {
1067 v.static_field("optimizer", &std::any::type_name::<Self>());
1068 Leaves::visit(self, v);
1069 }
1070
1071 fn visit_state_mut(&mut self, v: &mut VisitMut<'_>) {
1072 Tree::visit_mut(self, v);
1073 }
1074
1075 fn learning_rate(&self) -> Array {
1076 self.default.learning_rate()
1077 }
1078
1079 fn set_learning_rate(&mut self, lr: f32) {
1080 for g in &mut self.groups {
1081 g.set_learning_rate(lr);
1082 }
1083 self.default.set_learning_rate(lr);
1084 }
1085
1086 fn clone_box(&self) -> Box<dyn Optimizer> {
1087 Box::new(self.clone())
1088 }
1089}
1090
1091#[track_caller]
1095pub fn clip_grad_norm<T: Tree>(grads: &T, max_norm: f32) -> (T, Array) {
1096 if max_norm < 0.0 {
1097 let failed = Array::failure(
1098 "clip_grad_norm",
1099 OpErrorKind::InvalidArgument(format!("max_norm should be >= 0, got {max_norm}")),
1100 );
1101 let clipped = grads.map(|_, g| failed.astype(g.dtype()).broadcast_to(g.shape()));
1102 return (clipped, failed);
1103 }
1104 let mut squared: Option<Array> = None;
1105 grads.for_each(|_, g| {
1106 let g = if g.dtype() == DType::F16 {
1107 g.astype(DType::F32)
1108 } else {
1109 g.clone()
1110 };
1111 let s = g.multiply(&g).sum(..);
1112 squared = Some(match squared.take() {
1113 Some(acc) => acc.add(&s),
1114 None => s,
1115 });
1116 });
1117 let total = squared.unwrap_or_else(|| Array::from(0.0f32)).sqrt();
1118 let normalizer = ops::divide(f64::from(max_norm), total.add(1e-6)).minimum(1.0);
1119 let clipped = grads.map(|_, g| {
1120 let scaled = g.multiply(&normalizer);
1121 if g.dtype().category() == Category::Float {
1122 scaled.astype(g.dtype())
1123 } else {
1124 scaled
1125 }
1126 });
1127 (clipped, total)
1128}