Skip to main content

rlx_optim/
radam.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! RAdam — Rectified Adam (Liu et al., 2019, "On the Variance of the
6//! Adaptive Learning Rate and Beyond").
7//!
8//! # Motivation
9//!
10//! Early in training, `v_t` is built from very few samples and its
11//! variance is huge — which makes Adam's effective learning rate
12//! noisy at the same iterations where stability matters most.
13//! Practitioners "fix" this with an LR warm-up; RAdam derives a
14//! *closed-form* warm-up from the variance of the inverse-square-root
15//! of `v̂_t`.
16//!
17//! # Update rule
18//!
19//! Let `ρ_∞ = 2/(1−β₂) − 1` and the "SMA length"
20//! `ρ_t = ρ_∞ − 2t·β₂ᵗ / (1 − β₂ᵗ)`. Define the rectification term
21//!
22//! ```text
23//! r_t = √( ((ρ_t − 4)(ρ_t − 2)·ρ_∞) / ((ρ_∞ − 4)(ρ_∞ − 2)·ρ_t) )
24//! ```
25//!
26//! When `ρ_t > 4` the second moment is "stable enough" — use the
27//! corrected Adam step with `r_t` scaling:
28//!
29//! ```text
30//! θ_t = θ_{t-1} − lr · r_t · m̂_t / (√v̂_t + ε)
31//! ```
32//!
33//! Otherwise (`ρ_t ≤ 4`, early steps), fall back to SGD-with-momentum
34//! using the first moment alone:
35//!
36//! ```text
37//! θ_t = θ_{t-1} − lr · m̂_t
38//! ```
39//!
40//! # When to use
41//!
42//! Drop-in replacement for Adam when you don't want to hand-tune a
43//! warm-up schedule. Same memory cost as Adam.
44
45use std::collections::HashMap;
46
47use crate::Optimizer;
48use crate::common::zeros_entry;
49
50/// Rectified Adam. Per-tensor state: two `f32` buffers.
51#[derive(Debug, Clone)]
52pub struct RAdam {
53    /// Learning rate.
54    pub lr: f32,
55    /// First-moment EMA decay β₁. Default `0.9`.
56    pub beta1: f32,
57    /// Second-moment EMA decay β₂. Default `0.999`.
58    pub beta2: f32,
59    /// Denominator stability constant. Default `1e-8`.
60    pub eps: f32,
61    /// L2 weight-decay coefficient (folded into the gradient — like
62    /// classical Adam, **not** decoupled). Default `0.0`.
63    pub weight_decay: f32,
64    step: u64,
65    m: HashMap<String, Vec<f32>>,
66    v: HashMap<String, Vec<f32>>,
67}
68
69impl RAdam {
70    /// Construct with `(β₁, β₂, ε, λ) = (0.9, 0.999, 1e-8, 0.0)`.
71    pub fn new(lr: f32) -> Self {
72        Self {
73            lr,
74            beta1: 0.9,
75            beta2: 0.999,
76            eps: 1e-8,
77            weight_decay: 0.0,
78            step: 0,
79            m: HashMap::new(),
80            v: HashMap::new(),
81        }
82    }
83
84    /// Override (β₁, β₂).
85    pub fn with_betas(mut self, b1: f32, b2: f32) -> Self {
86        self.beta1 = b1;
87        self.beta2 = b2;
88        self
89    }
90
91    /// Override the L2 weight-decay coefficient.
92    pub fn with_weight_decay(mut self, wd: f32) -> Self {
93        self.weight_decay = wd;
94        self
95    }
96}
97
98impl Optimizer for RAdam {
99    fn set_lr(&mut self, lr: f32) {
100        self.lr = lr;
101    }
102
103    fn step(&mut self, name: &str, _shape: &[usize], param: &mut [f32], grad: &[f32]) {
104        debug_assert_eq!(param.len(), grad.len());
105        let t = (self.step + 1) as f64;
106        let b1 = self.beta1 as f64;
107        let b2 = self.beta2 as f64;
108        let bc1 = 1.0 - b1.powf(t);
109        let bc2 = 1.0 - b2.powf(t);
110        let rho_inf = 2.0 / (1.0 - b2) - 1.0;
111        let rho_t = rho_inf - 2.0 * t * b2.powf(t) / bc2;
112        let eps = self.eps as f64;
113        let lr = self.lr as f64;
114        let wd = self.weight_decay;
115        // Variance-rectification term `r_t` (Liu et al. eq. 14).
116        let r_t = if rho_t > 4.0 {
117            (((rho_t - 4.0) * (rho_t - 2.0) * rho_inf)
118                / ((rho_inf - 4.0) * (rho_inf - 2.0) * rho_t))
119                .sqrt()
120        } else {
121            0.0
122        };
123        let m = zeros_entry(&mut self.m, name, param.len());
124        let v = zeros_entry(&mut self.v, name, param.len());
125        for i in 0..param.len() {
126            let g = (grad[i] + wd * param[i]) as f64;
127            let mi = b1 * m[i] as f64 + (1.0 - b1) * g;
128            let vi = b2 * v[i] as f64 + (1.0 - b2) * g * g;
129            m[i] = mi as f32;
130            v[i] = vi as f32;
131            let m_hat = mi / bc1;
132            let update = if rho_t > 4.0 {
133                let v_hat = (vi / bc2).sqrt();
134                r_t * m_hat / (v_hat + eps)
135            } else {
136                m_hat
137            };
138            param[i] -= (lr * update) as f32;
139        }
140    }
141
142    fn end_iteration(&mut self) {
143        self.step += 1;
144    }
145}