use crate::{
Optimizer, OptimizerError, OptimizerResult, OptimizerState, ParamGroup, ParamGroupState,
};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::ops::Add;
use std::sync::Arc;
use torsh_core::error::{Result, TorshError};
use torsh_tensor::Tensor;
#[derive(Clone)]
pub struct NewtonCGConfig {
pub max_cg_iter: usize,
pub cg_tolerance: f32,
pub use_trust_region: bool,
pub initial_trust_radius: f32,
pub trust_tolerance: f32,
pub max_trust_radius: f32,
pub min_trust_radius: f32,
}
impl Default for NewtonCGConfig {
fn default() -> Self {
Self {
max_cg_iter: 50,
cg_tolerance: 1e-6,
use_trust_region: true,
initial_trust_radius: 1.0,
trust_tolerance: 1e-4,
max_trust_radius: 100.0,
min_trust_radius: 1e-6,
}
}
}
pub struct NewtonCG {
param_groups: Vec<ParamGroup>,
state: HashMap<String, HashMap<String, Tensor>>,
step_count: usize,
config: NewtonCGConfig,
tolerance_grad: f32,
tolerance_change: f32,
}
impl NewtonCG {
pub fn new(
params: Vec<Arc<RwLock<Tensor>>>,
lr: Option<f32>,
tolerance_grad: Option<f32>,
tolerance_change: Option<f32>,
config: Option<NewtonCGConfig>,
) -> Self {
let lr = lr.unwrap_or(1.0);
let param_group = ParamGroup::new(params, lr);
Self {
param_groups: vec![param_group],
state: HashMap::new(),
step_count: 0,
config: config.unwrap_or_default(),
tolerance_grad: tolerance_grad.unwrap_or(1e-7),
tolerance_change: tolerance_change.unwrap_or(1e-9),
}
}
pub fn builder() -> NewtonCGBuilder {
NewtonCGBuilder::new()
}
fn get_param_id(param: &Arc<RwLock<Tensor>>) -> String {
format!("{:p}", Arc::as_ptr(param))
}
fn flatten_params(&self) -> Result<Tensor> {
let mut flattened = Vec::new();
for group in &self.param_groups {
for param in &group.params {
let param_read = param.read();
let param_flat = param_read.flatten()?;
let param_data = param_flat.data()?;
flattened.extend_from_slice(¶m_data);
}
}
let len = flattened.len();
Ok(Tensor::from_data(
flattened,
vec![len],
torsh_core::device::DeviceType::Cpu,
)?)
}
fn flatten_grads(&self) -> Result<Tensor> {
let mut flattened = Vec::new();
for group in &self.param_groups {
for param in &group.params {
let param_read = param.read();
let grad = param_read.grad().ok_or_else(|| {
TorshError::invalid_argument_with_context(
"Parameter has no gradient",
"newton_cg_step",
)
})?;
let grad_flat = grad.flatten()?;
let grad_data = grad_flat.data()?;
flattened.extend_from_slice(&grad_data);
}
}
let len = flattened.len();
Ok(Tensor::from_data(
flattened,
vec![len],
torsh_core::device::DeviceType::Cpu,
)?)
}
fn update_params_from_flat(&self, flat_params: &Tensor) -> Result<()> {
let flat_data = flat_params.data()?;
let mut offset = 0;
for group in &self.param_groups {
for param in &group.params {
let mut param_write = param.write();
let param_shape = param_write.shape();
let param_size = param_shape.numel();
let param_data = &flat_data[offset..offset + param_size];
*param_write = Tensor::from_data(
param_data.to_vec(),
param_shape.dims().to_vec(),
param_write.device(),
)?;
offset += param_size;
}
}
Ok(())
}
fn hessian_vector_product(
&self,
grad: &Tensor,
vector: &Tensor,
epsilon: f32,
) -> Result<Tensor> {
let grad_norm = grad.norm()?.item()?;
let scale = if grad_norm > 1e-8 { grad_norm } else { 1.0 };
Ok(vector.mul_scalar(scale * epsilon)?)
}
fn solve_newton_cg(&self, grad: &Tensor, trust_radius: Option<f32>) -> Result<Tensor> {
let n = grad.shape().numel();
let mut x = Tensor::zeros(&[n], grad.device())?; let mut r = grad.neg()?; let mut p = r.clone(); let mut rsold = r.dot(&r)?.item()?;
let tolerance = self.config.cg_tolerance;
let max_iter = self.config.max_cg_iter.min(n);
for _i in 0..max_iter {
if rsold.sqrt() < tolerance {
break;
}
let hp = self.hessian_vector_product(grad, &p, 1e-7)?;
let pap = p.dot(&hp)?.item()?;
if pap <= 0.0 {
let grad_norm = grad.norm()?.item()?;
if grad_norm > 1e-12 {
let alpha = if let Some(radius) = trust_radius {
(radius / grad_norm).min(1.0)
} else {
1.0
};
return Ok(grad.mul_scalar(-alpha)?);
} else {
return Ok(x);
}
}
let alpha = rsold / pap;
let x_new = x.add(&p.mul_scalar(alpha)?)?;
if let Some(radius) = trust_radius {
let x_norm = x_new.norm()?.item()?;
if x_norm > radius {
let x_norm_old = x.norm()?.item()?;
if x_norm_old >= radius {
return Ok(x);
}
let xp = x.dot(&p)?.item()?;
let pp = p.dot(&p)?.item()?;
let xx = x.dot(&x)?.item()?;
let discriminant = xp * xp - pp * (xx - radius * radius);
if discriminant >= 0.0 {
let t = (-xp + discriminant.sqrt()) / pp;
return Ok(x.add(&p.mul_scalar(t)?)?);
} else {
return Ok(x);
}
}
}
x = x_new;
let r_new = r.sub(&hp.mul_scalar(alpha)?)?;
let rsnew = r_new.dot(&r_new)?.item()?;
if rsnew.sqrt() < tolerance {
break;
}
let beta = rsnew / rsold;
p = r_new.add(&p.mul_scalar(beta)?)?;
r = r_new;
rsold = rsnew;
}
Ok(x)
}
fn compute_reduction_ratio(
&self,
_old_params: &Tensor,
_new_params: &Tensor,
_old_grad: &Tensor,
_step: &Tensor,
) -> Result<f32> {
Ok(0.75) }
fn update_trust_radius(&self, current_radius: f32, reduction_ratio: f32) -> f32 {
let config = &self.config;
if reduction_ratio < 0.25 {
(current_radius * 0.25).max(config.min_trust_radius)
} else if reduction_ratio > 0.75 {
(current_radius * 2.0).min(config.max_trust_radius)
} else {
current_radius
}
}
}
impl Optimizer for NewtonCG {
fn step(&mut self) -> OptimizerResult<()> {
self.step_count += 1;
let current_params = self.flatten_params()?;
let current_grad = self.flatten_grads()?;
let grad_norm = current_grad.norm()?.item()?;
if grad_norm < self.tolerance_grad {
return Ok(());
}
let state_id = "newton_cg_state".to_string();
let mut trust_radius = if self.config.use_trust_region {
let param_state = self.state.entry(state_id.clone()).or_default();
if let Some(radius_tensor) = param_state.get("trust_radius") {
radius_tensor.item()?
} else {
let radius = self.config.initial_trust_radius;
param_state.insert("trust_radius".to_string(), Tensor::scalar(radius)?);
radius
}
} else {
0.0 };
let trust_radius_opt = if self.config.use_trust_region {
Some(trust_radius)
} else {
None
};
let newton_step = self.solve_newton_cg(¤t_grad, trust_radius_opt)?;
let new_params = current_params.add(&newton_step)?;
if self.config.use_trust_region {
let reduction_ratio = self.compute_reduction_ratio(
¤t_params,
&new_params,
¤t_grad,
&newton_step,
)?;
trust_radius = self.update_trust_radius(trust_radius, reduction_ratio);
let param_state = self
.state
.get_mut(&state_id)
.expect("state should exist for state_id");
param_state.insert("trust_radius".to_string(), Tensor::scalar(trust_radius)?);
if reduction_ratio > self.config.trust_tolerance {
self.update_params_from_flat(&new_params)?;
}
} else {
self.update_params_from_flat(&new_params)?;
}
Ok(())
}
fn zero_grad(&mut self) {
for group in &self.param_groups {
for param in &group.params {
param.write().zero_grad();
}
}
}
fn get_lr(&self) -> Vec<f32> {
self.param_groups.iter().map(|g| g.lr).collect()
}
fn set_lr(&mut self, lr: f32) {
for group in &mut self.param_groups {
group.lr = lr;
}
}
fn add_param_group(&mut self, params: Vec<Arc<RwLock<Tensor>>>, options: HashMap<String, f32>) {
let lr = options.get("lr").copied().unwrap_or(1.0);
let group = ParamGroup::new(params, lr).with_options(options);
self.param_groups.push(group);
}
fn parameters(&self) -> Vec<Arc<RwLock<Tensor>>> {
crate::optimizer::collect_parameters(&self.param_groups)
}
fn state_dict(&self) -> OptimizerResult<OptimizerState> {
let param_groups = self
.param_groups
.iter()
.map(|g| ParamGroupState {
lr: g.lr,
options: g.options.clone(),
param_count: g.params.len(),
})
.collect();
Ok(OptimizerState {
optimizer_type: "Newton-CG".to_string(),
version: "1.0".to_string(),
param_groups,
state: self.state.clone(),
global_state: HashMap::new(),
})
}
fn load_state_dict(&mut self, state: OptimizerState) -> OptimizerResult<()> {
if state.param_groups.len() != self.param_groups.len() {
return Err(OptimizerError::TensorError(TorshError::InvalidArgument(
"Parameter group count mismatch".to_string(),
)));
}
for (i, group_state) in state.param_groups.iter().enumerate() {
self.param_groups[i].lr = group_state.lr;
self.param_groups[i].options = group_state.options.clone();
}
self.state = state.state;
Ok(())
}
}
pub struct NewtonCGBuilder {
lr: f32,
tolerance_grad: f32,
tolerance_change: f32,
config: NewtonCGConfig,
}
impl NewtonCGBuilder {
pub fn new() -> Self {
Self {
lr: 1.0,
tolerance_grad: 1e-7,
tolerance_change: 1e-9,
config: NewtonCGConfig::default(),
}
}
pub fn lr(mut self, lr: f32) -> Self {
self.lr = lr;
self
}
pub fn tolerance_grad(mut self, tol: f32) -> Self {
self.tolerance_grad = tol;
self
}
pub fn tolerance_change(mut self, tol: f32) -> Self {
self.tolerance_change = tol;
self
}
pub fn max_cg_iter(mut self, max_iter: usize) -> Self {
self.config.max_cg_iter = max_iter;
self
}
pub fn cg_tolerance(mut self, tol: f32) -> Self {
self.config.cg_tolerance = tol;
self
}
pub fn use_trust_region(mut self, use_tr: bool) -> Self {
self.config.use_trust_region = use_tr;
self
}
pub fn initial_trust_radius(mut self, radius: f32) -> Self {
self.config.initial_trust_radius = radius;
self
}
pub fn trust_tolerance(mut self, tol: f32) -> Self {
self.config.trust_tolerance = tol;
self
}
pub fn max_trust_radius(mut self, radius: f32) -> Self {
self.config.max_trust_radius = radius;
self
}
pub fn min_trust_radius(mut self, radius: f32) -> Self {
self.config.min_trust_radius = radius;
self
}
pub fn build(self, params: Vec<Arc<RwLock<Tensor>>>) -> NewtonCG {
NewtonCG::new(
params,
Some(self.lr),
Some(self.tolerance_grad),
Some(self.tolerance_change),
Some(self.config),
)
}
}
impl Default for NewtonCGBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use torsh_tensor::creation::randn;
#[test]
fn test_newton_cg_creation() {
let params = vec![Arc::new(RwLock::new(randn::<f32>(&[2, 2]).unwrap()))];
let optimizer = NewtonCG::new(params, None, None, None, None);
assert_eq!(optimizer.get_lr()[0], 1.0);
}
#[test]
fn test_newton_cg_builder() {
let params = vec![Arc::new(RwLock::new(randn::<f32>(&[2, 2]).unwrap()))];
let optimizer = NewtonCG::builder()
.lr(0.1)
.max_cg_iter(20)
.cg_tolerance(1e-5)
.use_trust_region(true)
.initial_trust_radius(0.5)
.build(params);
assert_eq!(optimizer.get_lr()[0], 0.1);
assert_eq!(optimizer.config.max_cg_iter, 20);
assert_eq!(optimizer.config.cg_tolerance, 1e-5);
assert!(optimizer.config.use_trust_region);
assert_eq!(optimizer.config.initial_trust_radius, 0.5);
}
#[test]
fn test_newton_cg_step() -> OptimizerResult<()> {
let param = Arc::new(RwLock::new(randn::<f32>(&[2, 2]).unwrap()));
let mut param_write = param.write();
param_write.set_grad(Some(randn::<f32>(&[2, 2]).unwrap()));
drop(param_write);
let params = vec![param];
let mut optimizer = NewtonCG::new(params, Some(0.1), None, None, None);
optimizer.step()?;
Ok(())
}
#[test]
fn test_newton_cg_config() {
let config = NewtonCGConfig {
max_cg_iter: 25,
cg_tolerance: 1e-7,
use_trust_region: false,
initial_trust_radius: 2.0,
trust_tolerance: 1e-5,
max_trust_radius: 50.0,
min_trust_radius: 1e-5,
};
assert_eq!(config.max_cg_iter, 25);
assert_eq!(config.cg_tolerance, 1e-7);
assert!(!config.use_trust_region);
assert_eq!(config.initial_trust_radius, 2.0);
}
}