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 nesterov: bool,
50 pub weight_decay: f32,
52 pub use_orthogonal: bool,
54}
55
56impl Default for MuonConfig {
57 fn default() -> Self {
58 Self {
59 learning_rate: 0.02,
60 momentum: 0.95,
61 ns_steps: 5,
62 min_dim_2d: 64,
63 fallback_lr: 1e-3,
64 fallback_momentum: 0.9,
65 nesterov: true,
66 weight_decay: 0.0,
67 use_orthogonal: true,
68 }
69 }
70}
71
72#[derive(Debug)]
78pub struct Muon {
79 config: MuonConfig,
80 state: OptimizerState,
81 momentum_2d: HashMap<String, Vec<Vec<f32>>>,
83 momentum_1d: HashMap<String, Vec<f32>>,
85 param_shapes: HashMap<String, (usize, usize)>,
87}
88
89impl Muon {
90 pub fn new() -> Self {
92 Self::with_config(MuonConfig::default())
93 }
94
95 pub fn new_with_lr(learning_rate: f32) -> Self {
97 let config = MuonConfig {
98 learning_rate,
99 ..Default::default()
100 };
101 Self::with_config(config)
102 }
103
104 pub fn for_nanogpt() -> Self {
106 let config = MuonConfig {
107 learning_rate: 0.01,
108 momentum: 0.95,
109 ns_steps: 5,
110 min_dim_2d: 32, fallback_lr: 5e-4,
112 fallback_momentum: 0.9,
113 nesterov: true,
114 weight_decay: 0.0,
115 use_orthogonal: true,
116 };
117 Self::with_config(config)
118 }
119
120 pub fn for_cifar10() -> Self {
122 let config = MuonConfig {
123 learning_rate: 0.03,
124 momentum: 0.9,
125 ns_steps: 4, min_dim_2d: 64,
127 fallback_lr: 1e-3,
128 fallback_momentum: 0.9,
129 nesterov: true,
130 weight_decay: 1e-4,
131 use_orthogonal: true,
132 };
133 Self::with_config(config)
134 }
135
136 pub fn for_large_lm() -> Self {
138 let config = MuonConfig {
139 learning_rate: 0.015,
140 momentum: 0.98, ns_steps: 6, min_dim_2d: 128, fallback_lr: 3e-4,
144 fallback_momentum: 0.95,
145 weight_decay: 0.01,
146 use_orthogonal: true,
147 nesterov: true,
148 };
149 Self::with_config(config)
150 }
151
152 pub fn with_config(config: MuonConfig) -> Self {
154 Self {
155 config,
156 state: OptimizerState::new(),
157 momentum_2d: HashMap::new(),
158 momentum_1d: HashMap::new(),
159 param_shapes: HashMap::new(),
160 }
161 }
162
163 fn should_use_2d_optimization(&self, rows: usize, cols: usize) -> bool {
165 rows >= self.config.min_dim_2d && cols >= self.config.min_dim_2d
166 }
167
168 fn newton_schulz_orthogonalize(&self, matrix: &mut [Vec<f32>]) {
184 if !self.config.use_orthogonal {
185 return;
186 }
187
188 let rows = matrix.len();
189 if rows == 0 {
190 return;
191 }
192 let cols = matrix[0].len();
193 if cols == 0 {
194 return;
195 }
196
197 let frobenius: f32 =
199 matrix.iter().flat_map(|row| row.iter()).map(|v| v * v).sum::<f32>().sqrt();
200 if !frobenius.is_finite() || frobenius <= f32::MIN_POSITIVE {
201 return;
202 }
203 let inv_norm = 1.0 / (frobenius + 1e-7);
204 for row in matrix.iter_mut() {
205 for value in row.iter_mut() {
206 *value *= inv_norm;
207 }
208 }
209
210 for _ in 0..self.config.ns_steps {
212 let mut xtx = vec![vec![0.0; cols]; cols];
214 for i in 0..cols {
215 for j in 0..cols {
216 let mut sum = 0.0;
217 for k in 0..rows {
218 sum += matrix[k][i] * matrix[k][j];
219 }
220 xtx[i][j] = sum;
221 }
222 }
223
224 for (i, row) in xtx.iter_mut().enumerate() {
226 for (j, value) in row.iter_mut().enumerate() {
227 *value = if i == j { 3.0 - *value } else { -*value };
228 }
229 }
230
231 let mut new_matrix = vec![vec![0.0; cols]; rows];
233 for i in 0..rows {
234 for j in 0..cols {
235 let mut sum = 0.0;
236 for k in 0..cols {
237 sum += matrix[i][k] * xtx[k][j];
238 }
239 new_matrix[i][j] = sum * 0.5;
240 }
241 }
242
243 for i in 0..rows {
245 for j in 0..cols {
246 matrix[i][j] = new_matrix[i][j];
247 }
248 }
249 }
250
251 let aspect = (rows as f32 / cols as f32).max(1.0).sqrt();
254 for row in matrix.iter_mut() {
255 for value in row.iter_mut() {
256 *value *= frobenius * aspect;
257 }
258 }
259 }
260
261 fn update_2d_parameter(
263 &mut self,
264 param_data: &mut [f32],
265 grad_data: &[f32],
266 param_id: &str,
267 rows: usize,
268 cols: usize,
269 ) -> Result<()> {
270 if !self.momentum_2d.contains_key(param_id) {
272 let momentum = vec![vec![0.0; cols]; rows];
273 self.momentum_2d.insert(param_id.to_string(), momentum);
274 }
275
276 let momentum = self.momentum_2d.get_mut(param_id).ok_or_else(|| {
277 TrustformersError::invalid_state(
278 "momentum_2d should contain param_id after insert".to_string(),
279 )
280 })?;
281
282 let mut param_matrix = vec![vec![0.0; cols]; rows];
284 let mut grad_matrix = vec![vec![0.0; cols]; rows];
285
286 for i in 0..rows {
288 for j in 0..cols {
289 let idx = i * cols + j;
290 param_matrix[i][j] = param_data[idx];
291 grad_matrix[i][j] = grad_data[idx];
292 }
293 }
294
295 if self.config.weight_decay > 0.0 {
297 for i in 0..rows {
298 for j in 0..cols {
299 grad_matrix[i][j] += self.config.weight_decay * param_matrix[i][j];
300 }
301 }
302 }
303
304 for i in 0..rows {
306 for j in 0..cols {
307 momentum[i][j] = self.config.momentum * momentum[i][j] + grad_matrix[i][j];
308 }
309 }
310
311 let mut update_matrix = momentum.clone();
314 if self.config.nesterov {
315 for i in 0..rows {
316 for j in 0..cols {
317 update_matrix[i][j] = grad_matrix[i][j] + self.config.momentum * momentum[i][j];
318 }
319 }
320 }
321
322 self.newton_schulz_orthogonalize(&mut update_matrix);
324
325 for i in 0..rows {
327 for j in 0..cols {
328 param_matrix[i][j] -= self.config.learning_rate * update_matrix[i][j];
329
330 let idx = i * cols + j;
332 param_data[idx] = param_matrix[i][j];
333 }
334 }
335
336 Ok(())
337 }
338
339 fn update_1d_parameter(
341 &mut self,
342 param_data: &mut [f32],
343 grad_data: &[f32],
344 param_id: &str,
345 ) -> Result<()> {
346 let param_size = param_data.len();
347
348 if !self.momentum_1d.contains_key(param_id) {
350 self.momentum_1d.insert(param_id.to_string(), vec![0.0; param_size]);
351 }
352
353 let momentum = self.momentum_1d.get_mut(param_id).ok_or_else(|| {
354 TrustformersError::invalid_state(
355 "momentum_1d should contain param_id after insert".to_string(),
356 )
357 })?;
358
359 for i in 0..param_size {
361 let mut grad = grad_data[i];
362
363 if self.config.weight_decay > 0.0 {
365 grad += self.config.weight_decay * param_data[i];
366 }
367
368 momentum[i] = self.config.fallback_momentum * momentum[i] + grad;
370
371 param_data[i] -= self.config.fallback_lr * momentum[i];
373 }
374
375 Ok(())
376 }
377
378 pub fn memory_stats(&self) -> StateMemoryStats {
380 self.memory_usage()
381 }
382
383 pub fn optimization_stats(&self) -> (usize, usize, f32) {
385 let params_2d = self.momentum_2d.len();
386 let params_1d = self.momentum_1d.len();
387 let total_params = params_2d + params_1d;
388 let ratio_2d = if total_params > 0 { params_2d as f32 / total_params as f32 } else { 0.0 };
389
390 (params_2d, params_1d, ratio_2d)
391 }
392}
393
394impl Default for Muon {
395 fn default() -> Self {
396 Self::new()
397 }
398}
399
400impl Optimizer for Muon {
401 fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
402 let param_id = self.state.param_key_for_tensor(parameter)?;
405 let param_data = parameter.data_mut()?;
406 let grad_data = grad.data()?;
407 let param_size = param_data.len();
408
409 let (rows, cols) = if let Some(&shape) = self.param_shapes.get(¶m_id) {
411 shape
412 } else {
413 let factors = self.find_good_factorization(param_size);
415 self.param_shapes.insert(param_id.clone(), factors);
416 factors
417 };
418
419 if self.should_use_2d_optimization(rows, cols) && rows * cols == param_size {
421 self.update_2d_parameter(param_data, &grad_data, ¶m_id, rows, cols)?;
422 } else {
423 self.update_1d_parameter(param_data, &grad_data, ¶m_id)?;
424 }
425
426 Ok(())
427 }
428
429 fn step(&mut self) {
430 self.state.step += 1;
431 }
432
433 fn zero_grad(&mut self) {
434 }
437
438 fn get_lr(&self) -> f32 {
439 self.config.learning_rate
440 }
441
442 fn set_lr(&mut self, lr: f32) {
443 self.config.learning_rate = lr;
444 }
445}
446
447impl Muon {
448 fn find_good_factorization(&self, size: usize) -> (usize, usize) {
450 if size < self.config.min_dim_2d {
451 return (1, size);
452 }
453
454 let sqrt_size = (size as f32).sqrt() as usize;
456
457 for offset in 0..=sqrt_size / 4 {
459 let candidate1 = sqrt_size + offset;
460 let candidate2 = sqrt_size - offset;
461
462 if candidate1 > 0 && size.is_multiple_of(candidate1) {
463 let other = size / candidate1;
464 if candidate1 >= self.config.min_dim_2d && other >= self.config.min_dim_2d {
465 return (candidate1, other);
466 }
467 }
468
469 if candidate2 > 0 && size.is_multiple_of(candidate2) {
470 let other = size / candidate2;
471 if candidate2 >= self.config.min_dim_2d && other >= self.config.min_dim_2d {
472 return (candidate2, other);
473 }
474 }
475 }
476
477 (1, size)
479 }
480}
481
482impl StatefulOptimizer for Muon {
483 type Config = MuonConfig;
484 type State = OptimizerState;
485
486 fn config(&self) -> &Self::Config {
487 &self.config
488 }
489
490 fn state(&self) -> &Self::State {
491 &self.state
492 }
493
494 fn state_mut(&mut self) -> &mut Self::State {
495 &mut self.state
496 }
497
498 fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
499 let mut state_dict = HashMap::new();
500
501 state_dict.insert(
503 "step".to_string(),
504 Tensor::new(vec![self.state.step as f32])?,
505 );
506
507 for (param_id, momentum) in &self.momentum_2d {
509 let mut flattened = Vec::new();
510 for row in momentum {
511 flattened.extend_from_slice(row);
512 }
513 state_dict.insert(format!("momentum_2d_{}", param_id), Tensor::new(flattened)?);
514 }
515
516 for (param_id, momentum) in &self.momentum_1d {
518 state_dict.insert(
519 format!("momentum_1d_{}", param_id),
520 Tensor::new(momentum.clone())?,
521 );
522 }
523
524 for (param_id, &(rows, cols)) in &self.param_shapes {
526 state_dict.insert(
527 format!("shape_{}", param_id),
528 Tensor::new(vec![rows as f32, cols as f32])?,
529 );
530 }
531
532 Ok(state_dict)
533 }
534
535 fn load_state_dict(&mut self, state_dict: HashMap<String, Tensor>) -> Result<()> {
536 if let Some(step_tensor) = state_dict.get("step") {
538 let step_data = step_tensor.data()?;
539 if !step_data.is_empty() {
540 self.state.step = step_data[0] as usize;
541 }
542 }
543
544 for (key, tensor) in &state_dict {
546 if let Some(param_id) = key.strip_prefix("shape_") {
547 let shape_data = tensor.data()?;
548 if shape_data.len() >= 2 {
549 let rows = shape_data[0] as usize;
550 let cols = shape_data[1] as usize;
551 self.param_shapes.insert(param_id.to_string(), (rows, cols));
552 }
553 }
554 }
555
556 for (key, tensor) in &state_dict {
558 let data = tensor.data()?;
559 if let Some(param_id) = key.strip_prefix("momentum_2d_") {
560 if let Some(&(rows, cols)) = self.param_shapes.get(param_id) {
561 let mut momentum = vec![vec![0.0; cols]; rows];
562 for i in 0..rows {
563 for j in 0..cols {
564 let idx = i * cols + j;
565 if idx < data.len() {
566 momentum[i][j] = data[idx];
567 }
568 }
569 }
570 self.momentum_2d.insert(param_id.to_string(), momentum);
571 }
572 } else if let Some(param_id) = key.strip_prefix("momentum_1d_") {
573 self.momentum_1d.insert(param_id.to_string(), data);
574 }
575 }
576
577 Ok(())
578 }
579
580 fn memory_usage(&self) -> StateMemoryStats {
581 let mut momentum_elements = 0;
582 let mut total_elements = 0;
583
584 for momentum in self.momentum_2d.values() {
586 let param_count = momentum.len() * momentum[0].len();
587 momentum_elements += param_count;
588 total_elements += param_count;
589 }
590
591 for momentum in self.momentum_1d.values() {
593 momentum_elements += momentum.len();
594 total_elements += momentum.len();
595 }
596
597 let total_bytes = total_elements * std::mem::size_of::<f32>();
598
599 StateMemoryStats {
600 momentum_elements,
601 variance_elements: 0,
602 third_moment_elements: 0,
603 total_bytes,
604 num_parameters: momentum_elements,
605 }
606 }
607
608 fn reset_state(&mut self) {
609 self.state = OptimizerState::new();
610 self.momentum_2d.clear();
611 self.momentum_1d.clear();
612 self.param_shapes.clear();
613 }
614
615 fn num_parameters(&self) -> usize {
616 let mut total = 0;
617 for momentum in self.momentum_2d.values() {
618 total += momentum.len() * momentum[0].len();
619 }
620 for momentum in self.momentum_1d.values() {
621 total += momentum.len();
622 }
623 total
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use approx::assert_relative_eq;
631
632 #[test]
633 fn test_muon_creation() {
634 let optimizer = Muon::new();
635 assert_eq!(optimizer.config.learning_rate, 0.02);
636 assert_eq!(optimizer.config.momentum, 0.95);
637 assert_eq!(optimizer.config.ns_steps, 5);
638 assert_eq!(optimizer.config.min_dim_2d, 64);
639 assert_eq!(optimizer.state.step, 0);
640 }
641
642 #[test]
643 fn test_muon_with_lr() {
644 let optimizer = Muon::new_with_lr(0.01);
645 assert_eq!(optimizer.config.learning_rate, 0.01);
646 }
647
648 #[test]
649 fn test_muon_nanogpt_preset() {
650 let optimizer = Muon::for_nanogpt();
651 assert_eq!(optimizer.config.learning_rate, 0.01);
652 assert_eq!(optimizer.config.min_dim_2d, 32);
653 assert_eq!(optimizer.config.fallback_lr, 5e-4);
654 }
655
656 #[test]
657 fn test_muon_cifar10_preset() {
658 let optimizer = Muon::for_cifar10();
659 assert_eq!(optimizer.config.learning_rate, 0.03);
660 assert_eq!(optimizer.config.ns_steps, 4);
661 assert_eq!(optimizer.config.weight_decay, 1e-4);
662 }
663
664 #[test]
665 fn test_muon_large_lm_preset() {
666 let optimizer = Muon::for_large_lm();
667 assert_eq!(optimizer.config.learning_rate, 0.015);
668 assert_eq!(optimizer.config.momentum, 0.98);
669 assert_eq!(optimizer.config.min_dim_2d, 128);
670 }
671
672 #[test]
673 fn test_should_use_2d_optimization() {
674 let optimizer = Muon::new();
675
676 assert!(optimizer.should_use_2d_optimization(128, 128));
678 assert!(optimizer.should_use_2d_optimization(64, 256));
679
680 assert!(!optimizer.should_use_2d_optimization(32, 32));
682 assert!(!optimizer.should_use_2d_optimization(64, 32));
683 assert!(!optimizer.should_use_2d_optimization(1, 1000));
684 }
685
686 #[test]
687 fn test_find_good_factorization() {
688 let optimizer = Muon::new();
689
690 let (rows, cols) = optimizer.find_good_factorization(64 * 64);
692 assert_eq!(rows * cols, 64 * 64);
693 assert!(rows >= optimizer.config.min_dim_2d);
694 assert!(cols >= optimizer.config.min_dim_2d);
695
696 let (rows, cols) = optimizer.find_good_factorization(10);
698 assert_eq!((rows, cols), (1, 10));
699
700 let (rows, cols) = optimizer.find_good_factorization(128 * 256);
702 assert_eq!(rows * cols, 128 * 256);
703 }
704
705 #[test]
706 fn test_optimization_stats() {
707 let mut optimizer = Muon::new();
708
709 let (params_2d, params_1d, ratio) = optimizer.optimization_stats();
711 assert_eq!(params_2d, 0);
712 assert_eq!(params_1d, 0);
713 assert_eq!(ratio, 0.0);
714
715 optimizer.momentum_2d.insert("param_0".to_string(), vec![vec![0.0; 128]; 128]);
717 optimizer.momentum_1d.insert("param_1".to_string(), vec![0.0; 10]);
718 optimizer.momentum_1d.insert("param_2".to_string(), vec![0.0; 20]);
719
720 let (params_2d, params_1d, ratio) = optimizer.optimization_stats();
721 assert_eq!(params_2d, 1);
722 assert_eq!(params_1d, 2);
723 assert_relative_eq!(ratio, 1.0 / 3.0, epsilon = 1e-6);
724 }
725
726 #[test]
727 fn test_memory_stats() {
728 let mut optimizer = Muon::new();
729
730 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();
735 assert_eq!(stats.num_parameters, 6000);
736 assert_eq!(stats.momentum_elements, 6000);
737 assert_eq!(stats.variance_elements, 0);
738 assert_eq!(stats.total_bytes, 6000 * 4); }
740
741 #[test]
742 fn test_state_dict_operations() {
743 let mut optimizer = Muon::new();
744 optimizer.state.step = 5;
745
746 optimizer.param_shapes.insert("param_0".to_string(), (2, 3));
748 optimizer.momentum_2d.insert(
749 "param_0".to_string(),
750 vec![vec![0.1, 0.2, 0.3], vec![0.4, 0.5, 0.6]],
751 );
752 optimizer.momentum_1d.insert("param_1".to_string(), vec![0.7, 0.8]);
753
754 let state_dict = optimizer.state_dict().expect("Failed to get state dict");
756 assert!(state_dict.contains_key("step"));
757 assert!(state_dict.contains_key("momentum_2d_param_0"));
758 assert!(state_dict.contains_key("momentum_1d_param_1"));
759 assert!(state_dict.contains_key("shape_param_0"));
760
761 let mut new_optimizer = Muon::new();
763 new_optimizer.load_state_dict(state_dict).expect("Failed to load state dict");
764
765 assert_eq!(new_optimizer.state.step, 5);
766 assert_eq!(new_optimizer.param_shapes["param_0"], (2, 3));
767 assert_eq!(new_optimizer.momentum_1d["param_1"], vec![0.7, 0.8]);
768 }
769
770 #[test]
771 fn test_lr_setter_getter() {
772 let mut optimizer = Muon::new();
773 assert_eq!(optimizer.get_lr(), 0.02);
774
775 optimizer.set_lr(0.01);
776 assert_eq!(optimizer.get_lr(), 0.01);
777 assert_eq!(optimizer.config.learning_rate, 0.01);
778 }
779
780 #[test]
781 fn test_reset() {
782 let mut optimizer = Muon::new();
783 optimizer.state.step = 10;
784 optimizer.momentum_2d.insert("param_0".to_string(), vec![vec![1.0]]);
785 optimizer.momentum_1d.insert("param_1".to_string(), vec![1.0]);
786 optimizer.param_shapes.insert("param_0".to_string(), (1, 1));
787
788 optimizer.reset_state();
789
790 assert_eq!(optimizer.state.step, 0);
791 assert!(optimizer.momentum_2d.is_empty());
792 assert!(optimizer.momentum_1d.is_empty());
793 assert!(optimizer.param_shapes.is_empty());
794 }
795
796 #[test]
797 fn test_config_serialization() {
798 let config = MuonConfig {
799 learning_rate: 0.01,
800 momentum: 0.9,
801 ns_steps: 3,
802 min_dim_2d: 32,
803 fallback_lr: 1e-4,
804 fallback_momentum: 0.8,
805 weight_decay: 1e-5,
806 use_orthogonal: false,
807 nesterov: true,
808 };
809
810 let serialized = serde_json::to_string(&config).expect("Serialization failed");
811 let deserialized: MuonConfig =
812 serde_json::from_str(&serialized).expect("Deserialization failed");
813
814 assert_relative_eq!(deserialized.learning_rate, config.learning_rate);
815 assert_eq!(deserialized.ns_steps, config.ns_steps);
816 assert_eq!(deserialized.use_orthogonal, config.use_orthogonal);
817 }
818}
819
820#[cfg(test)]
821mod newton_schulz_tests {
822 use super::*;
823
824 #[test]
829 fn orthogonalization_stays_finite_for_a_large_matrix() {
830 let optimizer = Muon::new();
831 let mut matrix = vec![vec![25.0_f32; 4]; 4];
833
834 optimizer.newton_schulz_orthogonalize(&mut matrix);
835
836 for row in &matrix {
837 for value in row {
838 assert!(value.is_finite(), "orthogonalization diverged: {value}");
839 }
840 }
841 }
842
843 #[test]
847 fn orthogonalization_normalizes_the_spectrum() {
848 let optimizer = Muon::new();
849 let mut matrix = vec![
851 vec![100.0_f32, 0.0, 0.0],
852 vec![0.0, 1.0, 0.0],
853 vec![0.0, 0.0, 0.01],
854 ];
855 let frobenius: f32 =
856 matrix.iter().flat_map(|r| r.iter()).map(|v| v * v).sum::<f32>().sqrt();
857
858 optimizer.newton_schulz_orthogonalize(&mut matrix);
859
860 let ratio = (matrix[0][0] / matrix[1][1]).abs();
864 assert!(ratio.is_finite(), "diverged");
865 assert!(ratio < 100.0, "spectrum was not equalised, ratio {ratio}");
866 let out_frobenius: f32 =
868 matrix.iter().flat_map(|r| r.iter()).map(|v| v * v).sum::<f32>().sqrt();
869 assert!(
870 out_frobenius > frobenius * 0.5,
871 "the original scale must be restored"
872 );
873 }
874
875 #[test]
877 fn orthogonalization_handles_degenerate_shapes() {
878 let optimizer = Muon::new();
879 let mut empty: Vec<Vec<f32>> = Vec::new();
880 optimizer.newton_schulz_orthogonalize(&mut empty);
881
882 let mut no_columns: Vec<Vec<f32>> = vec![Vec::new(), Vec::new()];
883 optimizer.newton_schulz_orthogonalize(&mut no_columns);
884
885 let mut zeros = vec![vec![0.0_f32; 2]; 2];
886 optimizer.newton_schulz_orthogonalize(&mut zeros);
887 assert!(zeros.iter().flatten().all(|v| v.is_finite()));
888 }
889
890 #[test]
892 fn muon_update_stays_finite_for_a_large_gradient() {
893 let mut optimizer = Muon::new();
894 let mut param = Tensor::from_vec(vec![0.1_f32; 64], &[8, 8]).expect("tensor");
895 let grad = Tensor::from_vec(vec![10.0_f32; 64], &[8, 8]).expect("grad");
896
897 for _ in 0..5 {
898 optimizer.update(&mut param, &grad).expect("update");
899 }
900
901 for value in param.data_f32().expect("data") {
902 assert!(
903 value.is_finite(),
904 "Muon wrote a non-finite parameter: {value}"
905 );
906 }
907 }
908}