1use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum AccumulationMode {
16 Sum,
18 Mean,
21}
22
23#[derive(Debug, Clone)]
29pub struct AccumulatorConfig {
30 pub accumulation_steps: usize,
32 pub mode: AccumulationMode,
34 pub max_grad_norm: Option<f64>,
37}
38
39impl Default for AccumulatorConfig {
40 fn default() -> Self {
41 Self {
42 accumulation_steps: 4,
43 mode: AccumulationMode::Sum,
44 max_grad_norm: None,
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
55pub struct GradBuffer {
56 pub name: String,
58 pub values: Vec<f64>,
60 pub steps_accumulated: usize,
63}
64
65#[derive(Debug, Clone)]
71pub struct AccumulatorStats {
72 pub buffer_count: usize,
74 pub current_step: usize,
76 pub accumulation_steps: usize,
78 pub total_accumulations: u64,
81 pub total_clips: u64,
84}
85
86pub struct TensorGradAccumulator {
119 config: AccumulatorConfig,
120 buffers: HashMap<String, GradBuffer>,
121 current_step: usize,
122 total_accumulations: u64,
123 total_clips: u64,
124}
125
126impl TensorGradAccumulator {
127 pub fn new(config: AccumulatorConfig) -> Self {
129 Self {
130 config,
131 buffers: HashMap::new(),
132 current_step: 0,
133 total_accumulations: 0,
134 total_clips: 0,
135 }
136 }
137
138 pub fn accumulate(&mut self, name: &str, gradients: &[f64]) -> Result<(), String> {
149 if let Some(buf) = self.buffers.get_mut(name) {
150 if buf.values.len() != gradients.len() {
151 return Err(format!(
152 "gradient size mismatch for '{}': expected {}, got {}",
153 name,
154 buf.values.len(),
155 gradients.len()
156 ));
157 }
158 for (dst, src) in buf.values.iter_mut().zip(gradients.iter()) {
159 *dst += *src;
160 }
161 buf.steps_accumulated += 1;
162 } else {
163 self.buffers.insert(
164 name.to_string(),
165 GradBuffer {
166 name: name.to_string(),
167 values: gradients.to_vec(),
168 steps_accumulated: 1,
169 },
170 );
171 }
172 self.current_step = self
177 .buffers
178 .values()
179 .map(|b| b.steps_accumulated)
180 .max()
181 .unwrap_or(0);
182 Ok(())
183 }
184
185 pub fn is_ready(&self) -> bool {
188 if self.buffers.is_empty() {
189 return false;
190 }
191 self.buffers
192 .values()
193 .all(|b| b.steps_accumulated >= self.config.accumulation_steps)
194 }
195
196 pub fn step(&mut self) -> Result<HashMap<String, Vec<f64>>, String> {
207 if !self.is_ready() {
208 return Err("accumulator is not ready: not all buffers have enough steps".to_string());
209 }
210
211 let mut result = HashMap::new();
212
213 for (name, buf) in &self.buffers {
214 let mut grad = buf.values.clone();
215
216 if self.config.mode == AccumulationMode::Mean && buf.steps_accumulated > 0 {
218 let scale = 1.0 / buf.steps_accumulated as f64;
219 for v in &mut grad {
220 *v *= scale;
221 }
222 }
223
224 if let Some(max_norm) = self.config.max_grad_norm {
226 let original_norm = Self::clip_grad_norm(&mut grad, max_norm);
227 if original_norm > max_norm {
228 self.total_clips += 1;
229 }
230 }
231
232 result.insert(name.clone(), grad);
233 }
234
235 self.total_accumulations += 1;
236 self.buffers.clear();
237 self.current_step = 0;
238
239 Ok(result)
240 }
241
242 pub fn clip_grad_norm(gradients: &mut [f64], max_norm: f64) -> f64 {
247 let norm_sq: f64 = gradients.iter().map(|x| x * x).sum();
248 let norm = norm_sq.sqrt();
249 if norm > max_norm && norm > 0.0 {
250 let scale = max_norm / norm;
251 for v in gradients.iter_mut() {
252 *v *= scale;
253 }
254 }
255 norm
256 }
257
258 pub fn get_buffer(&self, name: &str) -> Option<&GradBuffer> {
260 self.buffers.get(name)
261 }
262
263 pub fn buffer_count(&self) -> usize {
265 self.buffers.len()
266 }
267
268 pub fn reset(&mut self) {
271 self.buffers.clear();
272 self.current_step = 0;
273 }
274
275 pub fn current_step(&self) -> usize {
277 self.current_step
278 }
279
280 pub fn stats(&self) -> AccumulatorStats {
282 AccumulatorStats {
283 buffer_count: self.buffers.len(),
284 current_step: self.current_step,
285 accumulation_steps: self.config.accumulation_steps,
286 total_accumulations: self.total_accumulations,
287 total_clips: self.total_clips,
288 }
289 }
290}
291
292#[cfg(test)]
297mod tests {
298 use super::*;
299
300 fn default_config() -> AccumulatorConfig {
301 AccumulatorConfig::default()
302 }
303
304 fn sum_config(steps: usize) -> AccumulatorConfig {
305 AccumulatorConfig {
306 accumulation_steps: steps,
307 mode: AccumulationMode::Sum,
308 max_grad_norm: None,
309 }
310 }
311
312 fn mean_config(steps: usize) -> AccumulatorConfig {
313 AccumulatorConfig {
314 accumulation_steps: steps,
315 mode: AccumulationMode::Mean,
316 max_grad_norm: None,
317 }
318 }
319
320 #[test]
325 fn test_new_default_config() {
326 let cfg = default_config();
327 assert_eq!(cfg.accumulation_steps, 4);
328 assert_eq!(cfg.mode, AccumulationMode::Sum);
329 assert!(cfg.max_grad_norm.is_none());
330 }
331
332 #[test]
333 fn test_new_accumulator_empty() {
334 let acc = TensorGradAccumulator::new(default_config());
335 assert_eq!(acc.buffer_count(), 0);
336 assert_eq!(acc.current_step(), 0);
337 assert!(!acc.is_ready());
338 }
339
340 #[test]
345 fn test_accumulate_sum_single_step() {
346 let mut acc = TensorGradAccumulator::new(sum_config(1));
347 acc.accumulate("w", &[1.0, 2.0, 3.0]).ok();
348 assert!(acc.is_ready());
349 let grads = acc.step().expect("step should succeed");
350 let w = &grads["w"];
351 assert!((w[0] - 1.0).abs() < 1e-12);
352 assert!((w[1] - 2.0).abs() < 1e-12);
353 assert!((w[2] - 3.0).abs() < 1e-12);
354 }
355
356 #[test]
357 fn test_accumulate_sum_two_steps() {
358 let mut acc = TensorGradAccumulator::new(sum_config(2));
359 acc.accumulate("w", &[1.0, 2.0]).ok();
360 assert!(!acc.is_ready());
361 acc.accumulate("w", &[3.0, 4.0]).ok();
362 assert!(acc.is_ready());
363 let grads = acc.step().expect("step should succeed");
364 let w = &grads["w"];
365 assert!((w[0] - 4.0).abs() < 1e-12);
366 assert!((w[1] - 6.0).abs() < 1e-12);
367 }
368
369 #[test]
370 fn test_accumulate_sum_four_steps() {
371 let mut acc = TensorGradAccumulator::new(sum_config(4));
372 for i in 0..4 {
373 acc.accumulate("w", &[i as f64]).ok();
374 }
375 assert!(acc.is_ready());
376 let grads = acc.step().expect("step should succeed");
377 assert!((grads["w"][0] - 6.0).abs() < 1e-12);
379 }
380
381 #[test]
386 fn test_accumulate_mean_single_step() {
387 let mut acc = TensorGradAccumulator::new(mean_config(1));
388 acc.accumulate("w", &[4.0]).ok();
389 let grads = acc.step().expect("step");
390 assert!((grads["w"][0] - 4.0).abs() < 1e-12);
391 }
392
393 #[test]
394 fn test_accumulate_mean_two_steps() {
395 let mut acc = TensorGradAccumulator::new(mean_config(2));
396 acc.accumulate("w", &[2.0, 6.0]).ok();
397 acc.accumulate("w", &[4.0, 8.0]).ok();
398 let grads = acc.step().expect("step");
399 assert!((grads["w"][0] - 3.0).abs() < 1e-12);
401 assert!((grads["w"][1] - 7.0).abs() < 1e-12);
402 }
403
404 #[test]
405 fn test_accumulate_mean_four_steps() {
406 let mut acc = TensorGradAccumulator::new(mean_config(4));
407 for _ in 0..4 {
408 acc.accumulate("w", &[8.0]).ok();
409 }
410 let grads = acc.step().expect("step");
411 assert!((grads["w"][0] - 8.0).abs() < 1e-12);
413 }
414
415 #[test]
420 fn test_clip_grad_norm_scales_down() {
421 let mut g = vec![3.0, 4.0]; let original = TensorGradAccumulator::clip_grad_norm(&mut g, 1.0);
423 assert!((original - 5.0).abs() < 1e-12);
424 let clipped_norm: f64 = g.iter().map(|x| x * x).sum::<f64>().sqrt();
425 assert!((clipped_norm - 1.0).abs() < 1e-10);
426 }
427
428 #[test]
429 fn test_clip_grad_norm_no_change_when_within() {
430 let mut g = vec![0.3, 0.4]; let original = TensorGradAccumulator::clip_grad_norm(&mut g, 1.0);
432 assert!((original - 0.5).abs() < 1e-12);
433 assert!((g[0] - 0.3).abs() < 1e-12);
434 assert!((g[1] - 0.4).abs() < 1e-12);
435 }
436
437 #[test]
438 fn test_clip_grad_norm_exact_boundary() {
439 let mut g = vec![3.0, 4.0]; let original = TensorGradAccumulator::clip_grad_norm(&mut g, 5.0);
441 assert!((original - 5.0).abs() < 1e-12);
442 assert!((g[0] - 3.0).abs() < 1e-12);
443 assert!((g[1] - 4.0).abs() < 1e-12);
444 }
445
446 #[test]
447 fn test_clip_grad_norm_zero_vector() {
448 let mut g = vec![0.0, 0.0];
449 let original = TensorGradAccumulator::clip_grad_norm(&mut g, 1.0);
450 assert!((original).abs() < 1e-12);
451 assert!((g[0]).abs() < 1e-12);
452 assert!((g[1]).abs() < 1e-12);
453 }
454
455 #[test]
456 fn test_step_with_clipping() {
457 let config = AccumulatorConfig {
458 accumulation_steps: 1,
459 mode: AccumulationMode::Sum,
460 max_grad_norm: Some(1.0),
461 };
462 let mut acc = TensorGradAccumulator::new(config);
463 acc.accumulate("w", &[3.0, 4.0]).ok(); let grads = acc.step().expect("step");
465 let clipped_norm: f64 = grads["w"].iter().map(|x| x * x).sum::<f64>().sqrt();
466 assert!((clipped_norm - 1.0).abs() < 1e-10);
467 }
468
469 #[test]
470 fn test_step_clips_increments_total_clips() {
471 let config = AccumulatorConfig {
472 accumulation_steps: 1,
473 mode: AccumulationMode::Sum,
474 max_grad_norm: Some(1.0),
475 };
476 let mut acc = TensorGradAccumulator::new(config);
477 acc.accumulate("w", &[3.0, 4.0]).ok(); acc.step().ok();
479 assert_eq!(acc.stats().total_clips, 1);
480 }
481
482 #[test]
483 fn test_step_no_clip_no_increment() {
484 let config = AccumulatorConfig {
485 accumulation_steps: 1,
486 mode: AccumulationMode::Sum,
487 max_grad_norm: Some(10.0),
488 };
489 let mut acc = TensorGradAccumulator::new(config);
490 acc.accumulate("w", &[0.1, 0.2]).ok(); acc.step().ok();
492 assert_eq!(acc.stats().total_clips, 0);
493 }
494
495 #[test]
500 fn test_is_ready_empty() {
501 let acc = TensorGradAccumulator::new(sum_config(2));
502 assert!(!acc.is_ready());
503 }
504
505 #[test]
506 fn test_is_ready_partial() {
507 let mut acc = TensorGradAccumulator::new(sum_config(3));
508 acc.accumulate("w", &[1.0]).ok();
509 acc.accumulate("w", &[2.0]).ok();
510 assert!(!acc.is_ready());
511 }
512
513 #[test]
514 fn test_is_ready_exact() {
515 let mut acc = TensorGradAccumulator::new(sum_config(2));
516 acc.accumulate("w", &[1.0]).ok();
517 acc.accumulate("w", &[2.0]).ok();
518 assert!(acc.is_ready());
519 }
520
521 #[test]
522 fn test_is_ready_multi_param_partial() {
523 let mut acc = TensorGradAccumulator::new(sum_config(2));
524 acc.accumulate("w", &[1.0]).ok();
525 acc.accumulate("w", &[2.0]).ok();
526 acc.accumulate("b", &[0.5]).ok(); assert!(!acc.is_ready());
528 }
529
530 #[test]
531 fn test_is_ready_multi_param_all_ready() {
532 let mut acc = TensorGradAccumulator::new(sum_config(2));
533 acc.accumulate("w", &[1.0]).ok();
534 acc.accumulate("b", &[0.5]).ok();
535 acc.accumulate("w", &[2.0]).ok();
536 acc.accumulate("b", &[0.7]).ok();
537 assert!(acc.is_ready());
538 }
539
540 #[test]
545 fn test_step_returns_all_params() {
546 let mut acc = TensorGradAccumulator::new(sum_config(1));
547 acc.accumulate("w", &[1.0, 2.0]).ok();
548 acc.accumulate("b", &[0.5]).ok();
549 let grads = acc.step().expect("step");
550 assert_eq!(grads.len(), 2);
551 assert!(grads.contains_key("w"));
552 assert!(grads.contains_key("b"));
553 }
554
555 #[test]
556 fn test_step_clears_buffers() {
557 let mut acc = TensorGradAccumulator::new(sum_config(1));
558 acc.accumulate("w", &[1.0]).ok();
559 acc.step().ok();
560 assert_eq!(acc.buffer_count(), 0);
561 assert_eq!(acc.current_step(), 0);
562 }
563
564 #[test]
565 fn test_step_error_when_not_ready() {
566 let mut acc = TensorGradAccumulator::new(sum_config(3));
567 acc.accumulate("w", &[1.0]).ok();
568 let err = acc.step().expect_err("should fail");
569 assert!(err.contains("not ready"));
570 }
571
572 #[test]
577 fn test_accumulate_size_mismatch() {
578 let mut acc = TensorGradAccumulator::new(sum_config(2));
579 acc.accumulate("w", &[1.0, 2.0]).ok();
580 let err = acc
581 .accumulate("w", &[1.0, 2.0, 3.0])
582 .expect_err("should fail");
583 assert!(err.contains("size mismatch"));
584 }
585
586 #[test]
587 fn test_accumulate_size_mismatch_shorter() {
588 let mut acc = TensorGradAccumulator::new(sum_config(2));
589 acc.accumulate("w", &[1.0, 2.0, 3.0]).ok();
590 let err = acc.accumulate("w", &[1.0]).expect_err("should fail");
591 assert!(err.contains("size mismatch"));
592 }
593
594 #[test]
599 fn test_reset_clears_buffers() {
600 let mut acc = TensorGradAccumulator::new(sum_config(2));
601 acc.accumulate("w", &[1.0]).ok();
602 acc.accumulate("b", &[2.0]).ok();
603 acc.reset();
604 assert_eq!(acc.buffer_count(), 0);
605 assert_eq!(acc.current_step(), 0);
606 assert!(!acc.is_ready());
607 }
608
609 #[test]
610 fn test_reset_preserves_lifetime_stats() {
611 let mut acc = TensorGradAccumulator::new(sum_config(1));
612 acc.accumulate("w", &[1.0]).ok();
613 acc.step().ok();
614 let accums_before = acc.stats().total_accumulations;
615 acc.reset();
616 assert_eq!(acc.stats().total_accumulations, accums_before);
617 }
618
619 #[test]
624 fn test_multiple_params_independent() {
625 let mut acc = TensorGradAccumulator::new(mean_config(2));
626 acc.accumulate("w1", &[2.0, 4.0]).ok();
627 acc.accumulate("w2", &[10.0]).ok();
628 acc.accumulate("w1", &[6.0, 8.0]).ok();
629 acc.accumulate("w2", &[20.0]).ok();
630
631 let grads = acc.step().expect("step");
632 assert!((grads["w1"][0] - 4.0).abs() < 1e-12);
634 assert!((grads["w1"][1] - 6.0).abs() < 1e-12);
635 assert!((grads["w2"][0] - 15.0).abs() < 1e-12);
637 }
638
639 #[test]
644 fn test_get_buffer_existing() {
645 let mut acc = TensorGradAccumulator::new(sum_config(2));
646 acc.accumulate("w", &[1.0, 2.0]).ok();
647 let buf = acc.get_buffer("w").expect("should exist");
648 assert_eq!(buf.name, "w");
649 assert_eq!(buf.values.len(), 2);
650 assert_eq!(buf.steps_accumulated, 1);
651 }
652
653 #[test]
654 fn test_get_buffer_missing() {
655 let acc = TensorGradAccumulator::new(sum_config(2));
656 assert!(acc.get_buffer("nonexistent").is_none());
657 }
658
659 #[test]
664 fn test_stats_initial() {
665 let acc = TensorGradAccumulator::new(sum_config(4));
666 let s = acc.stats();
667 assert_eq!(s.buffer_count, 0);
668 assert_eq!(s.current_step, 0);
669 assert_eq!(s.accumulation_steps, 4);
670 assert_eq!(s.total_accumulations, 0);
671 assert_eq!(s.total_clips, 0);
672 }
673
674 #[test]
675 fn test_stats_after_step() {
676 let mut acc = TensorGradAccumulator::new(sum_config(1));
677 acc.accumulate("w", &[1.0]).ok();
678 acc.step().ok();
679 let s = acc.stats();
680 assert_eq!(s.total_accumulations, 1);
681 assert_eq!(s.buffer_count, 0); }
683
684 #[test]
685 fn test_stats_multiple_steps() {
686 let mut acc = TensorGradAccumulator::new(sum_config(1));
687 for _ in 0..5 {
688 acc.accumulate("w", &[1.0]).ok();
689 acc.step().ok();
690 }
691 assert_eq!(acc.stats().total_accumulations, 5);
692 }
693
694 #[test]
699 fn test_step_empty_accumulator() {
700 let mut acc = TensorGradAccumulator::new(sum_config(1));
701 assert!(acc.step().is_err());
702 }
703
704 #[test]
705 fn test_buffer_count_empty() {
706 let acc = TensorGradAccumulator::new(sum_config(1));
707 assert_eq!(acc.buffer_count(), 0);
708 }
709
710 #[test]
711 fn test_current_step_tracks_max() {
712 let mut acc = TensorGradAccumulator::new(sum_config(3));
713 acc.accumulate("w", &[1.0]).ok();
714 assert_eq!(acc.current_step(), 1);
715 acc.accumulate("w", &[2.0]).ok();
716 assert_eq!(acc.current_step(), 2);
717 }
718
719 #[test]
724 fn test_clip_preserves_direction() {
725 let mut g = vec![6.0, 8.0]; TensorGradAccumulator::clip_grad_norm(&mut g, 5.0);
727 assert!((g[0] - 3.0).abs() < 1e-10);
729 assert!((g[1] - 4.0).abs() < 1e-10);
730 }
731
732 #[test]
737 fn test_reset_then_reuse() {
738 let mut acc = TensorGradAccumulator::new(sum_config(1));
739 acc.accumulate("w", &[1.0]).ok();
740 acc.step().ok();
741 acc.reset();
742 acc.accumulate("w", &[1.0, 2.0, 3.0]).ok();
744 let grads = acc.step().expect("step");
745 assert_eq!(grads["w"].len(), 3);
746 }
747
748 #[test]
753 fn test_mean_with_clipping() {
754 let config = AccumulatorConfig {
755 accumulation_steps: 2,
756 mode: AccumulationMode::Mean,
757 max_grad_norm: Some(1.0),
758 };
759 let mut acc = TensorGradAccumulator::new(config);
760 acc.accumulate("w", &[6.0, 8.0]).ok(); acc.accumulate("w", &[6.0, 8.0]).ok();
762 let grads = acc.step().expect("step");
763 let clipped_norm: f64 = grads["w"].iter().map(|x| x * x).sum::<f64>().sqrt();
765 assert!((clipped_norm - 1.0).abs() < 1e-10);
766 assert_eq!(acc.stats().total_clips, 1);
767 }
768}