Files
rustytorch/crates/training/rtx-transformers/src/schedulers/warmup.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

196 lines
5.4 KiB
Rust

//! Warmup learning rate scheduler
//!
//! Implements linear warmup for learning rate, essential for stable transformer training.
use crate::schedulers::LearningRateScheduler;
use crate::{Result, TransformerError};
use serde::{Deserialize, Serialize};
use tracing::{debug, trace};
/// Linear warmup learning rate scheduler
///
/// During the warmup phase, the learning rate increases linearly from 0 to the base learning rate.
/// This helps stabilize training in the early stages, especially for large models.
///
/// # Mathematical Foundation
///
/// For step t during warmup (t < `warmup_steps)`:
/// - lr(t) = `base_lr` * (t / `warmup_steps`)
///
/// After warmup (t >= `warmup_steps)`:
/// - lr(t) = `base_lr`
///
/// # Benefits
/// - Prevents gradient explosion in early training
/// - Enables training with larger learning rates
/// - Essential for transformer model stability
/// - Commonly used in BERT, GPT, and other transformers
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WarmupScheduler {
/// Target learning rate after warmup
base_lr: f64,
/// Number of warmup steps
warmup_steps: usize,
/// Current step count
current_step: usize,
}
impl WarmupScheduler {
/// Create a new warmup scheduler
///
/// # Arguments
/// * `base_lr` - Target learning rate after warmup (must be positive)
/// * `warmup_steps` - Number of steps for warmup (must be positive)
///
/// # Errors
/// Returns error if parameters are invalid
pub fn new(base_lr: f64, warmup_steps: usize) -> Result<Self> {
if base_lr <= 0.0 {
return Err(TransformerError::generic(format!(
"base_lr {base_lr} must be positive"
)));
}
if warmup_steps == 0 {
return Err(TransformerError::generic(format!(
"warmup_steps {warmup_steps} must be greater than 0"
)));
}
debug!(
"Creating warmup scheduler: base_lr={}, warmup_steps={}",
base_lr, warmup_steps
);
Ok(Self {
base_lr,
warmup_steps,
current_step: 0,
})
}
/// Get the number of warmup steps
#[must_use]
pub fn warmup_steps(&self) -> usize {
self.warmup_steps
}
/// Check if currently in warmup phase
#[must_use]
pub fn in_warmup(&self, step: usize) -> bool {
step < self.warmup_steps
}
/// Get the warmup progress (0.0 to 1.0)
#[must_use]
pub fn warmup_progress(&self, step: usize) -> f64 {
if step >= self.warmup_steps {
1.0
} else {
step as f64 / self.warmup_steps as f64
}
}
}
impl LearningRateScheduler for WarmupScheduler {
fn get_lr(&self, _epoch: usize, step: usize) -> f64 {
if step < self.warmup_steps {
// Linear warmup: lr = base_lr * (step / warmup_steps)
let progress = step as f64 / self.warmup_steps as f64;
let lr = self.base_lr * progress;
trace!(
"Warmup step {}: progress={:.4}, lr={:.6}",
step, progress, lr
);
lr
} else {
// After warmup: lr = base_lr
trace!("Post-warmup step {}: lr={:.6}", step, self.base_lr);
self.base_lr
}
}
fn step(&mut self) {
self.current_step += 1;
trace!("Warmup scheduler stepped to: {}", self.current_step);
}
fn current_step(&self) -> usize {
self.current_step
}
fn reset(&mut self) {
self.current_step = 0;
debug!("Reset warmup scheduler");
}
fn scheduler_type(&self) -> &'static str {
"Warmup"
}
fn base_lr(&self) -> f64 {
self.base_lr
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_warmup_creation() {
let scheduler = WarmupScheduler::new(0.001, 1000).unwrap();
assert_eq!(scheduler.base_lr(), 0.001);
assert_eq!(scheduler.warmup_steps(), 1000);
assert_eq!(scheduler.current_step(), 0);
}
#[test]
fn test_warmup_invalid_params() {
assert!(WarmupScheduler::new(-0.001, 1000).is_err());
assert!(WarmupScheduler::new(0.001, 0).is_err());
}
#[test]
fn test_warmup_progression() {
let scheduler = WarmupScheduler::new(0.001, 100).unwrap();
// At step 0
let lr_0 = scheduler.get_lr(0, 0);
assert_eq!(lr_0, 0.0);
// At step 50 (halfway)
let lr_50 = scheduler.get_lr(0, 50);
assert!((lr_50 - 0.0005).abs() < 1e-10);
// At step 100 (end of warmup)
let lr_100 = scheduler.get_lr(0, 100);
assert!((lr_100 - 0.001).abs() < 1e-10);
// After warmup
let lr_150 = scheduler.get_lr(0, 150);
assert!((lr_150 - 0.001).abs() < 1e-10);
}
#[test]
fn test_warmup_progress() {
let scheduler = WarmupScheduler::new(0.001, 100).unwrap();
assert_eq!(scheduler.warmup_progress(0), 0.0);
assert_eq!(scheduler.warmup_progress(50), 0.5);
assert_eq!(scheduler.warmup_progress(100), 1.0);
assert_eq!(scheduler.warmup_progress(150), 1.0);
}
#[test]
fn test_warmup_in_warmup() {
let scheduler = WarmupScheduler::new(0.001, 100).unwrap();
assert!(scheduler.in_warmup(0));
assert!(scheduler.in_warmup(50));
assert!(scheduler.in_warmup(99));
assert!(!scheduler.in_warmup(100));
assert!(!scheduler.in_warmup(150));
}
}