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}