use crate::{Optimizer, OptimizerResult, OptimizerState};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use torsh_core::error::Result;
use torsh_tensor::Tensor;
pub struct GradientAccumulator<O: Optimizer> {
optimizer: O,
accumulation_steps: u32,
current_step: u32,
accumulated_grads: HashMap<String, Tensor>,
}
impl<O: Optimizer> GradientAccumulator<O> {
pub fn new(optimizer: O, accumulation_steps: u32) -> Self {
if accumulation_steps == 0 {
panic!("Accumulation steps must be greater than 0");
}
Self {
optimizer,
accumulation_steps,
current_step: 0,
accumulated_grads: HashMap::new(),
}
}
pub fn accumulation_steps(&self) -> u32 {
self.accumulation_steps
}
pub fn current_step(&self) -> u32 {
self.current_step
}
pub fn should_step(&self) -> bool {
(self.current_step + 1) % self.accumulation_steps == 0
}
pub fn accumulate_gradients(&mut self) -> Result<()> {
self.current_step += 1;
Ok(())
}
pub fn step(&mut self) -> OptimizerResult<()> {
if self.should_step() {
self.average_accumulated_gradients()?;
self.optimizer.step()?;
self.reset_accumulation();
} else {
self.accumulate_gradients()?;
}
Ok(())
}
pub fn step_force(&mut self) -> Result<()> {
if self.current_step > 0 {
self.average_accumulated_gradients()?;
self.optimizer.step()?;
self.reset_accumulation();
}
Ok(())
}
pub fn reset_accumulation(&mut self) {
self.current_step = 0;
self.accumulated_grads.clear();
}
fn average_accumulated_gradients(&mut self) -> Result<()> {
let divisor = if self.current_step == 0 {
1.0
} else {
self.current_step as f32
};
for (_, accumulated_grad) in self.accumulated_grads.iter_mut() {
accumulated_grad.div_scalar_(divisor)?;
}
Ok(())
}
pub fn optimizer(&self) -> &O {
&self.optimizer
}
pub fn optimizer_mut(&mut self) -> &mut O {
&mut self.optimizer
}
}
impl<O: Optimizer> Optimizer for GradientAccumulator<O> {
fn step(&mut self) -> OptimizerResult<()> {
self.step()
}
fn zero_grad(&mut self) {
self.optimizer.zero_grad();
}
fn get_lr(&self) -> Vec<f32> {
self.optimizer.get_lr()
}
fn set_lr(&mut self, lr: f32) {
self.optimizer.set_lr(lr);
}
fn add_param_group(&mut self, params: Vec<Arc<RwLock<Tensor>>>, options: HashMap<String, f32>) {
self.optimizer.add_param_group(params, options);
}
fn state_dict(&self) -> OptimizerResult<OptimizerState> {
self.optimizer.state_dict()
}
fn load_state_dict(&mut self, state: OptimizerState) -> OptimizerResult<()> {
self.optimizer.load_state_dict(state)
}
}
pub fn with_gradient_accumulation<O: Optimizer>(
optimizer: O,
accumulation_steps: u32,
) -> GradientAccumulator<O> {
GradientAccumulator::new(optimizer, accumulation_steps)
}
pub trait GradientAccumulationSupport {
fn accumulate_gradients(&mut self) -> Result<()>;
fn should_average_gradients(&self) -> bool;
fn set_accumulation_steps(&mut self, steps: u32);
fn get_accumulation_steps(&self) -> u32;
}
pub struct AccumulatingOptimizer<O: Optimizer> {
optimizer: O,
accumulation_steps: u32,
current_step: u32,
auto_average: bool,
}
impl<O: Optimizer> AccumulatingOptimizer<O> {
pub fn new(optimizer: O) -> Self {
Self {
optimizer,
accumulation_steps: 1,
current_step: 0,
auto_average: true,
}
}
pub fn set_auto_average(&mut self, auto_average: bool) {
self.auto_average = auto_average;
}
pub fn inner(&self) -> &O {
&self.optimizer
}
pub fn inner_mut(&mut self) -> &mut O {
&mut self.optimizer
}
pub fn into_inner(self) -> O {
self.optimizer
}
}
impl<O: Optimizer> Optimizer for AccumulatingOptimizer<O> {
fn step(&mut self) -> OptimizerResult<()> {
self.current_step += 1;
if self.current_step % self.accumulation_steps == 0 {
self.optimizer.step()?;
self.current_step = 0;
}
Ok(())
}
fn zero_grad(&mut self) {
self.optimizer.zero_grad();
}
fn get_lr(&self) -> Vec<f32> {
self.optimizer.get_lr()
}
fn set_lr(&mut self, lr: f32) {
self.optimizer.set_lr(lr);
}
fn add_param_group(&mut self, params: Vec<Arc<RwLock<Tensor>>>, options: HashMap<String, f32>) {
self.optimizer.add_param_group(params, options);
}
fn state_dict(&self) -> OptimizerResult<OptimizerState> {
self.optimizer.state_dict()
}
fn load_state_dict(&mut self, state: OptimizerState) -> OptimizerResult<()> {
self.optimizer.load_state_dict(state)
}
}
impl<O: Optimizer> GradientAccumulationSupport for AccumulatingOptimizer<O> {
fn accumulate_gradients(&mut self) -> Result<()> {
self.current_step += 1;
Ok(())
}
fn should_average_gradients(&self) -> bool {
self.auto_average
&& self.current_step > 0
&& self.current_step % self.accumulation_steps == 0
}
fn set_accumulation_steps(&mut self, steps: u32) {
if steps == 0 {
panic!("Accumulation steps must be greater than 0");
}
self.accumulation_steps = steps;
}
fn get_accumulation_steps(&self) -> u32 {
self.accumulation_steps
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sgd::SGD;
use torsh_core::device::Device;
use torsh_tensor::creation;
#[test]
fn test_gradient_accumulator_creation() {
let param = Arc::new(RwLock::new(creation::randn::<f32>(&[2, 3]).unwrap()));
let sgd = SGD::new(vec![param], 0.01, None, None, None, false);
let accumulator = GradientAccumulator::new(sgd, 4);
assert_eq!(accumulator.accumulation_steps(), 4);
assert_eq!(accumulator.current_step(), 0);
assert!(!accumulator.should_step());
}
#[test]
fn test_gradient_accumulator_step_logic() {
let param = Arc::new(RwLock::new(creation::randn::<f32>(&[2, 3]).unwrap()));
let sgd = SGD::new(vec![param], 0.01, None, None, None, false);
let mut accumulator = GradientAccumulator::new(sgd, 3);
assert!(!accumulator.should_step());
accumulator.accumulate_gradients().unwrap();
assert_eq!(accumulator.current_step(), 1);
assert!(!accumulator.should_step());
accumulator.accumulate_gradients().unwrap();
assert_eq!(accumulator.current_step(), 2);
assert!(accumulator.should_step());
}
#[test]
fn test_accumulating_optimizer() {
let param = Arc::new(RwLock::new(creation::randn::<f32>(&[2, 3]).unwrap()));
let sgd = SGD::new(vec![param], 0.01, None, None, None, false);
let mut acc_optimizer = AccumulatingOptimizer::new(sgd);
acc_optimizer.set_accumulation_steps(2);
assert_eq!(acc_optimizer.get_accumulation_steps(), 2);
assert!(!acc_optimizer.should_average_gradients());
acc_optimizer.accumulate_gradients().unwrap();
assert!(!acc_optimizer.should_average_gradients());
acc_optimizer.accumulate_gradients().unwrap();
assert!(acc_optimizer.should_average_gradients());
}
#[test]
fn test_with_gradient_accumulation_helper() {
let param = Arc::new(RwLock::new(creation::randn::<f32>(&[2, 3]).unwrap()));
let sgd = SGD::new(vec![param], 0.01, None, None, None, false);
let accumulator = with_gradient_accumulation(sgd, 5);
assert_eq!(accumulator.accumulation_steps(), 5);
}
#[test]
#[should_panic(expected = "Accumulation steps must be greater than 0")]
fn test_zero_accumulation_steps_panics() {
let param = Arc::new(RwLock::new(creation::randn::<f32>(&[2, 3]).unwrap()));
let sgd = SGD::new(vec![param], 0.01, None, None, None, false);
GradientAccumulator::new(sgd, 0);
}
}