Skip to main content

optirs_core/memory_efficient/
mod.rs

1// Memory-efficient optimizers and utilities
2//
3// This module provides in-place parameter update capabilities and
4// memory-efficient implementations of optimization algorithms.
5
6use 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
14/// Process-wide running total of bytes currently tracked by every
15/// [`gradient_checkpointing::MemoryTracker`] in this process (F82).
16///
17/// This is what turns the module-level [`adaptive::get_memory_usage_ratio`]
18/// from a hardcoded `0.5` placeholder into a real measurement: each
19/// tracker feeds its allocations/deallocations here, so the stateless
20/// ratio function reports actual tracked bytes over the real system-memory
21/// budget instead of a fabricated constant.
22static GLOBAL_TRACKED_BYTES: AtomicUsize = AtomicUsize::new(0);
23
24/// Best-effort total physical system memory in bytes, read from the OS via
25/// a dependency-free (pure-Rust) path where one exists.
26///
27/// On Linux this parses `/proc/meminfo`; on platforms without a
28/// FFI-free API it falls back to a conservative 8 GiB. It is only ever
29/// used as the denominator of a usage ratio, so an approximate value
30/// degrades gracefully rather than producing a wrong absolute figure.
31fn 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                    // Format: `MemTotal:       16384000 kB`
40                    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
55/// Trait for in-place parameter updates
56pub trait InPlaceOptimizer<A: Float + ScalarOperand + Debug, D: Dimension> {
57    /// Update parameters in-place using the given gradients
58    ///
59    /// This method modifies the parameters directly rather than returning new arrays,
60    /// which can significantly reduce memory usage for large models.
61    fn step_inplace(&mut self, params: &mut Array<A, D>, gradients: &Array<A, D>) -> Result<()>;
62
63    /// Update multiple parameter arrays in-place
64    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/// Memory-efficient SGD optimizer with in-place updates
85#[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    /// Create a new in-place SGD optimizer
94    pub fn new(_learningrate: A) -> Self {
95        Self {
96            _learningrate,
97            momentum: A::zero(),
98            weight_decay: A::zero(),
99        }
100    }
101
102    /// Set momentum
103    pub fn with_momentum(mut self, momentum: A) -> Self {
104        self.momentum = momentum;
105        self
106    }
107
108    /// Set weight decay
109    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        // Apply weight decay if configured
120        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            // Simple gradient descent
126            params.zip_mut_with(gradients, |p, &g| {
127                *p = *p - self._learningrate * g;
128            });
129        }
130        Ok(())
131    }
132}
133
134/// Adam's two moment accumulators.
135///
136/// They are created together, always share the parameter shape, and are only
137/// ever absent before the first step -- so they live behind a *single*
138/// `Option` rather than two independent ones. That makes "one is initialised
139/// and the other is not" unrepresentable instead of a state the step function
140/// has to defend against with `expect`.
141#[derive(Debug, Clone)]
142struct AdamMoments<A: Float, D: Dimension> {
143    /// First moment estimate (momentum)
144    m: Array<A, D>,
145    /// Second moment estimate (RMSprop)
146    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/// Memory-efficient Adam optimizer with in-place updates
159#[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    /// Moment estimates, absent until the first step
168    moments: Option<AdamMoments<A, D>>,
169}
170
171impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> InPlaceAdam<A, D> {
172    /// Create a new in-place Adam optimizer
173    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    /// Set beta1 (momentum decay)
186    pub fn with_beta1(mut self, beta1: A) -> Self {
187        self.beta1 = beta1;
188        self
189    }
190
191    /// Set beta2 (RMSprop decay)
192    pub fn with_beta2(mut self, beta2: A) -> Self {
193        self.beta2 = beta2;
194        self
195    }
196
197    /// Set weight decay
198    pub fn with_weight_decay(mut self, weightdecay: A) -> Self {
199        self.weight_decay = weightdecay;
200        self
201    }
202
203    /// Set epsilon
204    pub fn with_epsilon(mut self, epsilon: A) -> Self {
205        self.epsilon = epsilon;
206        self
207    }
208
209    /// Reset optimizer state
210    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        // Initialize the moment estimates on the first step. `get_or_insert_with`
224        // yields them directly, so there is no "initialised a line ago, now
225        // unwrap it again" round-trip to defend with `expect`.
226        let moments = self
227            .moments
228            .get_or_insert_with(|| AdamMoments::zeros(params.raw_dim()));
229
230        // A caller that changes the parameter shape between steps would
231        // otherwise reach `zip_mut_with` with mismatched shapes and panic
232        // inside ndarray. Carrying stale moments across a reshape is not
233        // meaningful either, so report it instead of guessing.
234        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        // Apply weight decay if configured
246        let grad_with_decay = if self.weight_decay > A::zero() {
247            // Create temporary with weight decay
248            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        // Update biased first moment estimate
258        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        // Update biased second raw moment estimate
263        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        // Compute bias-corrected moments
268        let bias1 = A::one() - self.beta1.powi(self.t);
269        let bias2 = A::one() - self.beta2.powi(self.t);
270
271        // Update parameters in-place
272        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
286/// Utility functions for memory-efficient operations
287pub mod utils {
288    use super::*;
289
290    /// Apply a scalar operation in-place
291    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    /// Add arrays in-place (a += b)
300    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    /// Subtract arrays in-place (a -= b)
309    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    /// Apply element-wise operation in-place
318    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    /// Clip values in-place
328    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    /// Normalize array in-place (divide by its norm)
343    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
355/// Fused operations for maximum memory efficiency
356pub mod fused {
357    use super::*;
358
359    /// Adam optimizer configuration
360    #[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    /// Fused Adam update operation: combines momentum, variance, and parameter update in one pass
372    ///
373    /// This operation fuses all Adam computations into a single loop iteration,
374    /// reducing memory allocations and improving cache efficiency.
375    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            // Fused Adam with weight _decay
391            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                // Apply weight _decay to gradient
399                let g_with_decay = g + *p * wd;
400
401                // Update momentum
402                *m_val = config.beta1 * *m_val + one_minus_beta1 * g_with_decay;
403
404                // Update variance
405                *v_val = config.beta2 * *v_val + one_minus_beta2 * g_with_decay * g_with_decay;
406
407                // Bias-corrected estimates and parameter update
408                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            // Fused Adam without weight _decay
414            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                // Update momentum
422                *m_val = config.beta1 * *m_val + one_minus_beta1 * g;
423
424                // Update variance
425                *v_val = config.beta2 * *v_val + one_minus_beta2 * g * g;
426
427                // Bias-corrected estimates and parameter update
428                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    /// Fused SGD with momentum and weight decay
436    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                // Fused SGD with momentum and weight _decay
451                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                // Fused SGD with momentum only
460                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            // Fused SGD with weight _decay only
469            for (p, g) in params.iter_mut().zip(gradients.iter()) {
470                *p = *p - lr * (*g + *p * wd);
471            }
472        } else {
473            // Simple fused SGD
474            for (p, g) in params.iter_mut().zip(gradients.iter()) {
475                *p = *p - lr * *g;
476            }
477        }
478    }
479
480    /// Fused gradient clipping and normalization
481    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            // First pass: clip values
491            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            // Second pass: normalize if _norm exceeds max_norm
502            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    /// Fused parameter constraint application
518    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        // Apply value _bounds first
527        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        // Apply L2 norm _constraint
538        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
555/// Mixed-precision training support
556pub mod mixed_precision {
557    use super::*;
558
559    /// Loss scaler for mixed-precision training
560    #[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        /// Create a new loss scaler
571        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        /// Get current scale factor
582        pub fn get_scale(&self) -> f32 {
583            self.scale
584        }
585
586        /// Scale loss for backward pass
587        pub fn scale_loss(&self, loss: f32) -> f32 {
588            loss * self.scale
589        }
590
591        /// Unscale gradients after backward pass
592        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        /// Update scale based on gradient overflow detection
604        pub fn update(&mut self, foundinf: bool) {
605            self.steps_since_update += 1;
606
607            if foundinf {
608                // Reduce scale if overflow detected
609                self.scale *= self.backoff_factor;
610                self.steps_since_update = 0;
611            } else if self.steps_since_update >= self.growth_interval {
612                // Increase scale if no overflow for growth_interval steps
613                self.scale *= self.growth_factor;
614                self.steps_since_update = 0;
615            }
616        }
617
618        /// Check if gradients contain infinite or NaN values
619        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
629/// Gradient checkpointing for memory optimization
630pub mod gradient_checkpointing {
631    use super::*;
632    use std::collections::VecDeque;
633
634    /// Checkpointing strategy for gradient computation
635    #[derive(Debug, Clone, PartialEq)]
636    pub enum CheckpointStrategy {
637        /// No checkpointing (store all intermediate values)
638        None,
639        /// Uniform checkpointing (checkpoint every N layers)
640        Uniform {
641            /// Interval between checkpoints
642            interval: usize,
643        },
644        /// Logarithmic checkpointing (checkpoint at exponential intervals)
645        Logarithmic {
646            /// Base for exponential intervals
647            base: f64,
648        },
649        /// Memory-aware checkpointing (adaptive based on memory usage)
650        MemoryAware {
651            /// Memory threshold for triggering checkpoints
652            memory_threshold: f64,
653        },
654        /// Custom checkpointing pattern
655        Custom {
656            /// Pattern of checkpointing decisions
657            pattern: Vec<bool>,
658        },
659    }
660
661    /// Gradient checkpointing manager
662    #[derive(Debug)]
663    pub struct GradientCheckpointer<A: Float, D: Dimension> {
664        /// Checkpointing strategy
665        strategy: CheckpointStrategy,
666        /// Stored checkpoints (layer_index -> activation)
667        checkpoints: std::collections::HashMap<usize, Array<A, D>>,
668        /// Memory usage tracker
669        memory_tracker: MemoryTracker,
670        /// Current computation depth
671        current_depth: usize,
672        /// Maximum depth for this computation
673        max_depth: usize,
674        /// Whether checkpointing is enabled
675        enabled: bool,
676    }
677
678    impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> GradientCheckpointer<A, D> {
679        /// Create a new gradient checkpointer
680        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        /// Set the maximum computation depth
692        pub fn set_max_depth(&mut self, depth: usize) {
693            self.max_depth = depth;
694        }
695
696        /// Enable or disable checkpointing
697        pub fn set_enabled(&mut self, enabled: bool) {
698            self.enabled = enabled;
699        }
700
701        /// Check if we should checkpoint at the current depth
702        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        /// Store a checkpoint
728        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        /// Retrieve a checkpoint
737        pub fn get_checkpoint(&self, depth: usize) -> Option<&Array<A, D>> {
738            self.checkpoints.get(&depth)
739        }
740
741        /// Remove a checkpoint to free memory
742        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        /// Clear all checkpoints
753        pub fn clear_checkpoints(&mut self) {
754            self.checkpoints.clear();
755            self.memory_tracker.reset();
756        }
757
758        /// Get memory usage information
759        pub fn memory_usage(&self) -> MemoryUsage {
760            self.memory_tracker.usage()
761        }
762
763        /// Optimize checkpointing strategy based on memory usage
764        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                // Increase checkpointing frequency to reduce memory _usage
769                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                // Decrease checkpointing frequency to improve performance
780                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        /// Execute a checkpointed computation
793        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            // Execute forward computation
805            let (output, activation) = forward_fn(input)?;
806
807            // Decide whether to store checkpoint
808            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        /// Recompute activations from checkpoint
819        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            // Find the nearest checkpoint at or before start_depth
829            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            // Recompute forward from checkpoint to target _depth
839            for _depth in (checkpoint_depth + 1)..=target_depth {
840                current_activation = recompute_fn(_depth, &current_activation)?;
841            }
842
843            Ok(current_activation)
844        }
845    }
846
847    impl<A: Float, D: Dimension> Drop for GradientCheckpointer<A, D> {
848        /// Withdraw this checkpointer's tracked bytes from the process-wide
849        /// `GLOBAL_TRACKED_BYTES` counter when it goes out of scope.
850        ///
851        /// Without this, any checkpointer dropped without an explicit
852        /// `clear_checkpoints()` call first leaks its `allocated_bytes`
853        /// contribution into the global counter permanently: since
854        /// `adaptive::get_memory_usage_ratio` (F82) derives its numerator
855        /// from that counter, a long-running process that creates and drops
856        /// many checkpointers would see the reported ratio climb toward 1.0
857        /// forever regardless of real memory pressure, silently defeating
858        /// `CheckpointStrategy::MemoryAware`. `MemoryTracker::reset` is
859        /// idempotent (subtracts exactly `allocated_bytes`, which is 0 after
860        /// the first call), so this is safe even if `clear_checkpoints` already
861        /// ran.
862        fn drop(&mut self) {
863            self.memory_tracker.reset();
864        }
865    }
866
867    /// Memory usage tracking
868    #[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        /// Create a new memory tracker
883        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        /// Add an allocation
892        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            // Feed the process-wide counter that backs the module-level
896            // `get_memory_usage_ratio` (F82).
897            super::GLOBAL_TRACKED_BYTES.fetch_add(bytes, super::Ordering::Relaxed);
898        }
899
900        /// Remove an allocation
901        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        /// Get current memory usage
908        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        /// Get memory usage ratio (0.0 to 1.0)
917        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        /// Reset memory tracking
926        pub fn reset(&mut self) {
927            // Withdraw this tracker's contribution from the process-wide
928            // counter before clearing local state (F82).
929            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        /// Estimate total physical system memory, reading the real value
935        /// from the OS where a pure-Rust path exists (see
936        /// [`super::total_system_memory_bytes`]).
937        fn estimate_system_memory() -> usize {
938            super::total_system_memory_bytes()
939        }
940    }
941
942    /// Memory usage information
943    #[derive(Debug, Clone, Copy)]
944    pub struct MemoryUsage {
945        /// Current allocated bytes
946        pub current_bytes: usize,
947        /// Peak allocated bytes
948        pub peak_bytes: usize,
949        /// Total system memory bytes
950        pub total_system_bytes: usize,
951    }
952
953    impl MemoryUsage {
954        /// Get current usage as a ratio (0.0 to 1.0)
955        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        /// Get peak usage as a ratio (0.0 to 1.0)
964        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        /// Format as human-readable string
973        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    /// Automatic checkpointing manager for optimization workflows
986    #[derive(Debug)]
987    pub struct AutoCheckpointer<A: Float, D: Dimension> {
988        checkpointer: GradientCheckpointer<A, D>,
989        /// History of memory usage for adaptive optimization
990        memory_history: VecDeque<f64>,
991        /// Target memory usage ratio
992        target_memoryratio: f64,
993        /// Adaptation frequency (steps)
994        adaptation_frequency: usize,
995        /// Current step count
996        step_count: usize,
997    }
998
999    impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> AutoCheckpointer<A, D> {
1000        /// Create a new auto checkpointer
1001        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        /// Set adaptation frequency
1012        pub fn with_adaptation_frequency(mut self, frequency: usize) -> Self {
1013            self.adaptation_frequency = frequency.max(1);
1014            self
1015        }
1016
1017        /// Execute a step with automatic checkpointing
1018        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            // Execute checkpointed forward
1030            let result = self
1031                .checkpointer
1032                .checkpointed_forward(depth, input, forward_fn)?;
1033
1034            // Track memory usage
1035            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            // Adapt strategy periodically
1042            if self.step_count.is_multiple_of(self.adaptation_frequency) {
1043                self.adapt_strategy();
1044            }
1045
1046            Ok(result)
1047        }
1048
1049        /// Adapt checkpointing strategy based on memory usage history
1050        fn adapt_strategy(&mut self) {
1051            if self.memory_history.len() < 5 {
1052                return;
1053            }
1054
1055            // Calculate average memory usage over recent history
1056            let recent_avg = self.memory_history.iter().rev().take(10).sum::<f64>()
1057                / 10.0.min(self.memory_history.len() as f64);
1058
1059            // Optimize strategy if we're significantly off target
1060            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        /// Get checkpointer reference
1067        pub fn checkpointer(&self) -> &GradientCheckpointer<A, D> {
1068            &self.checkpointer
1069        }
1070
1071        /// Get mutable checkpointer reference
1072        pub fn checkpointer_mut(&mut self) -> &mut GradientCheckpointer<A, D> {
1073            &mut self.checkpointer
1074        }
1075
1076        /// Get memory usage statistics
1077        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    /// Memory usage statistics
1096    #[derive(Debug, Clone, Copy)]
1097    pub struct MemoryStats {
1098        /// Current memory usage ratio
1099        pub current_usage: f64,
1100        /// Peak memory usage ratio
1101        pub peak_usage: f64,
1102        /// Average memory usage ratio
1103        pub average_usage: f64,
1104        /// Target memory usage ratio
1105        pub target_usage: f64,
1106        /// Number of checkpoints currently stored
1107        pub checkpoints_stored: usize,
1108    }
1109
1110    impl MemoryStats {
1111        /// Check if memory usage is within target range
1112        pub fn is_within_target(&self, tolerance: f64) -> bool {
1113            (self.current_usage - self.target_usage).abs() <= tolerance
1114        }
1115
1116        /// Get efficiency score (how close to target without exceeding)
1117        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
1127/// Dynamic resource adaptation
1128pub mod adaptive {
1129    use super::*;
1130
1131    /// Memory-aware batch size adapter
1132    #[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, // Memory usage threshold (0.0 to 1.0)
1139        adaptation_factor: f64,
1140    }
1141
1142    impl MemoryAwareBatchSizer {
1143        /// Create a new memory-aware batch sizer
1144        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        /// Set memory threshold (0.0 to 1.0)
1156        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        /// Set adaptation factor
1162        pub fn with_adaptation_factor(mut self, factor: f64) -> Self {
1163            self.adaptation_factor = factor.max(1.0);
1164            self
1165        }
1166
1167        /// Get current batch size
1168        pub fn current_batch_size(&self) -> usize {
1169            self.current_batch_size
1170        }
1171
1172        /// Adapt batch size based on memory usage
1173        pub fn adapt(&mut self, memory_usageratio: f64) {
1174            if memory_usageratio > self.memory_threshold {
1175                // Reduce batch size if memory usage is high
1176                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                // Increase batch size if memory usage is low
1180                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        /// Reset to initial batch size
1186        pub fn reset(&mut self) {
1187            self.current_batch_size = self._initial_batchsize;
1188        }
1189    }
1190
1191    /// Memory usage estimator for arrays
1192    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    /// Get the current memory-usage ratio in `[0, 1]` (F82).
1204    ///
1205    /// This reports the process-wide total of bytes currently tracked by
1206    /// every [`super::gradient_checkpointing::MemoryTracker`] divided by the
1207    /// real system-memory budget (see
1208    /// `super::total_system_memory_bytes`). It replaces the previous
1209    /// hardcoded `0.5` placeholder: the numerator is the actual sum of
1210    /// tracked tensor bytes, so the value now moves with real allocations
1211    /// instead of being a fabricated constant.
1212    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
1223// Re-export utility functions at module level for convenience
1224pub use utils::{
1225    add_inplace, apply_inplace, clip_inplace, normalize_inplace, scale_inplace, subtract_inplace,
1226};
1227
1228// Re-export new modules
1229pub 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        // Multiple steps to see momentum effects
1262        for _ in 0..5 {
1263            optimizer
1264                .step_inplace(&mut params, &gradients)
1265                .expect("unwrap failed");
1266        }
1267
1268        // Verify parameters have been updated
1269        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        // Test that in-place operations don't allocate new arrays
1293        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        // Verify the same memory is being used
1303        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        // Verify parameters were updated
1326        assert!(params[0] < 1.0);
1327        assert!(params[1] < 2.0);
1328        assert!(params[2] < 3.0);
1329
1330        // Verify momentum and variance were updated
1331        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,        // lr
1346            0.9,        // momentum
1347            Some(0.01), // weight_decay
1348            0.0,        // dampening
1349        );
1350
1351        // Verify parameters were updated
1352        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), // max_norm
1364            Some(1.0), // clip_value
1365        );
1366
1367        // Verify values were clipped
1368        assert!(gradients.iter().all(|&x| x.abs() <= 1.0));
1369
1370        // Verify norm constraint
1371        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        // Test loss scaling
1380        let loss = 0.5;
1381        let scaled_loss = scaler.scale_loss(loss);
1382        assert_eq!(scaled_loss, 0.5 * 65536.0);
1383
1384        // Test gradient unscaling
1385        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        // Test overflow detection
1391        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); // Use smaller factor for more predictable behavior
1403
1404        assert_eq!(sizer.current_batch_size(), 32);
1405
1406        // High memory usage should reduce batch size
1407        sizer.adapt(0.9);
1408        let reduced_size = sizer.current_batch_size();
1409        assert!(reduced_size < 32);
1410
1411        // Low memory usage should increase batch size (multiple calls to ensure growth)
1412        sizer.adapt(0.3);
1413        sizer.adapt(0.3); // Call twice to ensure we exceed original size
1414        assert!(sizer.current_batch_size() >= 32);
1415
1416        // Reset should restore initial size
1417        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        // Should be roughly 300 * size_of::<f64>()
1430        let expected_size = 300 * std::mem::size_of::<f64>();
1431        assert_eq!(estimated_size, expected_size);
1432    }
1433
1434    /// F82: `get_memory_usage_ratio` must reflect real tracked bytes, not a
1435    /// hardcoded `0.5`. Tracking a known allocation must raise the ratio,
1436    /// and releasing it must return the ratio to its prior value.
1437    #[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; // 256 MiB
1449        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    /// Regression (found while implementing F82): a `GradientCheckpointer`
1469    /// dropped *without* an explicit `clear_checkpoints()` call must still
1470    /// release its tracked bytes from the process-wide counter, or
1471    /// `adaptive::get_memory_usage_ratio` leaks upward forever across the
1472    /// lifetime of the process (defeating `CheckpointStrategy::MemoryAware`
1473    /// for every checkpointer created afterward).
1474    #[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]); // ~8 MB
1487            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            // `checkpointer` drops here without calling `clear_checkpoints()`.
1495        }
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        // Should checkpoint at depths 0, 2, 4, 6, 8
1516        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        // Store a checkpoint
1523        let activation = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1524        checkpointer.store_checkpoint(2, activation.clone());
1525
1526        // Retrieve checkpoint
1527        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        // Non-checkpointed depth should return None
1534        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        // Set max depth to enable checkpointing
1547        checkpointer.set_max_depth(10);
1548
1549        // Should checkpoint at powers of 2: 1, 2, 4, 8, 16...
1550        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        // Set max depth to enable checkpointing
1571        checkpointer.set_max_depth(10);
1572
1573        // Should follow the custom pattern
1574        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)); // Beyond pattern length
1580    }
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        // Remove first checkpoint
1604        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        // Simple forward function that doubles the input
1622        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); // 1 + 2 + 3
1633        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        // Store checkpoints at depths 0, 2, 4
1652        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        // Recompute function that adds 1 to each element
1659        let recompute_fn =
1660            |_depth: usize, x: &Array1<f64>| -> Result<Array1<f64>> { Ok(x.mapv(|val| val + 1.0)) };
1661
1662        // Recompute from checkpoint 2 to depth 4
1663        let result = checkpointer
1664            .recompute_from_checkpoint(2, 4, recompute_fn)
1665            .expect("unwrap failed");
1666
1667        // Should be [3,4] + 1 + 1 = [5,6]
1668        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, // target 60% memory usage
1677            );
1678
1679        let input = Array1::from_vec(vec![1.0, 2.0]);
1680
1681        // Simple forward function
1682        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        // Execute several steps
1689        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); // 1 + 2
1694        }
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,                 // 1 MB
1721            peak_bytes: 2 * 1024 * 1024,                // 2 MB
1722            total_system_bytes: 8 * 1024 * 1024 * 1024, // 8 GB
1723        };
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        // Set max depth to enable checkpointing
1744        checkpointer.set_max_depth(10);
1745
1746        // Add some memory usage first to trigger optimization
1747        let checkpoint = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1748        checkpointer.store_checkpoint(0, checkpoint);
1749
1750        // Simulate high memory usage - should reduce interval
1751        checkpointer.optimize_strategy(0.3); // Target 30% usage
1752
1753        // Check that strategy was adapted (should checkpoint more frequently)
1754        // With interval 4, should checkpoint at 0, 4, 8... but optimization might change this
1755        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        // Should not checkpoint when disabled
1773        assert!(!checkpointer.should_checkpoint(0));
1774        assert!(!checkpointer.should_checkpoint(1));
1775        assert!(!checkpointer.should_checkpoint(2));
1776    }
1777}