Skip to main content

optirs_core/gradient_accumulation/
mod.rs

1// Gradient accumulation for large batch training
2//
3// This module provides utilities for accumulating gradients across multiple
4// micro-batches to simulate larger batch sizes without increasing memory usage.
5
6use crate::error::{OptimError, Result};
7use crate::utils::try_scalar;
8use scirs2_core::ndarray::{Array, Dimension, ScalarOperand, Zip};
9use scirs2_core::numeric::Float;
10use std::fmt::Debug;
11
12/// Type alias for adaptive step conditions
13pub type AdaptiveStepCondition = Box<dyn Fn(usize) -> bool>;
14
15/// Gradient accumulation mode
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum AccumulationMode {
18    /// Sum gradients (standard accumulation)
19    Sum,
20    /// Average gradients (normalize by number of accumulations)
21    Average,
22}
23
24/// Gradient accumulator for micro-batch training
25#[derive(Debug)]
26pub struct GradientAccumulator<A: Float, D: Dimension> {
27    /// Accumulated gradients
28    accumulated_gradients: Vec<Array<A, D>>,
29    /// Number of accumulation steps taken
30    accumulation_count: usize,
31    /// Target number of accumulations before update
32    target_accumulations: usize,
33    /// Accumulation mode
34    mode: AccumulationMode,
35    /// Whether accumulator has been initialized
36    initialized: bool,
37}
38
39impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> GradientAccumulator<A, D> {
40    /// Create a new gradient accumulator
41    pub fn new(_targetaccumulations: usize, mode: AccumulationMode) -> Self {
42        Self {
43            accumulated_gradients: Vec::new(),
44            accumulation_count: 0,
45            target_accumulations: _targetaccumulations,
46            mode,
47            initialized: false,
48        }
49    }
50
51    /// Initialize accumulator with gradient shapes
52    pub fn initialize(&mut self, gradients: &[Array<A, D>]) -> Result<()> {
53        if self.initialized {
54            return Err(OptimError::InvalidConfig(
55                "Accumulator already initialized".to_string(),
56            ));
57        }
58
59        self.accumulated_gradients = gradients
60            .iter()
61            .map(|g| Array::zeros(g.raw_dim()))
62            .collect();
63
64        self.initialized = true;
65        Ok(())
66    }
67
68    /// Accumulate gradients from a micro-batch
69    pub fn accumulate(&mut self, gradients: &[Array<A, D>]) -> Result<()> {
70        if !self.initialized {
71            self.initialize(gradients)?;
72        }
73
74        if gradients.len() != self.accumulated_gradients.len() {
75            return Err(OptimError::DimensionMismatch(format!(
76                "Expected {} gradient arrays, got {}",
77                self.accumulated_gradients.len(),
78                gradients.len()
79            )));
80        }
81
82        // Accumulate gradients
83        for (acc_grad, micro_grad) in self.accumulated_gradients.iter_mut().zip(gradients.iter()) {
84            if acc_grad.raw_dim() != micro_grad.raw_dim() {
85                return Err(OptimError::DimensionMismatch(
86                    "Gradient dimensions don't match".to_string(),
87                ));
88            }
89
90            Zip::from(acc_grad).and(micro_grad).for_each(|acc, &micro| {
91                *acc = *acc + micro;
92            });
93        }
94
95        self.accumulation_count += 1;
96        Ok(())
97    }
98
99    /// Check if accumulation is complete
100    pub fn is_ready(&self) -> bool {
101        self.accumulation_count >= self.target_accumulations
102    }
103
104    /// Get accumulated gradients and reset accumulator
105    pub fn get_and_reset(&mut self) -> Result<Vec<Array<A, D>>> {
106        if !self.is_ready() {
107            return Err(OptimError::InvalidConfig(format!(
108                "Accumulation not ready: {}/{} steps completed",
109                self.accumulation_count, self.target_accumulations
110            )));
111        }
112
113        let mut result = self.accumulated_gradients.clone();
114
115        // Apply accumulation mode
116        match self.mode {
117            AccumulationMode::Sum => {
118                // Gradients are already summed, nothing to do
119            }
120            AccumulationMode::Average => {
121                let scale = A::one() / try_scalar::<A, _>(self.accumulation_count)?;
122                for grad in &mut result {
123                    grad.mapv_inplace(|x| x * scale);
124                }
125            }
126        }
127
128        // Reset accumulator
129        self.reset();
130
131        Ok(result)
132    }
133
134    /// Reset accumulator state
135    pub fn reset(&mut self) {
136        for grad in &mut self.accumulated_gradients {
137            grad.fill(A::zero());
138        }
139        self.accumulation_count = 0;
140    }
141
142    /// Get current accumulation count
143    pub fn accumulation_count(&self) -> usize {
144        self.accumulation_count
145    }
146
147    /// Get target accumulation count
148    pub fn target_accumulations(&self) -> usize {
149        self.target_accumulations
150    }
151
152    /// Set new target accumulation count
153    pub fn set_target_accumulations(&mut self, target: usize) {
154        self.target_accumulations = target;
155    }
156
157    /// Get accumulation mode
158    pub fn mode(&self) -> AccumulationMode {
159        self.mode
160    }
161
162    /// Set accumulation mode
163    pub fn set_mode(&mut self, mode: AccumulationMode) {
164        self.mode = mode;
165    }
166
167    /// Check if accumulator is initialized
168    pub fn is_initialized(&self) -> bool {
169        self.initialized
170    }
171
172    /// Get current progress as a fraction (0.0 to 1.0)
173    pub fn progress(&self) -> f64 {
174        if self.target_accumulations == 0 {
175            1.0
176        } else {
177            self.accumulation_count as f64 / self.target_accumulations as f64
178        }
179    }
180}
181
182/// Variable accumulation scheduler
183pub struct VariableAccumulator<A: Float, D: Dimension> {
184    /// Base accumulator
185    accumulator: GradientAccumulator<A, D>,
186    /// Variable accumulation steps based on conditions
187    adaptive_steps: Vec<(AdaptiveStepCondition, usize)>,
188    /// Current step count
189    step_count: usize,
190}
191
192impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> VariableAccumulator<A, D> {
193    /// Create a new variable accumulator
194    pub fn new(_initialtarget: usize, mode: AccumulationMode) -> Self {
195        Self {
196            accumulator: GradientAccumulator::new(_initialtarget, mode),
197            adaptive_steps: Vec::new(),
198            step_count: 0,
199        }
200    }
201
202    /// Add a condition-based accumulation rule
203    pub fn add_adaptive_rule<F>(&mut self, condition: F, accumulationsteps: usize)
204    where
205        F: Fn(usize) -> bool + 'static,
206    {
207        self.adaptive_steps
208            .push((Box::new(condition), accumulationsteps));
209    }
210
211    /// Update target accumulations based on current step
212    fn update_target(&mut self) {
213        for (condition, steps) in &self.adaptive_steps {
214            if condition(self.step_count) {
215                self.accumulator.set_target_accumulations(*steps);
216                break;
217            }
218        }
219    }
220
221    /// Accumulate gradients with adaptive targeting
222    pub fn accumulate(&mut self, gradients: &[Array<A, D>]) -> Result<()> {
223        self.update_target();
224        self.accumulator.accumulate(gradients)
225    }
226
227    /// Check if accumulation is ready
228    pub fn is_ready(&self) -> bool {
229        self.accumulator.is_ready()
230    }
231
232    /// Get accumulated gradients and advance step
233    pub fn get_and_step(&mut self) -> Result<Vec<Array<A, D>>> {
234        let result = self.accumulator.get_and_reset()?;
235        self.step_count += 1;
236        Ok(result)
237    }
238
239    /// Get current step count
240    pub fn step_count(&self) -> usize {
241        self.step_count
242    }
243
244    /// Get underlying accumulator
245    pub fn accumulator(&self) -> &GradientAccumulator<A, D> {
246        &self.accumulator
247    }
248
249    /// Get mutable reference to underlying accumulator
250    pub fn accumulator_mut(&mut self) -> &mut GradientAccumulator<A, D> {
251        &mut self.accumulator
252    }
253}
254
255/// Micro-batch trainer that uses gradient accumulation
256#[derive(Debug)]
257pub struct MicroBatchTrainer<A: Float, D: Dimension> {
258    /// Gradient accumulator
259    accumulator: GradientAccumulator<A, D>,
260    /// Micro-batch size
261    micro_batch_size: usize,
262    /// Effective batch size (micro_batch_size * accumulation_steps)
263    effective_batch_size: usize,
264}
265
266impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> MicroBatchTrainer<A, D> {
267    /// Create a new micro-batch trainer
268    pub fn new(
269        micro_batch_size: usize,
270        effective_batch_size: usize,
271        mode: AccumulationMode,
272    ) -> Result<Self> {
273        if effective_batch_size < micro_batch_size {
274            return Err(OptimError::InvalidConfig(
275                "Effective batch _size must be >= micro batch _size".to_string(),
276            ));
277        }
278
279        let accumulation_steps = effective_batch_size / micro_batch_size;
280        let accumulator = GradientAccumulator::new(accumulation_steps, mode);
281
282        Ok(Self {
283            accumulator,
284            micro_batch_size,
285            effective_batch_size,
286        })
287    }
288
289    /// Process a micro-batch and accumulate gradients
290    pub fn process_micro_batch(&mut self, gradients: &[Array<A, D>]) -> Result<()> {
291        self.accumulator.accumulate(gradients)
292    }
293
294    /// Check if ready for optimizer step
295    pub fn ready_for_step(&self) -> bool {
296        self.accumulator.is_ready()
297    }
298
299    /// Get accumulated gradients for optimizer step
300    pub fn get_accumulated_gradients(&mut self) -> Result<Vec<Array<A, D>>> {
301        self.accumulator.get_and_reset()
302    }
303
304    /// Get micro-batch size
305    pub fn micro_batch_size(&self) -> usize {
306        self.micro_batch_size
307    }
308
309    /// Get effective batch size
310    pub fn effective_batch_size(&self) -> usize {
311        self.effective_batch_size
312    }
313
314    /// Get accumulation progress
315    pub fn progress(&self) -> f64 {
316        self.accumulator.progress()
317    }
318
319    /// Set new effective batch size
320    pub fn set_effective_batch_size(&mut self, effective_batchsize: usize) -> Result<()> {
321        if effective_batchsize < self.micro_batch_size {
322            return Err(OptimError::InvalidConfig(
323                "Effective batch _size must be >= micro batch _size".to_string(),
324            ));
325        }
326
327        self.effective_batch_size = effective_batchsize;
328        let accumulation_steps = effective_batchsize / self.micro_batch_size;
329        self.accumulator
330            .set_target_accumulations(accumulation_steps);
331        Ok(())
332    }
333}
334
335/// Utility functions for gradient accumulation
336pub mod utils {
337    use super::*;
338
339    /// Calculate optimal micro-batch size given memory constraints
340    pub fn calculate_micro_batch_size(
341        total_batch_size: usize,
342        max_memory_mb: usize,
343        param_count: usize,
344        bytes_per_param: usize,
345    ) -> usize {
346        // Estimate memory usage per sample
347        let memory_per_sample = param_count * bytes_per_param * 3; // params + grads + activations
348        let max_samples = (max_memory_mb * 1_000_000) / memory_per_sample;
349
350        // Choose micro-batch _size that divides total batch _size evenly
351        let mut micro_batch_size = max_samples.min(total_batch_size);
352        while !total_batch_size.is_multiple_of(micro_batch_size) && micro_batch_size > 1 {
353            micro_batch_size -= 1;
354        }
355
356        micro_batch_size.max(1)
357    }
358
359    /// Calculate accumulation steps needed
360    pub fn calculate_accumulation_steps(
361        _total_batch_size: usize,
362        micro_batch_size: usize,
363    ) -> usize {
364        _total_batch_size.div_ceil(micro_batch_size) // Ceiling division
365    }
366
367    /// Validate gradient accumulation configuration
368    pub fn validate_config(
369        micro_batch_size: usize,
370        effective_batch_size: usize,
371        accumulation_steps: usize,
372    ) -> Result<()> {
373        if micro_batch_size == 0 {
374            return Err(OptimError::InvalidConfig(
375                "Micro batch _size must be > 0".to_string(),
376            ));
377        }
378
379        if effective_batch_size == 0 {
380            return Err(OptimError::InvalidConfig(
381                "Effective batch _size must be > 0".to_string(),
382            ));
383        }
384
385        if accumulation_steps == 0 {
386            return Err(OptimError::InvalidConfig(
387                "Accumulation _steps must be > 0".to_string(),
388            ));
389        }
390
391        if effective_batch_size != micro_batch_size * accumulation_steps {
392            return Err(OptimError::InvalidConfig(format!(
393                "Effective batch _size ({}) != micro batch _size ({}) * accumulation _steps ({})",
394                effective_batch_size, micro_batch_size, accumulation_steps
395            )));
396        }
397
398        Ok(())
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use approx::assert_relative_eq;
406    use scirs2_core::ndarray::Array1;
407
408    #[test]
409    fn test_gradient_accumulator_sum() {
410        let mut accumulator = GradientAccumulator::new(3, AccumulationMode::Sum);
411
412        // First micro-batch
413        let grad1 = vec![Array1::from_vec(vec![1.0, 2.0, 3.0])];
414        accumulator.accumulate(&grad1).expect("unwrap failed");
415        assert!(!accumulator.is_ready());
416
417        // Second micro-batch
418        let grad2 = vec![Array1::from_vec(vec![2.0, 3.0, 4.0])];
419        accumulator.accumulate(&grad2).expect("unwrap failed");
420        assert!(!accumulator.is_ready());
421
422        // Third micro-batch
423        let grad3 = vec![Array1::from_vec(vec![1.0, 1.0, 1.0])];
424        accumulator.accumulate(&grad3).expect("unwrap failed");
425        assert!(accumulator.is_ready());
426
427        // Get accumulated gradients
428        let result = accumulator.get_and_reset().expect("unwrap failed");
429        assert_eq!(result.len(), 1);
430        assert_eq!(
431            result[0].as_slice().expect("unwrap failed"),
432            &[4.0, 6.0, 8.0]
433        ); // Sum of all gradients
434
435        // Should be reset
436        assert!(!accumulator.is_ready());
437        assert_eq!(accumulator.accumulation_count(), 0);
438    }
439
440    #[test]
441    fn test_gradient_accumulator_average() {
442        let mut accumulator = GradientAccumulator::new(2, AccumulationMode::Average);
443
444        let grad1 = vec![Array1::from_vec(vec![2.0, 4.0])];
445        let grad2 = vec![Array1::from_vec(vec![4.0, 2.0])];
446
447        accumulator.accumulate(&grad1).expect("unwrap failed");
448        accumulator.accumulate(&grad2).expect("unwrap failed");
449
450        let result = accumulator.get_and_reset().expect("unwrap failed");
451        assert_eq!(result[0].as_slice().expect("unwrap failed"), &[3.0, 3.0]); // Average of gradients
452    }
453
454    #[test]
455    fn test_variable_accumulator() {
456        let mut var_accumulator = VariableAccumulator::new(2, AccumulationMode::Sum);
457
458        // Add rule: if step > 5, use 4 accumulation steps
459        var_accumulator.add_adaptive_rule(|step| step > 5, 4);
460
461        // First few steps should use 2 accumulations
462        let grad = vec![Array1::from_vec(vec![1.0])];
463        var_accumulator.accumulate(&grad).expect("unwrap failed");
464        var_accumulator.accumulate(&grad).expect("unwrap failed");
465        assert!(var_accumulator.is_ready());
466
467        let _result = var_accumulator.get_and_step().expect("unwrap failed");
468
469        // Simulate more steps to trigger adaptive rule
470        for _ in 0..6 {
471            var_accumulator.accumulate(&grad).expect("unwrap failed");
472            var_accumulator.accumulate(&grad).expect("unwrap failed");
473            if var_accumulator.is_ready() {
474                var_accumulator.get_and_step().expect("unwrap failed");
475            }
476        }
477
478        // Now should require 4 accumulations
479        assert_eq!(var_accumulator.accumulator().target_accumulations(), 4);
480    }
481
482    #[test]
483    fn test_micro_batch_trainer() {
484        let mut trainer = MicroBatchTrainer::new(
485            2, // micro batch size
486            6, // effective batch size
487            AccumulationMode::Sum,
488        )
489        .expect("unwrap failed");
490
491        assert_eq!(trainer.micro_batch_size(), 2);
492        assert_eq!(trainer.effective_batch_size(), 6);
493
494        let grad = vec![Array1::from_vec(vec![1.0, 1.0])];
495
496        // Process 3 micro-batches (to reach effective batch size of 6)
497        trainer.process_micro_batch(&grad).expect("unwrap failed");
498        assert!(!trainer.ready_for_step());
499
500        trainer.process_micro_batch(&grad).expect("unwrap failed");
501        assert!(!trainer.ready_for_step());
502
503        trainer.process_micro_batch(&grad).expect("unwrap failed");
504        assert!(trainer.ready_for_step());
505
506        let result = trainer.get_accumulated_gradients().expect("unwrap failed");
507        assert_eq!(result[0].as_slice().expect("unwrap failed"), &[3.0, 3.0]); // Sum of 3 micro-batches
508    }
509
510    #[test]
511    fn test_calculate_micro_batch_size() {
512        let micro_batch = utils::calculate_micro_batch_size(
513            128,  // total batch size
514            100,  // max memory MB
515            1000, // param count
516            8,    // bytes per param (f64)
517        );
518
519        // Should return a size that divides 128 evenly
520        assert!(128 % micro_batch == 0);
521        assert!(micro_batch > 0);
522    }
523
524    #[test]
525    fn test_accumulation_steps_calculation() {
526        assert_eq!(utils::calculate_accumulation_steps(128, 32), 4);
527        assert_eq!(utils::calculate_accumulation_steps(100, 32), 4); // Ceiling division
528        assert_eq!(utils::calculate_accumulation_steps(96, 32), 3);
529    }
530
531    #[test]
532    fn test_config_validation() {
533        // Valid config
534        utils::validate_config(32, 128, 4).expect("unwrap failed");
535
536        // Invalid: micro batch size is 0
537        assert!(utils::validate_config(0, 128, 4).is_err());
538
539        // Invalid: sizes don't match
540        assert!(utils::validate_config(32, 100, 4).is_err());
541    }
542
543    #[test]
544    fn test_accumulator_progress() {
545        let mut accumulator = GradientAccumulator::new(4, AccumulationMode::Sum);
546
547        assert_relative_eq!(accumulator.progress(), 0.0);
548
549        let grad = vec![Array1::from_vec(vec![1.0])];
550
551        accumulator.accumulate(&grad).expect("unwrap failed");
552        assert_relative_eq!(accumulator.progress(), 0.25);
553
554        accumulator.accumulate(&grad).expect("unwrap failed");
555        assert_relative_eq!(accumulator.progress(), 0.5);
556
557        accumulator.accumulate(&grad).expect("unwrap failed");
558        assert_relative_eq!(accumulator.progress(), 0.75);
559
560        accumulator.accumulate(&grad).expect("unwrap failed");
561        assert_relative_eq!(accumulator.progress(), 1.0);
562    }
563
564    #[test]
565    fn test_dimension_mismatch_error() {
566        let mut accumulator = GradientAccumulator::new(2, AccumulationMode::Sum);
567
568        let grad1 = vec![Array1::from_vec(vec![1.0, 2.0])];
569        accumulator.accumulate(&grad1).expect("unwrap failed");
570
571        // Try to accumulate gradients with different dimensions
572        let grad2 = vec![Array1::from_vec(vec![1.0, 2.0, 3.0])];
573        assert!(accumulator.accumulate(&grad2).is_err());
574
575        // Try to accumulate different number of arrays
576        let grad3 = vec![
577            Array1::from_vec(vec![1.0, 2.0]),
578            Array1::from_vec(vec![3.0, 4.0]),
579        ];
580        assert!(accumulator.accumulate(&grad3).is_err());
581    }
582
583    #[test]
584    fn test_get_before_ready_error() {
585        let mut accumulator = GradientAccumulator::new(3, AccumulationMode::Sum);
586
587        let grad = vec![Array1::from_vec(vec![1.0])];
588        accumulator.accumulate(&grad).expect("unwrap failed");
589
590        // Try to get gradients before accumulation is complete
591        assert!(accumulator.get_and_reset().is_err());
592    }
593}