Skip to main content

trustformers_optim/
muon.rs

1//! # Muon Optimizer
2//!
3//! Implementation of the Muon optimizer, a second-order optimization algorithm designed for
4//! neural network training, particularly with hidden layers having 2D weight matrices.
5//!
6//! Muon is used in the current training speed records for both NanoGPT and CIFAR-10 speedrunning.
7//!
8//! ## Key Features
9//!
10//! - **Second-Order Optimization**: Uses Newton-Schulz iteration for efficient orthogonalization
11//! - **Low FLOP Overhead**: Below 1% FLOP overhead for typical LM training scenarios
12//! - **2D Parameter Focus**: Designed specifically for 2D weight matrices (linear layers)
13//! - **Speed Records**: Achieves state-of-the-art training speed on multiple benchmarks
14//!
15//! ## Design Philosophy
16//!
17//! Muon only applies to 2D parameters (weight matrices), while scalar and vector parameters
18//! must be optimized using a standard method (e.g., AdamW). This hybrid approach provides
19//! the best of both worlds: second-order benefits for main parameters and proven stability
20//! for auxiliary parameters.
21
22use 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/// Configuration for Muon optimizer
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct MuonConfig {
33    /// Learning rate (default: 0.02)
34    pub learning_rate: f32,
35    /// Momentum coefficient (default: 0.95)
36    pub momentum: f32,
37    /// Newton-Schulz iteration steps (default: 5)
38    pub ns_steps: usize,
39    /// Minimum dimension for 2D optimization (default: 64)
40    pub min_dim_2d: usize,
41    /// Fallback optimizer learning rate for 1D parameters (default: 1e-3)
42    pub fallback_lr: f32,
43    /// Fallback momentum for 1D parameters (default: 0.9)
44    pub fallback_momentum: f32,
45    /// Weight decay coefficient (default: 0.0)
46    pub weight_decay: f32,
47    /// Whether to use orthogonalization (default: true)
48    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/// Muon optimizer implementation
67///
68/// Muon uses Newton-Schulz iteration for orthogonalization of 2D weight matrices,
69/// providing efficient second-order optimization. For 1D parameters, it falls back
70/// to a standard momentum-based update.
71#[derive(Debug)]
72pub struct Muon {
73    config: MuonConfig,
74    state: OptimizerState,
75    /// Momentum buffers for 2D parameters
76    momentum_2d: HashMap<String, Vec<Vec<f32>>>,
77    /// Momentum buffers for 1D parameters (AdamW-style fallback)
78    momentum_1d: HashMap<String, Vec<f32>>,
79    /// Parameter shapes for tracking 2D vs 1D
80    param_shapes: HashMap<String, (usize, usize)>,
81}
82
83impl Muon {
84    /// Create a new Muon optimizer with default configuration
85    pub fn new() -> Self {
86        Self::with_config(MuonConfig::default())
87    }
88
89    /// Create Muon with custom learning rate
90    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    /// Create Muon optimized for NanoGPT training
99    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, // Lower threshold for smaller models
105            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    /// Create Muon optimized for CIFAR-10 training
114    pub fn for_cifar10() -> Self {
115        let config = MuonConfig {
116            learning_rate: 0.03,
117            momentum: 0.9,
118            ns_steps: 4, // Fewer steps for vision tasks
119            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    /// Create Muon optimized for large language models
129    pub fn for_large_lm() -> Self {
130        let config = MuonConfig {
131            learning_rate: 0.015,
132            momentum: 0.98,  // Higher momentum for large models
133            ns_steps: 6,     // More steps for better approximation
134            min_dim_2d: 128, // Higher threshold for large models
135            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    /// Create Muon with custom configuration
144    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    /// Check if parameter should use 2D optimization
155    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    /// Newton-Schulz iteration for matrix orthogonalization
160    /// Approximates the orthogonal polar factor of a matrix
161    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        // Newton-Schulz iteration: X_{k+1} = X_k * (3I - X_k^T * X_k) / 2
170        for _ in 0..self.config.ns_steps {
171            // Compute X^T * X
172            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            // Compute 3I - X^T * X
184            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            // Compute X * (3I - X^T * X) / 2
195            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            // Update matrix
207            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    /// Update 2D parameter using Muon algorithm
216    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        // Initialize momentum if needed
225        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        // Reshape flat arrays to 2D views
237        let mut param_matrix = vec![vec![0.0; cols]; rows];
238        let mut grad_matrix = vec![vec![0.0; cols]; rows];
239
240        // Convert flat to 2D
241        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        // Apply weight decay
250        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        // Update momentum: m = momentum * m + grad
259        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        // Create update matrix (copy of momentum for orthogonalization)
266        let mut update_matrix = momentum.clone();
267
268        // Apply Newton-Schulz orthogonalization
269        self.newton_schulz_orthogonalize(&mut update_matrix);
270
271        // Apply update: param = param - lr * orthogonalized_momentum
272        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                // Convert back to flat array
277                let idx = i * cols + j;
278                param_data[idx] = param_matrix[i][j];
279            }
280        }
281
282        Ok(())
283    }
284
285    /// Update 1D parameter using fallback method (momentum SGD)
286    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        // Initialize momentum if needed
295        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        // Apply momentum SGD update
306        for i in 0..param_size {
307            let mut grad = grad_data[i];
308
309            // Apply weight decay
310            if self.config.weight_decay > 0.0 {
311                grad += self.config.weight_decay * param_data[i];
312            }
313
314            // Update momentum
315            momentum[i] = self.config.fallback_momentum * momentum[i] + grad;
316
317            // Update parameter
318            param_data[i] -= self.config.fallback_lr * momentum[i];
319        }
320
321        Ok(())
322    }
323
324    /// Get memory statistics for Muon state (deprecated - use memory_usage instead)
325    pub fn memory_stats(&self) -> StateMemoryStats {
326        self.memory_usage()
327    }
328
329    /// Get optimization statistics
330    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        // Generate unique parameter ID based on memory address
352        let param_id = format!("param_{:p}", param_data.as_ptr());
353        let param_size = param_data.len();
354
355        // Determine parameter shape
356        let (rows, cols) = if let Some(&shape) = self.param_shapes.get(&param_id) {
357            shape
358        } else {
359            // Try common factorizations for typical NN layers
360            let factors = self.find_good_factorization(param_size);
361            self.param_shapes.insert(param_id.clone(), factors);
362            factors
363        };
364
365        // Choose optimization method based on parameter shape
366        if self.should_use_2d_optimization(rows, cols) && rows * cols == param_size {
367            self.update_2d_parameter(param_data, &grad_data, &param_id, rows, cols)?;
368        } else {
369            self.update_1d_parameter(param_data, &grad_data, &param_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        // This is typically handled by the training framework
381        // No action needed here as gradients are managed externally
382    }
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    /// Find a good factorization for a given parameter size
395    fn find_good_factorization(&self, size: usize) -> (usize, usize) {
396        if size < self.config.min_dim_2d {
397            return (1, size);
398        }
399
400        // Common neural network layer sizes
401        let sqrt_size = (size as f32).sqrt() as usize;
402
403        // Try factors close to square root
404        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        // If no good factorization found, treat as 1D
424        (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        // Save step count
448        state_dict.insert(
449            "step".to_string(),
450            Tensor::new(vec![self.state.step as f32])?,
451        );
452
453        // Save 2D momentum buffers (flattened)
454        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        // Save 1D momentum buffers
463        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        // Save parameter shapes
471        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        // Load step count
483        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        // Load parameter shapes first
491        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        // Load momentum buffers
503        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        // Count 2D momentum elements
531        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        // Count 1D momentum elements
538        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        // Should use 2D for large matrices
623        assert!(optimizer.should_use_2d_optimization(128, 128));
624        assert!(optimizer.should_use_2d_optimization(64, 256));
625
626        // Should not use 2D for small matrices
627        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        // Perfect square
637        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        // Small size should be treated as 1D
643        let (rows, cols) = optimizer.find_good_factorization(10);
644        assert_eq!((rows, cols), (1, 10));
645
646        // Common NN layer size
647        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        // Initially no parameters
656        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        // Add some 2D and 1D parameters
662        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        // Add momentum buffers
677        optimizer.momentum_2d.insert("param_0".to_string(), vec![vec![0.0; 100]; 50]); // 5000 params
678        optimizer.momentum_1d.insert("param_1".to_string(), vec![0.0; 1000]); // 1000 params
679
680        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); // 4 bytes per f32
685    }
686
687    #[test]
688    fn test_state_dict_operations() {
689        let mut optimizer = Muon::new();
690        optimizer.state.step = 5;
691
692        // Add parameter shapes and momentum
693        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        // Save state
701        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        // Create new optimizer and load state
708        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}