optirs_core/gradient_accumulation/
mod.rs1use 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
12pub type AdaptiveStepCondition = Box<dyn Fn(usize) -> bool>;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum AccumulationMode {
18 Sum,
20 Average,
22}
23
24#[derive(Debug)]
26pub struct GradientAccumulator<A: Float, D: Dimension> {
27 accumulated_gradients: Vec<Array<A, D>>,
29 accumulation_count: usize,
31 target_accumulations: usize,
33 mode: AccumulationMode,
35 initialized: bool,
37}
38
39impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> GradientAccumulator<A, D> {
40 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 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 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 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, µ| {
91 *acc = *acc + micro;
92 });
93 }
94
95 self.accumulation_count += 1;
96 Ok(())
97 }
98
99 pub fn is_ready(&self) -> bool {
101 self.accumulation_count >= self.target_accumulations
102 }
103
104 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 match self.mode {
117 AccumulationMode::Sum => {
118 }
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 self.reset();
130
131 Ok(result)
132 }
133
134 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 pub fn accumulation_count(&self) -> usize {
144 self.accumulation_count
145 }
146
147 pub fn target_accumulations(&self) -> usize {
149 self.target_accumulations
150 }
151
152 pub fn set_target_accumulations(&mut self, target: usize) {
154 self.target_accumulations = target;
155 }
156
157 pub fn mode(&self) -> AccumulationMode {
159 self.mode
160 }
161
162 pub fn set_mode(&mut self, mode: AccumulationMode) {
164 self.mode = mode;
165 }
166
167 pub fn is_initialized(&self) -> bool {
169 self.initialized
170 }
171
172 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
182pub struct VariableAccumulator<A: Float, D: Dimension> {
184 accumulator: GradientAccumulator<A, D>,
186 adaptive_steps: Vec<(AdaptiveStepCondition, usize)>,
188 step_count: usize,
190}
191
192impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> VariableAccumulator<A, D> {
193 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 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 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 pub fn accumulate(&mut self, gradients: &[Array<A, D>]) -> Result<()> {
223 self.update_target();
224 self.accumulator.accumulate(gradients)
225 }
226
227 pub fn is_ready(&self) -> bool {
229 self.accumulator.is_ready()
230 }
231
232 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 pub fn step_count(&self) -> usize {
241 self.step_count
242 }
243
244 pub fn accumulator(&self) -> &GradientAccumulator<A, D> {
246 &self.accumulator
247 }
248
249 pub fn accumulator_mut(&mut self) -> &mut GradientAccumulator<A, D> {
251 &mut self.accumulator
252 }
253}
254
255#[derive(Debug)]
257pub struct MicroBatchTrainer<A: Float, D: Dimension> {
258 accumulator: GradientAccumulator<A, D>,
260 micro_batch_size: usize,
262 effective_batch_size: usize,
264}
265
266impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> MicroBatchTrainer<A, D> {
267 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 pub fn process_micro_batch(&mut self, gradients: &[Array<A, D>]) -> Result<()> {
291 self.accumulator.accumulate(gradients)
292 }
293
294 pub fn ready_for_step(&self) -> bool {
296 self.accumulator.is_ready()
297 }
298
299 pub fn get_accumulated_gradients(&mut self) -> Result<Vec<Array<A, D>>> {
301 self.accumulator.get_and_reset()
302 }
303
304 pub fn micro_batch_size(&self) -> usize {
306 self.micro_batch_size
307 }
308
309 pub fn effective_batch_size(&self) -> usize {
311 self.effective_batch_size
312 }
313
314 pub fn progress(&self) -> f64 {
316 self.accumulator.progress()
317 }
318
319 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
335pub mod utils {
337 use super::*;
338
339 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 let memory_per_sample = param_count * bytes_per_param * 3; let max_samples = (max_memory_mb * 1_000_000) / memory_per_sample;
349
350 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 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) }
366
367 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 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 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 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 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 ); 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]); }
453
454 #[test]
455 fn test_variable_accumulator() {
456 let mut var_accumulator = VariableAccumulator::new(2, AccumulationMode::Sum);
457
458 var_accumulator.add_adaptive_rule(|step| step > 5, 4);
460
461 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 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 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, 6, 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 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]); }
509
510 #[test]
511 fn test_calculate_micro_batch_size() {
512 let micro_batch = utils::calculate_micro_batch_size(
513 128, 100, 1000, 8, );
518
519 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); assert_eq!(utils::calculate_accumulation_steps(96, 32), 3);
529 }
530
531 #[test]
532 fn test_config_validation() {
533 utils::validate_config(32, 128, 4).expect("unwrap failed");
535
536 assert!(utils::validate_config(0, 128, 4).is_err());
538
539 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 let grad2 = vec![Array1::from_vec(vec![1.0, 2.0, 3.0])];
573 assert!(accumulator.accumulate(&grad2).is_err());
574
575 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 assert!(accumulator.get_and_reset().is_err());
592 }
593}