Skip to main content

trustformers_optim/
advanced_features.rs

1//! Advanced Optimizer Features
2//!
3//! This module implements advanced optimization techniques including optimizer fusion,
4//! multi-optimizer training, warm-up strategies, and checkpointing optimizations.
5
6use crate::LRScheduler;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::{Arc, Mutex};
10use trustformers_core::errors::{Result, TrustformersError};
11use trustformers_core::traits::Optimizer;
12use trustformers_core::Tensor;
13
14/// Configuration for optimizer fusion
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct FusionConfig {
17    /// Whether to enable parameter fusion
18    pub fuse_parameters: bool,
19    /// Whether to enable gradient fusion
20    pub fuse_gradients: bool,
21    /// Whether to enable state fusion
22    pub fuse_state: bool,
23    /// Fusion window size
24    pub window_size: usize,
25    /// Memory threshold for fusion (in bytes)
26    pub memory_threshold: usize,
27}
28
29impl Default for FusionConfig {
30    fn default() -> Self {
31        Self {
32            fuse_parameters: true,
33            fuse_gradients: true,
34            fuse_state: true,
35            window_size: 32,
36            memory_threshold: 1024 * 1024 * 100, // 100MB
37        }
38    }
39}
40
41/// Fused optimizer that combines multiple optimizers for efficiency
42pub struct FusedOptimizer {
43    optimizers: Vec<Box<dyn Optimizer>>,
44    config: FusionConfig,
45    fused_parameters: Arc<Mutex<HashMap<String, Tensor>>>,
46    fused_gradients: Arc<Mutex<HashMap<String, Tensor>>>,
47    fusion_groups: Vec<Vec<usize>>, // Groups of optimizer indices that can be fused
48}
49
50impl FusedOptimizer {
51    /// Create a new fused optimizer
52    pub fn new(optimizers: Vec<Box<dyn Optimizer>>, config: FusionConfig) -> Result<Self> {
53        let fusion_groups = Self::compute_fusion_groups(&optimizers, &config);
54
55        Ok(Self {
56            optimizers,
57            config,
58            fused_parameters: Arc::new(Mutex::new(HashMap::new())),
59            fused_gradients: Arc::new(Mutex::new(HashMap::new())),
60            fusion_groups,
61        })
62    }
63
64    /// Compute fusion groups based on optimizer compatibility
65    fn compute_fusion_groups(
66        optimizers: &[Box<dyn Optimizer>],
67        config: &FusionConfig,
68    ) -> Vec<Vec<usize>> {
69        let mut groups = Vec::new();
70        let mut used = vec![false; optimizers.len()];
71
72        for i in 0..optimizers.len() {
73            if used[i] {
74                continue;
75            }
76
77            let mut group = vec![i];
78            used[i] = true;
79
80            // Find compatible optimizers to fuse with
81            for j in (i + 1)..optimizers.len() {
82                if used[j] {
83                    continue;
84                }
85
86                if Self::can_fuse(&optimizers[i], &optimizers[j], config) {
87                    group.push(j);
88                    used[j] = true;
89                }
90            }
91
92            groups.push(group);
93        }
94
95        groups
96    }
97
98    /// Check if two optimizers can be fused
99    fn can_fuse(
100        _opt1: &Box<dyn Optimizer>,
101        _opt2: &Box<dyn Optimizer>,
102        _config: &FusionConfig,
103    ) -> bool {
104        // Simple heuristic: for now, assume all optimizers can be fused
105        // In a real implementation, we would check optimizer types and configurations
106        true
107    }
108
109    /// Fuse parameters across optimizer groups
110    fn fuse_parameters(&self, parameters: &mut HashMap<String, Tensor>) -> Result<()> {
111        if !self.config.fuse_parameters {
112            return Ok(());
113        }
114
115        let mut fused_params = self.fused_parameters.lock().map_err(|_| {
116            TrustformersError::tensor_op_error("Failed to lock fused parameters", "fuse_parameters")
117        })?;
118        fused_params.clear();
119
120        // Group parameters by fusion groups
121        for group in &self.fusion_groups {
122            if group.len() > 1 {
123                // Create fused parameter tensor for this group
124                let group_params: Vec<_> = parameters
125                    .iter()
126                    .filter(|(name, _)| {
127                        // Check if parameter belongs to this group (simplified)
128                        group.iter().any(|&i| name.contains(&format!("opt_{}", i)))
129                    })
130                    .collect();
131
132                if !group_params.is_empty() {
133                    // Concatenate parameters
134                    let fused_name = format!("fused_group_{}", group[0]);
135                    let fused_tensor = self.concatenate_tensors(
136                        &group_params.iter().map(|(_, t)| *t).collect::<Vec<_>>(),
137                    )?;
138                    fused_params.insert(fused_name, fused_tensor);
139                }
140            }
141        }
142
143        Ok(())
144    }
145
146    /// Concatenate tensors for fusion
147    fn concatenate_tensors(&self, tensors: &[&Tensor]) -> Result<Tensor> {
148        if tensors.is_empty() {
149            return Err(TrustformersError::invalid_argument(
150                "Empty tensor list".to_string(),
151            ));
152        }
153
154        // Flatten all tensors and concatenate
155        let mut total_size = 0;
156        for tensor in tensors {
157            total_size += tensor.len();
158        }
159
160        // Create concatenated tensor (simplified implementation)
161        Tensor::zeros(&[total_size])
162    }
163
164    /// Perform fused optimization step
165    pub fn fused_step(&mut self, parameters: &mut HashMap<String, Tensor>) -> Result<()> {
166        // Fuse parameters
167        self.fuse_parameters(parameters)?;
168
169        // Apply optimization steps to fused groups
170        let fusion_groups = self.fusion_groups.clone();
171        for group in &fusion_groups {
172            if group.len() > 1 {
173                // Apply fused optimization
174                self.apply_fused_group_optimization(group)?;
175            } else {
176                // Apply single optimizer
177                let optimizer_idx = group[0];
178                // Apply optimization for single optimizer (simplified)
179                for (name, param) in parameters.iter_mut() {
180                    if let Some(grad) = self.get_gradient_for_param(name) {
181                        self.optimizers[optimizer_idx].update(param, &grad)?;
182                    }
183                }
184            }
185        }
186
187        Ok(())
188    }
189
190    /// Apply optimization to a fused group
191    fn apply_fused_group_optimization(&mut self, group: &[usize]) -> Result<()> {
192        // Use the first optimizer in the group as the representative
193        let primary_optimizer_idx = group[0];
194
195        let mut fused_params = self.fused_parameters.lock().map_err(|_| {
196            TrustformersError::tensor_op_error(
197                "Failed to lock fused parameters",
198                "apply_fused_group_optimization",
199            )
200        })?;
201        let fused_gradients = self.fused_gradients.lock().map_err(|_| {
202            TrustformersError::tensor_op_error(
203                "Failed to lock fused gradients",
204                "apply_fused_group_optimization",
205            )
206        })?;
207
208        let group_name = format!("fused_group_{}", primary_optimizer_idx);
209
210        if let (Some(param), Some(grad)) = (
211            fused_params.get_mut(&group_name),
212            fused_gradients.get(&group_name),
213        ) {
214            self.optimizers[primary_optimizer_idx].update(param, grad)?;
215        }
216
217        Ok(())
218    }
219
220    /// Get gradient for parameter
221    ///
222    /// Integrates with the automatic differentiation system to retrieve
223    /// accumulated gradients for the specified parameter.
224    fn get_gradient_for_param(&self, param_name: &str) -> Option<Tensor> {
225        // Check fused gradients first (higher priority)
226        {
227            let fused_gradients = self.fused_gradients.lock().ok()?;
228            if let Some(gradient) = fused_gradients.get(param_name) {
229                return Some(gradient.clone());
230            }
231        }
232
233        // Check individual optimizer gradients by parameter name
234        // Parameter names are typically formatted as "optimizer_{idx}_{param_name}"
235        for (idx, _optimizer) in self.optimizers.iter().enumerate() {
236            let full_param_name = format!("optimizer_{}_{}", idx, param_name);
237
238            // Try to get gradient from fused gradients with full name
239            let fused_gradients = self.fused_gradients.lock().ok()?;
240            if let Some(gradient) = fused_gradients.get(&full_param_name) {
241                return Some(gradient.clone());
242            }
243            drop(fused_gradients);
244
245            // For individual parameters, we would need access to the parameter
246            // registry maintained by the automatic differentiation system.
247            // This is typically maintained at the model level rather than optimizer level.
248        }
249
250        // Return None if gradient not found in any registry
251        None
252    }
253
254    /// Register gradient for parameter in the fused gradient registry
255    ///
256    /// This method allows external automatic differentiation systems to register
257    /// computed gradients with the fused optimizer for parameter updates.
258    pub fn register_gradient(&self, param_name: &str, gradient: Tensor) -> Result<()> {
259        let mut fused_gradients = self.fused_gradients.lock().map_err(|_| {
260            TrustformersError::tensor_op_error(
261                "Failed to lock fused gradients",
262                "register_gradient",
263            )
264        })?;
265
266        fused_gradients.insert(param_name.to_string(), gradient);
267        Ok(())
268    }
269
270    /// Clear all registered gradients
271    ///
272    /// This should be called after each optimization step to clear accumulated gradients.
273    pub fn clear_gradients(&self) -> Result<()> {
274        let mut fused_gradients = self.fused_gradients.lock().map_err(|_| {
275            TrustformersError::tensor_op_error("Failed to lock fused gradients", "clear_gradients")
276        })?;
277
278        fused_gradients.clear();
279        Ok(())
280    }
281
282    /// Get all available gradient parameter names
283    ///
284    /// Returns a list of parameter names for which gradients are currently available.
285    pub fn get_available_gradient_names(&self) -> Result<Vec<String>> {
286        let fused_gradients = self.fused_gradients.lock().map_err(|_| {
287            TrustformersError::tensor_op_error(
288                "Failed to lock fused gradients",
289                "get_available_gradient_names",
290            )
291        })?;
292
293        Ok(fused_gradients.keys().cloned().collect())
294    }
295
296    /// Get fusion statistics
297    pub fn get_fusion_stats(&self) -> FusionStats {
298        let total_optimizers = self.optimizers.len();
299        let fused_groups = self.fusion_groups.iter().filter(|group| group.len() > 1).count();
300        let unfused_optimizers = self.fusion_groups.iter().filter(|group| group.len() == 1).count();
301
302        FusionStats {
303            total_optimizers,
304            fused_groups,
305            unfused_optimizers,
306            fusion_ratio: fused_groups as f64 / total_optimizers as f64,
307            memory_saved: self.estimate_memory_savings(),
308        }
309    }
310
311    /// Estimate memory savings from fusion
312    fn estimate_memory_savings(&self) -> usize {
313        let fused_params =
314            self.fused_parameters.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
315        let total_fused_size: usize = fused_params.values()
316            .map(|t| t.len() * 4) // Assuming f32 tensors
317            .sum();
318
319        // Estimate original size
320        let estimated_original_size = total_fused_size * 2; // Conservative estimate
321
322        estimated_original_size.saturating_sub(total_fused_size)
323    }
324}
325
326/// Statistics for optimizer fusion
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct FusionStats {
329    pub total_optimizers: usize,
330    pub fused_groups: usize,
331    pub unfused_optimizers: usize,
332    pub fusion_ratio: f64,
333    pub memory_saved: usize,
334}
335
336/// Multi-optimizer training system
337pub struct MultiOptimizerTrainer {
338    optimizers: HashMap<String, Box<dyn Optimizer>>,
339    parameter_assignments: HashMap<String, String>, // param_name -> optimizer_name
340    schedulers: HashMap<String, Box<dyn LRScheduler>>,
341    weights: HashMap<String, f64>, // optimizer weights for ensemble
342}
343
344impl Default for MultiOptimizerTrainer {
345    fn default() -> Self {
346        Self::new()
347    }
348}
349
350impl MultiOptimizerTrainer {
351    /// Create a new multi-optimizer trainer
352    pub fn new() -> Self {
353        Self {
354            optimizers: HashMap::new(),
355            parameter_assignments: HashMap::new(),
356            schedulers: HashMap::new(),
357            weights: HashMap::new(),
358        }
359    }
360
361    /// Add an optimizer with a name
362    pub fn add_optimizer(
363        &mut self,
364        name: String,
365        optimizer: Box<dyn Optimizer>,
366        weight: f64,
367    ) -> Result<()> {
368        self.optimizers.insert(name.clone(), optimizer);
369        self.weights.insert(name, weight);
370        Ok(())
371    }
372
373    /// Add a scheduler for an optimizer
374    pub fn add_scheduler(
375        &mut self,
376        optimizer_name: String,
377        scheduler: Box<dyn LRScheduler>,
378    ) -> Result<()> {
379        if !self.optimizers.contains_key(&optimizer_name) {
380            return Err(TrustformersError::invalid_argument(format!(
381                "Optimizer {} not found",
382                optimizer_name
383            )));
384        }
385
386        self.schedulers.insert(optimizer_name, scheduler);
387        Ok(())
388    }
389
390    /// Assign parameters to optimizers
391    pub fn assign_parameters(&mut self, assignments: HashMap<String, String>) -> Result<()> {
392        // Validate that all assigned optimizers exist
393        for optimizer_name in assignments.values() {
394            if !self.optimizers.contains_key(optimizer_name) {
395                return Err(TrustformersError::invalid_argument(format!(
396                    "Optimizer {} not found",
397                    optimizer_name
398                )));
399            }
400        }
401
402        self.parameter_assignments = assignments;
403        Ok(())
404    }
405
406    /// Perform multi-optimizer training step
407    pub fn step(
408        &mut self,
409        parameters: &HashMap<String, Tensor>,
410        gradients: &HashMap<String, Tensor>,
411    ) -> Result<()> {
412        // Group parameters by optimizer
413        let mut optimizer_params: HashMap<String, Vec<(String, Tensor, Tensor)>> = HashMap::new();
414
415        for (param_name, param) in parameters {
416            if let Some(grad) = gradients.get(param_name) {
417                let optimizer_name = self
418                    .parameter_assignments
419                    .get(param_name)
420                    .cloned()
421                    .unwrap_or_else(|| "default".to_string());
422
423                optimizer_params.entry(optimizer_name).or_default().push((
424                    param_name.clone(),
425                    param.clone(),
426                    grad.clone(),
427                ));
428            }
429        }
430
431        // Apply optimizers
432        for (optimizer_name, param_grad_pairs) in optimizer_params {
433            if let Some(optimizer) = self.optimizers.get_mut(&optimizer_name) {
434                let weight = self.weights.get(&optimizer_name).copied().unwrap_or(1.0);
435
436                for (_, param, grad) in param_grad_pairs {
437                    // Scale gradient by optimizer weight
438                    let scaled_grad = grad.mul_scalar(weight as f32)?;
439                    optimizer.update(&mut param.clone(), &scaled_grad)?;
440                }
441            }
442        }
443
444        Ok(())
445    }
446
447    /// Update learning rates using schedulers
448    pub fn step_schedulers(&mut self, epoch: usize) -> Result<()> {
449        for (optimizer_name, scheduler) in &mut self.schedulers {
450            let new_lr = scheduler.get_lr(epoch);
451
452            if let Some(optimizer) = self.optimizers.get_mut(optimizer_name) {
453                optimizer.set_lr(new_lr);
454            }
455        }
456
457        Ok(())
458    }
459
460    /// Get training statistics
461    pub fn get_stats(&self) -> MultiOptimizerStats {
462        MultiOptimizerStats {
463            num_optimizers: self.optimizers.len(),
464            num_schedulers: self.schedulers.len(),
465            num_assigned_params: self.parameter_assignments.len(),
466            optimizer_weights: self.weights.clone(),
467        }
468    }
469}
470
471/// Statistics for multi-optimizer training
472#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct MultiOptimizerStats {
474    pub num_optimizers: usize,
475    pub num_schedulers: usize,
476    pub num_assigned_params: usize,
477    pub optimizer_weights: HashMap<String, f64>,
478}
479
480/// Optimizer warm-up strategies
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub enum WarmupStrategy {
483    /// Linear warmup
484    Linear { steps: usize },
485    /// Exponential warmup
486    Exponential { steps: usize, base: f64 },
487    /// Cosine warmup
488    Cosine { steps: usize },
489    /// Custom warmup function
490    Custom { steps: usize },
491}
492
493/// Warmup optimizer wrapper
494pub struct WarmupOptimizer {
495    inner: Box<dyn Optimizer>,
496    strategy: WarmupStrategy,
497    current_step: usize,
498    base_lr: f64,
499    target_lr: f64,
500}
501
502impl WarmupOptimizer {
503    /// Create a new warmup optimizer
504    pub fn new(
505        optimizer: Box<dyn Optimizer>,
506        strategy: WarmupStrategy,
507        base_lr: f64,
508        target_lr: f64,
509    ) -> Self {
510        Self {
511            inner: optimizer,
512            strategy,
513            current_step: 0,
514            base_lr,
515            target_lr,
516        }
517    }
518
519    /// Get current learning rate based on warmup strategy
520    fn get_warmup_lr(&self) -> f64 {
521        let warmup_steps = match &self.strategy {
522            WarmupStrategy::Linear { steps } => *steps,
523            WarmupStrategy::Exponential { steps, .. } => *steps,
524            WarmupStrategy::Cosine { steps } => *steps,
525            WarmupStrategy::Custom { steps } => *steps,
526        };
527
528        if self.current_step >= warmup_steps {
529            return self.target_lr;
530        }
531
532        let progress = self.current_step as f64 / warmup_steps as f64;
533
534        match &self.strategy {
535            WarmupStrategy::Linear { .. } => {
536                self.base_lr + (self.target_lr - self.base_lr) * progress
537            },
538            WarmupStrategy::Exponential { base, .. } => {
539                self.base_lr + (self.target_lr - self.base_lr) * base.powf(1.0 - progress)
540            },
541            WarmupStrategy::Cosine { .. } => {
542                let cosine_progress = 0.5 * (1.0 - (std::f64::consts::PI * progress).cos());
543                self.base_lr + (self.target_lr - self.base_lr) * cosine_progress
544            },
545            WarmupStrategy::Custom { .. } => {
546                // Custom implementation would go here
547                self.base_lr + (self.target_lr - self.base_lr) * progress
548            },
549        }
550    }
551
552    /// Check if warmup is complete
553    pub fn is_warmup_complete(&self) -> bool {
554        let warmup_steps = match &self.strategy {
555            WarmupStrategy::Linear { steps } => *steps,
556            WarmupStrategy::Exponential { steps, .. } => *steps,
557            WarmupStrategy::Cosine { steps } => *steps,
558            WarmupStrategy::Custom { steps } => *steps,
559        };
560
561        self.current_step >= warmup_steps
562    }
563}
564
565impl Optimizer for WarmupOptimizer {
566    fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
567        // Update learning rate based on warmup strategy
568        let current_lr = self.get_warmup_lr();
569        self.inner.set_lr(current_lr as f32);
570
571        // Perform the actual optimization step
572        self.inner.update(parameter, grad)
573    }
574
575    fn zero_grad(&mut self) {
576        self.inner.zero_grad()
577    }
578
579    fn step(&mut self) {
580        self.inner.step();
581        self.current_step += 1;
582    }
583
584    fn get_lr(&self) -> f32 {
585        self.get_warmup_lr() as f32
586    }
587
588    fn set_lr(&mut self, lr: f32) {
589        self.target_lr = lr as f64;
590        self.inner.set_lr(lr);
591    }
592}
593
594/// Checkpointing optimization configuration
595#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct CheckpointConfig {
597    /// Save interval (in steps)
598    pub save_interval: usize,
599    /// Whether to compress checkpoints
600    pub compress: bool,
601    /// Maximum number of checkpoints to keep
602    pub max_checkpoints: usize,
603    /// Whether to save only state diffs
604    pub incremental: bool,
605}
606
607impl Default for CheckpointConfig {
608    fn default() -> Self {
609        Self {
610            save_interval: 1000,
611            compress: true,
612            max_checkpoints: 5,
613            incremental: false,
614        }
615    }
616}
617
618/// Memory-bandwidth co-optimization
619pub struct MemoryBandwidthOptimizer {
620    inner: Box<dyn Optimizer>,
621    memory_threshold: usize,
622    bandwidth_threshold: f64,
623    adaptive_batch_size: bool,
624    current_batch_size: usize,
625    base_batch_size: usize,
626}
627
628impl MemoryBandwidthOptimizer {
629    /// Create a new memory-bandwidth co-optimizer
630    pub fn new(
631        optimizer: Box<dyn Optimizer>,
632        memory_threshold: usize,
633        bandwidth_threshold: f64,
634        base_batch_size: usize,
635    ) -> Self {
636        Self {
637            inner: optimizer,
638            memory_threshold,
639            bandwidth_threshold,
640            adaptive_batch_size: true,
641            current_batch_size: base_batch_size,
642            base_batch_size,
643        }
644    }
645
646    /// Adjust batch size based on memory and bandwidth usage
647    pub fn adjust_batch_size(&mut self, memory_usage: usize, bandwidth_usage: f64) -> usize {
648        if !self.adaptive_batch_size {
649            return self.current_batch_size;
650        }
651
652        let memory_pressure = memory_usage as f64 / self.memory_threshold as f64;
653        let bandwidth_pressure = bandwidth_usage / self.bandwidth_threshold;
654
655        let pressure = memory_pressure.max(bandwidth_pressure);
656
657        if pressure > 1.1 {
658            // High pressure - reduce batch size
659            self.current_batch_size = (self.current_batch_size as f64 * 0.9) as usize;
660            self.current_batch_size = self.current_batch_size.max(1);
661        } else if pressure < 0.8 {
662            // Low pressure - increase batch size
663            self.current_batch_size = (self.current_batch_size as f64 * 1.1) as usize;
664            self.current_batch_size = self.current_batch_size.min(self.base_batch_size * 4);
665        }
666
667        self.current_batch_size
668    }
669
670    /// Get current resource utilization
671    pub fn get_utilization(&self) -> ResourceUtilization {
672        ResourceUtilization {
673            current_batch_size: self.current_batch_size,
674            base_batch_size: self.base_batch_size,
675            memory_threshold: self.memory_threshold,
676            bandwidth_threshold: self.bandwidth_threshold,
677            adaptive_enabled: self.adaptive_batch_size,
678        }
679    }
680}
681
682impl Optimizer for MemoryBandwidthOptimizer {
683    fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
684        self.inner.update(parameter, grad)
685    }
686
687    fn zero_grad(&mut self) {
688        self.inner.zero_grad()
689    }
690
691    fn step(&mut self) {
692        self.inner.step()
693    }
694
695    fn get_lr(&self) -> f32 {
696        self.inner.get_lr()
697    }
698
699    fn set_lr(&mut self, lr: f32) {
700        self.inner.set_lr(lr)
701    }
702}
703
704/// Resource utilization statistics
705#[derive(Debug, Clone, Serialize, Deserialize)]
706pub struct ResourceUtilization {
707    pub current_batch_size: usize,
708    pub base_batch_size: usize,
709    pub memory_threshold: usize,
710    pub bandwidth_threshold: f64,
711    pub adaptive_enabled: bool,
712}
713
714#[cfg(test)]
715mod tests {
716    use super::*;
717    use crate::Adam;
718
719    #[test]
720    fn test_fusion_config_default() {
721        let config = FusionConfig::default();
722        assert!(config.fuse_parameters);
723        assert!(config.fuse_gradients);
724        assert!(config.fuse_state);
725        assert_eq!(config.window_size, 32);
726    }
727
728    #[test]
729    fn test_warmup_strategy_linear() {
730        let strategy = WarmupStrategy::Linear { steps: 100 };
731
732        let adam = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
733
734        let warmup_optimizer = WarmupOptimizer::new(Box::new(adam), strategy, 0.0, 0.001);
735
736        assert!(!warmup_optimizer.is_warmup_complete());
737        assert_eq!(warmup_optimizer.get_warmup_lr(), 0.0);
738    }
739
740    #[test]
741    fn test_multi_optimizer_trainer_creation() {
742        let mut trainer = MultiOptimizerTrainer::new();
743
744        let adam = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
745        trainer
746            .add_optimizer("adam".to_string(), Box::new(adam), 1.0)
747            .expect("Construction failed");
748
749        let stats = trainer.get_stats();
750        assert_eq!(stats.num_optimizers, 1);
751        assert_eq!(stats.optimizer_weights.get("adam"), Some(&1.0));
752    }
753
754    #[test]
755    fn test_memory_bandwidth_optimizer() {
756        let adam = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
757        let mut mb_optimizer = MemoryBandwidthOptimizer::new(
758            Box::new(adam),
759            1024 * 1024 * 100, // 100MB
760            100.0,             // 100 MB/s
761            32,
762        );
763
764        let utilization = mb_optimizer.get_utilization();
765        assert_eq!(utilization.current_batch_size, 32);
766        assert_eq!(utilization.base_batch_size, 32);
767
768        // Test batch size adjustment under high memory pressure
769        let new_batch_size = mb_optimizer.adjust_batch_size(
770            1024 * 1024 * 120, // 120MB (above threshold)
771            50.0,
772        );
773        assert!(new_batch_size < 32);
774    }
775
776    #[test]
777    fn test_checkpoint_config_default() {
778        let config = CheckpointConfig::default();
779        assert_eq!(config.save_interval, 1000);
780        assert!(config.compress);
781        assert_eq!(config.max_checkpoints, 5);
782        assert!(!config.incremental);
783    }
784}