use crate::{
optimizer::BaseOptimizer, Optimizer, OptimizerError, OptimizerResult, OptimizerState,
ParamGroup,
};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::ops::Add;
use std::sync::Arc;
use torsh_core::error::{Result, TorshError};
use torsh_linalg::solve;
use torsh_tensor::{
creation::{eye, zeros_like},
Tensor,
};
pub struct KFAC {
base: BaseOptimizer,
lr: f32,
momentum: f32,
damping: f32,
kfac_update_freq: usize,
stat_decay: f32,
#[allow(dead_code)]
kl_clip: f32,
step_count: usize,
use_preconditioning: bool,
}
impl KFAC {
#[allow(clippy::too_many_arguments)]
pub fn new(
params: Vec<Arc<RwLock<Tensor>>>,
lr: Option<f32>,
momentum: Option<f32>,
damping: Option<f32>,
kfac_update_freq: Option<usize>,
stat_decay: Option<f32>,
kl_clip: Option<f32>,
use_preconditioning: Option<bool>,
) -> Self {
let lr = lr.unwrap_or(0.001);
let momentum = momentum.unwrap_or(0.9);
let damping = damping.unwrap_or(0.003);
let kfac_update_freq = kfac_update_freq.unwrap_or(10);
let stat_decay = stat_decay.unwrap_or(0.95);
let kl_clip = kl_clip.unwrap_or(0.001);
let use_preconditioning = use_preconditioning.unwrap_or(true);
let mut defaults = HashMap::new();
defaults.insert("lr".to_string(), lr);
defaults.insert("momentum".to_string(), momentum);
defaults.insert("damping".to_string(), damping);
let param_group = ParamGroup::new(params, lr);
let base = BaseOptimizer {
param_groups: vec![param_group],
state: HashMap::new(),
optimizer_type: "KFAC".to_string(),
defaults,
};
Self {
base,
lr,
momentum,
damping,
kfac_update_freq,
stat_decay,
kl_clip,
step_count: 0,
use_preconditioning,
}
}
pub fn builder() -> KFACBuilder {
KFACBuilder::default()
}
fn compute_kronecker_factors(
&self,
weight: &Tensor,
_grad: &Tensor,
) -> Result<(Tensor, Tensor)> {
let shape = weight.shape();
let dims = shape.dims();
if dims.len() != 2 {
return Err(TorshError::Other(
"K-FAC currently only supports 2D weight matrices".to_string(),
));
}
let input_size = dims[0];
let output_size = dims[1];
let activation_cov = eye(input_size);
let grad_cov = eye(output_size);
Ok((activation_cov?, grad_cov?))
}
fn apply_kfac_preconditioning(
&self,
grad: &Tensor,
a_inv: &Tensor,
g_inv: &Tensor,
) -> Result<Tensor> {
let preconditioned = a_inv.matmul(grad)?.matmul(g_inv)?;
Ok(preconditioned)
}
fn damped_inverse(&self, matrix: &Tensor) -> Result<Tensor> {
let shape = matrix.shape();
let dims = shape.dims();
if dims.len() != 2 || dims[0] != dims[1] {
return Err(TorshError::Other(
"Matrix must be square for inversion".to_string(),
));
}
let damping_matrix = eye(dims[0])?.mul_scalar(self.damping)?;
let damped_matrix = matrix.add(&damping_matrix)?;
damped_matrix.reciprocal()
}
}
impl Optimizer for KFAC {
fn step(&mut self) -> OptimizerResult<()> {
self.step_count += 1;
let should_update_kfac = self.step_count % self.kfac_update_freq == 0;
for group in &mut self.base.param_groups {
for param_arc in &group.params {
let mut param = param_arc.write();
if !param.has_grad() {
continue;
}
let grad = param
.grad()
.expect("gradient should exist after has_grad check");
let param_id = format!("{:p}", param_arc.as_ref());
if param.shape().ndim() != 2 {
let update = grad.mul_scalar(self.lr)?;
*param = param.sub(&update)?;
continue;
}
let needs_init = !self.base.state.contains_key(¶m_id);
let state = self
.base
.state
.entry(param_id.clone())
.or_insert_with(HashMap::new);
if needs_init {
state.insert("momentum_buffer".to_string(), zeros_like(¶m)?);
state.insert("activation_cov".to_string(), eye(param.shape().dims()[0])?);
state.insert("gradient_cov".to_string(), eye(param.shape().dims()[1])?);
state.insert("a_inv".to_string(), eye(param.shape().dims()[0])?);
state.insert("g_inv".to_string(), eye(param.shape().dims()[1])?);
state.insert("step".to_string(), zeros_like(¶m)?);
}
let mut momentum_buffer = state
.get("momentum_buffer")
.expect("momentum_buffer state should exist")
.clone();
let mut activation_cov = state
.get("activation_cov")
.expect("activation_cov state should exist")
.clone();
let mut gradient_cov = state
.get("gradient_cov")
.expect("gradient_cov state should exist")
.clone();
let mut a_inv = state
.get("a_inv")
.expect("a_inv state should exist")
.clone();
let mut g_inv = state
.get("g_inv")
.expect("g_inv state should exist")
.clone();
let mut step_tensor = state.get("step").expect("step state should exist").clone();
step_tensor.add_scalar_(1.0)?;
if should_update_kfac {
let param_data = param.clone();
let _grad_data = grad.clone();
let stat_decay = self.stat_decay;
let damping = self.damping;
drop(param);
let (new_a, new_g) = {
let shape = param_data.shape();
let dims = shape.dims();
if dims.len() != 2 {
return Err(OptimizerError::TensorError(TorshError::InvalidArgument(
"K-FAC only supports 2D weight matrices".to_string(),
)));
}
let input_size = dims[0];
let output_size = dims[1];
let a = Tensor::ones(&[input_size, input_size], param_data.device())?;
let g = Tensor::ones(&[output_size, output_size], param_data.device())?;
(a, g)
};
activation_cov = activation_cov
.mul_scalar(stat_decay)?
.add(&new_a.mul_scalar(1.0 - stat_decay)?)?;
gradient_cov = gradient_cov
.mul_scalar(stat_decay)?
.add(&new_g.mul_scalar(1.0 - stat_decay)?)?;
let damped_a = activation_cov.add_scalar(damping)?;
let damped_g = gradient_cov.add_scalar(damping)?;
a_inv = solve::inv(&damped_a)?;
g_inv = solve::inv(&damped_g)?;
param = param_arc.write();
}
let grad = if should_update_kfac {
param.grad().expect("gradient should exist after update")
} else {
grad
};
let preconditioned_grad = if self.use_preconditioning {
grad.matmul(&g_inv)?
.transpose(0, 1)?
.matmul(&a_inv)?
.transpose(0, 1)?
} else {
grad
};
if self.momentum != 0.0 {
momentum_buffer = momentum_buffer
.mul_scalar(self.momentum)?
.add(&preconditioned_grad)?;
let update = momentum_buffer.mul_scalar(self.lr)?;
*param = param.sub(&update)?;
} else {
let update = preconditioned_grad.mul_scalar(self.lr)?;
*param = param.sub(&update)?;
}
state.insert("momentum_buffer".to_string(), momentum_buffer);
state.insert("activation_cov".to_string(), activation_cov);
state.insert("gradient_cov".to_string(), gradient_cov);
state.insert("a_inv".to_string(), a_inv);
state.insert("g_inv".to_string(), g_inv);
state.insert("step".to_string(), step_tensor);
}
}
Ok(())
}
fn zero_grad(&mut self) {
self.base.zero_grad();
}
fn get_lr(&self) -> Vec<f32> {
self.base.get_lr()
}
fn set_lr(&mut self, lr: f32) {
self.lr = lr;
self.base.set_lr(lr);
}
fn add_param_group(&mut self, params: Vec<Arc<RwLock<Tensor>>>, options: HashMap<String, f32>) {
self.base.add_param_group(params, options);
}
fn state_dict(&self) -> OptimizerResult<OptimizerState> {
self.base.state_dict()
}
fn load_state_dict(&mut self, state: OptimizerState) -> OptimizerResult<()> {
self.base.load_state_dict(state)
}
}
pub struct KFACBuilder {
params: Vec<Arc<RwLock<Tensor>>>,
lr: f32,
momentum: f32,
damping: f32,
kfac_update_freq: usize,
stat_decay: f32,
kl_clip: f32,
use_preconditioning: bool,
}
impl Default for KFACBuilder {
fn default() -> Self {
Self {
params: Vec::new(),
lr: 0.001,
momentum: 0.9,
damping: 0.003,
kfac_update_freq: 10,
stat_decay: 0.95,
kl_clip: 0.001,
use_preconditioning: true,
}
}
}
impl KFACBuilder {
pub fn params(mut self, params: Vec<Arc<RwLock<Tensor>>>) -> Self {
self.params = params;
self
}
pub fn lr(mut self, lr: f32) -> Self {
self.lr = lr;
self
}
pub fn momentum(mut self, momentum: f32) -> Self {
self.momentum = momentum;
self
}
pub fn damping(mut self, damping: f32) -> Self {
self.damping = damping;
self
}
pub fn kfac_update_freq(mut self, freq: usize) -> Self {
self.kfac_update_freq = freq;
self
}
pub fn stat_decay(mut self, decay: f32) -> Self {
self.stat_decay = decay;
self
}
pub fn kl_clip(mut self, clip: f32) -> Self {
self.kl_clip = clip;
self
}
pub fn use_preconditioning(mut self, use_preconditioning: bool) -> Self {
self.use_preconditioning = use_preconditioning;
self
}
pub fn build(self) -> KFAC {
KFAC::new(
self.params,
Some(self.lr),
Some(self.momentum),
Some(self.damping),
Some(self.kfac_update_freq),
Some(self.stat_decay),
Some(self.kl_clip),
Some(self.use_preconditioning),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use torsh_tensor::creation::randn;
#[test]
fn test_kfac_creation() {
let param = Arc::new(RwLock::new(randn::<f32>(&[10, 5]).unwrap()));
let params = vec![param];
let optimizer = KFAC::new(
params,
Some(0.001),
Some(0.9),
Some(0.003),
Some(10),
Some(0.95),
Some(0.001),
Some(true),
);
assert_eq!(optimizer.lr, 0.001);
assert_eq!(optimizer.momentum, 0.9);
assert_eq!(optimizer.damping, 0.003);
assert_eq!(optimizer.kfac_update_freq, 10);
}
#[test]
fn test_kfac_builder() {
let param = Arc::new(RwLock::new(randn::<f32>(&[8, 4]).unwrap()));
let params = vec![param];
let optimizer = KFAC::builder()
.params(vec![Arc::new(RwLock::new(randn::<f32>(&[10, 5]).unwrap()))])
.lr(0.01)
.momentum(0.8)
.damping(0.01)
.kfac_update_freq(5)
.stat_decay(0.9)
.kl_clip(0.01)
.use_preconditioning(false)
.build();
assert_eq!(optimizer.lr, 0.01);
assert_eq!(optimizer.momentum, 0.8);
assert_eq!(optimizer.damping, 0.01);
assert_eq!(optimizer.kfac_update_freq, 5);
assert!(!optimizer.use_preconditioning);
}
#[test]
fn test_kfac_step() {
let param = Arc::new(RwLock::new(randn::<f32>(&[4, 3]).unwrap()));
let initial_param = param.read().clone();
{
let mut p = param.write();
let grad = randn::<f32>(&[4, 3]).unwrap();
p.set_grad(Some(grad));
}
let params = vec![param.clone()];
let mut optimizer = KFAC::new(params, Some(0.01), None, None, None, None, None, None);
optimizer.step().unwrap();
let final_param = param.read().clone();
let diff = initial_param.sub(&final_param).unwrap();
let norm = diff
.norm()
.unwrap()
.to_vec()
.expect("tensor to vec conversion should succeed")[0];
assert!(
norm > 0.0,
"Parameter should change after optimization step"
);
}
#[test]
#[allow(dead_code)]
fn test_kronecker_factors() {
let weight = randn::<f32>(&[3, 2]).unwrap();
let grad = randn::<f32>(&[3, 2]).unwrap();
let optimizer = KFAC::new(vec![], Some(0.001), None, None, None, None, None, None);
let (a, g) = optimizer.compute_kronecker_factors(&weight, &grad).unwrap();
assert_eq!(a.shape().dims(), &[3, 3]);
assert_eq!(g.shape().dims(), &[2, 2]);
}
#[test]
#[allow(dead_code)]
fn test_damped_inverse() -> OptimizerResult<()> {
let matrix = eye(3).unwrap();
let optimizer = KFAC::new(vec![], Some(0.001), None, Some(0.1), None, None, None, None);
let inv = optimizer.damped_inverse(&matrix).unwrap();
let expected_diag = 1.0 / (1.0 + 0.1);
let inv_vec = inv.to_vec()?;
assert_relative_eq!(inv_vec[0], expected_diag, epsilon = 1e-5);
assert_relative_eq!(inv_vec[4], expected_diag, epsilon = 1e-5);
assert_relative_eq!(inv_vec[8], expected_diag, epsilon = 1e-5);
Ok(())
}
}