1#![allow(dead_code)]
9
10pub mod legacy;
11
12pub use legacy::{CAMEConfig, CAME};
13
14use trustformers_core::errors::TrustformersError;
17
18#[derive(Debug, thiserror::Error)]
20pub enum OptimError {
21 #[error("length mismatch: param length {param} != grad length {grad}")]
23 LengthMismatch { param: usize, grad: usize },
24 #[error("dimension mismatch: rows * cols ({rows} * {cols} = {product}) != size {size}")]
26 DimensionMismatch {
27 rows: usize,
28 cols: usize,
29 product: usize,
30 size: usize,
31 },
32 #[error("no state initialised for parameter group index {0}")]
34 StateNotInitialised(usize),
35 #[error("numerical error: {0}")]
37 NumericalError(String),
38}
39
40impl From<OptimError> for TrustformersError {
41 fn from(e: OptimError) -> Self {
42 TrustformersError::invalid_operation(e.to_string())
43 }
44}
45
46#[derive(Debug, Clone)]
50pub struct CameConfig {
51 pub lr: f64,
53 pub betas: (f64, f64, f64),
56 pub eps: (f64, f64),
59 pub weight_decay: f64,
61 pub clip_threshold: f64,
63 pub decay_rate: f64,
66}
67
68impl Default for CameConfig {
69 fn default() -> Self {
70 Self {
71 lr: 2e-4,
72 betas: (0.9, 0.999, 0.9999),
73 eps: (1e-30, 1e-16),
74 weight_decay: 0.0,
75 clip_threshold: 1.0,
76 decay_rate: -0.8,
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct CameParamState {
84 pub step: u64,
86 pub exp_avg: Vec<f32>,
88 pub exp_avg_sq_row: Vec<f32>,
90 pub exp_avg_sq_col: Vec<f32>,
92 pub exp_avg_sq: Option<Vec<f32>>,
94 pub exp_avg_insta_sq_row: Vec<f32>,
96 pub exp_avg_insta_sq_col: Vec<f32>,
98}
99
100impl CameParamState {
101 pub fn new_2d(size: usize, rows: usize, cols: usize) -> Self {
103 Self {
104 step: 0,
105 exp_avg: vec![0.0_f32; size],
106 exp_avg_sq_row: vec![0.0_f32; rows],
107 exp_avg_sq_col: vec![0.0_f32; cols],
108 exp_avg_sq: None,
109 exp_avg_insta_sq_row: vec![0.0_f32; rows],
110 exp_avg_insta_sq_col: vec![0.0_f32; cols],
111 }
112 }
113
114 pub fn new_1d(size: usize) -> Self {
116 Self {
117 step: 0,
118 exp_avg: vec![0.0_f32; size],
119 exp_avg_sq_row: Vec::new(),
120 exp_avg_sq_col: Vec::new(),
121 exp_avg_sq: Some(vec![0.0_f32; size]),
122 exp_avg_insta_sq_row: Vec::new(),
123 exp_avg_insta_sq_col: Vec::new(),
124 }
125 }
126}
127
128#[inline]
130fn rms(v: &[f32]) -> f32 {
131 if v.is_empty() {
132 return 0.0;
133 }
134 let sq_sum: f32 = v.iter().map(|x| x * x).sum();
135 (sq_sum / v.len() as f32).sqrt()
136}
137
138pub fn came_update(
153 param: &mut [f32],
154 grad: &[f32],
155 state: &mut CameParamState,
156 config: &CameConfig,
157 rows: usize,
158 cols: usize,
159) -> Result<(), OptimError> {
160 let size = param.len();
162 if grad.len() != size {
163 return Err(OptimError::LengthMismatch {
164 param: size,
165 grad: grad.len(),
166 });
167 }
168 let expected = rows * cols;
169 if expected != size {
170 return Err(OptimError::DimensionMismatch {
171 rows,
172 cols,
173 product: expected,
174 size,
175 });
176 }
177
178 state.step += 1;
180 let step = state.step as f64;
181
182 let beta2_t = (1.0 - step.powf(config.decay_rate)).min(config.betas.1) as f32;
185
186 let beta1 = config.betas.0 as f32;
187 let beta3 = config.betas.2 as f32;
188 let eps1 = config.eps.0 as f32;
189 let eps2 = config.eps.1 as f32;
190
191 let grad_rms = rms(grad);
193 let clip_scale = if grad_rms > config.clip_threshold as f32 {
194 config.clip_threshold as f32 / (grad_rms + eps1)
195 } else {
196 1.0
197 };
198
199 for (m, &g) in state.exp_avg.iter_mut().zip(grad.iter()) {
204 let g_clipped = g * clip_scale;
205 *m = beta1 * *m + (1.0 - beta1) * g_clipped;
206 }
207
208 if rows == 1 {
210 let sq = state
212 .exp_avg_sq
213 .as_mut()
214 .ok_or_else(|| OptimError::NumericalError("1-D state missing exp_avg_sq".into()))?;
215 for (s, &g) in sq.iter_mut().zip(grad.iter()) {
216 let g_clipped = g * clip_scale;
217 *s = beta2_t * *s + (1.0 - beta2_t) * (g_clipped * g_clipped + eps1);
218 }
219
220 for ((p, &m), &s) in param.iter_mut().zip(state.exp_avg.iter()).zip(sq.iter()) {
222 let denom = s.sqrt() + eps2;
223 let update = m / denom;
224 if config.weight_decay != 0.0 {
225 *p -= config.lr as f32 * config.weight_decay as f32 * *p;
226 }
227 *p -= config.lr as f32 * update;
228 }
229 } else {
230 let mut row_mean = vec![0.0_f32; rows];
233 let mut col_mean = vec![0.0_f32; cols];
234
235 for i in 0..rows {
236 let mut s = 0.0_f32;
237 for j in 0..cols {
238 let g = grad[i * cols + j] * clip_scale;
239 s += g * g;
240 }
241 row_mean[i] = s / cols as f32 + eps1;
242 }
243 for j in 0..cols {
244 let mut s = 0.0_f32;
245 for i in 0..rows {
246 let g = grad[i * cols + j] * clip_scale;
247 s += g * g;
248 }
249 col_mean[j] = s / rows as f32 + eps1;
250 }
251
252 for (r, &rm) in state.exp_avg_sq_row.iter_mut().zip(row_mean.iter()) {
254 *r = beta2_t * *r + (1.0 - beta2_t) * rm;
255 }
256 for (c, &cm) in state.exp_avg_sq_col.iter_mut().zip(col_mean.iter()) {
257 *c = beta2_t * *c + (1.0 - beta2_t) * cm;
258 }
259
260 for (r, &rm) in state.exp_avg_insta_sq_row.iter_mut().zip(row_mean.iter()) {
262 *r = beta3 * *r + (1.0 - beta3) * rm;
263 }
264 for (c, &cm) in state.exp_avg_insta_sq_col.iter_mut().zip(col_mean.iter()) {
265 *c = beta3 * *c + (1.0 - beta3) * cm;
266 }
267
268 let row_sum: f32 = state.exp_avg_sq_row.iter().sum();
270 let row_normaliser = (row_sum / rows as f32).max(eps1);
271
272 for i in 0..rows {
274 let smoothed_row = state.exp_avg_sq_row[i];
275 let insta_row = state.exp_avg_insta_sq_row[i];
276
277 for j in 0..cols {
278 let smoothed_col = state.exp_avg_sq_col[j];
279 let insta_col = state.exp_avg_insta_sq_col[j];
280
281 let v_approx = (smoothed_row * smoothed_col / row_normaliser).sqrt();
283
284 let smoothed_insta_row = (insta_row * insta_col / row_normaliser).sqrt();
286 let confidence = if smoothed_insta_row > eps1 {
287 (v_approx / (smoothed_insta_row + eps2)).min(1.0_f32)
288 } else {
289 1.0_f32
290 };
291
292 let denom = v_approx + eps2;
293 let idx = i * cols + j;
294 let m = state.exp_avg[idx];
295 let update = confidence * m / denom;
296
297 let p = &mut param[idx];
298 if config.weight_decay != 0.0 {
299 *p -= config.lr as f32 * config.weight_decay as f32 * *p;
300 }
301 *p -= config.lr as f32 * update;
302 }
303 }
304 }
305
306 Ok(())
307}
308
309#[derive(Debug, Clone)]
311struct ParamGroupMeta {
312 size: usize,
313 rows: usize,
314 cols: usize,
315}
316
317#[derive(Debug)]
322pub struct CameOptimizer {
323 pub config: CameConfig,
325 pub states: Vec<CameParamState>,
327 meta: Vec<ParamGroupMeta>,
329}
330
331impl CameOptimizer {
332 pub fn new(config: CameConfig) -> Self {
334 Self {
335 config,
336 states: Vec::new(),
337 meta: Vec::new(),
338 }
339 }
340
341 pub fn add_param_group(&mut self, param_size: usize, rows: usize, cols: usize) {
346 let state = if rows == 1 {
347 CameParamState::new_1d(param_size)
348 } else {
349 CameParamState::new_2d(param_size, rows, cols)
350 };
351 self.states.push(state);
352 self.meta.push(ParamGroupMeta {
353 size: param_size,
354 rows,
355 cols,
356 });
357 }
358
359 pub fn step(&mut self, params: &mut [Vec<f32>], grads: &[Vec<f32>]) -> Result<(), OptimError> {
370 for (idx, ((param, grad), state)) in
371 params.iter_mut().zip(grads.iter()).zip(self.states.iter_mut()).enumerate()
372 {
373 let meta = self.meta.get(idx).ok_or(OptimError::StateNotInitialised(idx))?;
374 came_update(param, grad, state, &self.config, meta.rows, meta.cols)?;
375 }
376 Ok(())
377 }
378}
379
380#[cfg(test)]
385mod tests {
386 use super::*;
387 use approx::assert_relative_eq;
388
389 #[test]
393 fn test_came_config_defaults() {
394 let cfg = CameConfig::default();
395 assert_relative_eq!(cfg.lr, 2e-4);
396 assert_relative_eq!(cfg.betas.0, 0.9);
397 assert_relative_eq!(cfg.betas.1, 0.999);
398 assert_relative_eq!(cfg.betas.2, 0.9999);
399 assert_relative_eq!(cfg.eps.0, 1e-30);
400 assert_relative_eq!(cfg.eps.1, 1e-16);
401 assert_relative_eq!(cfg.weight_decay, 0.0);
402 assert_relative_eq!(cfg.clip_threshold, 1.0);
403 assert_relative_eq!(cfg.decay_rate, -0.8);
404 }
405
406 #[test]
410 fn test_state_init_2d() {
411 let state = CameParamState::new_2d(6, 2, 3);
412 assert_eq!(state.step, 0);
413 assert_eq!(state.exp_avg.len(), 6);
414 assert_eq!(state.exp_avg_sq_row.len(), 2);
415 assert_eq!(state.exp_avg_sq_col.len(), 3);
416 assert!(state.exp_avg_sq.is_none());
417 assert_eq!(state.exp_avg_insta_sq_row.len(), 2);
418 assert_eq!(state.exp_avg_insta_sq_col.len(), 3);
419 assert!(state.exp_avg.iter().all(|&x| x == 0.0));
420 }
421
422 #[test]
426 fn test_state_init_1d() {
427 let state = CameParamState::new_1d(5);
428 assert_eq!(state.step, 0);
429 assert_eq!(state.exp_avg.len(), 5);
430 assert!(state.exp_avg_sq_row.is_empty());
431 assert!(state.exp_avg_sq_col.is_empty());
432 assert!(state.exp_avg_sq.is_some());
433 assert_eq!(state.exp_avg_sq.as_ref().map(|v| v.len()), Some(5));
434 }
435
436 #[test]
440 fn test_step_counter() {
441 let cfg = CameConfig::default();
442 let mut state = CameParamState::new_1d(2);
443 let mut param = vec![1.0_f32; 2];
444 let grad = vec![0.1_f32; 2];
445
446 came_update(&mut param, &grad, &mut state, &cfg, 1, 2).expect("update failed");
447 assert_eq!(state.step, 1);
448 came_update(&mut param, &grad, &mut state, &cfg, 1, 2).expect("update failed");
449 assert_eq!(state.step, 2);
450 }
451
452 #[test]
456 fn test_factored_second_moment_update() {
457 let cfg = CameConfig {
458 lr: 0.0,
459 ..CameConfig::default()
460 };
461 let rows = 2_usize;
462 let cols = 3_usize;
463 let size = rows * cols;
464 let mut state = CameParamState::new_2d(size, rows, cols);
465 let mut param = vec![0.0_f32; size];
466 let grad = vec![1.0_f32; size];
467
468 came_update(&mut param, &grad, &mut state, &cfg, rows, cols).expect("update failed");
470 assert!(state.exp_avg_sq_row.iter().all(|&x| x > 0.0));
471 assert!(state.exp_avg_sq_col.iter().all(|&x| x > 0.0));
472 }
473
474 #[test]
478 fn test_dynamic_beta2_schedule() {
479 let cfg = CameConfig::default();
480 let step = 1_f64;
482 let beta2_t = (1.0 - step.powf(cfg.decay_rate)).min(cfg.betas.1);
483 assert_relative_eq!(beta2_t, 0.0, epsilon = 1e-9);
484
485 let step100 = 100_f64;
487 let beta2_100 = (1.0 - step100.powf(cfg.decay_rate)).min(cfg.betas.1);
488 assert!(beta2_100 > 0.9 && beta2_100 < 1.0);
489 }
490
491 #[test]
495 fn test_confidence_adaptation() {
496 let cfg = CameConfig::default();
497 let rows = 2_usize;
498 let cols = 2_usize;
499 let size = rows * cols;
500 let mut state = CameParamState::new_2d(size, rows, cols);
501 let mut param = vec![0.0_f32; size];
502 let grad = vec![1.0_f32; size];
503
504 came_update(&mut param, &grad, &mut state, &cfg, rows, cols).expect("update failed");
505
506 assert!(state.exp_avg_insta_sq_row.iter().all(|&x| x > 0.0));
508 assert!(state.exp_avg_insta_sq_col.iter().all(|&x| x > 0.0));
509 }
510
511 #[test]
515 fn test_weight_decay() {
516 let cfg = CameConfig {
517 lr: 1e-1,
518 weight_decay: 0.1,
519 ..CameConfig::default()
520 };
521 let mut state = CameParamState::new_1d(2);
522 let initial_param = vec![1.0_f32; 2];
523 let mut param = initial_param.clone();
524 let grad = vec![0.0_f32; 2]; came_update(&mut param, &grad, &mut state, &cfg, 1, 2).expect("update failed");
527
528 for (p_new, p_old) in param.iter().zip(initial_param.iter()) {
530 assert!(
531 p_new.abs() < p_old.abs(),
532 "weight decay did not reduce param"
533 );
534 }
535 }
536
537 #[test]
541 fn test_single_step_direction() {
542 let cfg = CameConfig::default();
543 let mut state = CameParamState::new_1d(3);
544 let mut param = vec![0.5_f32; 3];
545 let grad = vec![0.1_f32; 3]; let param_before = param.clone();
548 came_update(&mut param, &grad, &mut state, &cfg, 1, 3).expect("update failed");
549
550 for (p_new, p_old) in param.iter().zip(param_before.iter()) {
552 assert!(
553 p_new < p_old,
554 "param did not decrease with positive gradient"
555 );
556 }
557 }
558
559 #[test]
563 fn test_gradient_clipping() {
564 let cfg_tight = CameConfig {
568 clip_threshold: 0.1,
569 ..CameConfig::default()
570 };
571 let cfg_loose = CameConfig {
572 clip_threshold: 1000.0,
573 ..CameConfig::default()
574 };
575
576 let large_grad = vec![5.0_f32; 4];
577
578 let mut state_tight = CameParamState::new_1d(4);
579 let mut param_tight = vec![0.0_f32; 4];
580 came_update(
581 &mut param_tight,
582 &large_grad,
583 &mut state_tight,
584 &cfg_tight,
585 1,
586 4,
587 )
588 .expect("tight update failed");
589
590 let mut state_loose = CameParamState::new_1d(4);
591 let mut param_loose = vec![0.0_f32; 4];
592 came_update(
593 &mut param_loose,
594 &large_grad,
595 &mut state_loose,
596 &cfg_loose,
597 1,
598 4,
599 )
600 .expect("loose update failed");
601
602 let m_tight: f32 = state_tight.exp_avg.iter().map(|x| x.abs()).sum();
605 let m_loose: f32 = state_loose.exp_avg.iter().map(|x| x.abs()).sum();
606 assert!(
607 m_tight < m_loose,
608 "tight clipping did not reduce first moment: m_tight={m_tight} m_loose={m_loose}"
609 );
610 }
611
612 #[test]
616 fn test_convergence_quadratic() {
617 let cfg = CameConfig {
619 lr: 1e-2,
620 ..CameConfig::default()
621 };
622 let mut state = CameParamState::new_1d(1);
623 let mut param = vec![5.0_f32];
624
625 for _ in 0..2000 {
626 let grad = param.clone(); came_update(&mut param, &grad, &mut state, &cfg, 1, 1).expect("update failed");
628 }
629
630 assert!(
631 param[0].abs() < 0.1,
632 "CAME did not converge on quadratic: final param = {}",
633 param[0]
634 );
635 }
636
637 #[test]
641 fn test_dimension_mismatch_error() {
642 let cfg = CameConfig::default();
643 let mut state = CameParamState::new_1d(4);
644 let mut param = vec![0.0_f32; 4];
645 let grad = vec![0.0_f32; 5]; let result = came_update(&mut param, &grad, &mut state, &cfg, 1, 4);
648 assert!(result.is_err());
649 matches!(result.unwrap_err(), OptimError::LengthMismatch { .. });
650 }
651
652 #[test]
656 fn test_came_optimizer_multi_param() {
657 let cfg = CameConfig::default();
658 let mut optimizer = CameOptimizer::new(cfg);
659 optimizer.add_param_group(4, 2, 2);
660 optimizer.add_param_group(3, 1, 3);
661
662 let mut params = vec![vec![1.0_f32; 4], vec![1.0_f32; 3]];
663 let grads = vec![vec![0.1_f32; 4], vec![0.1_f32; 3]];
664
665 optimizer.step(&mut params, &grads).expect("step failed");
666 assert_eq!(optimizer.states[0].step, 1);
667 assert_eq!(optimizer.states[1].step, 1);
668 }
669}
670
671#[cfg(test)]
672mod extended_tests {
673 use super::*;
674 use approx::assert_relative_eq;
675
676 #[test]
677 fn test_came_state_step_zero_at_init() {
678 let state = CameParamState::new_2d(6, 2, 3);
679 assert_eq!(state.step, 0);
680 let state1d = CameParamState::new_1d(4);
681 assert_eq!(state1d.step, 0);
682 }
683
684 #[test]
685 fn test_came_confidence_factors_nonzero_after_step() {
686 let cfg = CameConfig::default();
687 let mut state = CameParamState::new_2d(6, 2, 3);
688 let mut param = vec![0.5_f32; 6];
689 let grad = vec![0.1_f32; 6];
690 came_update(&mut param, &grad, &mut state, &cfg, 2, 3).expect("update failed");
691 assert!(
692 state.exp_avg_insta_sq_row.iter().all(|&x| x > 0.0),
693 "insta_sq_row should be nonzero after update"
694 );
695 assert!(
696 state.exp_avg_insta_sq_col.iter().all(|&x| x > 0.0),
697 "insta_sq_col should be nonzero after update"
698 );
699 }
700
701 #[test]
702 fn test_came_positive_grad_decreases_params() {
703 let cfg = CameConfig::default();
704 let mut state = CameParamState::new_1d(4);
705 let mut param = vec![1.0_f32; 4];
706 let grad = vec![0.5_f32; 4];
707 let before = param.clone();
708 came_update(&mut param, &grad, &mut state, &cfg, 1, 4).expect("update failed");
709 for (p_new, p_old) in param.iter().zip(before.iter()) {
710 assert!(
711 p_new < p_old,
712 "param should decrease with positive gradient"
713 );
714 }
715 }
716
717 #[test]
718 fn test_came_1d_vs_2d_single_element_both_decrease() {
719 let cfg = CameConfig::default();
720 let grad = vec![0.2_f32];
721
722 let mut state_1d = CameParamState::new_1d(1);
724 let mut param_1d = vec![1.0_f32];
725 came_update(&mut param_1d, &grad, &mut state_1d, &cfg, 1, 1).expect("1d update failed");
726 assert!(param_1d[0] < 1.0, "1D param should decrease");
727
728 let grad_2d = vec![0.2_f32; 4];
730 let mut state_2d = CameParamState::new_2d(4, 2, 2);
731 let mut param_2d = vec![1.0_f32; 4];
732 came_update(&mut param_2d, &grad_2d, &mut state_2d, &cfg, 2, 2).expect("2d update failed");
733 for &p in ¶m_2d {
734 assert!(p < 1.0, "2D param should decrease");
735 }
736 }
737
738 #[test]
739 fn test_came_weight_decay_larger_shrinks_more() {
740 let grad = vec![0.0_f32; 3];
741
742 let cfg_small = CameConfig {
743 lr: 0.1,
744 weight_decay: 0.01,
745 ..CameConfig::default()
746 };
747 let mut state_small = CameParamState::new_1d(3);
748 let mut param_small = vec![1.0_f32; 3];
749 came_update(&mut param_small, &grad, &mut state_small, &cfg_small, 1, 3)
750 .expect("small wd update failed");
751
752 let cfg_large = CameConfig {
753 lr: 0.1,
754 weight_decay: 0.1,
755 ..CameConfig::default()
756 };
757 let mut state_large = CameParamState::new_1d(3);
758 let mut param_large = vec![1.0_f32; 3];
759 came_update(&mut param_large, &grad, &mut state_large, &cfg_large, 1, 3)
760 .expect("large wd update failed");
761
762 for (ps, pl) in param_small.iter().zip(param_large.iter()) {
763 assert!(
764 ps.abs() > pl.abs(),
765 "larger weight_decay should shrink more: small={ps}, large={pl}"
766 );
767 }
768 }
769
770 #[test]
771 fn test_came_zero_grad_zero_wd_params_unchanged() {
772 let cfg = CameConfig {
773 lr: 0.1,
774 weight_decay: 0.0,
775 ..CameConfig::default()
776 };
777 let mut state = CameParamState::new_1d(3);
778 let mut param = vec![2.0_f32; 3];
779 let original = param.clone();
780 let grad = vec![0.0_f32; 3];
781 came_update(&mut param, &grad, &mut state, &cfg, 1, 3).expect("update failed");
782 for (p_new, p_old) in param.iter().zip(original.iter()) {
783 assert_relative_eq!(*p_new, *p_old, epsilon = 1e-6);
784 }
785 }
786
787 #[test]
788 fn test_came_multiple_steps_move_toward_zero() {
789 let cfg = CameConfig {
790 lr: 1e-2,
791 weight_decay: 0.0,
792 ..CameConfig::default()
793 };
794 let mut state = CameParamState::new_1d(1);
795 let mut param = vec![3.0_f32];
796 for _ in 0..500 {
797 let grad = param.clone();
798 came_update(&mut param, &grad, &mut state, &cfg, 1, 1).expect("update failed");
799 }
800 assert!(
801 param[0].abs() < 3.0,
802 "param should move toward 0 over many steps"
803 );
804 }
805
806 #[test]
807 fn test_came_state_not_initialised_no_panic() {
808 let cfg = CameConfig::default();
809 let mut optimizer = CameOptimizer::new(cfg);
810 let mut params = vec![vec![1.0_f32; 3]];
812 let grads = vec![vec![0.1_f32; 3]];
813 let result = optimizer.step(&mut params, &grads);
814 let _ = result;
816 }
817
818 #[test]
819 fn test_came_batch_2d_params_step_count() {
820 let cfg = CameConfig::default();
821 let mut optimizer = CameOptimizer::new(cfg);
822 optimizer.add_param_group(6, 2, 3);
823 optimizer.add_param_group(9, 3, 3);
824 let mut params = vec![vec![0.5_f32; 6], vec![0.5_f32; 9]];
825 let grads = vec![vec![0.1_f32; 6], vec![0.1_f32; 9]];
826 optimizer.step(&mut params, &grads).expect("step failed");
827 assert_eq!(optimizer.states[0].step, 1);
828 assert_eq!(optimizer.states[1].step, 1);
829 }
830
831 #[test]
832 fn test_came_clipping_bounds_param_change() {
833 let large_grad = vec![100.0_f32; 4];
838
839 let cfg_tight = CameConfig {
840 lr: 1.0,
841 clip_threshold: 0.001,
842 weight_decay: 0.0,
843 ..CameConfig::default()
844 };
845 let mut s_tight = CameParamState::new_1d(4);
846 let mut p_tight = vec![0.0_f32; 4];
847 came_update(&mut p_tight, &large_grad, &mut s_tight, &cfg_tight, 1, 4)
848 .expect("tight failed");
849
850 let cfg_loose = CameConfig {
851 lr: 1.0,
852 clip_threshold: 1000.0,
853 weight_decay: 0.0,
854 ..CameConfig::default()
855 };
856 let mut s_loose = CameParamState::new_1d(4);
857 let mut p_loose = vec![0.0_f32; 4];
858 came_update(&mut p_loose, &large_grad, &mut s_loose, &cfg_loose, 1, 4)
859 .expect("loose failed");
860
861 let m_tight: f32 = s_tight.exp_avg.iter().map(|x| x.abs()).sum();
863 let m_loose: f32 = s_loose.exp_avg.iter().map(|x| x.abs()).sum();
864 assert!(
865 m_tight < m_loose,
866 "tight clipping should reduce first moment: tight={m_tight}, loose={m_loose}"
867 );
868 }
869
870 #[test]
871 fn test_came_2d_factored_memory_efficiency() {
872 let rows = 100_usize;
873 let cols = 200_usize;
874 let size = rows * cols;
875 let state = CameParamState::new_2d(size, rows, cols);
876 let factored_size = state.exp_avg_sq_row.len() + state.exp_avg_sq_col.len();
877 assert!(
878 factored_size < size,
879 "factored memory ({factored_size}) should be less than full size ({size})"
880 );
881 }
882
883 #[test]
884 fn test_came_beta3_effect_on_insta_sq() {
885 let rows = 2_usize;
886 let cols = 2_usize;
887 let grad = vec![1.0_f32; 4];
888
889 let cfg_high = CameConfig {
890 betas: (0.9, 0.999, 0.9999),
891 ..CameConfig::default()
892 };
893 let mut state_high = CameParamState::new_2d(4, rows, cols);
894 let mut param_high = vec![0.5_f32; 4];
895 came_update(
896 &mut param_high,
897 &grad,
898 &mut state_high,
899 &cfg_high,
900 rows,
901 cols,
902 )
903 .expect("high beta3 update failed");
904
905 let cfg_low = CameConfig {
906 betas: (0.9, 0.999, 0.5),
907 ..CameConfig::default()
908 };
909 let mut state_low = CameParamState::new_2d(4, rows, cols);
910 let mut param_low = vec![0.5_f32; 4];
911 came_update(&mut param_low, &grad, &mut state_low, &cfg_low, rows, cols)
912 .expect("low beta3 update failed");
913
914 let sum_high: f32 = state_high.exp_avg_insta_sq_row.iter().sum();
915 let sum_low: f32 = state_low.exp_avg_insta_sq_row.iter().sum();
916 assert!(
917 sum_high < sum_low,
918 "higher β3 should give smaller insta_sq update: high={sum_high}, low={sum_low}"
919 );
920 }
921
922 #[test]
923 fn test_came_three_groups_distinct_states() {
924 let cfg = CameConfig::default();
925 let mut optimizer = CameOptimizer::new(cfg);
926 optimizer.add_param_group(2, 1, 2);
927 optimizer.add_param_group(4, 2, 2);
928 optimizer.add_param_group(6, 2, 3);
929
930 let mut params = vec![vec![1.0_f32; 2], vec![1.0_f32; 4], vec![1.0_f32; 6]];
931 let grads = vec![vec![0.1_f32; 2], vec![0.1_f32; 4], vec![0.1_f32; 6]];
932 optimizer.step(&mut params, &grads).expect("step failed");
933 assert_eq!(optimizer.states[0].step, 1);
934 assert_eq!(optimizer.states[1].step, 1);
935 assert_eq!(optimizer.states[2].step, 1);
936 assert_eq!(optimizer.states[0].exp_avg.len(), 2);
937 assert_eq!(optimizer.states[1].exp_avg.len(), 4);
938 assert_eq!(optimizer.states[2].exp_avg.len(), 6);
939 }
940
941 #[test]
942 fn test_came_lr_scaling_effect() {
943 let grad = vec![0.1_f32; 3];
944
945 let cfg_small_lr = CameConfig {
946 lr: 1e-4,
947 weight_decay: 0.0,
948 ..CameConfig::default()
949 };
950 let mut s_small = CameParamState::new_1d(3);
951 let mut p_small = vec![2.0_f32; 3];
952 came_update(&mut p_small, &grad, &mut s_small, &cfg_small_lr, 1, 3)
953 .expect("small lr failed");
954
955 let cfg_large_lr = CameConfig {
956 lr: 1e-1,
957 weight_decay: 0.0,
958 ..CameConfig::default()
959 };
960 let mut s_large = CameParamState::new_1d(3);
961 let mut p_large = vec![2.0_f32; 3];
962 came_update(&mut p_large, &grad, &mut s_large, &cfg_large_lr, 1, 3)
963 .expect("large lr failed");
964
965 let change_small: f32 = (2.0 - p_small[0]).abs();
966 let change_large: f32 = (2.0 - p_large[0]).abs();
967 assert!(
968 change_large > change_small,
969 "larger lr should produce larger change: small={change_small}, large={change_large}"
970 );
971 }
972
973 #[test]
974 fn test_came_dimension_mismatch_rows_cols_wrong() {
975 let cfg = CameConfig::default();
976 let mut state = CameParamState::new_2d(9, 3, 3);
977 let mut param = vec![0.0_f32; 8];
979 let grad = vec![0.0_f32; 8];
980 let result = came_update(&mut param, &grad, &mut state, &cfg, 3, 3);
981 assert!(result.is_err(), "should return error on dimension mismatch");
982 }
983
984 #[test]
985 fn test_came_exp_avg_direction_matches_grad() {
986 let cfg = CameConfig::default();
987 let mut state = CameParamState::new_1d(3);
988 let mut param = vec![0.0_f32; 3];
989 let grad = vec![0.5_f32, -0.5_f32, 0.3_f32];
990 came_update(&mut param, &grad, &mut state, &cfg, 1, 3).expect("update failed");
991 assert!(
992 state.exp_avg[0] > 0.0,
993 "positive grad → positive exp_avg[0]"
994 );
995 assert!(
996 state.exp_avg[1] < 0.0,
997 "negative grad → negative exp_avg[1]"
998 );
999 assert!(
1000 state.exp_avg[2] > 0.0,
1001 "positive grad → positive exp_avg[2]"
1002 );
1003 }
1004}