Skip to main content

trustformers_optim/
common.rs

1//! Common optimization operations and utilities.
2//!
3//! This module provides shared functionality that is used across multiple optimizers,
4//! reducing code duplication and ensuring consistent behavior.
5//!
6//! # Features
7//!
8//! - **State Management**: Unified parameter state tracking
9//! - **Bias Correction**: Standard bias correction calculations for momentum methods
10//! - **Parameter Updates**: Common update patterns with weight decay variants
11//! - **Gradient Processing**: Shared gradient manipulation utilities
12//! - **Memory Management**: Efficient buffer allocation and reuse
13
14use crate::param_id::{ParamId, ParamRegistry};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use trustformers_core::errors::{Result, TrustformersError};
18use trustformers_core::tensor::Tensor;
19
20/// Unified state management for optimizer parameters.
21///
22/// This struct provides a consistent interface for tracking optimizer state
23/// across different algorithms, reducing code duplication and memory overhead.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct OptimizerState {
26    /// Current step counter for bias correction and scheduling
27    pub step: usize,
28
29    /// First moment estimates (momentum buffers)
30    pub momentum: HashMap<String, Vec<f32>>,
31
32    /// Second moment estimates (squared gradient buffers)
33    pub variance: HashMap<String, Vec<f32>>,
34
35    /// Optional third moment estimates (for higher-order methods)
36    pub third_moment: HashMap<String, Vec<f32>>,
37
38    /// Per-parameter step counts (for adaptive methods)
39    pub param_steps: HashMap<String, usize>,
40
41    /// Velocity buffers for optimization methods like SGD with momentum
42    pub velocity: HashMap<String, Vec<f32>>,
43
44    /// Stable parameter identity registry.
45    ///
46    /// Every buffer map above is keyed by the canonical keys handed out by this
47    /// registry. It is deliberately **not** serialised: identity is reconstructed on
48    /// load from the checkpointed keys themselves (see [`crate::param_id`]), which
49    /// keeps the on-disk state format unchanged.
50    #[serde(skip)]
51    pub params: ParamRegistry,
52}
53
54impl OptimizerState {
55    /// Creates a new optimizer state with empty buffers.
56    pub fn new() -> Self {
57        Self {
58            step: 0,
59            momentum: HashMap::new(),
60            variance: HashMap::new(),
61            third_moment: HashMap::new(),
62            param_steps: HashMap::new(),
63            velocity: HashMap::new(),
64            params: ParamRegistry::new(),
65        }
66    }
67
68    /// Resolves the stable state key for an anonymous parameter tensor.
69    ///
70    /// This is the supported replacement for the old `format!("{:p}", …)` idiom: the
71    /// returned key survives checkpoint save/load, whereas a heap address does not.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error when the tensor dtype has no addressable buffer, or when the
76    /// parameter ordering does not match a restored checkpoint.
77    pub fn param_key_for_tensor(&mut self, tensor: &Tensor) -> Result<String> {
78        self.params.key_for_tensor(tensor)
79    }
80
81    /// Resolves the stable state key for an anonymous parameter given its buffer
82    /// address and element count.
83    ///
84    /// # Errors
85    ///
86    /// See [`OptimizerState::param_key_for_tensor`].
87    pub fn param_key(&mut self, addr: usize, numel: usize) -> Result<String> {
88        self.params.key_for_addr(addr, numel)
89    }
90
91    /// Resolves the stable state key for a parameter that has a caller-supplied name.
92    ///
93    /// Named keys make checkpoint resume independent of parameter visit order and
94    /// should be preferred wherever the surrounding API carries names.
95    pub fn param_key_named(&mut self, name: &str, addr: usize, numel: usize) -> String {
96        self.params.key_for_named_addr(name, addr, numel)
97    }
98
99    /// Rebuilds one registry slot from a checkpointed state key.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error when `key` carries no recognised identity prefix.
104    pub fn restore_param_key(&mut self, key: &str, numel: usize) -> Result<ParamId> {
105        self.params.restore_key(key, numel)
106    }
107
108    /// Gets or creates momentum buffer for a parameter.
109    pub fn get_or_create_momentum(&mut self, param_id: String, size: usize) -> &mut Vec<f32> {
110        self.momentum.entry(param_id).or_insert_with(|| vec![0.0; size])
111    }
112
113    /// Gets or creates variance buffer for a parameter.
114    pub fn get_or_create_variance(&mut self, param_id: String, size: usize) -> &mut Vec<f32> {
115        self.variance.entry(param_id).or_insert_with(|| vec![0.0; size])
116    }
117
118    /// Gets or creates third moment buffer for a parameter.
119    pub fn get_or_create_third_moment(&mut self, param_id: String, size: usize) -> &mut Vec<f32> {
120        self.third_moment.entry(param_id).or_insert_with(|| vec![0.0; size])
121    }
122
123    /// Increments the global step counter.
124    pub fn step(&mut self) {
125        self.step += 1;
126    }
127
128    /// Increments the step counter for a specific parameter.
129    pub fn step_param(&mut self, param_id: String) {
130        *self.param_steps.entry(param_id).or_insert(0) += 1;
131    }
132
133    /// Gets the step count for a specific parameter.
134    pub fn get_param_step(&self, param_id: &str) -> usize {
135        self.param_steps.get(param_id).copied().unwrap_or(0)
136    }
137
138    /// Clears all state buffers to free memory.
139    ///
140    /// The parameter identity registry is cleared alongside the buffers: leaving
141    /// registrations behind while dropping their state would keep `params.len()`
142    /// reporting parameters whose buffers no longer exist, and would leave the
143    /// registry's bind cursor advanced past slots that are ready to be reused.
144    pub fn clear(&mut self) {
145        self.step = 0;
146        self.momentum.clear();
147        self.variance.clear();
148        self.third_moment.clear();
149        self.param_steps.clear();
150        self.velocity.clear();
151        self.params.clear();
152    }
153
154    /// Gets memory usage statistics.
155    pub fn memory_usage(&self) -> StateMemoryStats {
156        let momentum_size: usize = self.momentum.values().map(|v| v.len()).sum();
157        let variance_size: usize = self.variance.values().map(|v| v.len()).sum();
158        let third_moment_size: usize = self.third_moment.values().map(|v| v.len()).sum();
159
160        StateMemoryStats {
161            momentum_elements: momentum_size,
162            variance_elements: variance_size,
163            third_moment_elements: third_moment_size,
164            total_bytes: (momentum_size + variance_size + third_moment_size)
165                * std::mem::size_of::<f32>(),
166            num_parameters: self.momentum.len(),
167        }
168    }
169}
170
171impl Default for OptimizerState {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177/// Memory usage statistics for optimizer state.
178#[derive(Debug, Clone)]
179pub struct StateMemoryStats {
180    pub momentum_elements: usize,
181    pub variance_elements: usize,
182    pub third_moment_elements: usize,
183    pub total_bytes: usize,
184    pub num_parameters: usize,
185}
186
187/// Common bias correction utilities for momentum-based optimizers.
188pub struct BiasCorrection;
189
190impl BiasCorrection {
191    /// Computes bias correction factor for exponential moving averages.
192    ///
193    /// Formula: 1 - beta^step
194    ///
195    /// # Arguments
196    ///
197    /// * `beta` - The exponential decay rate (e.g., 0.9 for momentum, 0.999 for variance)
198    /// * `step` - The current step number (1-indexed)
199    pub fn compute_correction(beta: f32, step: usize) -> f32 {
200        1.0 - beta.powi(step as i32)
201    }
202
203    /// Applies bias correction to a value.
204    ///
205    /// # Arguments
206    ///
207    /// * `value` - The biased estimate
208    /// * `beta` - The exponential decay rate
209    /// * `step` - The current step number (1-indexed)
210    pub fn apply_correction(value: f32, beta: f32, step: usize) -> f32 {
211        value / Self::compute_correction(beta, step)
212    }
213
214    /// Computes both first and second moment bias corrections.
215    ///
216    /// # Returns
217    ///
218    /// Tuple of (bias_correction1, bias_correction2) for Adam-style optimizers.
219    pub fn compute_adam_corrections(beta1: f32, beta2: f32, step: usize) -> (f32, f32) {
220        (
221            Self::compute_correction(beta1, step),
222            Self::compute_correction(beta2, step),
223        )
224    }
225}
226
227/// Weight decay application strategies.
228#[derive(Debug, Clone)]
229pub enum WeightDecayMode {
230    /// L2 regularization applied to gradients (traditional Adam)
231    L2Regularization,
232    /// Decoupled weight decay applied directly to parameters (AdamW style)
233    Decoupled,
234}
235
236/// Common parameter update operations.
237pub struct ParameterUpdate;
238
239impl ParameterUpdate {
240    /// Applies weight decay to gradients (L2 regularization).
241    ///
242    /// # Arguments
243    ///
244    /// * `grad` - The gradient value
245    /// * `param` - The parameter value
246    /// * `weight_decay` - The weight decay coefficient
247    pub fn apply_l2_regularization(grad: f32, param: f32, weight_decay: f32) -> f32 {
248        grad + weight_decay * param
249    }
250
251    /// Applies decoupled weight decay directly to parameter.
252    ///
253    /// # Arguments
254    ///
255    /// * `param` - The parameter value to update
256    /// * `lr` - The learning rate
257    /// * `weight_decay` - The weight decay coefficient
258    pub fn apply_decoupled_weight_decay(param: &mut f32, lr: f32, weight_decay: f32) {
259        *param *= 1.0 - lr * weight_decay;
260    }
261
262    /// Updates parameter using Adam-style formula.
263    ///
264    /// # Arguments
265    ///
266    /// * `param` - The parameter to update
267    /// * `lr` - Learning rate
268    /// * `m_hat` - Bias-corrected first moment
269    /// * `v_hat` - Bias-corrected second moment
270    /// * `eps` - Epsilon for numerical stability
271    pub fn adam_update(param: &mut f32, lr: f32, m_hat: f32, v_hat: f32, eps: f32) {
272        *param -= lr * m_hat / (v_hat.sqrt() + eps);
273    }
274
275    /// Updates parameter using SGD with momentum.
276    ///
277    /// # Arguments
278    ///
279    /// * `param` - The parameter to update
280    /// * `lr` - Learning rate
281    /// * `momentum` - Momentum buffer value
282    pub fn sgd_momentum_update(param: &mut f32, lr: f32, momentum: f32) {
283        *param -= lr * momentum;
284    }
285
286    /// Updates momentum buffer for SGD.
287    ///
288    /// # Arguments
289    ///
290    /// * `momentum` - The momentum buffer to update
291    /// * `grad` - The gradient
292    /// * `momentum_coeff` - Momentum coefficient (typically 0.9)
293    /// * `dampening` - Dampening factor (typically 0.0)
294    /// * `nesterov` - Whether to use Nesterov momentum
295    pub fn update_sgd_momentum(
296        momentum: &mut f32,
297        grad: f32,
298        momentum_coeff: f32,
299        dampening: f32,
300        nesterov: bool,
301    ) -> f32 {
302        *momentum = momentum_coeff * *momentum + (1.0 - dampening) * grad;
303        if nesterov {
304            grad + momentum_coeff * *momentum
305        } else {
306            *momentum
307        }
308    }
309
310    /// Updates exponential moving average (for Adam-style methods).
311    ///
312    /// # Arguments
313    ///
314    /// * `ema` - The exponential moving average to update
315    /// * `value` - The new value
316    /// * `beta` - The decay coefficient
317    pub fn update_ema(ema: &mut f32, value: f32, beta: f32) {
318        *ema = beta * *ema + (1.0 - beta) * value;
319    }
320}
321
322/// Gradient processing utilities.
323#[derive(Debug, Clone)]
324pub struct GradientProcessor;
325
326impl GradientProcessor {
327    /// Clips gradient by norm.
328    ///
329    /// # Arguments
330    ///
331    /// * `grad` - The gradient to clip
332    /// * `max_norm` - Maximum allowed norm
333    pub fn clip_by_norm(grad: &mut [f32], max_norm: f32) {
334        let norm: f32 = grad.iter().map(|g| g * g).sum::<f32>().sqrt();
335        if norm > max_norm {
336            let scale = max_norm / norm;
337            for g in grad.iter_mut() {
338                *g *= scale;
339            }
340        }
341    }
342
343    /// Clips gradient by value.
344    ///
345    /// # Arguments
346    ///
347    /// * `grad` - The gradient to clip
348    /// * `min_value` - Minimum allowed value
349    /// * `max_value` - Maximum allowed value
350    pub fn clip_by_value(grad: &mut [f32], min_value: f32, max_value: f32) {
351        for g in grad.iter_mut() {
352            *g = g.clamp(min_value, max_value);
353        }
354    }
355
356    /// Applies gradient scaling for mixed precision training.
357    ///
358    /// # Arguments
359    ///
360    /// * `grad` - The gradient to scale
361    /// * `scale` - The scaling factor
362    pub fn scale_gradient(grad: &mut [f32], scale: f32) {
363        for g in grad.iter_mut() {
364            *g *= scale;
365        }
366    }
367
368    /// Checks for non-finite gradients (NaN or Inf).
369    ///
370    /// # Arguments
371    ///
372    /// * `grad` - The gradient to check
373    ///
374    /// # Returns
375    ///
376    /// True if all gradients are finite.
377    pub fn is_finite(grad: &[f32]) -> bool {
378        grad.iter().all(|g| g.is_finite())
379    }
380}
381
382/// Utility functions for creating parameter IDs.
383pub struct ParameterIds;
384
385impl ParameterIds {
386    /// Creates a parameter ID from a tensor.
387    ///
388    /// This helper is **stateless**, so it cannot assign a durable identity: it needs
389    /// a [`ParamRegistry`] to remember which parameter it has already seen. Use
390    /// [`OptimizerState::param_key_for_tensor`] (or [`ParamRegistry`] directly)
391    /// instead — an id derived from the tensor's address alone changes in every
392    /// process and silently breaks checkpoint resume.
393    ///
394    /// # Errors
395    ///
396    /// Always returns an error; see above for the supported replacement.
397    #[deprecated(
398        since = "0.2.1",
399        note = "stateless ids cannot survive a checkpoint; use OptimizerState::param_key_for_tensor"
400    )]
401    pub fn from_tensor(_tensor: &Tensor) -> Result<String> {
402        Err(TrustformersError::not_implemented(
403            "ParameterIds::from_tensor cannot assign a durable parameter identity; \
404             use OptimizerState::param_key_for_tensor or ParamRegistry instead"
405                .to_string(),
406        ))
407    }
408
409    /// Creates a parameter ID from name.
410    ///
411    /// # Arguments
412    ///
413    /// * `name` - The parameter name
414    pub fn from_name(name: &str) -> String {
415        name.to_string()
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn test_optimizer_state_creation() {
425        let state = OptimizerState::new();
426        assert_eq!(state.step, 0);
427        assert!(state.momentum.is_empty());
428        assert!(state.variance.is_empty());
429    }
430
431    #[test]
432    fn test_bias_correction() {
433        let correction1 = BiasCorrection::compute_correction(0.9, 1);
434        assert!((correction1 - 0.1).abs() < 1e-6);
435
436        let correction2 = BiasCorrection::compute_correction(0.999, 1);
437        assert!((correction2 - 0.001).abs() < 1e-6);
438
439        let corrected = BiasCorrection::apply_correction(0.09, 0.9, 1);
440        assert!((corrected - 0.9).abs() < 1e-6);
441    }
442
443    #[test]
444    fn test_parameter_update() {
445        let mut param = 1.0;
446        ParameterUpdate::apply_decoupled_weight_decay(&mut param, 0.01, 0.1);
447        assert!((param - 0.999).abs() < 1e-6);
448
449        let mut param2 = 1.0;
450        ParameterUpdate::adam_update(&mut param2, 0.01, 0.1, 0.01, 1e-8);
451        assert!((param2 - 0.99).abs() < 1e-6);
452    }
453
454    #[test]
455    fn test_gradient_processing() {
456        let mut grad = vec![3.0, 4.0];
457        GradientProcessor::clip_by_norm(&mut grad, 1.0);
458        let norm: f32 = grad.iter().map(|g| g * g).sum::<f32>().sqrt();
459        assert!((norm - 1.0).abs() < 1e-6);
460
461        assert!(GradientProcessor::is_finite(&grad));
462
463        let bad_grad = vec![f32::NAN, 1.0];
464        assert!(!GradientProcessor::is_finite(&bad_grad));
465    }
466
467    #[test]
468    fn test_memory_stats() {
469        let mut state = OptimizerState::new();
470        state.get_or_create_momentum("param1".to_string(), 100);
471        state.get_or_create_variance("param1".to_string(), 100);
472
473        let stats = state.memory_usage();
474        assert_eq!(stats.momentum_elements, 100);
475        assert_eq!(stats.variance_elements, 100);
476        assert_eq!(stats.num_parameters, 1);
477        assert_eq!(stats.total_bytes, 200 * std::mem::size_of::<f32>());
478    }
479
480    #[test]
481    fn test_ema_update() {
482        let mut ema = 0.0;
483        ParameterUpdate::update_ema(&mut ema, 1.0, 0.9);
484        assert!((ema - 0.1).abs() < 1e-6);
485
486        ParameterUpdate::update_ema(&mut ema, 1.0, 0.9);
487        assert!((ema - 0.19).abs() < 1e-6);
488    }
489}