Skip to main content

optirs_learned/transformer/strategies/
gradient_processing.rs

1use std::fmt::Debug;
2// Gradient processing strategies for transformer optimization
3//
4// This module implements various gradient transformation and processing strategies
5// used by the transformer optimizer to improve optimization performance.
6//
7// All stateful strategies (smoothing, accumulation, adaptive scaling) keep their
8// state per parameter name, so that a model with parameters of differing shapes
9// can be processed without shape conflicts.
10
11use scirs2_core::ndarray::Array1;
12use scirs2_core::numeric::Float;
13use std::collections::{HashMap, VecDeque};
14
15use crate::error::Result;
16
17/// Gradient processing strategies
18#[derive(Debug, Clone, Copy)]
19pub enum GradientProcessingStrategy {
20    /// Raw gradients without processing
21    Raw,
22    /// Gradient clipping
23    Clipping,
24    /// Gradient normalization
25    Normalization,
26    /// Adaptive gradient scaling
27    AdaptiveScaling,
28    /// Adaptive processing (general)
29    Adaptive,
30    /// Gradient smoothing
31    Smoothing,
32    /// Gradient accumulation
33    Accumulation,
34    /// Gradient dropout
35    Dropout,
36    /// Gradient compression
37    Compression,
38}
39
40/// Per-parameter processing state.
41#[derive(Debug, Clone)]
42struct PerParameterState<T: Float + Debug + Default + Clone + Send + Sync + 'static> {
43    /// Gradient history for smoothing
44    gradient_history: VecDeque<Array1<T>>,
45
46    /// Accumulated gradients
47    accumulated_gradients: Option<Array1<T>>,
48
49    /// Statistics for this parameter only
50    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    /// Drop state whose width no longer matches the incoming gradient.
63    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/// Gradient processor for transformer optimizer
82#[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    /// Processing strategy
95    strategy: GradientProcessingStrategy,
96
97    /// Per-parameter state (history, accumulation, statistics)
98    param_states: HashMap<String, PerParameterState<T>>,
99
100    /// Aggregate gradient statistics across all parameters
101    gradient_stats: GradientStatistics<T>,
102
103    /// Processing parameters
104    processing_params: GradientProcessingParams<T>,
105}
106
107/// Gradient processing parameters
108#[derive(Debug, Clone)]
109pub struct GradientProcessingParams<T: Float + Debug + Send + Sync + 'static> {
110    /// Clipping threshold
111    pub clip_threshold: T,
112
113    /// Smoothing factor: weight applied to the *new* gradient
114    pub smoothing_factor: T,
115
116    /// Accumulation steps
117    pub accumulation_steps: usize,
118
119    /// Dropout probability
120    pub dropout_prob: f64,
121
122    /// Compression ratio
123    pub compression_ratio: f64,
124
125    /// Normalization epsilon
126    pub norm_eps: T,
127}
128
129/// Gradient statistics tracking
130#[derive(Debug, Clone)]
131pub struct GradientStatistics<T: Float + Debug + Send + Sync + 'static> {
132    /// Running mean of gradient magnitudes
133    mean_magnitude: T,
134
135    /// Running sum of squared deviations (Welford aggregate)
136    var_magnitude: T,
137
138    /// Maximum gradient magnitude seen
139    max_magnitude: T,
140
141    /// Minimum gradient magnitude seen
142    min_magnitude: T,
143
144    /// Update count
145    update_count: usize,
146
147    /// Gradient sparsity
148    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    /// Create new gradient processor
164    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    /// Create with custom parameters
174    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    /// Process gradients according to the selected strategy.
187    ///
188    /// `param_name` selects the per-parameter state slot; callers that only have a
189    /// single parameter tensor may pass any stable identifier.
190    pub fn process_gradients(
191        &mut self,
192        param_name: &str,
193        gradients: &Array1<T>,
194    ) -> Result<Array1<T>> {
195        // Update aggregate statistics first
196        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(&params, gradients)),
211            GradientProcessingStrategy::Normalization => {
212                Ok(Self::normalize_gradients(&params, 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, &params, gradients))
219            }
220            GradientProcessingStrategy::Accumulation => {
221                Ok(Self::accumulate_gradients(state, &params, gradients))
222            }
223            GradientProcessingStrategy::Dropout => Ok(Self::dropout_gradients(&params, gradients)),
224            GradientProcessingStrategy::Compression => {
225                Ok(Self::compress_gradients(&params, gradients))
226            }
227        }
228    }
229
230    /// Clip gradients to prevent explosion
231    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    /// Normalize gradients
243    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    /// Adaptively scale gradients based on this parameter's magnitude statistics.
257    ///
258    /// The scale is always strictly positive, so the descent direction is preserved.
259    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        // Guard against division by (near) zero: a vanishing gradient is passed
264        // through unchanged instead of being blown up to infinity/NaN.
265        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    /// Smooth gradients using an exponential moving average
278    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    /// Accumulate gradients over multiple steps
299    fn accumulate_gradients(
300        state: &mut PerParameterState<T>,
301        params: &GradientProcessingParams<T>,
302        gradients: &Array1<T>,
303    ) -> Array1<T> {
304        // Zero accumulation steps would make the modulo below panic.
305        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            // Intermediate steps contribute nothing until the window closes.
323            Array1::zeros(gradients.len())
324        }
325    }
326
327    /// Apply a deterministic structured dropout pattern to gradients
328    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        // Deterministic "dropout" pattern for reproducibility, with the inverted
335        // dropout rescaling applied to the surviving entries.
336        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    /// Compress gradients (magnitude based sparsification)
348    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    /// Compute L2 norm of gradients
366    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    /// Get aggregate gradient statistics
375    pub fn statistics(&self) -> &GradientStatistics<T> {
376        &self.gradient_stats
377    }
378
379    /// Get gradient statistics for a specific parameter
380    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    /// Get the current processing strategy
385    pub fn strategy(&self) -> GradientProcessingStrategy {
386        self.strategy
387    }
388
389    /// Get the current processing parameters
390    pub fn parameters(&self) -> &GradientProcessingParams<T> {
391        &self.processing_params
392    }
393
394    /// Update processing strategy
395    pub fn set_strategy(&mut self, strategy: GradientProcessingStrategy) {
396        self.strategy = strategy;
397    }
398
399    /// Update processing parameters
400    pub fn set_parameters(&mut self, params: GradientProcessingParams<T>) {
401        self.processing_params = params;
402    }
403
404    /// Reset processor state
405    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    /// Create new gradient statistics
419    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    /// Update statistics with new gradients (Welford's online algorithm)
432    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        // Welford update for the running mean and the sum of squared deviations
444        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        // Update sparsity (fraction of near-zero elements)
457        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    /// Get mean magnitude
473    pub fn mean_magnitude(&self) -> T {
474        self.mean_magnitude
475    }
476
477    /// Get maximum magnitude
478    pub fn max_magnitude(&self) -> T {
479        self.max_magnitude
480    }
481
482    /// Get minimum magnitude observed (zero before the first update)
483    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    /// Get number of updates
492    pub fn update_count(&self) -> usize {
493        self.update_count
494    }
495
496    /// Get variance of magnitude
497    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    /// Get standard deviation of magnitude
509    pub fn std_magnitude(&self) -> T {
510        self.variance_magnitude().sqrt()
511    }
512
513    /// Get gradient sparsity
514    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        // Direction preserved
551        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        // Prime the statistics with a non-zero gradient so mean_magnitude > 0.
568        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])); // magnitude 5
619        stats.update(&grad(&[0.0, 1.0])); // magnitude 1
620        assert!((stats.mean_magnitude() - 3.0).abs() < 1e-12);
621        // Sample variance of {5, 1} is 8
622        assert!((stats.variance_magnitude() - 8.0).abs() < 1e-12);
623    }
624}