Skip to main content

trustformers_optim/
prodigy.rs

1//! # Prodigy Optimizer
2//!
3//! Implementation of the Prodigy optimizer, a cutting-edge 2024 optimization algorithm
4//! that adaptively estimates the distance to optimality and adjusts learning rates accordingly.
5//!
6//! Prodigy often outperforms Adam and other optimizers without requiring manual learning rate tuning.
7//!
8//! ## Key Features
9//!
10//! - **Adaptive Learning Rate**: Automatically estimates optimal learning rate without manual tuning
11//! - **Distance Estimation**: Estimates distance to optimality for better convergence
12//! - **Superior Performance**: Often outperforms Adam, AdamW, and other optimizers
13//! - **No LR Scheduling**: Eliminates need for learning rate schedules
14//! - **Robust Convergence**: Stable convergence across different problem types
15//!
16//! ## Research Foundation
17//!
18//! Based on "Prodigy: An Expeditiously Adaptive Parameter-Free Learner" and related research
19//! demonstrating superior convergence properties and automatic learning rate adaptation.
20//!
21//! ## Example Usage
22//!
23//! ```rust
24//! use trustformers_optim::prodigy::{Prodigy, ProdigyConfig};
25//!
26//! // Create with default configuration (no learning rate needed!)
27//! let optimizer = Prodigy::new();
28//!
29//! // Or customize configuration
30//! let config = ProdigyConfig {
31//!     d0: 1e-6,           // Initial distance estimate
32//!     beta1: 0.9,         // Momentum coefficient
33//!     beta2: 0.999,       // Variance coefficient
34//!     eps: 1e-8,          // Numerical stability
35//!     weight_decay: 0.01, // L2 regularization
36//!     growth_rate: 1.02,  // Distance growth rate
37//!     ..Default::default()
38//! };
39//! let optimizer = Prodigy::with_config(config);
40//! ```
41
42// reason: research-stage module — reserved API/scaffolding fields and methods
43// retained intentionally for in-progress features; not yet on active call paths.
44#![allow(dead_code)]
45
46use crate::traits::StatefulOptimizer;
47use serde::{Deserialize, Serialize};
48use std::collections::HashMap;
49use trustformers_core::errors::{Result, TrustformersError};
50use trustformers_core::tensor::Tensor;
51use trustformers_core::traits::Optimizer;
52
53/// Configuration for the Prodigy optimizer.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ProdigyConfig {
56    /// Initial distance estimate (d0)
57    pub d0: f64,
58    /// Momentum coefficient for first moment (β1)
59    pub beta1: f64,
60    /// Momentum coefficient for second moment (β2)
61    pub beta2: f64,
62    /// Numerical stability constant (ε)
63    pub eps: f64,
64    /// Weight decay coefficient
65    pub weight_decay: f64,
66    /// Growth rate for distance estimation
67    pub growth_rate: f64,
68    /// Warmup steps for stability
69    pub warmup_steps: usize,
70    /// Use bias correction
71    pub bias_correction: bool,
72    /// Safeguard bound for distance estimation
73    pub safeguard_bound: f64,
74}
75
76impl Default for ProdigyConfig {
77    fn default() -> Self {
78        Self {
79            d0: 1e-6,
80            beta1: 0.9,
81            beta2: 0.999,
82            eps: 1e-8,
83            weight_decay: 0.0,
84            growth_rate: 1.02,
85            warmup_steps: 0,
86            bias_correction: true,
87            safeguard_bound: 2.0,
88        }
89    }
90}
91
92impl ProdigyConfig {
93    /// Configuration optimized for language model training.
94    pub fn for_language_models() -> Self {
95        Self {
96            d0: 1e-6,
97            beta1: 0.9,
98            beta2: 0.999,
99            eps: 1e-8,
100            weight_decay: 0.1,
101            growth_rate: 1.02,
102            warmup_steps: 1000,
103            bias_correction: true,
104            safeguard_bound: 2.0,
105        }
106    }
107
108    /// Configuration optimized for computer vision tasks.
109    pub fn for_vision() -> Self {
110        Self {
111            d0: 1e-6,
112            beta1: 0.9,
113            beta2: 0.999,
114            eps: 1e-8,
115            weight_decay: 0.05,
116            growth_rate: 1.01,
117            warmup_steps: 100,
118            bias_correction: true,
119            safeguard_bound: 1.5,
120        }
121    }
122
123    /// Configuration for fast training with aggressive adaptation.
124    pub fn for_fast_training() -> Self {
125        Self {
126            d0: 1e-5,
127            beta1: 0.9,
128            beta2: 0.99,
129            eps: 1e-8,
130            weight_decay: 0.01,
131            growth_rate: 1.05,
132            warmup_steps: 10,
133            bias_correction: false,
134            safeguard_bound: 3.0,
135        }
136    }
137
138    /// Configuration for stable, conservative training.
139    pub fn for_stable_training() -> Self {
140        Self {
141            d0: 1e-7,
142            beta1: 0.95,
143            beta2: 0.9999,
144            eps: 1e-8,
145            weight_decay: 0.001,
146            growth_rate: 1.005,
147            warmup_steps: 2000,
148            bias_correction: true,
149            safeguard_bound: 1.2,
150        }
151    }
152}
153
154/// Optimizer state for individual parameters.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ProdigyParameterState {
157    /// First moment estimate (momentum)
158    pub momentum: Vec<f32>,
159    /// Second moment estimate (variance)
160    pub variance: Vec<f32>,
161    /// Current distance estimate
162    pub distance: f64,
163    /// Step count for this parameter
164    pub step: usize,
165}
166
167impl ProdigyParameterState {
168    pub fn new(param_size: usize, initial_distance: f64) -> Self {
169        Self {
170            momentum: vec![0.0; param_size],
171            variance: vec![0.0; param_size],
172            distance: initial_distance,
173            step: 0,
174        }
175    }
176
177    /// Get memory usage statistics for this parameter state.
178    pub fn memory_usage(&self) -> ProdigyMemoryStats {
179        let momentum_bytes = self.momentum.len() * std::mem::size_of::<f32>();
180        let variance_bytes = self.variance.len() * std::mem::size_of::<f32>();
181        let metadata_bytes = std::mem::size_of::<f64>() + std::mem::size_of::<usize>();
182
183        ProdigyMemoryStats {
184            momentum_bytes,
185            variance_bytes,
186            metadata_bytes,
187            total_bytes: momentum_bytes + variance_bytes + metadata_bytes,
188        }
189    }
190}
191
192/// Memory usage statistics for Prodigy optimizer.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ProdigyMemoryStats {
195    pub momentum_bytes: usize,
196    pub variance_bytes: usize,
197    pub metadata_bytes: usize,
198    pub total_bytes: usize,
199}
200
201/// Global optimizer state containing all parameters.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ProdigyOptimizerState {
204    /// Per-parameter states
205    pub parameters: HashMap<String, ProdigyParameterState>,
206    /// Global step count
207    pub global_step: usize,
208    /// Global distance estimate
209    pub global_distance: f64,
210    /// Distance growth history for adaptive adjustment
211    pub distance_history: Vec<f64>,
212}
213
214impl Default for ProdigyOptimizerState {
215    fn default() -> Self {
216        Self {
217            parameters: HashMap::new(),
218            global_step: 0,
219            global_distance: 1e-6,
220            distance_history: Vec::new(),
221        }
222    }
223}
224
225impl ProdigyOptimizerState {
226    /// Clear all optimizer state.
227    pub fn clear(&mut self) {
228        self.parameters.clear();
229        self.global_step = 0;
230        self.global_distance = 1e-6;
231        self.distance_history.clear();
232    }
233
234    /// Get total memory usage across all parameters.
235    pub fn total_memory_usage(&self) -> ProdigyMemoryStats {
236        let mut total_momentum = 0;
237        let mut total_variance = 0;
238        let mut total_metadata = 0;
239
240        for param_state in self.parameters.values() {
241            let stats = param_state.memory_usage();
242            total_momentum += stats.momentum_bytes;
243            total_variance += stats.variance_bytes;
244            total_metadata += stats.metadata_bytes;
245        }
246
247        // Add global state memory
248        total_metadata += std::mem::size_of::<usize>()
249            + std::mem::size_of::<f64>()
250            + self.distance_history.len() * std::mem::size_of::<f64>();
251
252        ProdigyMemoryStats {
253            momentum_bytes: total_momentum,
254            variance_bytes: total_variance,
255            metadata_bytes: total_metadata,
256            total_bytes: total_momentum + total_variance + total_metadata,
257        }
258    }
259}
260
261/// Prodigy optimizer with adaptive learning rate estimation.
262pub struct Prodigy {
263    config: ProdigyConfig,
264    state: ProdigyOptimizerState,
265}
266
267impl Prodigy {
268    /// Create a new Prodigy optimizer with default configuration.
269    pub fn new() -> Self {
270        Self {
271            config: ProdigyConfig::default(),
272            state: ProdigyOptimizerState::default(),
273        }
274    }
275
276    /// Create Prodigy optimizer with custom configuration.
277    pub fn with_config(config: ProdigyConfig) -> Self {
278        let state = ProdigyOptimizerState {
279            global_distance: config.d0,
280            ..Default::default()
281        };
282
283        Self { config, state }
284    }
285
286    /// Create Prodigy optimizer optimized for language models.
287    pub fn for_language_models() -> Self {
288        Self::with_config(ProdigyConfig::for_language_models())
289    }
290
291    /// Create Prodigy optimizer optimized for computer vision.
292    pub fn for_vision() -> Self {
293        Self::with_config(ProdigyConfig::for_vision())
294    }
295
296    /// Create Prodigy optimizer for fast training.
297    pub fn for_fast_training() -> Self {
298        Self::with_config(ProdigyConfig::for_fast_training())
299    }
300
301    /// Create Prodigy optimizer for stable training.
302    pub fn for_stable_training() -> Self {
303        Self::with_config(ProdigyConfig::for_stable_training())
304    }
305
306    /// Get current global learning rate estimate.
307    pub fn get_lr(&self) -> f64 {
308        self.state.global_distance
309    }
310
311    /// Set global distance estimate (equivalent to learning rate).
312    pub fn set_lr(&mut self, distance: f64) {
313        self.state.global_distance = distance.max(1e-10);
314    }
315
316    /// Reset optimizer state.
317    pub fn reset(&mut self) {
318        self.state.clear();
319        self.state.global_distance = self.config.d0;
320    }
321
322    /// Get memory usage statistics.
323    pub fn memory_usage(&self) -> ProdigyMemoryStats {
324        self.state.total_memory_usage()
325    }
326
327    /// Update distance estimate based on gradient and parameter norms.
328    fn update_distance_estimate(&mut self, grad_norm: f64, param_norm: f64) {
329        if grad_norm > 0.0 && param_norm > 0.0 {
330            // Estimate distance to optimality using gradient and parameter norms
331            let distance_estimate = (param_norm / grad_norm).min(self.config.safeguard_bound);
332
333            // Apply exponential moving average for stability
334            let alpha = 0.01; // Smoothing factor
335            self.state.global_distance = (1.0 - alpha) * self.state.global_distance
336                + alpha * distance_estimate * self.config.growth_rate;
337
338            // Keep history for adaptive adjustment
339            self.state.distance_history.push(self.state.global_distance);
340            if self.state.distance_history.len() > 100 {
341                self.state.distance_history.remove(0);
342            }
343        }
344    }
345
346    /// Compute bias correction factors.
347    fn bias_correction(&self, step: usize) -> (f64, f64) {
348        if self.config.bias_correction && step > 0 {
349            let beta1_correction = 1.0 - self.config.beta1.powi(step as i32);
350            let beta2_correction = 1.0 - self.config.beta2.powi(step as i32);
351            (beta1_correction, beta2_correction)
352        } else {
353            (1.0, 1.0)
354        }
355    }
356
357    /// Apply warmup scaling to learning rate.
358    fn warmup_scaling(&self, step: usize) -> f64 {
359        if self.config.warmup_steps > 0 && step < self.config.warmup_steps {
360            (step as f64 + 1.0) / (self.config.warmup_steps as f64)
361        } else {
362            1.0
363        }
364    }
365
366    /// Updates a named parameter with its gradient.
367    pub fn update_parameter(
368        &mut self,
369        param_name: &str,
370        param: &mut Tensor,
371        grad: &Tensor,
372    ) -> Result<()> {
373        let mut param_data = param.data().map_err(|e| {
374            TrustformersError::tensor_op_error(
375                &format!("Failed to get parameter data: {}", e),
376                "prodigy_update",
377            )
378        })?;
379        let grad_data = grad.data().map_err(|e| {
380            TrustformersError::tensor_op_error(
381                &format!("Failed to get gradient data: {}", e),
382                "prodigy_update",
383            )
384        })?;
385
386        if param_data.len() != grad_data.len() {
387            return Err(TrustformersError::tensor_op_error(
388                "Parameter and gradient size mismatch",
389                "prodigy_update",
390            ));
391        }
392
393        // Get or create parameter state
394        let param_size = param_data.len();
395
396        // Compute gradient and parameter norms for distance estimation
397        let grad_norm: f64 = grad_data.iter().map(|&g| (g as f64).powi(2)).sum::<f64>().sqrt();
398        let param_norm: f64 = param_data.iter().map(|&p| (p as f64).powi(2)).sum::<f64>().sqrt();
399
400        // Update global distance estimate first (before borrowing param_state)
401        self.update_distance_estimate(grad_norm, param_norm);
402
403        // Now get or create parameter state
404        let param_state = self
405            .state
406            .parameters
407            .entry(param_name.to_string())
408            .or_insert_with(|| ProdigyParameterState::new(param_size, self.config.d0));
409
410        // Resize state if needed
411        if param_state.momentum.len() != param_size {
412            param_state.momentum.resize(param_size, 0.0);
413            param_state.variance.resize(param_size, 0.0);
414        }
415
416        param_state.step += 1;
417        let current_step = param_state.step;
418
419        // Apply warmup scaling (using local variable to avoid borrow issues)
420        let warmup_scale =
421            if self.config.warmup_steps > 0 && current_step < self.config.warmup_steps {
422                (current_step as f64 + 1.0) / (self.config.warmup_steps as f64)
423            } else {
424                1.0
425            };
426        let effective_distance = self.state.global_distance * warmup_scale;
427
428        // Bias correction (using local variables to avoid borrow issues)
429        let (beta1_correction, beta2_correction) =
430            if self.config.bias_correction && current_step > 0 {
431                let beta1_correction = 1.0 - self.config.beta1.powi(current_step as i32);
432                let beta2_correction = 1.0 - self.config.beta2.powi(current_step as i32);
433                (beta1_correction, beta2_correction)
434            } else {
435                (1.0, 1.0)
436            };
437
438        // Update momentum and variance
439        for i in 0..param_size {
440            let grad_val = grad_data[i] as f64;
441
442            // Apply weight decay to gradient if specified
443            let grad_with_decay = if self.config.weight_decay > 0.0 {
444                grad_val + self.config.weight_decay * (param_data[i] as f64)
445            } else {
446                grad_val
447            };
448
449            // Update biased first moment estimate
450            param_state.momentum[i] = (self.config.beta1 * param_state.momentum[i] as f64
451                + (1.0 - self.config.beta1) * grad_with_decay)
452                as f32;
453
454            // Update biased second moment estimate
455            param_state.variance[i] = (self.config.beta2 * param_state.variance[i] as f64
456                + (1.0 - self.config.beta2) * grad_with_decay.powi(2))
457                as f32;
458
459            // Bias-corrected moments
460            let m_hat = param_state.momentum[i] as f64 / beta1_correction;
461            let v_hat = param_state.variance[i] as f64 / beta2_correction;
462
463            // Compute parameter update using adaptive distance
464            let denominator = v_hat.sqrt() + self.config.eps;
465            let update = effective_distance * m_hat / denominator;
466
467            // Apply update
468            param_data[i] = (param_data[i] as f64 - update) as f32;
469        }
470
471        // Update parameter tensor with new data
472        *param = Tensor::new(param_data)?;
473
474        Ok(())
475    }
476}
477
478impl Default for Prodigy {
479    fn default() -> Self {
480        Self::new()
481    }
482}
483
484impl Optimizer for Prodigy {
485    fn step(&mut self) {
486        self.state.global_step += 1;
487    }
488
489    fn zero_grad(&mut self) {
490        // Prodigy doesn't need to explicitly zero gradients
491        // as it processes them immediately during update
492    }
493
494    fn update(&mut self, param: &mut Tensor, grad: &Tensor) -> Result<()> {
495        // Use a default parameter name for the core update
496        self.update_parameter("default", param, grad)
497    }
498
499    fn get_lr(&self) -> f32 {
500        self.state.global_distance as f32
501    }
502
503    fn set_lr(&mut self, lr: f32) {
504        self.state.global_distance = (lr as f64).max(1e-10);
505    }
506}
507
508impl StatefulOptimizer for Prodigy {
509    type Config = ProdigyConfig;
510    type State = ProdigyOptimizerState;
511
512    fn config(&self) -> &Self::Config {
513        &self.config
514    }
515
516    fn state(&self) -> &Self::State {
517        &self.state
518    }
519
520    fn state_mut(&mut self) -> &mut Self::State {
521        &mut self.state
522    }
523
524    fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
525        let mut state_dict = HashMap::new();
526
527        // Save configuration as tensors
528        state_dict.insert("lr".to_string(), Tensor::new(vec![self.config.d0 as f32])?);
529        state_dict.insert(
530            "beta1".to_string(),
531            Tensor::new(vec![self.config.beta1 as f32])?,
532        );
533        state_dict.insert(
534            "beta2".to_string(),
535            Tensor::new(vec![self.config.beta2 as f32])?,
536        );
537        state_dict.insert(
538            "eps".to_string(),
539            Tensor::new(vec![self.config.eps as f32])?,
540        );
541        state_dict.insert(
542            "weight_decay".to_string(),
543            Tensor::new(vec![self.config.weight_decay as f32])?,
544        );
545        state_dict.insert(
546            "growth_rate".to_string(),
547            Tensor::new(vec![self.config.growth_rate as f32])?,
548        );
549        state_dict.insert(
550            "warmup_steps".to_string(),
551            Tensor::new(vec![self.config.warmup_steps as f32])?,
552        );
553        state_dict.insert(
554            "global_step".to_string(),
555            Tensor::new(vec![self.state.global_step as f32])?,
556        );
557        state_dict.insert(
558            "global_distance".to_string(),
559            Tensor::new(vec![self.state.global_distance as f32])?,
560        );
561
562        // Save parameter states
563        for (param_name, param_state) in &self.state.parameters {
564            state_dict.insert(
565                format!("momentum_{}", param_name),
566                Tensor::new(param_state.momentum.clone())?,
567            );
568            state_dict.insert(
569                format!("variance_{}", param_name),
570                Tensor::new(param_state.variance.clone())?,
571            );
572            state_dict.insert(
573                format!("distance_{}", param_name),
574                Tensor::new(vec![param_state.distance as f32])?,
575            );
576            state_dict.insert(
577                format!("step_{}", param_name),
578                Tensor::new(vec![param_state.step as f32])?,
579            );
580        }
581
582        Ok(state_dict)
583    }
584
585    fn load_state_dict(&mut self, state_dict: HashMap<String, Tensor>) -> Result<()> {
586        // Load configuration
587        if let Some(lr_tensor) = state_dict.get("lr") {
588            if let Ok(lr_vec) = lr_tensor.data() {
589                if !lr_vec.is_empty() {
590                    self.config.d0 = lr_vec[0] as f64;
591                }
592            }
593        }
594        if let Some(beta1_tensor) = state_dict.get("beta1") {
595            if let Ok(beta1_vec) = beta1_tensor.data() {
596                if !beta1_vec.is_empty() {
597                    self.config.beta1 = beta1_vec[0] as f64;
598                }
599            }
600        }
601        if let Some(beta2_tensor) = state_dict.get("beta2") {
602            if let Ok(beta2_vec) = beta2_tensor.data() {
603                if !beta2_vec.is_empty() {
604                    self.config.beta2 = beta2_vec[0] as f64;
605                }
606            }
607        }
608
609        // Load global state
610        if let Some(global_step_tensor) = state_dict.get("global_step") {
611            if let Ok(global_step_vec) = global_step_tensor.data() {
612                if !global_step_vec.is_empty() {
613                    self.state.global_step = global_step_vec[0] as usize;
614                }
615            }
616        }
617        if let Some(global_distance_tensor) = state_dict.get("global_distance") {
618            if let Ok(global_distance_vec) = global_distance_tensor.data() {
619                if !global_distance_vec.is_empty() {
620                    self.state.global_distance = global_distance_vec[0] as f64;
621                }
622            }
623        }
624
625        // Load parameter states (simplified for now)
626        // In a full implementation, we'd reconstruct all parameter states
627        // This would require iterating through the state_dict to find matching patterns
628
629        Ok(())
630    }
631
632    fn memory_usage(&self) -> crate::common::StateMemoryStats {
633        let total_momentum_elements: usize =
634            self.state.parameters.values().map(|p| p.momentum.len()).sum();
635        let total_variance_elements: usize =
636            self.state.parameters.values().map(|p| p.variance.len()).sum();
637
638        let momentum_bytes = total_momentum_elements * std::mem::size_of::<f32>();
639        let variance_bytes = total_variance_elements * std::mem::size_of::<f32>();
640        let metadata_bytes = self.state.parameters.len()
641            * (std::mem::size_of::<f64>() + std::mem::size_of::<usize>());
642
643        crate::common::StateMemoryStats {
644            momentum_elements: total_momentum_elements,
645            variance_elements: total_variance_elements,
646            third_moment_elements: 0,
647            total_bytes: momentum_bytes + variance_bytes + metadata_bytes,
648            num_parameters: self.state.parameters.len(),
649        }
650    }
651
652    fn reset_state(&mut self) {
653        self.reset();
654    }
655
656    fn num_parameters(&self) -> usize {
657        self.state.parameters.len()
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    #[test]
666    fn test_prodigy_creation() {
667        let optimizer = Prodigy::new();
668        assert_eq!(optimizer.config.d0, 1e-6);
669        assert_eq!(optimizer.config.beta1, 0.9);
670        assert_eq!(optimizer.config.beta2, 0.999);
671    }
672
673    #[test]
674    fn test_prodigy_with_config() {
675        let config = ProdigyConfig {
676            d0: 1e-5,
677            beta1: 0.95,
678            beta2: 0.99,
679            weight_decay: 0.1,
680            ..Default::default()
681        };
682        let optimizer = Prodigy::with_config(config.clone());
683        assert_eq!(optimizer.config.d0, config.d0);
684        assert_eq!(optimizer.config.beta1, config.beta1);
685        assert_eq!(optimizer.config.weight_decay, config.weight_decay);
686    }
687
688    #[test]
689    fn test_prodigy_presets() {
690        let lm_optimizer = Prodigy::for_language_models();
691        assert_eq!(lm_optimizer.config.warmup_steps, 1000);
692        assert_eq!(lm_optimizer.config.weight_decay, 0.1);
693
694        let vision_optimizer = Prodigy::for_vision();
695        assert_eq!(vision_optimizer.config.warmup_steps, 100);
696        assert_eq!(vision_optimizer.config.weight_decay, 0.05);
697
698        let fast_optimizer = Prodigy::for_fast_training();
699        assert_eq!(fast_optimizer.config.growth_rate, 1.05);
700        assert!(!fast_optimizer.config.bias_correction);
701
702        let stable_optimizer = Prodigy::for_stable_training();
703        assert_eq!(stable_optimizer.config.warmup_steps, 2000);
704        assert_eq!(stable_optimizer.config.safeguard_bound, 1.2);
705    }
706
707    #[test]
708    fn test_lr_getter_setter() {
709        let mut optimizer = Prodigy::new();
710        let initial_lr = optimizer.get_lr();
711        assert_eq!(initial_lr, 1e-6);
712
713        optimizer.set_lr(0.001);
714        assert_eq!(optimizer.get_lr(), 0.001);
715
716        // Test minimum bound
717        optimizer.set_lr(-1.0);
718        assert!(optimizer.get_lr() >= 1e-10);
719    }
720
721    #[test]
722    fn test_parameter_state_creation() {
723        let param_state = ProdigyParameterState::new(100, 1e-6);
724        assert_eq!(param_state.momentum.len(), 100);
725        assert_eq!(param_state.variance.len(), 100);
726        assert_eq!(param_state.distance, 1e-6);
727        assert_eq!(param_state.step, 0);
728        assert!(param_state.momentum.iter().all(|&x| x == 0.0));
729        assert!(param_state.variance.iter().all(|&x| x == 0.0));
730    }
731
732    #[test]
733    fn test_memory_usage_tracking() {
734        let param_state = ProdigyParameterState::new(1000, 1e-6);
735        let memory_stats = param_state.memory_usage();
736
737        assert_eq!(memory_stats.momentum_bytes, 1000 * 4); // f32 = 4 bytes
738        assert_eq!(memory_stats.variance_bytes, 1000 * 4);
739        assert!(memory_stats.metadata_bytes > 0);
740        assert_eq!(
741            memory_stats.total_bytes,
742            memory_stats.momentum_bytes + memory_stats.variance_bytes + memory_stats.metadata_bytes
743        );
744    }
745
746    #[test]
747    fn test_optimizer_state_operations() {
748        let mut state = ProdigyOptimizerState::default();
749        state
750            .parameters
751            .insert("param1".to_string(), ProdigyParameterState::new(100, 1e-6));
752        state
753            .parameters
754            .insert("param2".to_string(), ProdigyParameterState::new(200, 1e-6));
755        state.global_step = 10;
756
757        let memory_stats = state.total_memory_usage();
758        assert!(memory_stats.total_bytes > 0);
759        assert_eq!(memory_stats.momentum_bytes, (100 + 200) * 4);
760
761        state.clear();
762        assert_eq!(state.parameters.len(), 0);
763        assert_eq!(state.global_step, 0);
764        assert_eq!(state.global_distance, 1e-6);
765    }
766
767    #[test]
768    fn test_reset() {
769        let mut optimizer = Prodigy::new();
770        optimizer.state.global_step = 100;
771        optimizer
772            .state
773            .parameters
774            .insert("test".to_string(), ProdigyParameterState::new(10, 1e-6));
775
776        optimizer.reset();
777        assert_eq!(optimizer.state.global_step, 0);
778        assert_eq!(optimizer.state.parameters.len(), 0);
779        assert_eq!(optimizer.state.global_distance, optimizer.config.d0);
780    }
781
782    #[test]
783    fn test_config_serialization() {
784        let config = ProdigyConfig::for_language_models();
785        let serialized = serde_json::to_string(&config).expect("Serialization failed");
786        let deserialized: ProdigyConfig =
787            serde_json::from_str(&serialized).expect("Deserialization failed");
788
789        assert_eq!(config.d0, deserialized.d0);
790        assert_eq!(config.beta1, deserialized.beta1);
791        assert_eq!(config.warmup_steps, deserialized.warmup_steps);
792    }
793
794    #[test]
795    fn test_state_dict_operations() {
796        let mut optimizer = Prodigy::for_vision();
797        optimizer.state.global_step = 50;
798        optimizer.state.parameters.insert(
799            "test_param".to_string(),
800            ProdigyParameterState::new(5, 1e-5),
801        );
802
803        // Save state dict
804        let state_dict = optimizer.state_dict().expect("Failed to get state dict");
805        assert!(state_dict.contains_key("lr"));
806        assert!(state_dict.contains_key("global_step"));
807
808        // Create new optimizer and load state
809        let mut new_optimizer = Prodigy::new();
810        new_optimizer.load_state_dict(state_dict).expect("Failed to load state dict");
811
812        assert_eq!(new_optimizer.state.global_step, 50);
813        // Note: parameter states are not fully implemented in load_state_dict yet
814        // This test validates that basic config and global state are loaded correctly
815    }
816
817    #[test]
818    fn test_step_and_zero_grad() {
819        let mut optimizer = Prodigy::new();
820        assert_eq!(optimizer.state.global_step, 0);
821
822        optimizer.step();
823        assert_eq!(optimizer.state.global_step, 1);
824
825        optimizer.zero_grad(); // Should not error
826    }
827
828    #[test]
829    fn test_stateful_optimizer_trait() {
830        let optimizer = Prodigy::for_fast_training();
831
832        // Test config access
833        let config = optimizer.config();
834        assert_eq!(config.growth_rate, 1.05);
835
836        // Test state access
837        let state = optimizer.state();
838        assert_eq!(state.global_step, 0);
839    }
840
841    #[test]
842    fn test_distance_estimation_bounds() {
843        let mut optimizer = Prodigy::with_config(ProdigyConfig {
844            safeguard_bound: 2.0,
845            ..Default::default()
846        });
847
848        // Test that distance estimation respects safeguard bounds
849        optimizer.update_distance_estimate(1.0, 10.0); // Would give 10.0 without bound
850        assert!(optimizer.get_lr() <= 2.0);
851    }
852
853    #[test]
854    fn test_bias_correction() {
855        let optimizer = Prodigy::new();
856
857        // With bias correction enabled
858        let (bc1, bc2) = optimizer.bias_correction(1);
859        assert!(bc1 > 0.0 && bc1 < 1.0);
860        assert!(bc2 > 0.0 && bc2 < 1.0);
861
862        // After many steps, bias correction should be positive and less than 1.0
863        let (bc1_late, bc2_late) = optimizer.bias_correction(1000);
864        assert!(bc1_late > 0.9);
865        assert!(bc2_late > 0.6); // 1.0 - 0.999^1000 ≈ 0.63
866    }
867
868    #[test]
869    fn test_warmup_scaling() {
870        let optimizer = Prodigy::with_config(ProdigyConfig {
871            warmup_steps: 100,
872            ..Default::default()
873        });
874
875        // During warmup
876        let scale_early = optimizer.warmup_scaling(10);
877        assert!(scale_early < 1.0);
878        assert_eq!(scale_early, 11.0 / 100.0);
879
880        // After warmup
881        let scale_late = optimizer.warmup_scaling(200);
882        assert_eq!(scale_late, 1.0);
883    }
884}