Skip to main content

rlx_tensor/
schedule.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//! Learning-rate schedules for [`Func::train_step_at`](crate::Func::train_step_at).
6//!
7//! A schedule maps a global step to a learning rate; `train_step_at` calls
8//! `Optimizer::set_lr` with that value before each step. Available with the
9//! `optim` feature.
10
11/// Common learning-rate schedules.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub enum LrSchedule {
14    /// Fixed learning rate.
15    Constant(f32),
16    /// Multiply `base` by `gamma` every `step_size` steps (staircase).
17    Step {
18        base: f32,
19        step_size: usize,
20        gamma: f32,
21    },
22    /// Cosine decay from `base` to `min` over `total` steps (then holds `min`).
23    Cosine { base: f32, min: f32, total: usize },
24    /// Linear warmup from 0 to `base` over `warmup` steps, then holds `base`.
25    Warmup { base: f32, warmup: usize },
26    /// Linear warmup to `base` over `warmup`, then cosine decay to `min` by
27    /// `total` — the standard transformer schedule.
28    WarmupCosine {
29        base: f32,
30        min: f32,
31        warmup: usize,
32        total: usize,
33    },
34}
35
36impl LrSchedule {
37    /// Learning rate at `step` (0-based).
38    pub fn lr_at(&self, step: usize) -> f32 {
39        match *self {
40            LrSchedule::Constant(lr) => lr,
41            LrSchedule::Step {
42                base,
43                step_size,
44                gamma,
45            } => {
46                let k = step.checked_div(step_size).unwrap_or(0);
47                base * gamma.powi(k as i32)
48            }
49            LrSchedule::Cosine { base, min, total } => cosine(base, min, step, total),
50            LrSchedule::Warmup { base, warmup } => {
51                if warmup > 0 && step < warmup {
52                    base * (step as f32 + 1.0) / warmup as f32
53                } else {
54                    base
55                }
56            }
57            LrSchedule::WarmupCosine {
58                base,
59                min,
60                warmup,
61                total,
62            } => {
63                if warmup > 0 && step < warmup {
64                    base * (step as f32 + 1.0) / warmup as f32
65                } else {
66                    cosine(base, min, step - warmup, total.saturating_sub(warmup))
67                }
68            }
69        }
70    }
71}
72
73/// Cosine decay from `base` to `min` over `total` steps; holds `min` after.
74fn cosine(base: f32, min: f32, step: usize, total: usize) -> f32 {
75    if total == 0 {
76        return min;
77    }
78    let t = (step.min(total)) as f32 / total as f32;
79    let factor = 0.5 * (1.0 + (std::f32::consts::PI * t).cos());
80    min + (base - min) * factor
81}