1use crate::error::{OptimError, Result};
7use crate::utils::{scalar_or, try_scalar};
8use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
9use scirs2_core::numeric::Float;
10use std::fmt::Debug;
11use std::ops::{AddAssign, MulAssign, SubAssign};
12use std::sync::atomic::{AtomicUsize, Ordering};
13
14static GLOBAL_TRACKED_BYTES: AtomicUsize = AtomicUsize::new(0);
23
24fn total_system_memory_bytes() -> usize {
32 const FALLBACK: usize = 8 * 1024 * 1024 * 1024;
33
34 #[cfg(target_os = "linux")]
35 {
36 if let Ok(contents) = std::fs::read_to_string("/proc/meminfo") {
37 for line in contents.lines() {
38 if let Some(rest) = line.strip_prefix("MemTotal:") {
39 if let Some(kb) = rest
41 .split_whitespace()
42 .next()
43 .and_then(|value| value.parse::<usize>().ok())
44 {
45 return kb.saturating_mul(1024);
46 }
47 }
48 }
49 }
50 }
51
52 FALLBACK
53}
54
55pub trait InPlaceOptimizer<A: Float + ScalarOperand + Debug, D: Dimension> {
57 fn step_inplace(&mut self, params: &mut Array<A, D>, gradients: &Array<A, D>) -> Result<()>;
62
63 fn step_list_inplace(
65 &mut self,
66 params_list: &mut [&mut Array<A, D>],
67 gradients_list: &[&Array<A, D>],
68 ) -> Result<()> {
69 if params_list.len() != gradients_list.len() {
70 return Err(OptimError::InvalidConfig(format!(
71 "Number of parameter arrays ({}) does not match number of gradient arrays ({})",
72 params_list.len(),
73 gradients_list.len()
74 )));
75 }
76
77 for (params, grads) in params_list.iter_mut().zip(gradients_list.iter()) {
78 self.step_inplace(params, grads)?;
79 }
80 Ok(())
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct InPlaceSGD<A: Float> {
87 _learningrate: A,
88 momentum: A,
89 weight_decay: A,
90}
91
92impl<A: Float + ScalarOperand + Debug + Send + Sync> InPlaceSGD<A> {
93 pub fn new(_learningrate: A) -> Self {
95 Self {
96 _learningrate,
97 momentum: A::zero(),
98 weight_decay: A::zero(),
99 }
100 }
101
102 pub fn with_momentum(mut self, momentum: A) -> Self {
104 self.momentum = momentum;
105 self
106 }
107
108 pub fn with_weight_decay(mut self, weightdecay: A) -> Self {
110 self.weight_decay = weightdecay;
111 self
112 }
113}
114
115impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> InPlaceOptimizer<A, D>
116 for InPlaceSGD<A>
117{
118 fn step_inplace(&mut self, params: &mut Array<A, D>, gradients: &Array<A, D>) -> Result<()> {
119 if self.weight_decay > A::zero() {
121 params.zip_mut_with(gradients, |p, &g| {
122 *p = *p - self._learningrate * (g + *p * self.weight_decay);
123 });
124 } else {
125 params.zip_mut_with(gradients, |p, &g| {
127 *p = *p - self._learningrate * g;
128 });
129 }
130 Ok(())
131 }
132}
133
134#[derive(Debug, Clone)]
142struct AdamMoments<A: Float, D: Dimension> {
143 m: Array<A, D>,
145 v: Array<A, D>,
147}
148
149impl<A: Float, D: Dimension> AdamMoments<A, D> {
150 fn zeros(shape: D) -> Self {
151 Self {
152 m: Array::zeros(shape.clone()),
153 v: Array::zeros(shape),
154 }
155 }
156}
157
158#[derive(Debug)]
160pub struct InPlaceAdam<A: Float, D: Dimension> {
161 _learningrate: A,
162 beta1: A,
163 beta2: A,
164 epsilon: A,
165 weight_decay: A,
166 t: i32,
167 moments: Option<AdamMoments<A, D>>,
169}
170
171impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> InPlaceAdam<A, D> {
172 pub fn new(_learningrate: A) -> Self {
174 Self {
175 _learningrate,
176 beta1: scalar_or(0.9, A::zero()),
177 beta2: scalar_or(0.999, A::zero()),
178 epsilon: scalar_or(1e-8, A::zero()),
179 weight_decay: A::zero(),
180 t: 0,
181 moments: None,
182 }
183 }
184
185 pub fn with_beta1(mut self, beta1: A) -> Self {
187 self.beta1 = beta1;
188 self
189 }
190
191 pub fn with_beta2(mut self, beta2: A) -> Self {
193 self.beta2 = beta2;
194 self
195 }
196
197 pub fn with_weight_decay(mut self, weightdecay: A) -> Self {
199 self.weight_decay = weightdecay;
200 self
201 }
202
203 pub fn with_epsilon(mut self, epsilon: A) -> Self {
205 self.epsilon = epsilon;
206 self
207 }
208
209 pub fn reset(&mut self) {
211 self.t = 0;
212 self.moments = None;
213 }
214}
215
216impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> InPlaceOptimizer<A, D>
217 for InPlaceAdam<A, D>
218{
219 fn step_inplace(&mut self, params: &mut Array<A, D>, gradients: &Array<A, D>) -> Result<()> {
220 self.t += 1;
221 let _t = try_scalar::<A, _>(self.t)?;
222
223 let moments = self
227 .moments
228 .get_or_insert_with(|| AdamMoments::zeros(params.raw_dim()));
229
230 if moments.m.raw_dim() != params.raw_dim() {
235 return Err(OptimError::DimensionMismatch(format!(
236 "InPlaceAdam moment state has shape {:?} but was given parameters of shape {:?}; \
237 call `reset()` before optimizing a differently-shaped parameter set",
238 moments.m.shape(),
239 params.shape()
240 )));
241 }
242
243 let AdamMoments { m, v } = moments;
244
245 let grad_with_decay = if self.weight_decay > A::zero() {
247 let mut temp = gradients.clone();
249 temp.zip_mut_with(params, |g, &p| {
250 *g = *g + p * self.weight_decay;
251 });
252 temp
253 } else {
254 gradients.clone()
255 };
256
257 m.zip_mut_with(&grad_with_decay, |m_i, &g| {
259 *m_i = self.beta1 * *m_i + (A::one() - self.beta1) * g;
260 });
261
262 v.zip_mut_with(&grad_with_decay, |v_i, &g| {
264 *v_i = self.beta2 * *v_i + (A::one() - self.beta2) * g * g;
265 });
266
267 let bias1 = A::one() - self.beta1.powi(self.t);
269 let bias2 = A::one() - self.beta2.powi(self.t);
270
271 let m_iter = m.iter();
273 let v_iter = v.iter();
274 let params_iter = params.iter_mut();
275
276 for ((p, &m_i), &v_i) in params_iter.zip(m_iter).zip(v_iter) {
277 let m_hat = m_i / bias1;
278 let v_hat = v_i / bias2;
279 *p = *p - self._learningrate * m_hat / (v_hat.sqrt() + self.epsilon);
280 }
281
282 Ok(())
283 }
284}
285
286pub mod utils {
288 use super::*;
289
290 pub fn scale_inplace<A, D>(array: &mut Array<A, D>, scalar: A)
292 where
293 A: Float + ScalarOperand + MulAssign,
294 D: Dimension,
295 {
296 array.map_inplace(|x| *x *= scalar);
297 }
298
299 pub fn add_inplace<A, D>(a: &mut Array<A, D>, b: &Array<A, D>)
301 where
302 A: Float + ScalarOperand + AddAssign,
303 D: Dimension,
304 {
305 a.zip_mut_with(b, |x, &y| *x += y);
306 }
307
308 pub fn subtract_inplace<A, D>(a: &mut Array<A, D>, b: &Array<A, D>)
310 where
311 A: Float + ScalarOperand + SubAssign,
312 D: Dimension,
313 {
314 a.zip_mut_with(b, |x, &y| *x -= y);
315 }
316
317 pub fn apply_inplace<A, D, F>(array: &mut Array<A, D>, f: F)
319 where
320 A: Float + ScalarOperand,
321 D: Dimension,
322 F: Fn(&mut A),
323 {
324 array.map_inplace(f);
325 }
326
327 pub fn clip_inplace<A, D>(array: &mut Array<A, D>, min: A, max: A)
329 where
330 A: Float + ScalarOperand,
331 D: Dimension,
332 {
333 array.map_inplace(|x| {
334 if *x < min {
335 *x = min;
336 } else if *x > max {
337 *x = max;
338 }
339 });
340 }
341
342 pub fn normalize_inplace<A, D>(array: &mut Array<A, D>)
344 where
345 A: Float + ScalarOperand + MulAssign,
346 D: Dimension,
347 {
348 let norm = array.mapv(|x| x * x).sum().sqrt();
349 if norm > A::zero() {
350 array.map_inplace(|x| *x *= A::one() / norm);
351 }
352 }
353}
354
355pub mod fused {
357 use super::*;
358
359 #[derive(Debug, Clone, Copy)]
361 pub struct AdamConfig<A> {
362 pub lr: A,
363 pub beta1: A,
364 pub beta2: A,
365 pub epsilon: A,
366 pub bias1: A,
367 pub bias2: A,
368 pub weight_decay: Option<A>,
369 }
370
371 pub fn fused_adam_update<A, D>(
376 params: &mut Array<A, D>,
377 gradients: &Array<A, D>,
378 m: &mut Array<A, D>,
379 v: &mut Array<A, D>,
380 config: AdamConfig<A>,
381 ) where
382 A: Float + ScalarOperand,
383 D: Dimension,
384 {
385 let one = A::one();
386 let one_minus_beta1 = one - config.beta1;
387 let one_minus_beta2 = one - config.beta2;
388
389 if let Some(wd) = config.weight_decay {
390 for ((((p, &g), m_val), v_val), bias_corrected) in params
392 .iter_mut()
393 .zip(gradients.iter())
394 .zip(m.iter_mut())
395 .zip(v.iter_mut())
396 .zip(std::iter::repeat((config.bias1, config.bias2)))
397 {
398 let g_with_decay = g + *p * wd;
400
401 *m_val = config.beta1 * *m_val + one_minus_beta1 * g_with_decay;
403
404 *v_val = config.beta2 * *v_val + one_minus_beta2 * g_with_decay * g_with_decay;
406
407 let m_hat = *m_val / bias_corrected.0;
409 let v_hat = *v_val / bias_corrected.1;
410 *p = *p - config.lr * m_hat / (v_hat.sqrt() + config.epsilon);
411 }
412 } else {
413 for ((((p, &g), m_val), v_val), bias_corrected) in params
415 .iter_mut()
416 .zip(gradients.iter())
417 .zip(m.iter_mut())
418 .zip(v.iter_mut())
419 .zip(std::iter::repeat((config.bias1, config.bias2)))
420 {
421 *m_val = config.beta1 * *m_val + one_minus_beta1 * g;
423
424 *v_val = config.beta2 * *v_val + one_minus_beta2 * g * g;
426
427 let m_hat = *m_val / bias_corrected.0;
429 let v_hat = *v_val / bias_corrected.1;
430 *p = *p - config.lr * m_hat / (v_hat.sqrt() + config.epsilon);
431 }
432 }
433 }
434
435 pub fn fused_sgd_update<A, D>(
437 params: &mut Array<A, D>,
438 gradients: &Array<A, D>,
439 momentum_buf: Option<&mut Array<A, D>>,
440 lr: A,
441 momentum: A,
442 weight_decay: Option<A>,
443 dampening: A,
444 ) where
445 A: Float + ScalarOperand,
446 D: Dimension,
447 {
448 if let Some(_buf) = momentum_buf {
449 if let Some(wd) = weight_decay {
450 for ((p, g), buf_val) in
452 params.iter_mut().zip(gradients.iter()).zip(_buf.iter_mut())
453 {
454 let g_with_decay = *g + *p * wd;
455 *buf_val = momentum * *buf_val + (A::one() - dampening) * g_with_decay;
456 *p = *p - lr * *buf_val;
457 }
458 } else {
459 for ((p, g), buf_val) in
461 params.iter_mut().zip(gradients.iter()).zip(_buf.iter_mut())
462 {
463 *buf_val = momentum * *buf_val + (A::one() - dampening) * *g;
464 *p = *p - lr * *buf_val;
465 }
466 }
467 } else if let Some(wd) = weight_decay {
468 for (p, g) in params.iter_mut().zip(gradients.iter()) {
470 *p = *p - lr * (*g + *p * wd);
471 }
472 } else {
473 for (p, g) in params.iter_mut().zip(gradients.iter()) {
475 *p = *p - lr * *g;
476 }
477 }
478 }
479
480 pub fn fused_gradient_clip_normalize<A, D>(
482 gradients: &mut Array<A, D>,
483 max_norm: Option<A>,
484 clip_value: Option<A>,
485 ) where
486 A: Float + ScalarOperand,
487 D: Dimension,
488 {
489 if let Some(clip_val) = clip_value {
490 for g in gradients.iter_mut() {
492 if *g > clip_val {
493 *g = clip_val;
494 } else if *g < -clip_val {
495 *g = -clip_val;
496 }
497 }
498 }
499
500 if let Some(max_norm_val) = max_norm {
501 let norm_sq = gradients
503 .iter()
504 .map(|&x| x * x)
505 .fold(A::zero(), |acc, x| acc + x);
506 let _norm = norm_sq.sqrt();
507
508 if _norm > max_norm_val {
509 let scale = max_norm_val / _norm;
510 for g in gradients.iter_mut() {
511 *g = *g * scale;
512 }
513 }
514 }
515 }
516
517 pub fn fused_apply_constraints<A, D>(
519 params: &mut Array<A, D>,
520 l2_constraint: Option<A>,
521 value_bounds: Option<(A, A)>,
522 ) where
523 A: Float + ScalarOperand,
524 D: Dimension,
525 {
526 if let Some((min_val, max_val)) = value_bounds {
528 for p in params.iter_mut() {
529 if *p < min_val {
530 *p = min_val;
531 } else if *p > max_val {
532 *p = max_val;
533 }
534 }
535 }
536
537 if let Some(max_norm) = l2_constraint {
539 let norm_sq = params
540 .iter()
541 .map(|&x| x * x)
542 .fold(A::zero(), |acc, x| acc + x);
543 let norm = norm_sq.sqrt();
544
545 if norm > max_norm {
546 let scale = max_norm / norm;
547 for p in params.iter_mut() {
548 *p = *p * scale;
549 }
550 }
551 }
552 }
553}
554
555pub mod mixed_precision {
557 use super::*;
558
559 #[derive(Debug, Clone)]
561 pub struct LossScaler {
562 scale: f32,
563 growth_factor: f32,
564 backoff_factor: f32,
565 growth_interval: usize,
566 steps_since_update: usize,
567 }
568
569 impl LossScaler {
570 pub fn new(_initialscale: f32) -> Self {
572 Self {
573 scale: _initialscale,
574 growth_factor: 2.0,
575 backoff_factor: 0.5,
576 growth_interval: 2000,
577 steps_since_update: 0,
578 }
579 }
580
581 pub fn get_scale(&self) -> f32 {
583 self.scale
584 }
585
586 pub fn scale_loss(&self, loss: f32) -> f32 {
588 loss * self.scale
589 }
590
591 pub fn unscale_gradients<A, D>(&self, gradients: &mut Array<A, D>)
593 where
594 A: Float + ScalarOperand,
595 D: Dimension,
596 {
597 let inv_scale = A::one() / scalar_or(self.scale, A::one());
598 for g in gradients.iter_mut() {
599 *g = *g * inv_scale;
600 }
601 }
602
603 pub fn update(&mut self, foundinf: bool) {
605 self.steps_since_update += 1;
606
607 if foundinf {
608 self.scale *= self.backoff_factor;
610 self.steps_since_update = 0;
611 } else if self.steps_since_update >= self.growth_interval {
612 self.scale *= self.growth_factor;
614 self.steps_since_update = 0;
615 }
616 }
617
618 pub fn check_gradients<A, D>(&self, gradients: &Array<A, D>) -> bool
620 where
621 A: Float + ScalarOperand,
622 D: Dimension,
623 {
624 gradients.iter().any(|&x| !x.is_finite())
625 }
626 }
627}
628
629pub mod gradient_checkpointing {
631 use super::*;
632 use std::collections::VecDeque;
633
634 #[derive(Debug, Clone, PartialEq)]
636 pub enum CheckpointStrategy {
637 None,
639 Uniform {
641 interval: usize,
643 },
644 Logarithmic {
646 base: f64,
648 },
649 MemoryAware {
651 memory_threshold: f64,
653 },
654 Custom {
656 pattern: Vec<bool>,
658 },
659 }
660
661 #[derive(Debug)]
663 pub struct GradientCheckpointer<A: Float, D: Dimension> {
664 strategy: CheckpointStrategy,
666 checkpoints: std::collections::HashMap<usize, Array<A, D>>,
668 memory_tracker: MemoryTracker,
670 current_depth: usize,
672 max_depth: usize,
674 enabled: bool,
676 }
677
678 impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> GradientCheckpointer<A, D> {
679 pub fn new(strategy: CheckpointStrategy) -> Self {
681 Self {
682 strategy,
683 checkpoints: std::collections::HashMap::new(),
684 memory_tracker: MemoryTracker::new(),
685 current_depth: 0,
686 max_depth: 0,
687 enabled: true,
688 }
689 }
690
691 pub fn set_max_depth(&mut self, depth: usize) {
693 self.max_depth = depth;
694 }
695
696 pub fn set_enabled(&mut self, enabled: bool) {
698 self.enabled = enabled;
699 }
700
701 pub fn should_checkpoint(&self, depth: usize) -> bool {
703 if !self.enabled || self.max_depth == 0 {
704 return false;
705 }
706
707 match self.strategy {
708 CheckpointStrategy::None => false,
709 CheckpointStrategy::Uniform { interval } => depth.is_multiple_of(interval),
710 CheckpointStrategy::Logarithmic { base } => {
711 let log_depth = (depth as f64).log(base).floor() as usize;
712 depth == base.powi(log_depth as i32) as usize
713 }
714 CheckpointStrategy::MemoryAware { memory_threshold } => {
715 self.memory_tracker.usage_ratio() > memory_threshold
716 }
717 CheckpointStrategy::Custom { ref pattern } => {
718 if depth < pattern.len() {
719 pattern[depth]
720 } else {
721 false
722 }
723 }
724 }
725 }
726
727 pub fn store_checkpoint(&mut self, depth: usize, activation: Array<A, D>) {
729 if self.should_checkpoint(depth) {
730 let memory_size = activation.len() * std::mem::size_of::<A>();
731 self.memory_tracker.add_allocation(memory_size);
732 self.checkpoints.insert(depth, activation);
733 }
734 }
735
736 pub fn get_checkpoint(&self, depth: usize) -> Option<&Array<A, D>> {
738 self.checkpoints.get(&depth)
739 }
740
741 pub fn remove_checkpoint(&mut self, depth: usize) -> Option<Array<A, D>> {
743 if let Some(checkpoint) = self.checkpoints.remove(&depth) {
744 let memory_size = checkpoint.len() * std::mem::size_of::<A>();
745 self.memory_tracker.remove_allocation(memory_size);
746 Some(checkpoint)
747 } else {
748 None
749 }
750 }
751
752 pub fn clear_checkpoints(&mut self) {
754 self.checkpoints.clear();
755 self.memory_tracker.reset();
756 }
757
758 pub fn memory_usage(&self) -> MemoryUsage {
760 self.memory_tracker.usage()
761 }
762
763 pub fn optimize_strategy(&mut self, target_memoryusage: f64) {
765 let current_usage = self.memory_tracker.usage_ratio();
766
767 if current_usage > target_memoryusage {
768 self.strategy = match &self.strategy {
770 CheckpointStrategy::Uniform { interval } => CheckpointStrategy::Uniform {
771 interval: (interval / 2).max(1),
772 },
773 CheckpointStrategy::MemoryAware { .. } => CheckpointStrategy::MemoryAware {
774 memory_threshold: target_memoryusage * 0.8,
775 },
776 other => other.clone(),
777 };
778 } else if current_usage < target_memoryusage * 0.5 {
779 self.strategy = match &self.strategy {
781 CheckpointStrategy::Uniform { interval } => CheckpointStrategy::Uniform {
782 interval: interval * 2,
783 },
784 CheckpointStrategy::MemoryAware { .. } => CheckpointStrategy::MemoryAware {
785 memory_threshold: target_memoryusage * 1.2,
786 },
787 other => other.clone(),
788 };
789 }
790 }
791
792 pub fn checkpointed_forward<F, Output>(
794 &mut self,
795 depth: usize,
796 input: &Array<A, D>,
797 forward_fn: F,
798 ) -> Result<(Output, Option<Array<A, D>>)>
799 where
800 F: FnOnce(&Array<A, D>) -> Result<(Output, Array<A, D>)>,
801 {
802 self.current_depth = depth;
803
804 let (output, activation) = forward_fn(input)?;
806
807 let checkpoint = if self.should_checkpoint(depth) {
809 self.store_checkpoint(depth, activation.clone());
810 Some(activation)
811 } else {
812 None
813 };
814
815 Ok((output, checkpoint))
816 }
817
818 pub fn recompute_from_checkpoint<F>(
820 &self,
821 start_depth: usize,
822 target_depth: usize,
823 recompute_fn: F,
824 ) -> Result<Array<A, D>>
825 where
826 F: Fn(usize, &Array<A, D>) -> Result<Array<A, D>>,
827 {
828 let checkpoint_depth = (0..=start_depth)
830 .rev()
831 .find(|&d| self.checkpoints.contains_key(&d))
832 .ok_or_else(|| {
833 OptimError::InvalidConfig("No checkpoint found for recomputation".to_string())
834 })?;
835
836 let mut current_activation = self.checkpoints[&checkpoint_depth].clone();
837
838 for _depth in (checkpoint_depth + 1)..=target_depth {
840 current_activation = recompute_fn(_depth, ¤t_activation)?;
841 }
842
843 Ok(current_activation)
844 }
845 }
846
847 impl<A: Float, D: Dimension> Drop for GradientCheckpointer<A, D> {
848 fn drop(&mut self) {
863 self.memory_tracker.reset();
864 }
865 }
866
867 #[derive(Debug, Clone)]
869 pub struct MemoryTracker {
870 allocated_bytes: usize,
871 peak_bytes: usize,
872 total_system_memory: usize,
873 }
874
875 impl Default for MemoryTracker {
876 fn default() -> Self {
877 Self::new()
878 }
879 }
880
881 impl MemoryTracker {
882 pub fn new() -> Self {
884 Self {
885 allocated_bytes: 0,
886 peak_bytes: 0,
887 total_system_memory: Self::estimate_system_memory(),
888 }
889 }
890
891 pub fn add_allocation(&mut self, bytes: usize) {
893 self.allocated_bytes += bytes;
894 self.peak_bytes = self.peak_bytes.max(self.allocated_bytes);
895 super::GLOBAL_TRACKED_BYTES.fetch_add(bytes, super::Ordering::Relaxed);
898 }
899
900 pub fn remove_allocation(&mut self, bytes: usize) {
902 let removed = bytes.min(self.allocated_bytes);
903 self.allocated_bytes -= removed;
904 super::GLOBAL_TRACKED_BYTES.fetch_sub(removed, super::Ordering::Relaxed);
905 }
906
907 pub fn usage(&self) -> MemoryUsage {
909 MemoryUsage {
910 current_bytes: self.allocated_bytes,
911 peak_bytes: self.peak_bytes,
912 total_system_bytes: self.total_system_memory,
913 }
914 }
915
916 pub fn usage_ratio(&self) -> f64 {
918 if self.total_system_memory == 0 {
919 0.0
920 } else {
921 self.allocated_bytes as f64 / self.total_system_memory as f64
922 }
923 }
924
925 pub fn reset(&mut self) {
927 super::GLOBAL_TRACKED_BYTES.fetch_sub(self.allocated_bytes, super::Ordering::Relaxed);
930 self.allocated_bytes = 0;
931 self.peak_bytes = 0;
932 }
933
934 fn estimate_system_memory() -> usize {
938 super::total_system_memory_bytes()
939 }
940 }
941
942 #[derive(Debug, Clone, Copy)]
944 pub struct MemoryUsage {
945 pub current_bytes: usize,
947 pub peak_bytes: usize,
949 pub total_system_bytes: usize,
951 }
952
953 impl MemoryUsage {
954 pub fn current_ratio(&self) -> f64 {
956 if self.total_system_bytes == 0 {
957 0.0
958 } else {
959 self.current_bytes as f64 / self.total_system_bytes as f64
960 }
961 }
962
963 pub fn peak_ratio(&self) -> f64 {
965 if self.total_system_bytes == 0 {
966 0.0
967 } else {
968 self.peak_bytes as f64 / self.total_system_bytes as f64
969 }
970 }
971
972 pub fn format(&self) -> String {
974 format!(
975 "Current: {:.1} MB ({:.1}%), Peak: {:.1} MB ({:.1}%), Total: {:.1} MB",
976 self.current_bytes as f64 / (1024.0 * 1024.0),
977 self.current_ratio() * 100.0,
978 self.peak_bytes as f64 / (1024.0 * 1024.0),
979 self.peak_ratio() * 100.0,
980 self.total_system_bytes as f64 / (1024.0 * 1024.0)
981 )
982 }
983 }
984
985 #[derive(Debug)]
987 pub struct AutoCheckpointer<A: Float, D: Dimension> {
988 checkpointer: GradientCheckpointer<A, D>,
989 memory_history: VecDeque<f64>,
991 target_memoryratio: f64,
993 adaptation_frequency: usize,
995 step_count: usize,
997 }
998
999 impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> AutoCheckpointer<A, D> {
1000 pub fn new(_initial_strategy: CheckpointStrategy, target_memoryratio: f64) -> Self {
1002 Self {
1003 checkpointer: GradientCheckpointer::new(_initial_strategy),
1004 memory_history: VecDeque::with_capacity(100),
1005 target_memoryratio: target_memoryratio.clamp(0.1, 0.9),
1006 adaptation_frequency: 10,
1007 step_count: 0,
1008 }
1009 }
1010
1011 pub fn with_adaptation_frequency(mut self, frequency: usize) -> Self {
1013 self.adaptation_frequency = frequency.max(1);
1014 self
1015 }
1016
1017 pub fn auto_step<F, Output>(
1019 &mut self,
1020 depth: usize,
1021 input: &Array<A, D>,
1022 forward_fn: F,
1023 ) -> Result<(Output, Option<Array<A, D>>)>
1024 where
1025 F: FnOnce(&Array<A, D>) -> Result<(Output, Array<A, D>)>,
1026 {
1027 self.step_count += 1;
1028
1029 let result = self
1031 .checkpointer
1032 .checkpointed_forward(depth, input, forward_fn)?;
1033
1034 let current_usage = self.checkpointer.memory_usage().current_ratio();
1036 self.memory_history.push_back(current_usage);
1037 if self.memory_history.len() > 100 {
1038 self.memory_history.pop_front();
1039 }
1040
1041 if self.step_count.is_multiple_of(self.adaptation_frequency) {
1043 self.adapt_strategy();
1044 }
1045
1046 Ok(result)
1047 }
1048
1049 fn adapt_strategy(&mut self) {
1051 if self.memory_history.len() < 5 {
1052 return;
1053 }
1054
1055 let recent_avg = self.memory_history.iter().rev().take(10).sum::<f64>()
1057 / 10.0.min(self.memory_history.len() as f64);
1058
1059 let deviation = (recent_avg - self.target_memoryratio).abs();
1061 if deviation > 0.1 {
1062 self.checkpointer.optimize_strategy(self.target_memoryratio);
1063 }
1064 }
1065
1066 pub fn checkpointer(&self) -> &GradientCheckpointer<A, D> {
1068 &self.checkpointer
1069 }
1070
1071 pub fn checkpointer_mut(&mut self) -> &mut GradientCheckpointer<A, D> {
1073 &mut self.checkpointer
1074 }
1075
1076 pub fn get_memory_stats(&self) -> MemoryStats {
1078 let usage = self.checkpointer.memory_usage();
1079 let avg_usage = if self.memory_history.is_empty() {
1080 0.0
1081 } else {
1082 self.memory_history.iter().sum::<f64>() / self.memory_history.len() as f64
1083 };
1084
1085 MemoryStats {
1086 current_usage: usage.current_ratio(),
1087 peak_usage: usage.peak_ratio(),
1088 average_usage: avg_usage,
1089 target_usage: self.target_memoryratio,
1090 checkpoints_stored: self.checkpointer.checkpoints.len(),
1091 }
1092 }
1093 }
1094
1095 #[derive(Debug, Clone, Copy)]
1097 pub struct MemoryStats {
1098 pub current_usage: f64,
1100 pub peak_usage: f64,
1102 pub average_usage: f64,
1104 pub target_usage: f64,
1106 pub checkpoints_stored: usize,
1108 }
1109
1110 impl MemoryStats {
1111 pub fn is_within_target(&self, tolerance: f64) -> bool {
1113 (self.current_usage - self.target_usage).abs() <= tolerance
1114 }
1115
1116 pub fn efficiency_score(&self) -> f64 {
1118 if self.current_usage <= self.target_usage {
1119 self.current_usage / self.target_usage
1120 } else {
1121 self.target_usage / self.current_usage
1122 }
1123 }
1124 }
1125}
1126
1127pub mod adaptive {
1129 use super::*;
1130
1131 #[derive(Debug, Clone)]
1133 pub struct MemoryAwareBatchSizer {
1134 _initial_batchsize: usize,
1135 max_batch_size: usize,
1136 min_batch_size: usize,
1137 current_batch_size: usize,
1138 memory_threshold: f64, adaptation_factor: f64,
1140 }
1141
1142 impl MemoryAwareBatchSizer {
1143 pub fn new(_initial_batchsize: usize) -> Self {
1145 Self {
1146 _initial_batchsize,
1147 max_batch_size: _initial_batchsize * 4,
1148 min_batch_size: _initial_batchsize.max(1) / 4,
1149 current_batch_size: _initial_batchsize,
1150 memory_threshold: 0.8,
1151 adaptation_factor: 1.2,
1152 }
1153 }
1154
1155 pub fn with_memory_threshold(mut self, threshold: f64) -> Self {
1157 self.memory_threshold = threshold.clamp(0.1, 0.95);
1158 self
1159 }
1160
1161 pub fn with_adaptation_factor(mut self, factor: f64) -> Self {
1163 self.adaptation_factor = factor.max(1.0);
1164 self
1165 }
1166
1167 pub fn current_batch_size(&self) -> usize {
1169 self.current_batch_size
1170 }
1171
1172 pub fn adapt(&mut self, memory_usageratio: f64) {
1174 if memory_usageratio > self.memory_threshold {
1175 let new_size = (self.current_batch_size as f64 / self.adaptation_factor) as usize;
1177 self.current_batch_size = new_size.max(self.min_batch_size);
1178 } else if memory_usageratio < self.memory_threshold * 0.7 {
1179 let new_size = (self.current_batch_size as f64 * self.adaptation_factor) as usize;
1181 self.current_batch_size = new_size.min(self.max_batch_size);
1182 }
1183 }
1184
1185 pub fn reset(&mut self) {
1187 self.current_batch_size = self._initial_batchsize;
1188 }
1189 }
1190
1191 pub fn estimate_memory_usage<A, D>(arrays: &[&Array<A, D>]) -> usize
1193 where
1194 A: Sized,
1195 D: Dimension,
1196 {
1197 arrays
1198 .iter()
1199 .map(|arr| arr.len() * std::mem::size_of::<A>())
1200 .sum()
1201 }
1202
1203 pub fn get_memory_usage_ratio() -> f64 {
1213 let tracked = super::GLOBAL_TRACKED_BYTES.load(super::Ordering::Relaxed);
1214 let total = super::total_system_memory_bytes();
1215 if total == 0 {
1216 0.0
1217 } else {
1218 (tracked as f64 / total as f64).clamp(0.0, 1.0)
1219 }
1220 }
1221}
1222
1223pub use utils::{
1225 add_inplace, apply_inplace, clip_inplace, normalize_inplace, scale_inplace, subtract_inplace,
1226};
1227
1228pub use adaptive::*;
1230pub use fused::*;
1231pub use gradient_checkpointing::*;
1232pub use mixed_precision::*;
1233
1234#[cfg(test)]
1235mod tests {
1236 use super::*;
1237 use approx::assert_relative_eq;
1238 use scirs2_core::ndarray::Array1;
1239
1240 #[test]
1241 fn test_inplace_sgd() {
1242 let mut optimizer = InPlaceSGD::new(0.1);
1243 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1244 let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
1245
1246 optimizer
1247 .step_inplace(&mut params, &gradients)
1248 .expect("unwrap failed");
1249
1250 assert_relative_eq!(params[0], 0.99, epsilon = 1e-6);
1251 assert_relative_eq!(params[1], 1.98, epsilon = 1e-6);
1252 assert_relative_eq!(params[2], 2.97, epsilon = 1e-6);
1253 }
1254
1255 #[test]
1256 fn test_inplace_adam() {
1257 let mut optimizer = InPlaceAdam::new(0.001);
1258 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1259 let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
1260
1261 for _ in 0..5 {
1263 optimizer
1264 .step_inplace(&mut params, &gradients)
1265 .expect("unwrap failed");
1266 }
1267
1268 assert!(params[0] < 1.0);
1270 assert!(params[1] < 2.0);
1271 assert!(params[2] < 3.0);
1272 }
1273
1274 #[test]
1275 fn test_utils_scale_inplace() {
1276 let mut array = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1277 utils::scale_inplace(&mut array, 2.0);
1278
1279 assert_eq!(array.as_slice().expect("unwrap failed"), &[2.0, 4.0, 6.0]);
1280 }
1281
1282 #[test]
1283 fn test_utils_clip_inplace() {
1284 let mut array = Array1::from_vec(vec![0.5, 1.5, 2.5]);
1285 utils::clip_inplace(&mut array, 1.0, 2.0);
1286
1287 assert_eq!(array.as_slice().expect("unwrap failed"), &[1.0, 1.5, 2.0]);
1288 }
1289
1290 #[test]
1291 fn test_memory_efficiency() {
1292 let mut params = Array1::from_vec(vec![1.0; 1000]);
1294 let gradients = Array1::from_vec(vec![0.01; 1000]);
1295 let params_ptr = params.as_ptr();
1296
1297 let mut optimizer = InPlaceSGD::new(0.1);
1298 optimizer
1299 .step_inplace(&mut params, &gradients)
1300 .expect("unwrap failed");
1301
1302 assert_eq!(params_ptr, params.as_ptr());
1304 }
1305
1306 #[test]
1307 fn test_fused_adam_update() {
1308 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1309 let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
1310 let mut m = Array1::zeros(3);
1311 let mut v = Array1::zeros(3);
1312
1313 let config = fused::AdamConfig {
1314 lr: 0.01,
1315 beta1: 0.9,
1316 beta2: 0.999,
1317 epsilon: 1e-8,
1318 bias1: 0.1,
1319 bias2: 0.001,
1320 weight_decay: None,
1321 };
1322
1323 fused::fused_adam_update(&mut params, &gradients, &mut m, &mut v, config);
1324
1325 assert!(params[0] < 1.0);
1327 assert!(params[1] < 2.0);
1328 assert!(params[2] < 3.0);
1329
1330 assert!(m[0] > 0.0);
1332 assert!(v[0] > 0.0);
1333 }
1334
1335 #[test]
1336 fn test_fused_sgd_update() {
1337 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1338 let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
1339 let mut momentum_buf = Array1::zeros(3);
1340
1341 fused::fused_sgd_update(
1342 &mut params,
1343 &gradients,
1344 Some(&mut momentum_buf),
1345 0.1, 0.9, Some(0.01), 0.0, );
1350
1351 assert!(params[0] < 1.0);
1353 assert!(params[1] < 2.0);
1354 assert!(params[2] < 3.0);
1355 }
1356
1357 #[test]
1358 fn test_fused_gradient_clip_normalize() {
1359 let mut gradients = Array1::from_vec(vec![5.0, -3.0, 2.0]);
1360
1361 fused::fused_gradient_clip_normalize(
1362 &mut gradients,
1363 Some(2.0), Some(1.0), );
1366
1367 assert!(gradients.iter().all(|&x| x.abs() <= 1.0));
1369
1370 let norm = gradients.iter().map(|&x| x * x).sum::<f64>().sqrt();
1372 assert!(norm <= 2.0 + 1e-6);
1373 }
1374
1375 #[test]
1376 fn test_mixed_precision_loss_scaler() {
1377 let scaler = mixed_precision::LossScaler::new(65536.0);
1378
1379 let loss = 0.5;
1381 let scaled_loss = scaler.scale_loss(loss);
1382 assert_eq!(scaled_loss, 0.5 * 65536.0);
1383
1384 let mut gradients = Array1::from_vec(vec![65536.0, 131072.0]);
1386 scaler.unscale_gradients(&mut gradients);
1387 assert_relative_eq!(gradients[0], 1.0, epsilon = 1e-6);
1388 assert_relative_eq!(gradients[1], 2.0, epsilon = 1e-6);
1389
1390 let inf_gradients = Array1::from_vec(vec![f64::INFINITY, 1.0]);
1392 assert!(scaler.check_gradients(&inf_gradients));
1393
1394 let finite_gradients = Array1::from_vec(vec![1.0, 2.0]);
1395 assert!(!scaler.check_gradients(&finite_gradients));
1396 }
1397
1398 #[test]
1399 fn test_memory_aware_batch_sizer() {
1400 let mut sizer = adaptive::MemoryAwareBatchSizer::new(32)
1401 .with_memory_threshold(0.8)
1402 .with_adaptation_factor(1.3); assert_eq!(sizer.current_batch_size(), 32);
1405
1406 sizer.adapt(0.9);
1408 let reduced_size = sizer.current_batch_size();
1409 assert!(reduced_size < 32);
1410
1411 sizer.adapt(0.3);
1413 sizer.adapt(0.3); assert!(sizer.current_batch_size() >= 32);
1415
1416 sizer.reset();
1418 assert_eq!(sizer.current_batch_size(), 32);
1419 }
1420
1421 #[test]
1422 fn test_memory_estimation() {
1423 let array1 = Array1::from_vec(vec![1.0; 100]);
1424 let array2 = Array1::from_vec(vec![2.0; 200]);
1425
1426 let arrays = vec![&array1, &array2];
1427 let estimated_size = adaptive::estimate_memory_usage(&arrays);
1428
1429 let expected_size = 300 * std::mem::size_of::<f64>();
1431 assert_eq!(estimated_size, expected_size);
1432 }
1433
1434 #[test]
1438 fn memory_usage_ratio_reflects_tracked_bytes() {
1439 use gradient_checkpointing::MemoryTracker;
1440
1441 let before = adaptive::get_memory_usage_ratio();
1442 assert!(
1443 (0.0..=1.0).contains(&before),
1444 "ratio out of range: {before}"
1445 );
1446
1447 let mut tracker = MemoryTracker::new();
1448 let bytes = 256 * 1024 * 1024; tracker.add_allocation(bytes);
1450
1451 let during = adaptive::get_memory_usage_ratio();
1452 assert!(
1453 during > before,
1454 "ratio did not rise with tracked bytes (F82 regression): \
1455 before={before}, during={during}"
1456 );
1457 assert!((0.0..=1.0).contains(&during));
1458
1459 tracker.remove_allocation(bytes);
1460 let after = adaptive::get_memory_usage_ratio();
1461 assert!(
1462 (after - before).abs() < 1e-9,
1463 "tracked bytes were not released (F82 regression): \
1464 before={before}, after={after}"
1465 );
1466 }
1467
1468 #[test]
1475 fn dropping_checkpointer_releases_tracked_bytes() {
1476 let before = adaptive::get_memory_usage_ratio();
1477
1478 {
1479 let mut checkpointer: gradient_checkpointing::GradientCheckpointer<
1480 f64,
1481 scirs2_core::ndarray::Ix1,
1482 > = gradient_checkpointing::GradientCheckpointer::new(
1483 gradient_checkpointing::CheckpointStrategy::Uniform { interval: 1 },
1484 );
1485 checkpointer.set_max_depth(4);
1486 let activation = Array::from_vec(vec![1.0_f64; 1_000_000]); checkpointer.store_checkpoint(0, activation);
1488
1489 let during = adaptive::get_memory_usage_ratio();
1490 assert!(
1491 during > before,
1492 "ratio did not rise with a stored checkpoint: before={before}, during={during}"
1493 );
1494 }
1496
1497 let after = adaptive::get_memory_usage_ratio();
1498 assert!(
1499 (after - before).abs() < 1e-9,
1500 "GradientCheckpointer leaked tracked bytes on drop (regression): \
1501 before={before}, after={after}"
1502 );
1503 }
1504
1505 #[test]
1506 fn test_gradient_checkpointing_uniform() {
1507 let mut checkpointer: gradient_checkpointing::GradientCheckpointer<
1508 f64,
1509 scirs2_core::ndarray::Ix1,
1510 > = gradient_checkpointing::GradientCheckpointer::new(
1511 gradient_checkpointing::CheckpointStrategy::Uniform { interval: 2 },
1512 );
1513 checkpointer.set_max_depth(10);
1514
1515 assert!(checkpointer.should_checkpoint(0));
1517 assert!(!checkpointer.should_checkpoint(1));
1518 assert!(checkpointer.should_checkpoint(2));
1519 assert!(!checkpointer.should_checkpoint(3));
1520 assert!(checkpointer.should_checkpoint(4));
1521
1522 let activation = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1524 checkpointer.store_checkpoint(2, activation.clone());
1525
1526 let retrieved = checkpointer.get_checkpoint(2).expect("unwrap failed");
1528 assert_eq!(
1529 retrieved.as_slice().expect("unwrap failed"),
1530 activation.as_slice().expect("unwrap failed")
1531 );
1532
1533 assert!(checkpointer.get_checkpoint(1).is_none());
1535 }
1536
1537 #[test]
1538 fn test_gradient_checkpointing_logarithmic() {
1539 let mut checkpointer: gradient_checkpointing::GradientCheckpointer<
1540 f64,
1541 scirs2_core::ndarray::Ix1,
1542 > = gradient_checkpointing::GradientCheckpointer::new(
1543 gradient_checkpointing::CheckpointStrategy::Logarithmic { base: 2.0 },
1544 );
1545
1546 checkpointer.set_max_depth(10);
1548
1549 assert!(checkpointer.should_checkpoint(1));
1551 assert!(checkpointer.should_checkpoint(2));
1552 assert!(!checkpointer.should_checkpoint(3));
1553 assert!(checkpointer.should_checkpoint(4));
1554 assert!(!checkpointer.should_checkpoint(5));
1555 assert!(!checkpointer.should_checkpoint(6));
1556 assert!(!checkpointer.should_checkpoint(7));
1557 assert!(checkpointer.should_checkpoint(8));
1558 }
1559
1560 #[test]
1561 fn test_gradient_checkpointing_custom() {
1562 let pattern = vec![true, false, false, true, false];
1563 let mut checkpointer: gradient_checkpointing::GradientCheckpointer<
1564 f64,
1565 scirs2_core::ndarray::Ix1,
1566 > = gradient_checkpointing::GradientCheckpointer::new(
1567 gradient_checkpointing::CheckpointStrategy::Custom { pattern },
1568 );
1569
1570 checkpointer.set_max_depth(10);
1572
1573 assert!(checkpointer.should_checkpoint(0));
1575 assert!(!checkpointer.should_checkpoint(1));
1576 assert!(!checkpointer.should_checkpoint(2));
1577 assert!(checkpointer.should_checkpoint(3));
1578 assert!(!checkpointer.should_checkpoint(4));
1579 assert!(!checkpointer.should_checkpoint(5)); }
1581
1582 #[test]
1583 fn test_gradient_checkpointing_memory_tracking() {
1584 let mut checkpointer: gradient_checkpointing::GradientCheckpointer<
1585 f64,
1586 scirs2_core::ndarray::Ix1,
1587 > = gradient_checkpointing::GradientCheckpointer::new(
1588 gradient_checkpointing::CheckpointStrategy::Uniform { interval: 1 },
1589 );
1590 checkpointer.set_max_depth(5);
1591
1592 let activation1 = Array1::from_vec(vec![1.0; 100]);
1593 let activation2 = Array1::from_vec(vec![2.0; 200]);
1594
1595 checkpointer.store_checkpoint(0, activation1);
1596 let usage_after_first = checkpointer.memory_usage();
1597 assert!(usage_after_first.current_bytes > 0);
1598
1599 checkpointer.store_checkpoint(1, activation2);
1600 let usage_after_second = checkpointer.memory_usage();
1601 assert!(usage_after_second.current_bytes > usage_after_first.current_bytes);
1602
1603 checkpointer.remove_checkpoint(0);
1605 let usage_after_removal = checkpointer.memory_usage();
1606 assert!(usage_after_removal.current_bytes < usage_after_second.current_bytes);
1607 }
1608
1609 #[test]
1610 fn test_checkpointed_forward() {
1611 let mut checkpointer: gradient_checkpointing::GradientCheckpointer<
1612 f64,
1613 scirs2_core::ndarray::Ix1,
1614 > = gradient_checkpointing::GradientCheckpointer::new(
1615 gradient_checkpointing::CheckpointStrategy::Uniform { interval: 1 },
1616 );
1617 checkpointer.set_max_depth(5);
1618
1619 let input = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1620
1621 let forward_fn = |x: &Array1<f64>| -> Result<(f64, Array1<f64>)> {
1623 let output = x.sum();
1624 let activation = x.mapv(|val| val * 2.0);
1625 Ok((output, activation))
1626 };
1627
1628 let (output, checkpoint) = checkpointer
1629 .checkpointed_forward(0, &input, forward_fn)
1630 .expect("unwrap failed");
1631
1632 assert_eq!(output, 6.0); assert!(checkpoint.is_some());
1634 let checkpoint = checkpoint.expect("unwrap failed");
1635 assert_eq!(
1636 checkpoint.as_slice().expect("unwrap failed"),
1637 &[2.0, 4.0, 6.0]
1638 );
1639 }
1640
1641 #[test]
1642 fn test_recompute_from_checkpoint() {
1643 let mut checkpointer: gradient_checkpointing::GradientCheckpointer<
1644 f64,
1645 scirs2_core::ndarray::Ix1,
1646 > = gradient_checkpointing::GradientCheckpointer::new(
1647 gradient_checkpointing::CheckpointStrategy::Uniform { interval: 2 },
1648 );
1649 checkpointer.set_max_depth(10);
1650
1651 let checkpoint0 = Array1::from_vec(vec![1.0, 2.0]);
1653 let checkpoint2 = Array1::from_vec(vec![3.0, 4.0]);
1654
1655 checkpointer.store_checkpoint(0, checkpoint0);
1656 checkpointer.store_checkpoint(2, checkpoint2);
1657
1658 let recompute_fn =
1660 |_depth: usize, x: &Array1<f64>| -> Result<Array1<f64>> { Ok(x.mapv(|val| val + 1.0)) };
1661
1662 let result = checkpointer
1664 .recompute_from_checkpoint(2, 4, recompute_fn)
1665 .expect("unwrap failed");
1666
1667 assert_eq!(result.as_slice().expect("unwrap failed"), &[5.0, 6.0]);
1669 }
1670
1671 #[test]
1672 fn test_auto_checkpointer() {
1673 let mut auto_checkpointer: AutoCheckpointer<f64, scirs2_core::ndarray::Ix1> =
1674 gradient_checkpointing::AutoCheckpointer::new(
1675 gradient_checkpointing::CheckpointStrategy::Uniform { interval: 2 },
1676 0.6, );
1678
1679 let input = Array1::from_vec(vec![1.0, 2.0]);
1680
1681 let forward_fn = |x: &Array1<f64>| -> Result<(f64, Array1<f64>)> {
1683 let output = x.sum();
1684 let activation = x.clone();
1685 Ok((output, activation))
1686 };
1687
1688 for depth in 0..5 {
1690 let (output_checkpoint, _) = auto_checkpointer
1691 .auto_step(depth, &input, forward_fn)
1692 .expect("unwrap failed");
1693 assert_eq!(output_checkpoint, 3.0); }
1695
1696 let stats = auto_checkpointer.get_memory_stats();
1697 assert!(stats.target_usage > 0.0);
1698 }
1699
1700 #[test]
1701 fn test_memory_stats() {
1702 let stats = gradient_checkpointing::MemoryStats {
1703 current_usage: 0.5,
1704 peak_usage: 0.7,
1705 average_usage: 0.6,
1706 target_usage: 0.6,
1707 checkpoints_stored: 3,
1708 };
1709
1710 assert!(stats.is_within_target(0.1));
1711 assert!(!stats.is_within_target(0.01));
1712
1713 let efficiency = stats.efficiency_score();
1714 assert!(efficiency > 0.8 && efficiency <= 1.0);
1715 }
1716
1717 #[test]
1718 fn test_memory_usage_formatting() {
1719 let usage = gradient_checkpointing::MemoryUsage {
1720 current_bytes: 1024 * 1024, peak_bytes: 2 * 1024 * 1024, total_system_bytes: 8 * 1024 * 1024 * 1024, };
1724
1725 let formatted = usage.format();
1726 assert!(formatted.contains("1.0 MB"));
1727 assert!(formatted.contains("2.0 MB"));
1728 assert!(formatted.contains("8192.0 MB"));
1729
1730 assert_relative_eq!(usage.current_ratio(), 1.0 / 8192.0, epsilon = 1e-6);
1731 assert_relative_eq!(usage.peak_ratio(), 2.0 / 8192.0, epsilon = 1e-6);
1732 }
1733
1734 #[test]
1735 fn test_checkpointing_strategy_optimization() {
1736 let mut checkpointer: gradient_checkpointing::GradientCheckpointer<
1737 f64,
1738 scirs2_core::ndarray::Ix1,
1739 > = gradient_checkpointing::GradientCheckpointer::new(
1740 gradient_checkpointing::CheckpointStrategy::Uniform { interval: 4 },
1741 );
1742
1743 checkpointer.set_max_depth(10);
1745
1746 let checkpoint = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1748 checkpointer.store_checkpoint(0, checkpoint);
1749
1750 checkpointer.optimize_strategy(0.3); assert!(
1756 checkpointer.should_checkpoint(0)
1757 || checkpointer.should_checkpoint(1)
1758 || checkpointer.should_checkpoint(2)
1759 );
1760 }
1761
1762 #[test]
1763 fn test_checkpointing_disabled() {
1764 let mut checkpointer: gradient_checkpointing::GradientCheckpointer<
1765 f64,
1766 scirs2_core::ndarray::Ix1,
1767 > = gradient_checkpointing::GradientCheckpointer::new(
1768 gradient_checkpointing::CheckpointStrategy::Uniform { interval: 1 },
1769 );
1770 checkpointer.set_enabled(false);
1771
1772 assert!(!checkpointer.should_checkpoint(0));
1774 assert!(!checkpointer.should_checkpoint(1));
1775 assert!(!checkpointer.should_checkpoint(2));
1776 }
1777}