1#![allow(dead_code)]
23
24use crate::common::{OptimizerState, StateMemoryStats};
25use crate::traits::StatefulOptimizer;
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28use trustformers_core::errors::{Result, TrustformersError};
29use trustformers_core::tensor::Tensor;
30use trustformers_core::traits::Optimizer;
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MicroAdamConfig {
35 pub learning_rate: f32,
37 pub beta1: f32,
39 pub beta2: f32,
41 pub epsilon: f32,
43 pub weight_decay: f32,
45 pub compression_ratio: f32,
47 pub min_block_size: usize,
49 pub adaptive_compression: bool,
51 pub compression_threshold: f32,
53 pub bias_correction: bool,
55 pub max_compression_error: f32,
57}
58
59impl Default for MicroAdamConfig {
60 fn default() -> Self {
61 Self {
62 learning_rate: 1e-3,
63 beta1: 0.9,
64 beta2: 0.999,
65 epsilon: 1e-8,
66 weight_decay: 0.01,
67 compression_ratio: 0.1,
68 min_block_size: 64,
69 adaptive_compression: true,
70 compression_threshold: 1e-6,
71 bias_correction: true,
72 max_compression_error: 1e-4,
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
79struct CompressedGradient {
80 compressed_data: Vec<f32>,
82 indices: Vec<usize>,
84 scale_factor: f32,
86 original_size: usize,
88 compression_type: CompressionType,
90}
91
92#[derive(Debug, Clone, Copy)]
94enum CompressionType {
95 TopK,
97 Threshold,
99 BlockWise,
101 Adaptive,
103}
104
105impl CompressedGradient {
106 fn compress(gradient: &[f32], config: &MicroAdamConfig) -> Self {
108 let original_size = gradient.len();
109 let target_size = (original_size as f32 * config.compression_ratio) as usize;
110 let target_size = target_size.max(config.min_block_size.min(original_size));
111
112 let compression_type = if config.adaptive_compression {
113 Self::choose_adaptive_compression(gradient, config)
115 } else {
116 CompressionType::TopK
117 };
118
119 match compression_type {
120 CompressionType::TopK => Self::compress_topk(gradient, target_size),
121 CompressionType::Threshold => Self::compress_threshold(gradient, config),
122 CompressionType::BlockWise => Self::compress_blockwise(gradient, config),
123 CompressionType::Adaptive => Self::compress_adaptive(gradient, config),
124 }
125 }
126
127 fn choose_adaptive_compression(gradient: &[f32], config: &MicroAdamConfig) -> CompressionType {
129 let mean_abs = gradient.iter().map(|x| x.abs()).sum::<f32>() / gradient.len() as f32;
130 let sparsity = gradient.iter().filter(|&&x| x.abs() < config.compression_threshold).count()
131 as f32
132 / gradient.len() as f32;
133
134 if sparsity > 0.8 {
135 CompressionType::Threshold
136 } else if mean_abs > 1e-3 {
137 CompressionType::BlockWise
138 } else {
139 CompressionType::TopK
140 }
141 }
142
143 fn compress_topk(gradient: &[f32], k: usize) -> Self {
145 let mut indexed_values: Vec<(usize, f32)> =
146 gradient.iter().enumerate().map(|(i, &val)| (i, val.abs())).collect();
147
148 indexed_values.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
150
151 let k = k.min(indexed_values.len());
152 let indices: Vec<usize> = indexed_values[..k].iter().map(|(i, _)| *i).collect();
153 let compressed_data: Vec<f32> = indices.iter().map(|&i| gradient[i]).collect();
154
155 let max_val = compressed_data.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
157 let scale_factor = if max_val > 0.0 { 1.0 / max_val } else { 1.0 };
158
159 Self {
160 compressed_data: compressed_data.iter().map(|x| x * scale_factor).collect(),
161 indices,
162 scale_factor: 1.0 / scale_factor,
163 original_size: gradient.len(),
164 compression_type: CompressionType::TopK,
165 }
166 }
167
168 fn compress_threshold(gradient: &[f32], config: &MicroAdamConfig) -> Self {
170 let threshold = config.compression_threshold;
171 let mut indices = Vec::new();
172 let mut compressed_data = Vec::new();
173
174 for (i, &val) in gradient.iter().enumerate() {
175 if val.abs() >= threshold {
176 indices.push(i);
177 compressed_data.push(val);
178 }
179 }
180
181 let max_val = compressed_data.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
182 let scale_factor = if max_val > 0.0 { 1.0 / max_val } else { 1.0 };
183
184 Self {
185 compressed_data: compressed_data.iter().map(|x| x * scale_factor).collect(),
186 indices,
187 scale_factor: 1.0 / scale_factor,
188 original_size: gradient.len(),
189 compression_type: CompressionType::Threshold,
190 }
191 }
192
193 fn compress_blockwise(gradient: &[f32], config: &MicroAdamConfig) -> Self {
195 let block_size = config.min_block_size;
196 let num_blocks = gradient.len().div_ceil(block_size);
197 let target_elements_per_block =
198 ((block_size as f32 * config.compression_ratio) as usize).max(1);
199
200 let mut indices = Vec::new();
201 let mut compressed_data = Vec::new();
202
203 for block_idx in 0..num_blocks {
204 let start = block_idx * block_size;
205 let end = (start + block_size).min(gradient.len());
206 let block = &gradient[start..end];
207
208 let mut block_indexed: Vec<(usize, f32)> =
210 block.iter().enumerate().map(|(i, &val)| (start + i, val.abs())).collect();
211
212 block_indexed
213 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
214
215 let k = target_elements_per_block.min(block_indexed.len());
216 for i in 0..k {
217 let global_idx = block_indexed[i].0;
218 indices.push(global_idx);
219 compressed_data.push(gradient[global_idx]);
220 }
221 }
222
223 let max_val = compressed_data.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
224 let scale_factor = if max_val > 0.0 { 1.0 / max_val } else { 1.0 };
225
226 Self {
227 compressed_data: compressed_data.iter().map(|x| x * scale_factor).collect(),
228 indices,
229 scale_factor: 1.0 / scale_factor,
230 original_size: gradient.len(),
231 compression_type: CompressionType::BlockWise,
232 }
233 }
234
235 fn compress_adaptive(gradient: &[f32], config: &MicroAdamConfig) -> Self {
237 let topk = Self::compress_topk(
239 gradient,
240 (gradient.len() as f32 * config.compression_ratio) as usize,
241 );
242 let threshold = Self::compress_threshold(gradient, config);
243 let blockwise = Self::compress_blockwise(gradient, config);
244
245 let topk_ratio = topk.compressed_data.len() as f32 / gradient.len() as f32;
247 let threshold_ratio = threshold.compressed_data.len() as f32 / gradient.len() as f32;
248 let blockwise_ratio = blockwise.compressed_data.len() as f32 / gradient.len() as f32;
249
250 if threshold_ratio <= config.compression_ratio && threshold_ratio < topk_ratio {
251 threshold
252 } else if blockwise_ratio <= config.compression_ratio && blockwise_ratio < topk_ratio {
253 blockwise
254 } else {
255 topk
256 }
257 }
258
259 fn decompress(&self) -> Vec<f32> {
261 let mut result = vec![0.0; self.original_size];
262 for (i, &idx) in self.indices.iter().enumerate() {
263 if idx < self.original_size && i < self.compressed_data.len() {
264 result[idx] = self.compressed_data[i] * self.scale_factor;
265 }
266 }
267 result
268 }
269
270 fn compression_ratio(&self) -> f32 {
272 self.compressed_data.len() as f32 / self.original_size as f32
273 }
274
275 fn compression_error(&self, original: &[f32]) -> f32 {
277 let decompressed = self.decompress();
278 let mut error_sum = 0.0;
279 let mut norm_sum = 0.0;
280
281 for (orig, decomp) in original.iter().zip(decompressed.iter()) {
282 error_sum += (orig - decomp).powi(2);
283 norm_sum += orig.powi(2);
284 }
285
286 if norm_sum > 0.0 {
287 (error_sum / norm_sum).sqrt()
288 } else {
289 0.0
290 }
291 }
292}
293
294#[derive(Debug)]
299pub struct MicroAdam {
300 config: MicroAdamConfig,
301 state: OptimizerState,
302 momentum: HashMap<String, CompressedGradient>,
304 variance: HashMap<String, CompressedGradient>,
306 compression_stats: CompressionStats,
308}
309
310#[derive(Debug, Default)]
312struct CompressionStats {
313 total_parameters: usize,
314 total_compressed_size: usize,
315 average_compression_ratio: f32,
316 average_compression_error: f32,
317 compression_method_usage: HashMap<String, usize>,
318}
319
320impl MicroAdam {
321 pub fn new() -> Self {
323 Self::with_config(MicroAdamConfig::default())
324 }
325
326 pub fn new_with_lr(learning_rate: f32) -> Self {
328 let config = MicroAdamConfig {
329 learning_rate,
330 ..Default::default()
331 };
332 Self::with_config(config)
333 }
334
335 pub fn for_large_models() -> Self {
337 let config = MicroAdamConfig {
338 learning_rate: 1e-4,
339 beta1: 0.9,
340 beta2: 0.999,
341 epsilon: 1e-8,
342 weight_decay: 0.01,
343 compression_ratio: 0.05, min_block_size: 128,
345 adaptive_compression: true,
346 compression_threshold: 1e-7,
347 bias_correction: true,
348 max_compression_error: 1e-5,
349 };
350 Self::with_config(config)
351 }
352
353 pub fn for_memory_constrained() -> Self {
355 let config = MicroAdamConfig {
356 learning_rate: 1e-3,
357 beta1: 0.9,
358 beta2: 0.999,
359 epsilon: 1e-8,
360 weight_decay: 0.01,
361 compression_ratio: 0.02, min_block_size: 32,
363 adaptive_compression: true,
364 compression_threshold: 1e-6,
365 bias_correction: true,
366 max_compression_error: 1e-4,
367 };
368 Self::with_config(config)
369 }
370
371 pub fn with_config(config: MicroAdamConfig) -> Self {
373 Self {
374 config,
375 state: OptimizerState::new(),
376 momentum: HashMap::new(),
377 variance: HashMap::new(),
378 compression_stats: CompressionStats::default(),
379 }
380 }
381
382 pub fn memory_savings_ratio(&self) -> f32 {
384 if self.compression_stats.total_parameters > 0 {
385 1.0 - (self.compression_stats.total_compressed_size as f32
386 / (self.compression_stats.total_parameters * 2) as f32)
387 } else {
388 0.0
389 }
390 }
391
392 pub fn compression_statistics(&self) -> String {
394 format!(
395 "MicroAdam Compression Stats:\n\
396 - Total parameters: {}\n\
397 - Compressed size: {}\n\
398 - Memory savings: {:.1}%\n\
399 - Average compression ratio: {:.3}\n\
400 - Average compression error: {:.2e}",
401 self.compression_stats.total_parameters,
402 self.compression_stats.total_compressed_size,
403 self.memory_savings_ratio() * 100.0,
404 self.compression_stats.average_compression_ratio,
405 self.compression_stats.average_compression_error
406 )
407 }
408
409 fn update_compression_stats(
411 &mut self,
412 _param_id: &str,
413 compressed: &CompressedGradient,
414 original_gradient: &[f32],
415 ) {
416 self.compression_stats.total_parameters += compressed.original_size;
417 self.compression_stats.total_compressed_size += compressed.compressed_data.len();
418
419 let compression_ratio = compressed.compression_ratio();
420 let compression_error = compressed.compression_error(original_gradient);
421
422 let total_params = self.compression_stats.total_parameters as f32;
424 self.compression_stats.average_compression_ratio =
425 (self.compression_stats.average_compression_ratio
426 * (total_params - compressed.original_size as f32)
427 + compression_ratio * compressed.original_size as f32)
428 / total_params;
429
430 self.compression_stats.average_compression_error =
431 (self.compression_stats.average_compression_error
432 * (total_params - compressed.original_size as f32)
433 + compression_error * compressed.original_size as f32)
434 / total_params;
435
436 let method_name = format!("{:?}", compressed.compression_type);
438 *self.compression_stats.compression_method_usage.entry(method_name).or_insert(0) += 1;
439 }
440}
441
442impl Default for MicroAdam {
443 fn default() -> Self {
444 Self::new()
445 }
446}
447
448impl Optimizer for MicroAdam {
449 fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
450 let param_id = format!("{:p}", parameter as *const Tensor);
452
453 let grad_data = grad.data()?;
455
456 let compressed_gradient = CompressedGradient::compress(&grad_data, &self.config);
458
459 let compression_error = compressed_gradient.compression_error(&grad_data);
461 if compression_error > self.config.max_compression_error {
462 return Err(TrustformersError::tensor_op_error(
463 &format!(
464 "Compression error {} exceeds maximum allowed {}",
465 compression_error, self.config.max_compression_error
466 ),
467 "MicroAdam::update",
468 ));
469 }
470
471 self.update_compression_stats(¶m_id, &compressed_gradient, &grad_data);
473
474 let momentum = self.momentum.entry(param_id.clone()).or_insert_with(|| {
476 CompressedGradient::compress(&vec![0.0; grad_data.len()], &self.config)
477 });
478
479 let variance = self.variance.entry(param_id.clone()).or_insert_with(|| {
481 CompressedGradient::compress(&vec![0.0; grad_data.len()], &self.config)
482 });
483
484 let mut m = momentum.decompress();
486 let mut v = variance.decompress();
487
488 m.resize(grad_data.len(), 0.0);
490 v.resize(grad_data.len(), 0.0);
491
492 self.state.step();
494
495 let bias_correction1 = if self.config.bias_correction {
497 1.0 - self.config.beta1.powf(self.state.step as f32)
498 } else {
499 1.0
500 };
501
502 let bias_correction2 = if self.config.bias_correction {
503 1.0 - self.config.beta2.powf(self.state.step as f32)
504 } else {
505 1.0
506 };
507
508 for i in 0..grad_data.len() {
510 m[i] = self.config.beta1 * m[i] + (1.0 - self.config.beta1) * grad_data[i];
511 }
512
513 for i in 0..grad_data.len() {
515 v[i] = self.config.beta2 * v[i] + (1.0 - self.config.beta2) * grad_data[i].powi(2);
516 }
517
518 let mut param_data = parameter.data()?;
520 for i in 0..grad_data.len() {
521 let m_hat = m[i] / bias_correction1;
522 let v_hat = v[i] / bias_correction2;
523 let update_val =
524 self.config.learning_rate * m_hat / (v_hat.sqrt() + self.config.epsilon);
525
526 if self.config.weight_decay > 0.0 {
528 param_data[i] *= 1.0 - self.config.learning_rate * self.config.weight_decay;
529 }
530
531 param_data[i] -= update_val;
533 }
534
535 *parameter = Tensor::new(param_data)?;
537
538 *momentum = CompressedGradient::compress(&m, &self.config);
540 *variance = CompressedGradient::compress(&v, &self.config);
541
542 Ok(())
543 }
544
545 fn zero_grad(&mut self) {
546 }
549
550 fn step(&mut self) {
551 }
553
554 fn get_lr(&self) -> f32 {
555 self.config.learning_rate
556 }
557
558 fn set_lr(&mut self, lr: f32) {
559 self.config.learning_rate = lr;
560 }
561}
562
563impl StatefulOptimizer for MicroAdam {
564 type Config = MicroAdamConfig;
565 type State = OptimizerState;
566
567 fn config(&self) -> &Self::Config {
568 &self.config
569 }
570
571 fn state(&self) -> &Self::State {
572 &self.state
573 }
574
575 fn state_mut(&mut self) -> &mut Self::State {
576 &mut self.state
577 }
578
579 fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
580 let mut state_dict = HashMap::new();
581
582 for (param_id, momentum) in &self.momentum {
584 let key = format!("momentum.{}", param_id);
585 let tensor = Tensor::new(momentum.decompress())?;
586 state_dict.insert(key, tensor);
587 }
588
589 for (param_id, variance) in &self.variance {
591 let key = format!("variance.{}", param_id);
592 let tensor = Tensor::new(variance.decompress())?;
593 state_dict.insert(key, tensor);
594 }
595
596 state_dict.insert(
598 "step".to_string(),
599 Tensor::new(vec![self.state.step as f32])?,
600 );
601
602 Ok(state_dict)
603 }
604
605 fn load_state_dict(&mut self, state_dict: HashMap<String, Tensor>) -> Result<()> {
606 if let Some(step_tensor) = state_dict.get("step") {
608 let step_data = step_tensor.data()?;
609 if !step_data.is_empty() {
610 self.state.step = step_data[0] as usize;
611 }
612 }
613
614 for (key, tensor) in &state_dict {
616 if let Some(param_id) = key.strip_prefix("momentum.") {
617 let values = tensor.data()?;
618 let compressed = CompressedGradient::compress(&values, &self.config);
619 self.momentum.insert(param_id.to_string(), compressed);
620 } else if let Some(param_id) = key.strip_prefix("variance.") {
621 let values = tensor.data()?;
622 let compressed = CompressedGradient::compress(&values, &self.config);
623 self.variance.insert(param_id.to_string(), compressed);
624 }
625 }
626
627 Ok(())
628 }
629
630 fn memory_usage(&self) -> StateMemoryStats {
631 let momentum_size: usize = self.momentum.values().map(|m| m.compressed_data.len()).sum();
632 let variance_size: usize = self.variance.values().map(|v| v.compressed_data.len()).sum();
633
634 StateMemoryStats {
635 momentum_elements: momentum_size,
636 variance_elements: variance_size,
637 third_moment_elements: 0,
638 total_bytes: (momentum_size + variance_size) * std::mem::size_of::<f32>(),
639 num_parameters: self.momentum.len(),
640 }
641 }
642
643 fn reset_state(&mut self) {
644 self.state.clear();
645 self.momentum.clear();
646 self.variance.clear();
647 self.compression_stats = CompressionStats::default();
648 }
649
650 fn num_parameters(&self) -> usize {
651 self.momentum.len()
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658
659 #[test]
660 fn test_microadam_creation() {
661 let optimizer = MicroAdam::new();
662 assert_eq!(optimizer.config.learning_rate, 1e-3);
663 assert_eq!(optimizer.config.beta1, 0.9);
664 assert_eq!(optimizer.config.beta2, 0.999);
665 }
667
668 #[test]
669 fn test_microadam_with_config() {
670 let config = MicroAdamConfig {
671 learning_rate: 2e-3,
672 compression_ratio: 0.2,
673 ..Default::default()
674 };
675 let optimizer = MicroAdam::with_config(config);
676 assert_eq!(optimizer.config.learning_rate, 2e-3);
677 assert_eq!(optimizer.config.compression_ratio, 0.2);
678 }
679
680 #[test]
681 fn test_microadam_for_large_models() {
682 let optimizer = MicroAdam::for_large_models();
683 assert_eq!(optimizer.config.learning_rate, 1e-4);
684 assert_eq!(optimizer.config.compression_ratio, 0.05);
685 assert_eq!(optimizer.config.min_block_size, 128);
686 assert!(optimizer.config.adaptive_compression);
687 }
688
689 #[test]
690 fn test_microadam_for_memory_constrained() {
691 let optimizer = MicroAdam::for_memory_constrained();
692 assert_eq!(optimizer.config.compression_ratio, 0.02);
693 assert_eq!(optimizer.config.min_block_size, 32);
694 assert!(optimizer.config.adaptive_compression);
695 }
696
697 #[test]
698 fn test_compressed_gradient_topk() {
699 let gradient = vec![0.1, 0.05, 0.2, 0.01, 0.15, 0.03];
700 let _config = MicroAdamConfig::default();
701 let compressed = CompressedGradient::compress_topk(&gradient, 3);
702
703 assert_eq!(compressed.compressed_data.len(), 3);
704 assert_eq!(compressed.indices.len(), 3);
705 assert_eq!(compressed.original_size, 6);
706
707 let mut expected_indices = vec![2, 4, 0];
709 let mut actual_indices = compressed.indices.clone();
710 expected_indices.sort();
711 actual_indices.sort();
712 assert_eq!(actual_indices, expected_indices);
713 }
714
715 #[test]
716 fn test_compressed_gradient_threshold() {
717 let gradient = vec![0.1, 0.001, 0.2, 0.0001, 0.15, 0.0003];
718 let config = MicroAdamConfig {
719 compression_threshold: 0.05,
720 ..Default::default()
721 };
722 let compressed = CompressedGradient::compress_threshold(&gradient, &config);
723
724 assert_eq!(compressed.compressed_data.len(), 3);
726 assert_eq!(compressed.indices.len(), 3);
727
728 let mut expected_indices = vec![0, 2, 4];
729 let mut actual_indices = compressed.indices.clone();
730 expected_indices.sort();
731 actual_indices.sort();
732 assert_eq!(actual_indices, expected_indices);
733 }
734
735 #[test]
736 fn test_compression_decompress_cycle() {
737 let gradient = vec![0.1, 0.05, 0.2, 0.01, 0.15, 0.03];
738 let config = MicroAdamConfig::default();
739 let compressed = CompressedGradient::compress(&gradient, &config);
740 let decompressed = compressed.decompress();
741
742 assert_eq!(decompressed.len(), gradient.len());
743
744 for (i, &original) in gradient.iter().enumerate() {
746 if original.abs() > 0.08 {
747 assert!(
749 decompressed[i].abs() > 0.0,
750 "Significant value at index {} was lost",
751 i
752 );
753 }
754 }
755 }
756
757 #[test]
758 fn test_compression_error_calculation() {
759 let gradient = vec![0.1, 0.05, 0.2, 0.01, 0.15, 0.03];
760 let config = MicroAdamConfig::default();
761 let compressed = CompressedGradient::compress(&gradient, &config);
762 let error = compressed.compression_error(&gradient);
763
764 assert!(error >= 0.0);
765 assert!(error <= 1.0); }
767
768 #[test]
769 fn test_microadam_update() -> Result<()> {
770 let mut optimizer = MicroAdam::new();
771 let gradient_data = vec![0.1, -0.05, 0.2, -0.01];
772 let gradient = Tensor::new(gradient_data.clone())?;
773 let mut parameter = Tensor::new(vec![1.0, 1.0, 1.0, 1.0])?;
774
775 optimizer.update(&mut parameter, &gradient)?;
776
777 assert_eq!(optimizer.state().step, 1);
779
780 let param_data = parameter.data()?;
782 assert_eq!(param_data.len(), gradient_data.len());
783
784 assert_ne!(param_data[0], 1.0);
786
787 Ok(())
788 }
789
790 #[test]
791 fn test_microadam_multiple_updates() -> Result<()> {
792 let mut optimizer = MicroAdam::new();
793 let gradient_data = vec![0.1, -0.05, 0.2, -0.01];
794 let gradient = Tensor::new(gradient_data)?;
795 let mut parameter = Tensor::new(vec![1.0, 1.0, 1.0, 1.0])?;
796
797 for i in 1..=5 {
799 optimizer.update(&mut parameter, &gradient)?;
800 assert_eq!(optimizer.state().step, i);
801 }
802
803 Ok(())
804 }
805
806 #[test]
807 fn test_memory_savings_ratio() {
808 let config = MicroAdamConfig {
809 max_compression_error: 1.0, ..MicroAdamConfig::default()
811 };
812 let mut optimizer = MicroAdam::with_config(config);
813
814 assert_eq!(optimizer.memory_savings_ratio(), 0.0);
816
817 let gradient_data = vec![0.1; 1000]; let gradient = Tensor::new(gradient_data).expect("Failed to create tensor");
820 let mut parameter = Tensor::new(vec![1.0; 1000]).expect("Failed to create tensor");
821 optimizer.update(&mut parameter, &gradient).expect("Optimizer update failed");
822
823 let savings = optimizer.memory_savings_ratio();
824 assert!(savings > 0.0, "Should show memory savings");
825 assert!(savings < 1.0, "Savings ratio should be less than 100%");
826 }
827
828 #[test]
829 fn test_compression_statistics() {
830 let config = MicroAdamConfig {
831 max_compression_error: 1.0, ..MicroAdamConfig::default()
833 };
834 let mut optimizer = MicroAdam::with_config(config);
835 let gradient_data = vec![0.1; 500];
836 let gradient = Tensor::new(gradient_data).expect("Failed to create tensor");
837 let mut parameter = Tensor::new(vec![1.0; 500]).expect("Failed to create tensor");
838
839 optimizer.update(&mut parameter, &gradient).expect("Optimizer update failed");
840
841 let stats = optimizer.compression_statistics();
842 assert!(stats.contains("MicroAdam Compression Stats"));
843 assert!(stats.contains("Total parameters: 500"));
844 assert!(stats.contains("Memory savings"));
845 assert!(stats.contains("compression ratio"));
846 }
847
848 #[test]
849 fn test_learning_rate_setter_getter() {
850 let mut optimizer = MicroAdam::new();
851 assert_eq!(optimizer.get_lr(), 1e-3);
852
853 optimizer.set_lr(2e-3);
854 assert_eq!(optimizer.get_lr(), 2e-3);
855 }
856
857 #[test]
858 fn test_state_dict_operations() -> Result<()> {
859 let mut optimizer = MicroAdam::new();
860 let gradient_data = vec![0.1, -0.05, 0.2];
861 let gradient = Tensor::new(gradient_data)?;
862 let mut param1 = Tensor::new(vec![1.0, 1.0, 1.0])?;
863 let mut param2 = Tensor::new(vec![2.0, 2.0, 2.0])?;
864
865 optimizer.update(&mut param1, &gradient)?;
867 optimizer.update(&mut param2, &gradient)?;
868
869 let state_dict = optimizer.state_dict()?;
871 assert!(state_dict.contains_key("step"));
872
873 let mut new_optimizer = MicroAdam::new();
875 new_optimizer.load_state_dict(state_dict)?;
876
877 assert_eq!(new_optimizer.state().step, optimizer.state().step);
878
879 Ok(())
880 }
881
882 #[test]
883 fn test_memory_usage_tracking() -> Result<()> {
884 let config = MicroAdamConfig {
885 max_compression_error: 1.0, ..MicroAdamConfig::default()
887 };
888 let mut optimizer = MicroAdam::with_config(config);
889 let initial_usage = optimizer.memory_usage();
890
891 let gradient_data = vec![0.1; 1000];
892 let gradient = Tensor::new(gradient_data)?;
893 let mut parameter = Tensor::new(vec![1.0; 1000])?;
894 optimizer.update(&mut parameter, &gradient)?;
895
896 let after_usage = optimizer.memory_usage();
897 assert!(after_usage.total_bytes > initial_usage.total_bytes);
898 assert!(after_usage.momentum_elements > 0);
899 assert!(after_usage.variance_elements > 0);
900
901 Ok(())
902 }
903
904 #[test]
905 fn test_adaptive_compression_selection() {
906 let sparse_gradient = vec![0.0; 1000]; let dense_gradient = vec![0.1; 1000]; let config = MicroAdamConfig {
910 adaptive_compression: true,
911 compression_threshold: 1e-6,
912 ..Default::default()
913 };
914
915 let sparse_compression =
916 CompressedGradient::choose_adaptive_compression(&sparse_gradient, &config);
917 let dense_compression =
918 CompressedGradient::choose_adaptive_compression(&dense_gradient, &config);
919
920 match sparse_compression {
923 CompressionType::Threshold
924 | CompressionType::TopK
925 | CompressionType::BlockWise
926 | CompressionType::Adaptive => {},
927 }
928
929 match dense_compression {
930 CompressionType::Threshold
931 | CompressionType::TopK
932 | CompressionType::BlockWise
933 | CompressionType::Adaptive => {},
934 }
935 }
936}