1use anyhow::Result;
44use std::collections::HashMap;
45use trustformers_core::tensor::Tensor;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum CurvatureSource {
54 None,
56 HutchinsonFromOracle,
60 EmpiricalFisherDiagonal,
67}
68
69#[derive(Debug, Clone)]
76struct RademacherStream {
77 seed: u64,
78 counter: u64,
79}
80
81impl RademacherStream {
82 fn new(seed: u64) -> Self {
83 Self { seed, counter: 0 }
84 }
85
86 fn next_bits(&mut self) -> u64 {
87 self.counter = self.counter.wrapping_add(1);
88 let mut z = self.seed.wrapping_add(self.counter.wrapping_mul(0x9E37_79B9_7F4A_7C15));
89 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
90 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
91 z ^ (z >> 31)
92 }
93
94 fn sample(&mut self, len: usize) -> Vec<f32> {
96 (0..len).map(|_| if self.next_bits() & 1 == 0 { -1.0 } else { 1.0 }).collect()
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct SOFOConfig {
103 pub learning_rate: f32,
105 pub batch_size: usize,
107 pub forward_passes: usize,
109 pub curvature_strength: f32,
111 pub damping: f32,
113 pub weight_decay: f32,
115 pub adaptive_curvature: bool,
117 pub momentum: f32,
119 pub nesterov: bool,
121 pub max_condition_number: f32,
123 pub memory_efficient: bool,
125 pub parallel_threshold: usize,
127 pub hvp_epsilon: f32,
133 pub probe_seed: u64,
135}
136
137impl Default for SOFOConfig {
138 fn default() -> Self {
139 Self {
140 learning_rate: 1e-3,
141 batch_size: 32,
142 forward_passes: 8,
143 curvature_strength: 0.1,
144 damping: 1e-6,
145 weight_decay: 0.0,
146 adaptive_curvature: true,
147 momentum: 0.9,
148 nesterov: true,
149 max_condition_number: 1e6,
150 memory_efficient: true,
151 parallel_threshold: 1000,
152 hvp_epsilon: 1e-3,
153 probe_seed: 0x5060_F0F0_1234_5678,
154 }
155 }
156}
157
158impl SOFOConfig {
159 pub fn new() -> Self {
161 Self::default()
162 }
163
164 pub fn learning_rate(mut self, lr: f32) -> Self {
166 self.learning_rate = lr;
167 self
168 }
169
170 pub fn batch_size(mut self, batch_size: usize) -> Self {
172 self.batch_size = batch_size;
173 self
174 }
175
176 pub fn forward_passes(mut self, passes: usize) -> Self {
178 self.forward_passes = passes;
179 self
180 }
181
182 pub fn curvature_strength(mut self, strength: f32) -> Self {
184 self.curvature_strength = strength;
185 self
186 }
187
188 pub fn damping(mut self, damping: f32) -> Self {
190 self.damping = damping;
191 self
192 }
193
194 pub fn weight_decay(mut self, decay: f32) -> Self {
196 self.weight_decay = decay;
197 self
198 }
199
200 pub fn momentum(mut self, momentum: f32) -> Self {
202 self.momentum = momentum;
203 self
204 }
205
206 pub fn hvp_epsilon(mut self, eps: f32) -> Self {
208 self.hvp_epsilon = eps;
209 self
210 }
211
212 pub fn probe_seed(mut self, seed: u64) -> Self {
214 self.probe_seed = seed;
215 self
216 }
217
218 pub fn adaptive_curvature(mut self, enable: bool) -> Self {
221 self.adaptive_curvature = enable;
222 self
223 }
224
225 pub fn max_condition_number(mut self, max_condition_number: f32) -> Self {
227 self.max_condition_number = max_condition_number;
228 self
229 }
230
231 pub fn build(self) -> Self {
232 self
233 }
234}
235
236#[derive(Debug, Clone)]
238pub struct SOFOState {
239 pub step: u64,
241 pub momentum_buffers: HashMap<String, Tensor>,
243 pub curvature_estimates: HashMap<String, Tensor>,
245 pub forward_gradients: HashMap<String, Vec<Tensor>>,
247 pub eigenvalue_estimates: HashMap<String, Tensor>,
249 pub adaptive_weights: HashMap<String, f32>,
251 pub forward_stats: ForwardModeStats,
253 pub memory_stats: MemoryStats,
255 pub curvature_source: CurvatureSource,
257}
258
259#[derive(Debug, Clone, Default)]
265pub struct ForwardModeStats {
266 pub total_forward_passes: u64,
268 pub total_oracle_time: std::time::Duration,
270}
271
272impl ForwardModeStats {
273 pub fn avg_forward_time(&self) -> Option<std::time::Duration> {
275 if self.total_forward_passes == 0 {
276 None
277 } else {
278 Some(self.total_oracle_time / self.total_forward_passes as u32)
279 }
280 }
281}
282
283#[derive(Debug, Clone, Default)]
285pub struct MemoryStats {
286 pub state_bytes: usize,
288 pub current_state_mb: f32,
290 pub peak_state_mb: f32,
292 pub num_parameters: usize,
294}
295
296impl Default for SOFOState {
297 fn default() -> Self {
298 Self {
299 step: 0,
300 momentum_buffers: HashMap::new(),
301 curvature_estimates: HashMap::new(),
302 forward_gradients: HashMap::new(),
303 eigenvalue_estimates: HashMap::new(),
304 adaptive_weights: HashMap::new(),
305 forward_stats: ForwardModeStats::default(),
306 memory_stats: MemoryStats::default(),
307 curvature_source: CurvatureSource::None,
308 }
309 }
310}
311
312pub struct SOFO {
317 config: SOFOConfig,
318 state: SOFOState,
319 rademacher: RademacherStream,
320}
321
322impl SOFO {
323 pub fn new(config: SOFOConfig) -> Self {
325 let rademacher = RademacherStream::new(config.probe_seed);
326 Self {
327 config,
328 state: SOFOState::default(),
329 rademacher,
330 }
331 }
332
333 pub fn learning_rate(&self) -> f32 {
335 self.config.learning_rate
336 }
337
338 pub fn set_learning_rate(&mut self, lr: f32) {
340 self.config.learning_rate = lr;
341 }
342
343 fn generate_random_directions(
349 &mut self,
350 parameters: &HashMap<String, Tensor>,
351 ) -> Result<Vec<HashMap<String, Tensor>>> {
352 let mut names: Vec<&String> = parameters.keys().collect();
355 names.sort();
356
357 let mut direction_sets = Vec::with_capacity(self.config.forward_passes);
358 for _ in 0..self.config.forward_passes {
359 let mut directions = HashMap::new();
360 for name in &names {
361 let Some(parameter) = parameters.get(*name) else {
362 continue;
363 };
364 let shape = parameter.shape();
365 let total: usize = shape.iter().product();
366 let probe = self.rademacher.sample(total);
367 directions.insert((*name).clone(), Tensor::from_vec(probe, &shape)?);
368 }
369 direction_sets.push(directions);
370 }
371
372 Ok(direction_sets)
373 }
374
375 fn empirical_fisher_curvature(
384 &self,
385 gradients: &HashMap<String, Tensor>,
386 ) -> Result<HashMap<String, Tensor>> {
387 let mut estimates = HashMap::new();
388 for (param_name, gradient) in gradients.iter() {
389 let squared = gradient.mul(gradient)?;
390 estimates.insert(param_name.clone(), squared.add_scalar(self.config.damping)?);
391 }
392 Ok(estimates)
393 }
394
395 fn hutchinson_curvature<F>(
408 &mut self,
409 parameters: &HashMap<String, Tensor>,
410 oracle: &mut F,
411 ) -> Result<HashMap<String, Tensor>>
412 where
413 F: FnMut(&HashMap<String, Tensor>) -> Result<HashMap<String, Tensor>>,
414 {
415 let eps = self.config.hvp_epsilon;
416 let direction_sets = self.generate_random_directions(parameters)?;
417
418 let mut accumulator: HashMap<String, Vec<f32>> = HashMap::new();
419 let mut probes_used = 0usize;
420
421 for directions in &direction_sets {
422 let mut plus = HashMap::new();
423 let mut minus = HashMap::new();
424 for (name, parameter) in parameters.iter() {
425 let Some(direction) = directions.get(name) else {
426 continue;
427 };
428 plus.insert(name.clone(), parameter.add(&direction.mul_scalar(eps)?)?);
429 minus.insert(name.clone(), parameter.sub(&direction.mul_scalar(eps)?)?);
430 }
431
432 let grad_plus = oracle(&plus)?;
433 let grad_minus = oracle(&minus)?;
434 self.state.forward_stats.total_forward_passes += 2;
435 probes_used += 1;
436
437 for (name, direction) in directions.iter() {
438 let (Some(gp), Some(gm)) = (grad_plus.get(name), grad_minus.get(name)) else {
439 continue;
440 };
441 let gp_data = gp.data_f32()?;
442 let gm_data = gm.data_f32()?;
443 let v_data = direction.data_f32()?;
444 if gp_data.len() != v_data.len() || gm_data.len() != v_data.len() {
445 return Err(anyhow::anyhow!(
446 "gradient oracle returned {} / {} elements for '{name}' but the \
447 parameter has {}",
448 gp_data.len(),
449 gm_data.len(),
450 v_data.len()
451 ));
452 }
453 let slot =
454 accumulator.entry(name.clone()).or_insert_with(|| vec![0.0; v_data.len()]);
455 for i in 0..v_data.len() {
456 let hv = (gp_data[i] - gm_data[i]) / (2.0 * eps);
458 slot[i] += v_data[i] * hv;
459 }
460 }
461 }
462
463 let mut estimates = HashMap::new();
464 let divisor = probes_used.max(1) as f32;
465 for (name, mut values) in accumulator {
466 for value in values.iter_mut() {
467 *value = (*value / divisor).abs() + self.config.damping;
470 }
471 let shape =
472 parameters.get(&name).map(|t| t.shape()).unwrap_or_else(|| vec![values.len()]);
473 estimates.insert(name, Tensor::from_vec(values, &shape)?);
474 }
475
476 Ok(estimates)
477 }
478
479 fn apply_adaptive_curvature(
481 &mut self,
482 param_name: &str,
483 curvature: &Tensor,
484 gradient: &Tensor,
485 ) -> Result<Tensor> {
486 if !self.config.adaptive_curvature {
487 return Ok(curvature.clone());
488 }
489
490 let grad_norm = gradient.norm()?;
492 let curv_norm = curvature.norm()?;
493
494 let alignment = if grad_norm > 0.0 && curv_norm > 0.0 {
495 let grad_data = gradient.data_f32()?;
496 let curv_data = curvature.data_f32()?;
497 let dot_product: f32 =
498 grad_data.iter().zip(curv_data.iter()).map(|(&a, &b)| a * b).sum();
499 dot_product / (grad_norm * curv_norm)
500 } else {
501 0.0
502 };
503
504 let adaptive_weight = (1.0 + alignment.abs()) * self.config.curvature_strength;
506 self.state.adaptive_weights.insert(param_name.to_string(), adaptive_weight);
507
508 Ok(curvature.mul_scalar(adaptive_weight)?)
510 }
511
512 fn update_momentum(&mut self, param_name: &str, gradient: &Tensor) -> Result<Tensor> {
514 let momentum = self.config.momentum;
515
516 let momentum_update =
517 if let Some(prev_momentum) = self.state.momentum_buffers.get(param_name) {
518 let momentum_tensor = Tensor::scalar(momentum)?;
519 let one_minus_momentum = Tensor::scalar(1.0 - momentum)?;
520
521 let weighted_prev = prev_momentum.mul(&momentum_tensor)?;
522 let weighted_grad = gradient.mul(&one_minus_momentum)?;
523 weighted_prev.add(&weighted_grad)?
524 } else {
525 gradient.mul(&Tensor::scalar(1.0 - momentum)?)?
526 };
527
528 self.state
529 .momentum_buffers
530 .insert(param_name.to_string(), momentum_update.clone());
531 Ok(momentum_update)
532 }
533
534 fn compute_second_order_update(&self, gradient: &Tensor, curvature: &Tensor) -> Result<Tensor> {
536 let regularized_curvature = curvature.add(&Tensor::scalar(self.config.damping)?)?;
540 let newton_direction = gradient.div(®ularized_curvature)?;
541
542 Ok(newton_direction)
543 }
544
545 fn control_condition_number(&self, curvature: &Tensor) -> Result<Tensor> {
547 let min_eigenvalue = self.config.damping;
549 let max_eigenvalue = min_eigenvalue * self.config.max_condition_number;
550
551 Ok(curvature.clamp(min_eigenvalue, max_eigenvalue)?)
552 }
553
554 fn update_memory_stats(&mut self, parameters: &HashMap<String, Tensor>) {
560 let scalar_count: usize =
561 parameters.values().map(|t| t.shape().iter().product::<usize>()).sum();
562
563 let state_bytes: usize = self
564 .state
565 .momentum_buffers
566 .values()
567 .chain(self.state.curvature_estimates.values())
568 .map(|t| t.shape().iter().product::<usize>() * std::mem::size_of::<f32>())
569 .sum();
570
571 let state_mb = state_bytes as f32 / (1024.0 * 1024.0);
572 self.state.memory_stats.current_state_mb = state_mb;
573 self.state.memory_stats.peak_state_mb = self.state.memory_stats.peak_state_mb.max(state_mb);
574 self.state.memory_stats.num_parameters = scalar_count;
575 self.state.memory_stats.state_bytes = state_bytes;
576 }
577
578 pub fn step(
591 &mut self,
592 parameters: &mut HashMap<String, Tensor>,
593 gradients: &HashMap<String, Tensor>,
594 ) -> Result<()> {
595 let curvature_estimates = self.empirical_fisher_curvature(gradients)?;
596 self.state.curvature_source = CurvatureSource::EmpiricalFisherDiagonal;
597 self.apply_step(parameters, gradients, curvature_estimates)
598 }
599
600 pub fn step_with_gradient_oracle<F>(
612 &mut self,
613 parameters: &mut HashMap<String, Tensor>,
614 gradients: &HashMap<String, Tensor>,
615 oracle: &mut F,
616 ) -> Result<()>
617 where
618 F: FnMut(&HashMap<String, Tensor>) -> Result<HashMap<String, Tensor>>,
619 {
620 let started = std::time::Instant::now();
621 let snapshot: HashMap<String, Tensor> =
622 parameters.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
623 let curvature_estimates = self.hutchinson_curvature(&snapshot, oracle)?;
624 self.state.forward_stats.total_oracle_time += started.elapsed();
625 self.state.curvature_source = CurvatureSource::HutchinsonFromOracle;
626 self.apply_step(parameters, gradients, curvature_estimates)
627 }
628
629 fn apply_step(
632 &mut self,
633 parameters: &mut HashMap<String, Tensor>,
634 gradients: &HashMap<String, Tensor>,
635 curvature_estimates: HashMap<String, Tensor>,
636 ) -> Result<()> {
637 self.state.step += 1;
638
639 for (param_name, gradient) in gradients.iter() {
640 if let Some(parameter) = parameters.get_mut(param_name) {
641 let mut effective_gradient = gradient.clone();
643 if self.config.weight_decay > 0.0 {
644 let weight_decay_term =
645 parameter.mul(&Tensor::scalar(self.config.weight_decay)?)?;
646 effective_gradient = effective_gradient.add(&weight_decay_term)?;
647 }
648
649 let curvature = if let Some(curv) = curvature_estimates.get(param_name) {
651 self.apply_adaptive_curvature(param_name, curv, &effective_gradient)?
652 } else {
653 Tensor::ones_like(&effective_gradient)?
655 .mul(&Tensor::scalar(self.config.damping)?)?
656 };
657
658 let controlled_curvature = self.control_condition_number(&curvature)?;
660
661 let second_order_direction =
663 self.compute_second_order_update(&effective_gradient, &controlled_curvature)?;
664
665 let momentum_update = self.update_momentum(param_name, &second_order_direction)?;
667
668 let final_update = if self.config.nesterov {
670 let momentum_tensor = Tensor::scalar(self.config.momentum)?;
672 momentum_update.mul(&momentum_tensor)?.add(&second_order_direction)?
673 } else {
674 momentum_update
675 };
676
677 let lr_tensor = Tensor::scalar(self.config.learning_rate)?;
679 let param_update = final_update.mul(&lr_tensor)?;
680
681 *parameter = parameter.sub(¶m_update)?;
682
683 self.state.curvature_estimates.insert(param_name.clone(), controlled_curvature);
685 }
686 }
687
688 self.update_memory_stats(parameters);
690
691 Ok(())
692 }
693
694 pub fn get_sofo_stats(&self) -> SOFOStats {
696 let avg_curvature_strength = if self.state.adaptive_weights.is_empty() {
697 self.config.curvature_strength
698 } else {
699 self.state.adaptive_weights.values().sum::<f32>()
700 / self.state.adaptive_weights.len() as f32
701 };
702
703 let avg_condition_number = if self.state.curvature_estimates.is_empty() {
706 1.0
707 } else {
708 let mut total_condition = 0.0;
709 let mut count = 0;
710
711 for curvature in self.state.curvature_estimates.values() {
712 if let Ok((min_val, max_val)) = curvature.min_max() {
713 if min_val > 0.0 {
714 total_condition += max_val / min_val;
715 count += 1;
716 }
717 }
718 }
719
720 if count > 0 {
721 total_condition / count as f32
722 } else {
723 1.0
724 }
725 };
726
727 SOFOStats {
728 step: self.state.step,
729 total_forward_passes: self.state.forward_stats.total_forward_passes,
730 avg_curvature_strength,
731 avg_condition_number,
732 curvature_source: self.state.curvature_source,
733 state_bytes: self.state.memory_stats.state_bytes,
734 current_state_mb: self.state.memory_stats.current_state_mb,
735 num_parameters: self.state.memory_stats.num_parameters,
736 }
737 }
738
739 pub fn curvature_source(&self) -> CurvatureSource {
741 self.state.curvature_source
742 }
743
744 pub fn get_forward_stats(&self) -> &ForwardModeStats {
746 &self.state.forward_stats
747 }
748
749 pub fn get_memory_stats(&self) -> &MemoryStats {
751 &self.state.memory_stats
752 }
753
754 pub fn reset_state(&mut self) {
756 self.state = SOFOState::default();
757 }
758
759 pub fn get_curvature_estimates(&self) -> &HashMap<String, Tensor> {
761 &self.state.curvature_estimates
762 }
763
764 pub fn get_adaptive_weights(&self) -> &HashMap<String, f32> {
766 &self.state.adaptive_weights
767 }
768}
769
770#[derive(Debug, Clone)]
772pub struct SOFOStats {
773 pub step: u64,
775 pub total_forward_passes: u64,
777 pub avg_curvature_strength: f32,
779 pub avg_condition_number: f32,
781 pub curvature_source: CurvatureSource,
783 pub state_bytes: usize,
785 pub current_state_mb: f32,
787 pub num_parameters: usize,
789}
790
791#[cfg(test)]
792mod tests {
793 use super::*;
794 use trustformers_core::tensor::Tensor;
795
796 #[test]
797 fn test_sofo_creation() {
798 let config = SOFOConfig::new().learning_rate(1e-3).batch_size(32).forward_passes(8).build();
799
800 let optimizer = SOFO::new(config);
801 assert_eq!(optimizer.learning_rate(), 1e-3);
802 }
803
804 #[test]
805 fn test_sofo_config_builder() {
806 let config = SOFOConfig::new()
807 .learning_rate(2e-3)
808 .batch_size(64)
809 .forward_passes(16)
810 .curvature_strength(0.2)
811 .damping(1e-5)
812 .weight_decay(1e-4)
813 .momentum(0.95)
814 .build();
815
816 assert_eq!(config.learning_rate, 2e-3);
817 assert_eq!(config.batch_size, 64);
818 assert_eq!(config.forward_passes, 16);
819 assert_eq!(config.curvature_strength, 0.2);
820 assert_eq!(config.damping, 1e-5);
821 assert_eq!(config.weight_decay, 1e-4);
822 assert_eq!(config.momentum, 0.95);
823 }
824
825 #[test]
826 fn test_sofo_step() -> Result<()> {
827 let config = SOFOConfig::new().learning_rate(1e-2).forward_passes(4).build();
828 let mut optimizer = SOFO::new(config);
829
830 let mut parameters = HashMap::new();
832 parameters.insert("weight".to_string(), Tensor::ones(&[2, 2])?);
833
834 let mut gradients = HashMap::new();
835 gradients.insert(
836 "weight".to_string(),
837 Tensor::ones(&[2, 2])?.mul_scalar(0.1)?,
838 );
839
840 let original_value =
842 parameters.get("weight").expect("Key not found").mean()?.to_scalar()?;
843
844 optimizer.step(&mut parameters, &gradients)?;
846
847 let updated_value = parameters.get("weight").expect("Key not found").mean()?.to_scalar()?;
849 assert_ne!(updated_value, original_value);
850
851 Ok(())
852 }
853
854 #[test]
855 fn test_random_direction_generation() -> Result<()> {
856 let config = SOFOConfig::new().forward_passes(3).build();
857 let mut optimizer = SOFO::new(config);
858
859 let mut parameters = HashMap::new();
860 parameters.insert("weight1".to_string(), Tensor::ones(&[2, 2])?);
861 parameters.insert("weight2".to_string(), Tensor::ones(&[3, 3])?);
862
863 let direction_sets = optimizer.generate_random_directions(¶meters)?;
864
865 assert_eq!(direction_sets.len(), 3);
866 for directions in &direction_sets {
867 assert_eq!(directions.len(), 2);
868 assert!(directions.contains_key("weight1"));
869 assert!(directions.contains_key("weight2"));
870 }
871
872 Ok(())
873 }
874
875 #[test]
877 fn test_empirical_fisher_curvature() -> Result<()> {
878 let config = SOFOConfig::new().damping(1e-3).build();
879 let optimizer = SOFO::new(config);
880
881 let mut gradients = HashMap::new();
882 gradients.insert(
883 "weight".to_string(),
884 Tensor::from_vec(vec![2.0_f32, -3.0, 0.5, 0.0], &[2, 2])?,
885 );
886
887 let curvature = optimizer.empirical_fisher_curvature(&gradients)?;
888 let values = curvature.get("weight").expect("curvature present").data_f32()?;
889
890 let expected = [4.0_f32 + 1e-3, 9.0 + 1e-3, 0.25 + 1e-3, 1e-3];
891 assert_eq!(values.len(), expected.len());
892 for (actual, want) in values.iter().zip(expected.iter()) {
893 assert!((actual - want).abs() < 1e-5, "got {actual}, want {want}");
894 }
895
896 Ok(())
897 }
898
899 #[test]
900 fn test_momentum_update() -> Result<()> {
901 let config = SOFOConfig::new().momentum(0.9).build();
902 let mut optimizer = SOFO::new(config);
903
904 let gradient = Tensor::ones(&[2, 2])?.mul_scalar(0.5)?;
905
906 let momentum1 = optimizer.update_momentum("test", &gradient)?;
908
909 let momentum2 = optimizer.update_momentum("test", &gradient)?;
911
912 assert_ne!(
914 momentum1.mean()?.to_scalar()?,
915 momentum2.mean()?.to_scalar()?
916 );
917
918 Ok(())
919 }
920
921 #[test]
922 fn test_second_order_update() -> Result<()> {
923 let config = SOFOConfig::new().build();
924 let optimizer = SOFO::new(config);
925
926 let gradient = Tensor::ones(&[2, 2])?.mul_scalar(0.5)?;
927 let curvature = Tensor::ones(&[2, 2])?.mul_scalar(2.0)?;
928
929 let update = optimizer.compute_second_order_update(&gradient, &curvature)?;
930
931 let expected = 0.5 / 2.0; let actual = update.mean()?.to_scalar()?;
934
935 assert!((actual - expected).abs() < 0.1);
936
937 Ok(())
938 }
939
940 #[test]
941 fn test_condition_number_control() -> Result<()> {
942 let config = SOFOConfig::new().damping(1e-3).max_condition_number(100.0).build();
943 let optimizer = SOFO::new(config);
944
945 let curvature = Tensor::from_vec(vec![1e-6_f32, 1e6, 1.0, 1e3], &[2, 2])?;
947
948 let controlled = optimizer.control_condition_number(&curvature)?;
949
950 let values = controlled.data_f32()?;
952 let max_val = values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
953 let min_val = values.iter().copied().fold(f32::INFINITY, f32::min);
954
955 assert!(
956 max_val / min_val <= 100.0 * 1.1,
957 "condition number {} exceeds the configured bound",
958 max_val / min_val
959 );
960
961 Ok(())
962 }
963
964 #[test]
965 fn test_sofo_stats() -> Result<()> {
966 let config = SOFOConfig::new().forward_passes(4).build();
967 let mut optimizer = SOFO::new(config);
968
969 let mut parameters = HashMap::new();
971 parameters.insert("weight".to_string(), Tensor::ones(&[2, 2])?);
972
973 let mut gradients = HashMap::new();
974 gradients.insert(
975 "weight".to_string(),
976 Tensor::ones(&[2, 2])?.mul_scalar(0.1)?,
977 );
978
979 for _ in 0..3 {
980 optimizer.step(&mut parameters, &gradients)?;
981 }
982
983 let stats = optimizer.get_sofo_stats();
984 assert_eq!(stats.step, 3);
985 assert!(stats.num_parameters > 0);
986 assert!(
987 stats.state_bytes > 0,
988 "state size must be measured, not invented"
989 );
990 assert_eq!(
993 stats.total_forward_passes, 0,
994 "the gradient-only path performs no forward-mode passes"
995 );
996 assert_eq!(
997 stats.curvature_source,
998 CurvatureSource::EmpiricalFisherDiagonal
999 );
1000
1001 Ok(())
1002 }
1003
1004 #[test]
1006 fn test_sofo_forward_passes_are_counted_only_when_performed() -> Result<()> {
1007 let config = SOFOConfig::new().forward_passes(2).build();
1008 let mut optimizer = SOFO::new(config);
1009
1010 let mut parameters = HashMap::new();
1011 parameters.insert("weight".to_string(), Tensor::ones(&[2, 2])?);
1012
1013 let mut calls = 0_usize;
1014 let mut oracle = |params: &HashMap<String, Tensor>| -> Result<HashMap<String, Tensor>> {
1015 calls += 1;
1016 let mut grads = HashMap::new();
1017 for (name, tensor) in params {
1018 grads.insert(name.clone(), tensor.mul_scalar(2.0)?);
1019 }
1020 Ok(grads)
1021 };
1022
1023 let mut gradients = HashMap::new();
1024 gradients.insert(
1025 "weight".to_string(),
1026 Tensor::ones(&[2, 2])?.mul_scalar(2.0)?,
1027 );
1028
1029 optimizer.step_with_gradient_oracle(&mut parameters, &gradients, &mut oracle)?;
1030
1031 let stats = optimizer.get_sofo_stats();
1032 assert!(calls > 0, "the oracle must actually be evaluated");
1033 assert_eq!(
1034 stats.total_forward_passes as usize, calls,
1035 "every counted forward pass must correspond to a real oracle call"
1036 );
1037 assert_eq!(
1038 stats.curvature_source,
1039 CurvatureSource::HutchinsonFromOracle
1040 );
1041
1042 Ok(())
1043 }
1044
1045 #[test]
1046 fn test_learning_rate_methods() {
1047 let config = SOFOConfig::new().learning_rate(1e-3).build();
1048 let mut optimizer = SOFO::new(config);
1049
1050 assert_eq!(optimizer.learning_rate(), 1e-3);
1051
1052 optimizer.set_learning_rate(2e-3);
1053 assert_eq!(optimizer.learning_rate(), 2e-3);
1054 }
1055
1056 #[test]
1057 fn test_weight_decay() -> Result<()> {
1058 let config = SOFOConfig::new()
1059 .learning_rate(1e-2)
1060 .weight_decay(1e-2)
1061 .forward_passes(2)
1062 .build();
1063 let mut optimizer = SOFO::new(config);
1064
1065 let mut parameters = HashMap::new();
1066 parameters.insert("weight".to_string(), Tensor::ones(&[2, 2])?);
1067
1068 let mut gradients = HashMap::new();
1069 gradients.insert("weight".to_string(), Tensor::zeros(&[2, 2])?);
1070
1071 let initial_param_value =
1072 parameters.get("weight").expect("Key not found").mean()?.to_scalar()?;
1073
1074 optimizer.step(&mut parameters, &gradients)?;
1075
1076 let final_param_value =
1077 parameters.get("weight").expect("Key not found").mean()?.to_scalar()?;
1078
1079 assert!(final_param_value < initial_param_value);
1081
1082 Ok(())
1083 }
1084
1085 #[test]
1086 fn test_adaptive_curvature() -> Result<()> {
1087 let config = SOFOConfig::new().adaptive_curvature(true).curvature_strength(0.1).build();
1088 let mut optimizer = SOFO::new(config);
1089
1090 let gradient = Tensor::ones(&[2, 2])?.mul_scalar(0.5)?;
1091 let curvature = Tensor::ones(&[2, 2])?.mul_scalar(2.0)?;
1092
1093 let adaptive_curvature =
1094 optimizer.apply_adaptive_curvature("test", &curvature, &gradient)?;
1095
1096 let original_mean = curvature.mean()?.to_scalar()?;
1098 let adaptive_mean = adaptive_curvature.mean()?.to_scalar()?;
1099
1100 assert_ne!(original_mean, adaptive_mean);
1101
1102 Ok(())
1103 }
1104}