use super::{SimpleOptimizer, adaptor::OptimizerAdaptor, state_tensor};
use crate::{LearningRate, grad_clipping::GradientClippingConfig};
use ruda_model::{
config::Config,
module::AutodiffModule,
record::{PrecisionSettings, Record},
tensor::{
DType, FloatDType, Tensor, TensorData,
backend::{AutodiffBackend, Backend},
},
};
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use num_traits::Float as _;
#[derive(Config, Debug)]
pub struct LambConfig {
#[config(default = 0.9)]
beta_1: f32,
#[config(default = 0.999)]
beta_2: f32,
#[config(default = 1e-6)]
epsilon: f32,
#[config(default = 0.0)]
weight_decay: f32,
#[config(default = true)]
use_trust_ratio: bool,
grad_clipping: Option<GradientClippingConfig>,
}
#[derive(Clone)]
pub struct Lamb {
beta_1: f32,
beta_2: f32,
epsilon: f32,
weight_decay: f32,
use_trust_ratio: bool,
}
#[derive(Clone)]
pub struct LambState<B: Backend, const D: usize> {
pub time: usize,
pub moment_1: Tensor<B, D>,
pub moment_2: Tensor<B, D>,
}
impl LambConfig {
pub fn build(&self) -> Lamb {
Lamb {
beta_1: self.beta_1,
beta_2: self.beta_2,
epsilon: self.epsilon,
weight_decay: self.weight_decay,
use_trust_ratio: self.use_trust_ratio,
}
}
pub fn init<B: AutodiffBackend, M: AutodiffModule<B>>(&self) -> OptimizerAdaptor<Lamb, M, B> {
let mut optimizer = OptimizerAdaptor::from(self.build());
if let Some(config) = &self.grad_clipping {
optimizer = optimizer.with_grad_clipping(config.init());
}
optimizer
}
}
impl<B: Backend> SimpleOptimizer<B> for Lamb {
type State<const D: usize> = LambState<B, D>;
fn step<const D: usize>(
&self,
lr: LearningRate,
tensor: Tensor<B, D>,
grad: Tensor<B, D>,
state: Option<Self::State<D>>,
) -> (Tensor<B, D>, Option<Self::State<D>>) {
let factor_1 = 1.0 - self.beta_1;
let factor_2 = 1.0 - self.beta_2;
let state = match state {
Some(mut state) => {
state.moment_1 = state
.moment_1
.mul_scalar(self.beta_1)
.add(grad.clone().mul_scalar(factor_1));
state.moment_2 = state
.moment_2
.mul_scalar(self.beta_2)
.add(grad.square().mul_scalar(factor_2));
state.time += 1;
state
}
None => LambState {
time: 1,
moment_1: grad.clone().mul_scalar(factor_1),
moment_2: grad.square().mul_scalar(factor_2),
},
};
let time = state.time as i32;
let moment_1 = state
.moment_1
.clone()
.div_scalar(1.0 - self.beta_1.powi(time));
let moment_2 = state
.moment_2
.clone()
.div_scalar(1.0 - self.beta_2.powi(time));
let mut update = moment_1.div(moment_2.sqrt().add_scalar(self.epsilon));
if self.weight_decay != 0.0 {
update = update.add(tensor.clone().mul_scalar(self.weight_decay));
}
if self.use_trust_ratio {
let parameter_norm = tensor.clone().square().sum().sqrt();
let update_norm = update.clone().square().sum().sqrt();
let valid_norms = parameter_norm
.clone()
.greater_elem(0.0)
.bool_and(update_norm.clone().greater_elem(0.0));
let min_positive = update
.dtype()
.finfo()
.unwrap_or(FloatDType::F32.finfo())
.min_positive;
let ratio = parameter_norm.div(update_norm.clamp_min(min_positive));
let trust_ratio = ratio.ones_like().mask_where(valid_norms, ratio);
update = update.mul(trust_ratio.reshape([1; D]));
}
(tensor - update.mul_scalar(lr), Some(state))
}
fn to_device<const D: usize>(mut state: Self::State<D>, device: &B::Device) -> Self::State<D> {
state.moment_1 = state.moment_1.to_device(device);
state.moment_2 = state.moment_2.to_device(device);
state
}
}
impl<B: Backend, const D: usize> Record<B> for LambState<B, D> {
fn map_parameters<M: ruda_model::module::ModuleMapper<B>>(mut self, mapper: &mut M) -> Self {
self.moment_1 = mapper.map_tensor(self.moment_1);
self.moment_2 = mapper.map_tensor(self.moment_2);
self
}
type Item<P: PrecisionSettings> = (usize, (DType, TensorData), (DType, TensorData));
fn into_item<P: PrecisionSettings>(self) -> Self::Item<P> {
(
self.time,
state_tensor::into_item::<B, D, P>(self.moment_1),
state_tensor::into_item::<B, D, P>(self.moment_2),
)
}
fn from_item<P: PrecisionSettings>(item: Self::Item<P>, device: &B::Device) -> Self {
Self {
time: item.0,
moment_1: state_tensor::from_item(item.1, device),
moment_2: state_tensor::from_item(item.2, device),
}
}
}