optirs_learned/transformer/strategies/
gradient_processing.rs1use std::fmt::Debug;
2use scirs2_core::ndarray::Array1;
12use scirs2_core::numeric::Float;
13use std::collections::{HashMap, VecDeque};
14
15use crate::error::Result;
16
17#[derive(Debug, Clone, Copy)]
19pub enum GradientProcessingStrategy {
20 Raw,
22 Clipping,
24 Normalization,
26 AdaptiveScaling,
28 Adaptive,
30 Smoothing,
32 Accumulation,
34 Dropout,
36 Compression,
38}
39
40#[derive(Debug, Clone)]
42struct PerParameterState<T: Float + Debug + Default + Clone + Send + Sync + 'static> {
43 gradient_history: VecDeque<Array1<T>>,
45
46 accumulated_gradients: Option<Array1<T>>,
48
49 stats: GradientStatistics<T>,
51}
52
53impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> PerParameterState<T> {
54 fn new() -> Self {
55 Self {
56 gradient_history: VecDeque::new(),
57 accumulated_gradients: None,
58 stats: GradientStatistics::new(),
59 }
60 }
61
62 fn ensure_width(&mut self, width: usize) {
64 if self
65 .gradient_history
66 .back()
67 .is_some_and(|g| g.len() != width)
68 {
69 self.gradient_history.clear();
70 }
71 if self
72 .accumulated_gradients
73 .as_ref()
74 .is_some_and(|a| a.len() != width)
75 {
76 self.accumulated_gradients = None;
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct GradientProcessor<
84 T: Float
85 + Debug
86 + Default
87 + Clone
88 + std::iter::Sum
89 + scirs2_core::ndarray::ScalarOperand
90 + Send
91 + Sync
92 + 'static,
93> {
94 strategy: GradientProcessingStrategy,
96
97 param_states: HashMap<String, PerParameterState<T>>,
99
100 gradient_stats: GradientStatistics<T>,
102
103 processing_params: GradientProcessingParams<T>,
105}
106
107#[derive(Debug, Clone)]
109pub struct GradientProcessingParams<T: Float + Debug + Send + Sync + 'static> {
110 pub clip_threshold: T,
112
113 pub smoothing_factor: T,
115
116 pub accumulation_steps: usize,
118
119 pub dropout_prob: f64,
121
122 pub compression_ratio: f64,
124
125 pub norm_eps: T,
127}
128
129#[derive(Debug, Clone)]
131pub struct GradientStatistics<T: Float + Debug + Send + Sync + 'static> {
132 mean_magnitude: T,
134
135 var_magnitude: T,
137
138 max_magnitude: T,
140
141 min_magnitude: T,
143
144 update_count: usize,
146
147 sparsity: T,
149}
150
151impl<
152 T: Float
153 + Debug
154 + Default
155 + Clone
156 + std::iter::Sum
157 + scirs2_core::ndarray::ScalarOperand
158 + Send
159 + Sync
160 + 'static,
161 > GradientProcessor<T>
162{
163 pub fn new(strategy: GradientProcessingStrategy) -> Self {
165 Self {
166 strategy,
167 param_states: HashMap::new(),
168 gradient_stats: GradientStatistics::new(),
169 processing_params: GradientProcessingParams::default(),
170 }
171 }
172
173 pub fn new_with_params(
175 strategy: GradientProcessingStrategy,
176 params: GradientProcessingParams<T>,
177 ) -> Self {
178 Self {
179 strategy,
180 param_states: HashMap::new(),
181 gradient_stats: GradientStatistics::new(),
182 processing_params: params,
183 }
184 }
185
186 pub fn process_gradients(
191 &mut self,
192 param_name: &str,
193 gradients: &Array1<T>,
194 ) -> Result<Array1<T>> {
195 self.gradient_stats.update(gradients);
197
198 let params = self.processing_params.clone();
199 let strategy = self.strategy;
200
201 let state = self
202 .param_states
203 .entry(param_name.to_string())
204 .or_insert_with(PerParameterState::new);
205 state.ensure_width(gradients.len());
206 state.stats.update(gradients);
207
208 match strategy {
209 GradientProcessingStrategy::Raw => Ok(gradients.clone()),
210 GradientProcessingStrategy::Clipping => Ok(Self::clip_gradients(¶ms, gradients)),
211 GradientProcessingStrategy::Normalization => {
212 Ok(Self::normalize_gradients(¶ms, gradients))
213 }
214 GradientProcessingStrategy::AdaptiveScaling | GradientProcessingStrategy::Adaptive => {
215 Ok(Self::adaptive_scale_gradients(state, gradients))
216 }
217 GradientProcessingStrategy::Smoothing => {
218 Ok(Self::smooth_gradients(state, ¶ms, gradients))
219 }
220 GradientProcessingStrategy::Accumulation => {
221 Ok(Self::accumulate_gradients(state, ¶ms, gradients))
222 }
223 GradientProcessingStrategy::Dropout => Ok(Self::dropout_gradients(¶ms, gradients)),
224 GradientProcessingStrategy::Compression => {
225 Ok(Self::compress_gradients(¶ms, gradients))
226 }
227 }
228 }
229
230 fn clip_gradients(params: &GradientProcessingParams<T>, gradients: &Array1<T>) -> Array1<T> {
232 let grad_norm = Self::compute_gradient_norm(gradients);
233
234 if grad_norm > params.clip_threshold && grad_norm > T::zero() {
235 let scale = params.clip_threshold / grad_norm;
236 gradients * scale
237 } else {
238 gradients.clone()
239 }
240 }
241
242 fn normalize_gradients(
244 params: &GradientProcessingParams<T>,
245 gradients: &Array1<T>,
246 ) -> Array1<T> {
247 let grad_norm = Self::compute_gradient_norm(gradients);
248
249 if grad_norm > params.norm_eps {
250 gradients / grad_norm
251 } else {
252 gradients.clone()
253 }
254 }
255
256 fn adaptive_scale_gradients(state: &PerParameterState<T>, gradients: &Array1<T>) -> Array1<T> {
260 let current_norm = Self::compute_gradient_norm(gradients);
261 let mean_norm = state.stats.mean_magnitude;
262
263 let eps: T = scirs2_core::numeric::NumCast::from(1e-12).unwrap_or_else(T::zero);
266 if mean_norm > T::zero() && current_norm > eps {
267 let adaptive_scale = scirs2_core::numeric::NumCast::from(0.9).unwrap_or_else(T::zero)
268 * mean_norm
269 / current_norm
270 + scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(T::zero);
271 gradients * adaptive_scale
272 } else {
273 gradients.clone()
274 }
275 }
276
277 fn smooth_gradients(
279 state: &mut PerParameterState<T>,
280 params: &GradientProcessingParams<T>,
281 gradients: &Array1<T>,
282 ) -> Array1<T> {
283 let alpha = params.smoothing_factor;
284
285 let smoothed = match state.gradient_history.back() {
286 Some(prev_grad) => gradients * alpha + prev_grad * (T::one() - alpha),
287 None => gradients.clone(),
288 };
289
290 state.gradient_history.push_back(smoothed.clone());
291 if state.gradient_history.len() > 10 {
292 state.gradient_history.pop_front();
293 }
294
295 smoothed
296 }
297
298 fn accumulate_gradients(
300 state: &mut PerParameterState<T>,
301 params: &GradientProcessingParams<T>,
302 gradients: &Array1<T>,
303 ) -> Array1<T> {
304 let steps = params.accumulation_steps.max(1);
306
307 match state.accumulated_gradients {
308 Some(ref mut accumulated) => *accumulated = accumulated.clone() + gradients,
309 None => state.accumulated_gradients = Some(gradients.clone()),
310 }
311
312 if state.stats.update_count.is_multiple_of(steps) {
313 match state.accumulated_gradients.take() {
314 Some(accumulated) => {
315 let scale = scirs2_core::numeric::NumCast::from(1.0 / steps as f64)
316 .unwrap_or_else(T::one);
317 accumulated * scale
318 }
319 None => gradients.clone(),
320 }
321 } else {
322 Array1::zeros(gradients.len())
324 }
325 }
326
327 fn dropout_gradients(params: &GradientProcessingParams<T>, gradients: &Array1<T>) -> Array1<T> {
329 let mut result = gradients.clone();
330 let keep_prob = (1.0 - params.dropout_prob).max(f64::EPSILON);
331 let scale: T = scirs2_core::numeric::NumCast::from(1.0 / keep_prob).unwrap_or_else(T::one);
332 let dropped = (params.dropout_prob * 10.0) as usize;
333
334 for (i, elem) in result.iter_mut().enumerate() {
337 if (i % 10) < dropped {
338 *elem = T::zero();
339 } else {
340 *elem = *elem * scale;
341 }
342 }
343
344 result
345 }
346
347 fn compress_gradients(
349 params: &GradientProcessingParams<T>,
350 gradients: &Array1<T>,
351 ) -> Array1<T> {
352 let mut result = gradients.clone();
353 let threshold = Self::compute_gradient_norm(gradients)
354 * scirs2_core::numeric::NumCast::from(params.compression_ratio).unwrap_or_else(T::zero);
355
356 for elem in result.iter_mut() {
357 if elem.abs() < threshold {
358 *elem = T::zero();
359 }
360 }
361
362 result
363 }
364
365 fn compute_gradient_norm(gradients: &Array1<T>) -> T {
367 gradients
368 .iter()
369 .map(|&x| x * x)
370 .fold(T::zero(), |a, b| a + b)
371 .sqrt()
372 }
373
374 pub fn statistics(&self) -> &GradientStatistics<T> {
376 &self.gradient_stats
377 }
378
379 pub fn statistics_for(&self, param_name: &str) -> Option<&GradientStatistics<T>> {
381 self.param_states.get(param_name).map(|s| &s.stats)
382 }
383
384 pub fn strategy(&self) -> GradientProcessingStrategy {
386 self.strategy
387 }
388
389 pub fn parameters(&self) -> &GradientProcessingParams<T> {
391 &self.processing_params
392 }
393
394 pub fn set_strategy(&mut self, strategy: GradientProcessingStrategy) {
396 self.strategy = strategy;
397 }
398
399 pub fn set_parameters(&mut self, params: GradientProcessingParams<T>) {
401 self.processing_params = params;
402 }
403
404 pub fn reset(&mut self) {
406 self.param_states.clear();
407 self.gradient_stats = GradientStatistics::new();
408 }
409}
410
411impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> Default for GradientStatistics<T> {
412 fn default() -> Self {
413 Self::new()
414 }
415}
416
417impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> GradientStatistics<T> {
418 pub fn new() -> Self {
420 Self {
421 mean_magnitude: T::zero(),
422 var_magnitude: T::zero(),
423 max_magnitude: T::zero(),
424 min_magnitude: scirs2_core::numeric::NumCast::from(f64::INFINITY)
425 .unwrap_or_else(T::zero),
426 update_count: 0,
427 sparsity: T::zero(),
428 }
429 }
430
431 pub fn update(&mut self, gradients: &Array1<T>) {
433 let magnitude = gradients
434 .iter()
435 .map(|&x| x * x)
436 .fold(T::zero(), |a, b| a + b)
437 .sqrt();
438
439 self.update_count += 1;
440 let count: T =
441 scirs2_core::numeric::NumCast::from(self.update_count as f64).unwrap_or_else(T::one);
442
443 let delta = magnitude - self.mean_magnitude;
445 self.mean_magnitude = self.mean_magnitude + delta / count;
446 let delta2 = magnitude - self.mean_magnitude;
447 self.var_magnitude = self.var_magnitude + delta * delta2;
448
449 if magnitude > self.max_magnitude {
450 self.max_magnitude = magnitude;
451 }
452 if magnitude < self.min_magnitude {
453 self.min_magnitude = magnitude;
454 }
455
456 if !gradients.is_empty() {
458 let zero_threshold: T =
459 scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(T::zero);
460 let zero_count = gradients
461 .iter()
462 .filter(|&&x| x.abs() < zero_threshold)
463 .count();
464 let current_sparsity: T =
465 scirs2_core::numeric::NumCast::from(zero_count as f64 / gradients.len() as f64)
466 .unwrap_or_else(T::zero);
467 let alpha: T = scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(T::zero);
468 self.sparsity = self.sparsity * (T::one() - alpha) + current_sparsity * alpha;
469 }
470 }
471
472 pub fn mean_magnitude(&self) -> T {
474 self.mean_magnitude
475 }
476
477 pub fn max_magnitude(&self) -> T {
479 self.max_magnitude
480 }
481
482 pub fn min_magnitude(&self) -> T {
484 if self.update_count == 0 {
485 T::zero()
486 } else {
487 self.min_magnitude
488 }
489 }
490
491 pub fn update_count(&self) -> usize {
493 self.update_count
494 }
495
496 pub fn variance_magnitude(&self) -> T {
498 if self.update_count > 1 {
499 let denominator: T =
500 scirs2_core::numeric::NumCast::from((self.update_count - 1) as f64)
501 .unwrap_or_else(T::one);
502 self.var_magnitude / denominator
503 } else {
504 T::zero()
505 }
506 }
507
508 pub fn std_magnitude(&self) -> T {
510 self.variance_magnitude().sqrt()
511 }
512
513 pub fn sparsity(&self) -> T {
515 self.sparsity
516 }
517}
518
519impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> Default
520 for GradientProcessingParams<T>
521{
522 fn default() -> Self {
523 Self {
524 clip_threshold: scirs2_core::numeric::NumCast::from(1.0).unwrap_or_else(T::one),
525 smoothing_factor: scirs2_core::numeric::NumCast::from(0.9).unwrap_or_else(T::one),
526 accumulation_steps: 4,
527 dropout_prob: 0.1,
528 compression_ratio: 0.1,
529 norm_eps: scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(T::zero),
530 }
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 fn grad(values: &[f64]) -> Array1<f64> {
539 Array1::from_vec(values.to_vec())
540 }
541
542 #[test]
543 fn clipping_caps_the_norm() {
544 let mut processor = GradientProcessor::<f64>::new(GradientProcessingStrategy::Clipping);
545 let out = processor
546 .process_gradients("w", &grad(&[3.0, 4.0]))
547 .expect("clipping must succeed");
548 let norm = (out[0] * out[0] + out[1] * out[1]).sqrt();
549 assert!(norm <= 1.0 + 1e-9, "norm {norm} exceeds clip threshold");
550 assert!((out[0] / out[1] - 0.75).abs() < 1e-9);
552 }
553
554 #[test]
555 fn clipping_leaves_small_gradients_untouched() {
556 let mut processor = GradientProcessor::<f64>::new(GradientProcessingStrategy::Clipping);
557 let input = grad(&[0.1, -0.2]);
558 let out = processor
559 .process_gradients("w", &input)
560 .expect("clipping must succeed");
561 assert_eq!(out, input);
562 }
563
564 #[test]
565 fn adaptive_scaling_stays_finite_for_zero_gradients() {
566 let mut processor = GradientProcessor::<f64>::new(GradientProcessingStrategy::Adaptive);
567 let _ = processor.process_gradients("w", &grad(&[1.0, 1.0]));
569 let out = processor
570 .process_gradients("w", &grad(&[0.0, 0.0]))
571 .expect("adaptive scaling must succeed");
572 assert!(out.iter().all(|v| v.is_finite()), "produced {out:?}");
573 assert!(out.iter().all(|v| *v == 0.0));
574 }
575
576 #[test]
577 fn adaptive_scaling_preserves_descent_direction() {
578 let mut processor = GradientProcessor::<f64>::new(GradientProcessingStrategy::Adaptive);
579 let _ = processor.process_gradients("w", &grad(&[2.0, -2.0]));
580 let out = processor
581 .process_gradients("w", &grad(&[1.0, -1.0]))
582 .expect("adaptive scaling must succeed");
583 assert!(out[0] > 0.0 && out[1] < 0.0);
584 }
585
586 #[test]
587 fn differing_parameter_widths_do_not_conflict() {
588 let mut processor = GradientProcessor::<f64>::new(GradientProcessingStrategy::Smoothing);
589 let a = processor
590 .process_gradients("a", &grad(&[1.0, 2.0, 3.0]))
591 .expect("processing a");
592 let b = processor
593 .process_gradients("b", &grad(&[1.0, 2.0]))
594 .expect("processing b");
595 assert_eq!(a.len(), 3);
596 assert_eq!(b.len(), 2);
597 }
598
599 #[test]
600 fn zero_accumulation_steps_do_not_panic() {
601 let params = GradientProcessingParams::<f64> {
602 accumulation_steps: 0,
603 ..GradientProcessingParams::default()
604 };
605 let mut processor = GradientProcessor::<f64>::new_with_params(
606 GradientProcessingStrategy::Accumulation,
607 params,
608 );
609 let out = processor
610 .process_gradients("w", &grad(&[1.0, 1.0]))
611 .expect("accumulation must succeed");
612 assert_eq!(out.len(), 2);
613 }
614
615 #[test]
616 fn statistics_track_welford_variance() {
617 let mut stats = GradientStatistics::<f64>::new();
618 stats.update(&grad(&[3.0, 4.0])); stats.update(&grad(&[0.0, 1.0])); assert!((stats.mean_magnitude() - 3.0).abs() < 1e-12);
621 assert!((stats.variance_magnitude() - 8.0).abs() < 1e-12);
623 }
624}