Skip to main content

trustformers_optim/
eva.rs

1//! # EVA Optimizer
2//!
3//! EVA (Exponential Moving Average with Variance Adaptation) is a state-of-the-art optimizer
4//! that adapts the learning rate based on the variance of gradient estimates.
5//!
6//! ## Key Features
7//!
8//! - **Adaptive Learning Rate**: Uses gradient variance to adapt learning rate
9//! - **Exponential Moving Averages**: Maintains momentum and variance estimates
10//! - **Robustness**: More stable than Adam in certain scenarios
11//! - **Computational Efficiency**: Low overhead compared to second-order methods
12//!
13//! ## Algorithm
14//!
15//! EVA updates parameters using:
16//! 1. Exponential moving average of gradients (momentum)
17//! 2. Exponential moving average of squared gradients (variance)
18//! 3. Variance-adapted learning rate scaling
19//! 4. Optional bias correction
20//!
21//! ## Usage Example
22//!
23//! ```rust,no_run
24//! use trustformers_optim::EVA;
25//!
26//! let mut optimizer = EVA::new(
27//!     1e-3,   // learning_rate
28//!     0.9,    // beta1
29//!     0.999,  // beta2
30//!     1e-8,   // epsilon
31//!     0.01,   // weight_decay
32//!     true,   // variance_adaptation
33//! );
34//! ```
35
36// reason: research-stage module — reserved API/scaffolding fields and methods
37// retained intentionally for in-progress features; not yet on active call paths.
38#![allow(dead_code)]
39
40use crate::common::{OptimizerState, StateMemoryStats};
41use crate::traits::StatefulOptimizer;
42use std::collections::HashMap;
43use trustformers_core::errors::{Result, TrustformersError};
44use trustformers_core::tensor::Tensor;
45use trustformers_core::traits::Optimizer;
46
47/// Configuration for EVA optimizer.
48#[derive(Debug, Clone)]
49pub struct EVAConfig {
50    /// Learning rate
51    pub lr: f32,
52    /// First moment coefficient
53    pub beta1: f32,
54    /// Second moment coefficient
55    pub beta2: f32,
56    /// Term added for numerical stability
57    pub eps: f32,
58    /// Weight decay (L2 penalty)
59    pub weight_decay: f32,
60    /// Whether to use variance adaptation
61    pub variance_adaptation: bool,
62    /// Whether to use bias correction
63    pub bias_correction: bool,
64    /// Variance adaptation strength
65    pub adaptation_strength: f32,
66}
67
68impl Default for EVAConfig {
69    fn default() -> Self {
70        Self {
71            lr: 1e-3,
72            beta1: 0.9,
73            beta2: 0.999,
74            eps: 1e-8,
75            weight_decay: 0.01,
76            variance_adaptation: true,
77            bias_correction: true,
78            adaptation_strength: 1.0,
79        }
80    }
81}
82
83/// EVA (Exponential Moving Average with Variance Adaptation) optimizer.
84#[derive(Debug)]
85pub struct EVA {
86    config: EVAConfig,
87    state: OptimizerState,
88    exp_avg: HashMap<String, Vec<f32>>,
89    exp_avg_sq: HashMap<String, Vec<f32>>,
90    var_adaptation: HashMap<String, Vec<f32>>,
91    step_count: usize,
92}
93
94impl EVA {
95    /// Creates a new EVA optimizer with default configuration.
96    pub fn new(
97        lr: f32,
98        beta1: f32,
99        beta2: f32,
100        eps: f32,
101        weight_decay: f32,
102        variance_adaptation: bool,
103    ) -> Self {
104        let config = EVAConfig {
105            lr,
106            beta1,
107            beta2,
108            eps,
109            weight_decay,
110            variance_adaptation,
111            bias_correction: true,
112            adaptation_strength: 1.0,
113        };
114
115        Self::with_config(config)
116    }
117
118    /// Creates a new EVA optimizer with custom configuration.
119    pub fn with_config(config: EVAConfig) -> Self {
120        Self {
121            config,
122            state: OptimizerState::new(),
123            exp_avg: HashMap::new(),
124            exp_avg_sq: HashMap::new(),
125            var_adaptation: HashMap::new(),
126            step_count: 0,
127        }
128    }
129
130    /// Convenience constructor for EVA with AdamW-like settings.
131    pub fn adamw_like(lr: f32, weight_decay: f32) -> Self {
132        Self::new(lr, 0.9, 0.999, 1e-8, weight_decay, true)
133    }
134
135    /// Convenience constructor for EVA with variance adaptation disabled.
136    pub fn no_variance_adaptation(lr: f32, beta1: f32, beta2: f32, eps: f32) -> Self {
137        Self::new(lr, beta1, beta2, eps, 0.0, false)
138    }
139
140    /// Gets the current learning rate.
141    pub fn get_lr(&self) -> f32 {
142        self.config.lr
143    }
144
145    /// Sets the learning rate.
146    pub fn set_lr(&mut self, lr: f32) {
147        self.config.lr = lr;
148    }
149
150    /// Gets current optimizer configuration.
151    pub fn config(&self) -> &EVAConfig {
152        &self.config
153    }
154
155    /// Gets memory statistics for the optimizer state.
156    pub fn memory_stats(&self) -> StateMemoryStats {
157        let mut total_parameters = 0;
158        let mut _total_buffers = 0;
159        for buffer in self.exp_avg.values() {
160            total_parameters += buffer.len();
161            _total_buffers += 1;
162        }
163
164        for buffer in self.exp_avg_sq.values() {
165            total_parameters += buffer.len();
166            _total_buffers += 1;
167        }
168
169        if self.config.variance_adaptation {
170            for buffer in self.var_adaptation.values() {
171                total_parameters += buffer.len();
172                _total_buffers += 1;
173            }
174        }
175
176        StateMemoryStats {
177            momentum_elements: total_parameters,
178            variance_elements: total_parameters,
179            third_moment_elements: if self.config.variance_adaptation {
180                total_parameters
181            } else {
182                0
183            },
184            total_bytes: total_parameters * 4, // f32 = 4 bytes
185            num_parameters: total_parameters,
186        }
187    }
188
189    /// Computes variance adaptation factor.
190    fn compute_variance_adaptation(&self, grad_var: f32, step: usize) -> f32 {
191        if !self.config.variance_adaptation || step == 0 {
192            return 1.0;
193        }
194
195        let adaptation = (grad_var + self.config.eps).sqrt();
196        let strength = self.config.adaptation_strength;
197
198        // Apply strength and clamp to reasonable range
199        let factor = 1.0 / (1.0 + strength * adaptation);
200        factor.clamp(0.1, 2.0)
201    }
202}
203
204impl Optimizer for EVA {
205    fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
206        self.step_count += 1;
207
208        match (parameter, grad) {
209            (Tensor::F32(param), Tensor::F32(grad_data)) => {
210                let param_id = format!("{:p}", param.as_ptr());
211                let size = grad_data.len();
212
213                // Initialize state if needed
214                let exp_avg =
215                    self.exp_avg.entry(param_id.clone()).or_insert_with(|| vec![0.0; size]);
216                let exp_avg_sq =
217                    self.exp_avg_sq.entry(param_id.clone()).or_insert_with(|| vec![0.0; size]);
218                let mut var_adapt = if self.config.variance_adaptation {
219                    Some(
220                        self.var_adaptation
221                            .entry(param_id.clone())
222                            .or_insert_with(|| vec![0.0; size]),
223                    )
224                } else {
225                    None
226                };
227
228                // Check buffer sizes
229                if exp_avg.len() != size || exp_avg_sq.len() != size {
230                    return Err(TrustformersError::tensor_op_error(
231                        "EVA buffer size mismatch",
232                        "EVA::update",
233                    ));
234                }
235
236                if let Some(ref va) = var_adapt {
237                    if va.len() != size {
238                        return Err(TrustformersError::tensor_op_error(
239                            "EVA variance adaptation buffer size mismatch",
240                            "EVA::update",
241                        ));
242                    }
243                }
244
245                // Compute bias correction factors
246                let bias_correction1 = if self.config.bias_correction {
247                    1.0 - self.config.beta1.powi(self.step_count as i32)
248                } else {
249                    1.0
250                };
251
252                let bias_correction2 = if self.config.bias_correction {
253                    1.0 - self.config.beta2.powi(self.step_count as i32)
254                } else {
255                    1.0
256                };
257
258                // Compute gradient variance for adaptation
259                let grad_var = if self.config.variance_adaptation {
260                    let mean_grad = grad_data.iter().sum::<f32>() / size as f32;
261                    grad_data.iter().map(|&g| (g - mean_grad).powi(2)).sum::<f32>() / size as f32
262                } else {
263                    0.0
264                };
265
266                let variance_factor = if self.config.variance_adaptation && self.step_count > 0 {
267                    let adaptation = (grad_var + self.config.eps).sqrt();
268                    let strength = self.config.adaptation_strength;
269                    let factor = 1.0 / (1.0 + strength * adaptation);
270                    factor.clamp(0.1, 2.0)
271                } else {
272                    1.0
273                };
274
275                // Update parameters
276                for (i, ((&g, p), (m, v))) in grad_data
277                    .iter()
278                    .zip(param.iter_mut())
279                    .zip(exp_avg.iter_mut().zip(exp_avg_sq.iter_mut()))
280                    .enumerate()
281                {
282                    // Apply weight decay
283                    let grad_with_decay = if self.config.weight_decay > 0.0 {
284                        g + self.config.weight_decay * (*p)
285                    } else {
286                        g
287                    };
288
289                    // Update biased first moment estimate
290                    *m = self.config.beta1 * (*m) + (1.0 - self.config.beta1) * grad_with_decay;
291
292                    // Update biased second moment estimate
293                    *v = self.config.beta2 * (*v)
294                        + (1.0 - self.config.beta2) * grad_with_decay * grad_with_decay;
295
296                    // Update variance adaptation if enabled
297                    if let Some(ref mut va) = var_adapt {
298                        va[i] = 0.9 * va[i] + 0.1 * grad_with_decay.abs();
299                    }
300
301                    // Compute bias-corrected estimates
302                    let m_hat = *m / bias_correction1;
303                    let v_hat = *v / bias_correction2;
304
305                    // Apply variance adaptation
306                    let adapted_lr = self.config.lr * variance_factor;
307
308                    // Update parameter
309                    *p -= adapted_lr * m_hat / (v_hat.sqrt() + self.config.eps);
310                }
311
312                Ok(())
313            },
314            _ => Err(TrustformersError::tensor_op_error(
315                "EVA optimizer only supports F32 tensors",
316                "EVA::update",
317            )),
318        }
319    }
320
321    fn zero_grad(&mut self) {
322        // EVA doesn't accumulate gradients, so this is a no-op
323    }
324
325    fn step(&mut self) {
326        // Update is called per parameter, so step is a no-op
327    }
328
329    fn get_lr(&self) -> f32 {
330        self.config.lr
331    }
332
333    fn set_lr(&mut self, lr: f32) {
334        self.config.lr = lr;
335    }
336}
337
338impl StatefulOptimizer for EVA {
339    type Config = EVAConfig;
340    type State = OptimizerState;
341
342    fn state(&self) -> &OptimizerState {
343        &self.state
344    }
345
346    fn state_mut(&mut self) -> &mut OptimizerState {
347        &mut self.state
348    }
349
350    fn config(&self) -> &Self::Config {
351        &self.config
352    }
353
354    fn memory_usage(&self) -> StateMemoryStats {
355        self.memory_stats()
356    }
357
358    fn reset_state(&mut self) {
359        self.exp_avg.clear();
360        self.exp_avg_sq.clear();
361        self.var_adaptation.clear();
362        self.step_count = 0;
363        self.state = OptimizerState::new();
364    }
365
366    fn num_parameters(&self) -> usize {
367        self.exp_avg.values().map(|v| v.len()).sum()
368    }
369
370    fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
371        let mut dict = HashMap::new();
372
373        for (key, value) in &self.exp_avg {
374            dict.insert(format!("exp_avg_{}", key), Tensor::new(value.clone())?);
375        }
376
377        for (key, value) in &self.exp_avg_sq {
378            dict.insert(format!("exp_avg_sq_{}", key), Tensor::new(value.clone())?);
379        }
380
381        if self.config.variance_adaptation {
382            for (key, value) in &self.var_adaptation {
383                dict.insert(
384                    format!("var_adaptation_{}", key),
385                    Tensor::new(value.clone())?,
386                );
387            }
388        }
389
390        dict.insert(
391            "step_count".to_string(),
392            Tensor::new(vec![self.step_count as f32])?,
393        );
394
395        Ok(dict)
396    }
397
398    fn load_state_dict(&mut self, state_dict: HashMap<String, Tensor>) -> Result<()> {
399        // Load step count
400        if let Some(Tensor::F32(data)) = state_dict.get("step_count") {
401            if !data.is_empty() {
402                self.step_count = data[0] as usize;
403            }
404        }
405
406        // Load exp_avg
407        for (key, value) in &state_dict {
408            if let Some(param_key) = key.strip_prefix("exp_avg_") {
409                if let Tensor::F32(data) = value {
410                    self.exp_avg.insert(
411                        param_key.to_string(),
412                        data.as_slice()
413                            .ok_or_else(|| {
414                                TrustformersError::invalid_state(
415                                    "F32 tensor should have valid slice".to_string(),
416                                )
417                            })?
418                            .to_vec(),
419                    );
420                }
421            }
422        }
423
424        // Load exp_avg_sq
425        for (key, value) in &state_dict {
426            if let Some(param_key) = key.strip_prefix("exp_avg_sq_") {
427                if let Tensor::F32(data) = value {
428                    self.exp_avg_sq.insert(
429                        param_key.to_string(),
430                        data.as_slice()
431                            .ok_or_else(|| {
432                                TrustformersError::invalid_state(
433                                    "F32 tensor should have valid slice".to_string(),
434                                )
435                            })?
436                            .to_vec(),
437                    );
438                }
439            }
440        }
441
442        // Load variance adaptation
443        if self.config.variance_adaptation {
444            for (key, value) in &state_dict {
445                if let Some(param_key) = key.strip_prefix("var_adaptation_") {
446                    if let Tensor::F32(data) = value {
447                        self.var_adaptation.insert(
448                            param_key.to_string(),
449                            data.as_slice()
450                                .ok_or_else(|| {
451                                    TrustformersError::invalid_state(
452                                        "F32 tensor should have valid slice".to_string(),
453                                    )
454                                })?
455                                .to_vec(),
456                        );
457                    }
458                }
459            }
460        }
461
462        Ok(())
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469    use trustformers_core::tensor::Tensor;
470
471    #[test]
472    fn test_eva_creation() {
473        let optimizer = EVA::new(1e-3, 0.9, 0.999, 1e-8, 0.01, true);
474        assert_eq!(optimizer.get_lr(), 1e-3);
475        assert_eq!(optimizer.config().beta1, 0.9);
476        assert_eq!(optimizer.config().beta2, 0.999);
477        assert_eq!(optimizer.config().eps, 1e-8);
478        assert_eq!(optimizer.config().weight_decay, 0.01);
479        assert!(optimizer.config().variance_adaptation);
480    }
481
482    #[test]
483    fn test_eva_adamw_like() {
484        let optimizer = EVA::adamw_like(1e-3, 0.01);
485        assert_eq!(optimizer.get_lr(), 1e-3);
486        assert_eq!(optimizer.config().weight_decay, 0.01);
487        assert!(optimizer.config().variance_adaptation);
488    }
489
490    #[test]
491    fn test_eva_no_variance_adaptation() {
492        let optimizer = EVA::no_variance_adaptation(1e-3, 0.9, 0.999, 1e-8);
493        assert_eq!(optimizer.get_lr(), 1e-3);
494        assert!(!optimizer.config().variance_adaptation);
495    }
496
497    #[test]
498    fn test_eva_lr_setter() {
499        let mut optimizer = EVA::new(1e-3, 0.9, 0.999, 1e-8, 0.01, true);
500        optimizer.set_lr(2e-3);
501        assert_eq!(optimizer.get_lr(), 2e-3);
502    }
503
504    #[test]
505    fn test_eva_memory_stats() {
506        let optimizer = EVA::new(1e-3, 0.9, 0.999, 1e-8, 0.01, true);
507        let stats = optimizer.memory_stats();
508        assert_eq!(stats.num_parameters, 0);
509        assert_eq!(stats.total_bytes, 0);
510    }
511
512    #[test]
513    fn test_eva_variance_adaptation() {
514        let optimizer = EVA::new(1e-3, 0.9, 0.999, 1e-8, 0.01, true);
515        let factor = optimizer.compute_variance_adaptation(0.1, 1);
516        assert!(factor > 0.1 && factor < 2.0);
517    }
518
519    #[test]
520    fn test_eva_state_dict() {
521        let optimizer = EVA::new(1e-3, 0.9, 0.999, 1e-8, 0.01, true);
522        let state_dict = optimizer.state_dict();
523        assert!(state_dict.expect("Operation failed in test").contains_key("step_count"));
524    }
525
526    #[test]
527    fn test_eva_load_state_dict() {
528        let mut optimizer = EVA::new(1e-3, 0.9, 0.999, 1e-8, 0.01, true);
529        let mut state_dict = HashMap::new();
530        state_dict.insert(
531            "step_count".to_string(),
532            Tensor::new(vec![10.0]).expect("Failed to create tensor"),
533        );
534
535        optimizer.load_state_dict(state_dict).expect("Failed to load state dict");
536        assert_eq!(optimizer.step_count, 10);
537    }
538}