use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FusedAdamWError {
InvalidOption(&'static str),
InvalidTensor(&'static str),
ShapeMismatch(&'static str),
DifferentExecutionQueue(&'static str),
InvalidState(&'static str),
StepOverflow,
}
impl fmt::Display for FusedAdamWError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidOption(s) => write!(f, "invalid fused AdamW option: {s}"),
Self::InvalidTensor(s) => write!(f, "invalid fused AdamW tensor: {s}"),
Self::ShapeMismatch(s) => write!(f, "fused AdamW shape mismatch: {s}"),
Self::DifferentExecutionQueue(s) => write!(f, "fused AdamW queue mismatch: {s}"),
Self::InvalidState(s) => write!(f, "invalid fused AdamW state: {s}"),
Self::StepOverflow => f.write_str("fused AdamW step counter overflow"),
}
}
}
impl std::error::Error for FusedAdamWError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AdamWOptions {
pub learning_rate: f32,
pub beta1: f32,
pub beta2: f32,
pub epsilon: f32,
pub weight_decay: f32,
pub amsgrad: bool,
pub maximize: bool,
}
impl Default for AdamWOptions {
fn default() -> Self {
Self {
learning_rate: 1e-3,
beta1: 0.9,
beta2: 0.999,
epsilon: 1e-5,
weight_decay: 1e-4,
amsgrad: false,
maximize: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StepControl {
pub gradient_scale: f32,
pub skip_update: bool,
}
impl Default for StepControl {
fn default() -> Self {
Self { gradient_scale: 1.0, skip_update: false }
}
}
#[derive(Debug, Clone, Copy)]
pub struct StepCoefficients {
pub step: u64,
pub inverse_gradient_scale: f32,
pub inverse_bias1: f32,
pub inverse_bias2: f32,
pub decay_multiplier: f32,
}
impl AdamWOptions {
pub fn validate(&self) -> Result<(), FusedAdamWError> {
for (name, value) in [("learning_rate", self.learning_rate), ("weight_decay", self.weight_decay)] {
if !value.is_finite() || value < 0.0 {
return Err(FusedAdamWError::InvalidOption(name));
}
}
for (name, value) in [("beta1", self.beta1), ("beta2", self.beta2)] {
if !value.is_finite() || !(0.0..1.0).contains(&value) {
return Err(FusedAdamWError::InvalidOption(name));
}
}
if !self.epsilon.is_finite() || self.epsilon <= 0.0 {
return Err(FusedAdamWError::InvalidOption("epsilon"));
}
if !(self.learning_rate * self.weight_decay).is_finite() {
return Err(FusedAdamWError::InvalidOption("learning_rate * weight_decay overflows"));
}
Ok(())
}
pub fn prepare_step(
&self,
completed_steps: u64,
control: StepControl,
) -> Result<StepCoefficients, FusedAdamWError> {
self.validate()?;
control.validate()?;
if control.skip_update {
return Err(FusedAdamWError::InvalidOption("cannot prepare a skipped step"));
}
let step = completed_steps.checked_add(1).ok_or(FusedAdamWError::StepOverflow)?;
Ok(StepCoefficients {
step,
inverse_gradient_scale: control.gradient_scale.recip(),
inverse_bias1: (1.0 / (1.0 - pow_u64(self.beta1 as f64, step))) as f32,
inverse_bias2: (1.0 / (1.0 - pow_u64(self.beta2 as f64, step))) as f32,
decay_multiplier: 1.0 - self.learning_rate * self.weight_decay,
})
}
}
impl StepControl {
pub fn validate(&self) -> Result<(), FusedAdamWError> {
if !self.gradient_scale.is_finite() || self.gradient_scale <= 0.0
|| !self.gradient_scale.recip().is_finite()
{
return Err(FusedAdamWError::InvalidOption("gradient_scale"));
}
Ok(())
}
}
fn pow_u64(mut base: f64, mut exponent: u64) -> f64 {
let mut value = 1.0;
while exponent != 0 {
if exponent & 1 != 0 { value *= base; }
exponent >>= 1;
if exponent != 0 { base *= base; }
}
value
}
pub(crate) const MAX_ELEMENTS: usize = u32::MAX as usize / 4;
pub(crate) fn checked_elements(shape: &[usize]) -> Result<usize, FusedAdamWError> {
if shape.iter().any(|&n| n > u32::MAX as usize) {
return Err(FusedAdamWError::InvalidTensor("dimension exceeds u32"));
}
let size = shape.iter().try_fold(1usize, |n, &d| n.checked_mul(d))
.ok_or(FusedAdamWError::InvalidTensor("element count overflow"))?;
if size > MAX_ELEMENTS {
return Err(FusedAdamWError::InvalidTensor("FP32 byte range exceeds u32"));
}
Ok(size)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_and_first_step() {
let o = AdamWOptions::default();
let c = o.prepare_step(0, StepControl::default()).unwrap();
assert_eq!(c.step, 1);
assert!((c.inverse_bias1 - 1.0 / (1.0 - o.beta1)).abs() < 1e-5);
assert!((c.inverse_bias2 - 1.0 / (1.0 - o.beta2)).abs() < 1e-3);
}
#[test]
fn option_validation() {
for x in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -1.0] {
assert!(AdamWOptions { learning_rate: x, ..Default::default() }.validate().is_err());
assert!(AdamWOptions { weight_decay: x, ..Default::default() }.validate().is_err());
}
for x in [0.0, -1.0, f32::NAN, f32::INFINITY] {
assert!(AdamWOptions { epsilon: x, ..Default::default() }.validate().is_err());
}
for x in [-0.1, 1.0, 1.1, f32::NAN, f32::INFINITY] {
assert!(AdamWOptions { beta1: x, ..Default::default() }.validate().is_err());
assert!(AdamWOptions { beta2: x, ..Default::default() }.validate().is_err());
}
assert!(AdamWOptions { learning_rate: f32::MAX, weight_decay: 2.0, ..Default::default() }.validate().is_err());
}
#[test]
fn scale_validation() {
for scale in [0.0, -1.0, f32::NAN, f32::INFINITY, f32::from_bits(1)] {
assert!(StepControl { gradient_scale: scale, skip_update: false }.validate().is_err());
}
assert_eq!(AdamWOptions::default().prepare_step(0, StepControl {
gradient_scale: 128.0, skip_update: false,
}).unwrap().inverse_gradient_scale, 1.0 / 128.0);
}
#[test]
fn step_counter_and_large_steps() {
let o = AdamWOptions::default();
assert_eq!(o.prepare_step(u64::MAX, StepControl::default()).unwrap_err(), FusedAdamWError::StepOverflow);
let c = o.prepare_step(1u64 << 40, StepControl::default()).unwrap();
assert_eq!(c.step, (1u64 << 40) + 1);
assert_eq!(c.inverse_bias1, 1.0);
assert_eq!(c.inverse_bias2, 1.0);
}
#[test]
fn zero_betas_are_valid() {
let c = AdamWOptions { beta1: 0.0, beta2: 0.0, ..Default::default() }
.prepare_step(0, StepControl::default()).unwrap();
assert_eq!(c.inverse_bias1, 1.0);
assert_eq!(c.inverse_bias2, 1.0);
}
#[test]
fn shape_bounds() {
assert_eq!(checked_elements(&[]).unwrap(), 1);
assert_eq!(checked_elements(&[0, 64]).unwrap(), 0);
assert_eq!(checked_elements(&[3, 257]).unwrap(), 771);
assert!(checked_elements(&[MAX_ELEMENTS, 2]).is_err());
if usize::BITS > 32 {
assert!(checked_elements(&[0, u32::MAX as usize + 1]).is_err());
assert!(checked_elements(&[usize::MAX, 2]).is_err());
}
}
}