1use crate::common::{OptimizerState, StateMemoryStats};
23use crate::traits::StatefulOptimizer;
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
26use trustformers_core::errors::{Result, TrustformersError};
27use trustformers_core::tensor::Tensor;
28use trustformers_core::traits::Optimizer;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct MuonConfig {
33 pub learning_rate: f32,
35 pub momentum: f32,
37 pub ns_steps: usize,
39 pub min_dim_2d: usize,
41 pub fallback_lr: f32,
43 pub fallback_momentum: f32,
45 pub weight_decay: f32,
47 pub use_orthogonal: bool,
49}
50
51impl Default for MuonConfig {
52 fn default() -> Self {
53 Self {
54 learning_rate: 0.02,
55 momentum: 0.95,
56 ns_steps: 5,
57 min_dim_2d: 64,
58 fallback_lr: 1e-3,
59 fallback_momentum: 0.9,
60 weight_decay: 0.0,
61 use_orthogonal: true,
62 }
63 }
64}
65
66#[derive(Debug)]
72pub struct Muon {
73 config: MuonConfig,
74 state: OptimizerState,
75 momentum_2d: HashMap<String, Vec<Vec<f32>>>,
77 momentum_1d: HashMap<String, Vec<f32>>,
79 param_shapes: HashMap<String, (usize, usize)>,
81}
82
83impl Muon {
84 pub fn new() -> Self {
86 Self::with_config(MuonConfig::default())
87 }
88
89 pub fn new_with_lr(learning_rate: f32) -> Self {
91 let config = MuonConfig {
92 learning_rate,
93 ..Default::default()
94 };
95 Self::with_config(config)
96 }
97
98 pub fn for_nanogpt() -> Self {
100 let config = MuonConfig {
101 learning_rate: 0.01,
102 momentum: 0.95,
103 ns_steps: 5,
104 min_dim_2d: 32, fallback_lr: 5e-4,
106 fallback_momentum: 0.9,
107 weight_decay: 0.0,
108 use_orthogonal: true,
109 };
110 Self::with_config(config)
111 }
112
113 pub fn for_cifar10() -> Self {
115 let config = MuonConfig {
116 learning_rate: 0.03,
117 momentum: 0.9,
118 ns_steps: 4, min_dim_2d: 64,
120 fallback_lr: 1e-3,
121 fallback_momentum: 0.9,
122 weight_decay: 1e-4,
123 use_orthogonal: true,
124 };
125 Self::with_config(config)
126 }
127
128 pub fn for_large_lm() -> Self {
130 let config = MuonConfig {
131 learning_rate: 0.015,
132 momentum: 0.98, ns_steps: 6, min_dim_2d: 128, fallback_lr: 3e-4,
136 fallback_momentum: 0.95,
137 weight_decay: 0.01,
138 use_orthogonal: true,
139 };
140 Self::with_config(config)
141 }
142
143 pub fn with_config(config: MuonConfig) -> Self {
145 Self {
146 config,
147 state: OptimizerState::new(),
148 momentum_2d: HashMap::new(),
149 momentum_1d: HashMap::new(),
150 param_shapes: HashMap::new(),
151 }
152 }
153
154 fn should_use_2d_optimization(&self, rows: usize, cols: usize) -> bool {
156 rows >= self.config.min_dim_2d && cols >= self.config.min_dim_2d
157 }
158
159 fn newton_schulz_orthogonalize(&self, matrix: &mut [Vec<f32>]) {
162 if !self.config.use_orthogonal {
163 return;
164 }
165
166 let rows = matrix.len();
167 let cols = matrix[0].len();
168
169 for _ in 0..self.config.ns_steps {
171 let mut xtx = vec![vec![0.0; cols]; cols];
173 for i in 0..cols {
174 for j in 0..cols {
175 let mut sum = 0.0;
176 for k in 0..rows {
177 sum += matrix[k][i] * matrix[k][j];
178 }
179 xtx[i][j] = sum;
180 }
181 }
182
183 for i in 0..cols {
185 for j in 0..cols {
186 if i == j {
187 xtx[i][j] = 3.0 - xtx[i][j];
188 } else {
189 xtx[i][j] = -xtx[i][j];
190 }
191 }
192 }
193
194 let mut new_matrix = vec![vec![0.0; cols]; rows];
196 for i in 0..rows {
197 for j in 0..cols {
198 let mut sum = 0.0;
199 for k in 0..cols {
200 sum += matrix[i][k] * xtx[k][j];
201 }
202 new_matrix[i][j] = sum * 0.5;
203 }
204 }
205
206 for i in 0..rows {
208 for j in 0..cols {
209 matrix[i][j] = new_matrix[i][j];
210 }
211 }
212 }
213 }
214
215 fn update_2d_parameter(
217 &mut self,
218 param_data: &mut [f32],
219 grad_data: &[f32],
220 param_id: &str,
221 rows: usize,
222 cols: usize,
223 ) -> Result<()> {
224 if !self.momentum_2d.contains_key(param_id) {
226 let momentum = vec![vec![0.0; cols]; rows];
227 self.momentum_2d.insert(param_id.to_string(), momentum);
228 }
229
230 let momentum = self.momentum_2d.get_mut(param_id).ok_or_else(|| {
231 TrustformersError::invalid_state(
232 "momentum_2d should contain param_id after insert".to_string(),
233 )
234 })?;
235
236 let mut param_matrix = vec![vec![0.0; cols]; rows];
238 let mut grad_matrix = vec![vec![0.0; cols]; rows];
239
240 for i in 0..rows {
242 for j in 0..cols {
243 let idx = i * cols + j;
244 param_matrix[i][j] = param_data[idx];
245 grad_matrix[i][j] = grad_data[idx];
246 }
247 }
248
249 if self.config.weight_decay > 0.0 {
251 for i in 0..rows {
252 for j in 0..cols {
253 grad_matrix[i][j] += self.config.weight_decay * param_matrix[i][j];
254 }
255 }
256 }
257
258 for i in 0..rows {
260 for j in 0..cols {
261 momentum[i][j] = self.config.momentum * momentum[i][j] + grad_matrix[i][j];
262 }
263 }
264
265 let mut update_matrix = momentum.clone();
267
268 self.newton_schulz_orthogonalize(&mut update_matrix);
270
271 for i in 0..rows {
273 for j in 0..cols {
274 param_matrix[i][j] -= self.config.learning_rate * update_matrix[i][j];
275
276 let idx = i * cols + j;
278 param_data[idx] = param_matrix[i][j];
279 }
280 }
281
282 Ok(())
283 }
284
285 fn update_1d_parameter(
287 &mut self,
288 param_data: &mut [f32],
289 grad_data: &[f32],
290 param_id: &str,
291 ) -> Result<()> {
292 let param_size = param_data.len();
293
294 if !self.momentum_1d.contains_key(param_id) {
296 self.momentum_1d.insert(param_id.to_string(), vec![0.0; param_size]);
297 }
298
299 let momentum = self.momentum_1d.get_mut(param_id).ok_or_else(|| {
300 TrustformersError::invalid_state(
301 "momentum_1d should contain param_id after insert".to_string(),
302 )
303 })?;
304
305 for i in 0..param_size {
307 let mut grad = grad_data[i];
308
309 if self.config.weight_decay > 0.0 {
311 grad += self.config.weight_decay * param_data[i];
312 }
313
314 momentum[i] = self.config.fallback_momentum * momentum[i] + grad;
316
317 param_data[i] -= self.config.fallback_lr * momentum[i];
319 }
320
321 Ok(())
322 }
323
324 pub fn memory_stats(&self) -> StateMemoryStats {
326 self.memory_usage()
327 }
328
329 pub fn optimization_stats(&self) -> (usize, usize, f32) {
331 let params_2d = self.momentum_2d.len();
332 let params_1d = self.momentum_1d.len();
333 let total_params = params_2d + params_1d;
334 let ratio_2d = if total_params > 0 { params_2d as f32 / total_params as f32 } else { 0.0 };
335
336 (params_2d, params_1d, ratio_2d)
337 }
338}
339
340impl Default for Muon {
341 fn default() -> Self {
342 Self::new()
343 }
344}
345
346impl Optimizer for Muon {
347 fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
348 let param_data = parameter.data_mut()?;
349 let grad_data = grad.data()?;
350
351 let param_id = format!("param_{:p}", param_data.as_ptr());
353 let param_size = param_data.len();
354
355 let (rows, cols) = if let Some(&shape) = self.param_shapes.get(¶m_id) {
357 shape
358 } else {
359 let factors = self.find_good_factorization(param_size);
361 self.param_shapes.insert(param_id.clone(), factors);
362 factors
363 };
364
365 if self.should_use_2d_optimization(rows, cols) && rows * cols == param_size {
367 self.update_2d_parameter(param_data, &grad_data, ¶m_id, rows, cols)?;
368 } else {
369 self.update_1d_parameter(param_data, &grad_data, ¶m_id)?;
370 }
371
372 Ok(())
373 }
374
375 fn step(&mut self) {
376 self.state.step += 1;
377 }
378
379 fn zero_grad(&mut self) {
380 }
383
384 fn get_lr(&self) -> f32 {
385 self.config.learning_rate
386 }
387
388 fn set_lr(&mut self, lr: f32) {
389 self.config.learning_rate = lr;
390 }
391}
392
393impl Muon {
394 fn find_good_factorization(&self, size: usize) -> (usize, usize) {
396 if size < self.config.min_dim_2d {
397 return (1, size);
398 }
399
400 let sqrt_size = (size as f32).sqrt() as usize;
402
403 for offset in 0..=sqrt_size / 4 {
405 let candidate1 = sqrt_size + offset;
406 let candidate2 = sqrt_size - offset;
407
408 if candidate1 > 0 && size.is_multiple_of(candidate1) {
409 let other = size / candidate1;
410 if candidate1 >= self.config.min_dim_2d && other >= self.config.min_dim_2d {
411 return (candidate1, other);
412 }
413 }
414
415 if candidate2 > 0 && size.is_multiple_of(candidate2) {
416 let other = size / candidate2;
417 if candidate2 >= self.config.min_dim_2d && other >= self.config.min_dim_2d {
418 return (candidate2, other);
419 }
420 }
421 }
422
423 (1, size)
425 }
426}
427
428impl StatefulOptimizer for Muon {
429 type Config = MuonConfig;
430 type State = OptimizerState;
431
432 fn config(&self) -> &Self::Config {
433 &self.config
434 }
435
436 fn state(&self) -> &Self::State {
437 &self.state
438 }
439
440 fn state_mut(&mut self) -> &mut Self::State {
441 &mut self.state
442 }
443
444 fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
445 let mut state_dict = HashMap::new();
446
447 state_dict.insert(
449 "step".to_string(),
450 Tensor::new(vec![self.state.step as f32])?,
451 );
452
453 for (param_id, momentum) in &self.momentum_2d {
455 let mut flattened = Vec::new();
456 for row in momentum {
457 flattened.extend_from_slice(row);
458 }
459 state_dict.insert(format!("momentum_2d_{}", param_id), Tensor::new(flattened)?);
460 }
461
462 for (param_id, momentum) in &self.momentum_1d {
464 state_dict.insert(
465 format!("momentum_1d_{}", param_id),
466 Tensor::new(momentum.clone())?,
467 );
468 }
469
470 for (param_id, &(rows, cols)) in &self.param_shapes {
472 state_dict.insert(
473 format!("shape_{}", param_id),
474 Tensor::new(vec![rows as f32, cols as f32])?,
475 );
476 }
477
478 Ok(state_dict)
479 }
480
481 fn load_state_dict(&mut self, state_dict: HashMap<String, Tensor>) -> Result<()> {
482 if let Some(step_tensor) = state_dict.get("step") {
484 let step_data = step_tensor.data()?;
485 if !step_data.is_empty() {
486 self.state.step = step_data[0] as usize;
487 }
488 }
489
490 for (key, tensor) in &state_dict {
492 if let Some(param_id) = key.strip_prefix("shape_") {
493 let shape_data = tensor.data()?;
494 if shape_data.len() >= 2 {
495 let rows = shape_data[0] as usize;
496 let cols = shape_data[1] as usize;
497 self.param_shapes.insert(param_id.to_string(), (rows, cols));
498 }
499 }
500 }
501
502 for (key, tensor) in &state_dict {
504 let data = tensor.data()?;
505 if let Some(param_id) = key.strip_prefix("momentum_2d_") {
506 if let Some(&(rows, cols)) = self.param_shapes.get(param_id) {
507 let mut momentum = vec![vec![0.0; cols]; rows];
508 for i in 0..rows {
509 for j in 0..cols {
510 let idx = i * cols + j;
511 if idx < data.len() {
512 momentum[i][j] = data[idx];
513 }
514 }
515 }
516 self.momentum_2d.insert(param_id.to_string(), momentum);
517 }
518 } else if let Some(param_id) = key.strip_prefix("momentum_1d_") {
519 self.momentum_1d.insert(param_id.to_string(), data);
520 }
521 }
522
523 Ok(())
524 }
525
526 fn memory_usage(&self) -> StateMemoryStats {
527 let mut momentum_elements = 0;
528 let mut total_elements = 0;
529
530 for momentum in self.momentum_2d.values() {
532 let param_count = momentum.len() * momentum[0].len();
533 momentum_elements += param_count;
534 total_elements += param_count;
535 }
536
537 for momentum in self.momentum_1d.values() {
539 momentum_elements += momentum.len();
540 total_elements += momentum.len();
541 }
542
543 let total_bytes = total_elements * std::mem::size_of::<f32>();
544
545 StateMemoryStats {
546 momentum_elements,
547 variance_elements: 0,
548 third_moment_elements: 0,
549 total_bytes,
550 num_parameters: momentum_elements,
551 }
552 }
553
554 fn reset_state(&mut self) {
555 self.state = OptimizerState::new();
556 self.momentum_2d.clear();
557 self.momentum_1d.clear();
558 self.param_shapes.clear();
559 }
560
561 fn num_parameters(&self) -> usize {
562 let mut total = 0;
563 for momentum in self.momentum_2d.values() {
564 total += momentum.len() * momentum[0].len();
565 }
566 for momentum in self.momentum_1d.values() {
567 total += momentum.len();
568 }
569 total
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use approx::assert_relative_eq;
577
578 #[test]
579 fn test_muon_creation() {
580 let optimizer = Muon::new();
581 assert_eq!(optimizer.config.learning_rate, 0.02);
582 assert_eq!(optimizer.config.momentum, 0.95);
583 assert_eq!(optimizer.config.ns_steps, 5);
584 assert_eq!(optimizer.config.min_dim_2d, 64);
585 assert_eq!(optimizer.state.step, 0);
586 }
587
588 #[test]
589 fn test_muon_with_lr() {
590 let optimizer = Muon::new_with_lr(0.01);
591 assert_eq!(optimizer.config.learning_rate, 0.01);
592 }
593
594 #[test]
595 fn test_muon_nanogpt_preset() {
596 let optimizer = Muon::for_nanogpt();
597 assert_eq!(optimizer.config.learning_rate, 0.01);
598 assert_eq!(optimizer.config.min_dim_2d, 32);
599 assert_eq!(optimizer.config.fallback_lr, 5e-4);
600 }
601
602 #[test]
603 fn test_muon_cifar10_preset() {
604 let optimizer = Muon::for_cifar10();
605 assert_eq!(optimizer.config.learning_rate, 0.03);
606 assert_eq!(optimizer.config.ns_steps, 4);
607 assert_eq!(optimizer.config.weight_decay, 1e-4);
608 }
609
610 #[test]
611 fn test_muon_large_lm_preset() {
612 let optimizer = Muon::for_large_lm();
613 assert_eq!(optimizer.config.learning_rate, 0.015);
614 assert_eq!(optimizer.config.momentum, 0.98);
615 assert_eq!(optimizer.config.min_dim_2d, 128);
616 }
617
618 #[test]
619 fn test_should_use_2d_optimization() {
620 let optimizer = Muon::new();
621
622 assert!(optimizer.should_use_2d_optimization(128, 128));
624 assert!(optimizer.should_use_2d_optimization(64, 256));
625
626 assert!(!optimizer.should_use_2d_optimization(32, 32));
628 assert!(!optimizer.should_use_2d_optimization(64, 32));
629 assert!(!optimizer.should_use_2d_optimization(1, 1000));
630 }
631
632 #[test]
633 fn test_find_good_factorization() {
634 let optimizer = Muon::new();
635
636 let (rows, cols) = optimizer.find_good_factorization(64 * 64);
638 assert_eq!(rows * cols, 64 * 64);
639 assert!(rows >= optimizer.config.min_dim_2d);
640 assert!(cols >= optimizer.config.min_dim_2d);
641
642 let (rows, cols) = optimizer.find_good_factorization(10);
644 assert_eq!((rows, cols), (1, 10));
645
646 let (rows, cols) = optimizer.find_good_factorization(128 * 256);
648 assert_eq!(rows * cols, 128 * 256);
649 }
650
651 #[test]
652 fn test_optimization_stats() {
653 let mut optimizer = Muon::new();
654
655 let (params_2d, params_1d, ratio) = optimizer.optimization_stats();
657 assert_eq!(params_2d, 0);
658 assert_eq!(params_1d, 0);
659 assert_eq!(ratio, 0.0);
660
661 optimizer.momentum_2d.insert("param_0".to_string(), vec![vec![0.0; 128]; 128]);
663 optimizer.momentum_1d.insert("param_1".to_string(), vec![0.0; 10]);
664 optimizer.momentum_1d.insert("param_2".to_string(), vec![0.0; 20]);
665
666 let (params_2d, params_1d, ratio) = optimizer.optimization_stats();
667 assert_eq!(params_2d, 1);
668 assert_eq!(params_1d, 2);
669 assert_relative_eq!(ratio, 1.0 / 3.0, epsilon = 1e-6);
670 }
671
672 #[test]
673 fn test_memory_stats() {
674 let mut optimizer = Muon::new();
675
676 optimizer.momentum_2d.insert("param_0".to_string(), vec![vec![0.0; 100]; 50]); optimizer.momentum_1d.insert("param_1".to_string(), vec![0.0; 1000]); let stats = optimizer.memory_stats();
681 assert_eq!(stats.num_parameters, 6000);
682 assert_eq!(stats.momentum_elements, 6000);
683 assert_eq!(stats.variance_elements, 0);
684 assert_eq!(stats.total_bytes, 6000 * 4); }
686
687 #[test]
688 fn test_state_dict_operations() {
689 let mut optimizer = Muon::new();
690 optimizer.state.step = 5;
691
692 optimizer.param_shapes.insert("param_0".to_string(), (2, 3));
694 optimizer.momentum_2d.insert(
695 "param_0".to_string(),
696 vec![vec![0.1, 0.2, 0.3], vec![0.4, 0.5, 0.6]],
697 );
698 optimizer.momentum_1d.insert("param_1".to_string(), vec![0.7, 0.8]);
699
700 let state_dict = optimizer.state_dict().expect("Failed to get state dict");
702 assert!(state_dict.contains_key("step"));
703 assert!(state_dict.contains_key("momentum_2d_param_0"));
704 assert!(state_dict.contains_key("momentum_1d_param_1"));
705 assert!(state_dict.contains_key("shape_param_0"));
706
707 let mut new_optimizer = Muon::new();
709 new_optimizer.load_state_dict(state_dict).expect("Failed to load state dict");
710
711 assert_eq!(new_optimizer.state.step, 5);
712 assert_eq!(new_optimizer.param_shapes["param_0"], (2, 3));
713 assert_eq!(new_optimizer.momentum_1d["param_1"], vec![0.7, 0.8]);
714 }
715
716 #[test]
717 fn test_lr_setter_getter() {
718 let mut optimizer = Muon::new();
719 assert_eq!(optimizer.get_lr(), 0.02);
720
721 optimizer.set_lr(0.01);
722 assert_eq!(optimizer.get_lr(), 0.01);
723 assert_eq!(optimizer.config.learning_rate, 0.01);
724 }
725
726 #[test]
727 fn test_reset() {
728 let mut optimizer = Muon::new();
729 optimizer.state.step = 10;
730 optimizer.momentum_2d.insert("param_0".to_string(), vec![vec![1.0]]);
731 optimizer.momentum_1d.insert("param_1".to_string(), vec![1.0]);
732 optimizer.param_shapes.insert("param_0".to_string(), (1, 1));
733
734 optimizer.reset_state();
735
736 assert_eq!(optimizer.state.step, 0);
737 assert!(optimizer.momentum_2d.is_empty());
738 assert!(optimizer.momentum_1d.is_empty());
739 assert!(optimizer.param_shapes.is_empty());
740 }
741
742 #[test]
743 fn test_config_serialization() {
744 let config = MuonConfig {
745 learning_rate: 0.01,
746 momentum: 0.9,
747 ns_steps: 3,
748 min_dim_2d: 32,
749 fallback_lr: 1e-4,
750 fallback_momentum: 0.8,
751 weight_decay: 1e-5,
752 use_orthogonal: false,
753 };
754
755 let serialized = serde_json::to_string(&config).expect("Serialization failed");
756 let deserialized: MuonConfig =
757 serde_json::from_str(&serialized).expect("Deserialization failed");
758
759 assert_relative_eq!(deserialized.learning_rate, config.learning_rate);
760 assert_eq!(deserialized.ns_steps, config.ns_steps);
761 assert_eq!(deserialized.use_orthogonal, config.use_orthogonal);
762 }
763}