use crate::error::{OptimError, Result};
use scirs2_core::ndarray::{Array, Array1, Array2, Dimension};
use scirs2_core::numeric::Float;
use std::collections::HashMap;
use std::fmt::Debug;
pub mod forward_mode;
pub mod higher_order;
pub mod meta_gradients;
pub mod reverse_mode;
#[derive(Debug, Clone)]
pub struct AutodiffConfig {
pub enable_forward_mode: bool,
pub enable_reverse_mode: bool,
pub max_derivative_order: usize,
pub enable_hessian: bool,
pub sparse_hessian: bool,
pub hessian_approximation: HessianApproximation,
pub enable_meta_gradients: bool,
pub gradient_checkpointing: bool,
pub checkpoint_chunk_size: usize,
}
impl Default for AutodiffConfig {
fn default() -> Self {
Self {
enable_forward_mode: true,
enable_reverse_mode: true,
max_derivative_order: 2,
enable_hessian: true,
sparse_hessian: false,
hessian_approximation: HessianApproximation::BFGS,
enable_meta_gradients: false,
gradient_checkpointing: true,
checkpoint_chunk_size: 1000,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum HessianApproximation {
Exact,
BFGS,
LBFGS,
GaussNewton,
Fisher,
Diagonal,
}
#[derive(Debug, Clone)]
pub struct ADNode<T: Float + Debug + Send + Sync + 'static> {
pub value: T,
pub gradient: Vec<T>,
pub hessian: Option<Array2<T>>,
pub operation: Operation,
pub parents: Vec<usize>,
pub id: usize,
}
#[derive(Debug, Clone)]
pub enum Operation {
Input,
Add,
Multiply,
Divide,
Power,
Exp,
Log,
Sin,
Cos,
Tanh,
Sigmoid,
ReLU,
MatMul,
Dot,
Sum,
Mean,
Custom(String),
}
pub struct AutodiffEngine<T: Float + Debug + Send + Sync + 'static> {
config: AutodiffConfig,
graph: Vec<ADNode<T>>,
variables: HashMap<String, usize>,
tape: Vec<TapeEntry<T>>,
context: ComputationContext,
hessian_state: Option<HessianApproximationState<T>>,
}
#[derive(Debug, Clone)]
struct TapeEntry<T: Float + Debug + Send + Sync + 'static> {
operation: Operation,
inputs: Vec<usize>,
output: usize,
local_gradients: Vec<T>,
}
#[derive(Debug, Clone)]
struct ComputationContext {
forward_pass_id: usize,
record_gradients: bool,
higher_order: bool,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct HessianApproximationState<T: Float + Debug + Send + Sync + 'static> {
method: HessianApproximation,
gradient_history: Vec<Array1<T>>,
update_history: Vec<Array1<T>>,
inverse_hessian: Option<Array2<T>>,
lbfgs_memory: Option<LBFGSMemory<T>>,
max_history: usize,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct LBFGSMemory<T: Float + Debug + Send + Sync + 'static> {
y_history: Vec<Array1<T>>,
s_history: Vec<Array1<T>>,
rho_history: Vec<T>,
memory_size: usize,
current_pos: usize,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct ComputationCheckpoint<T: Float + Debug + Send + Sync + 'static> {
values: Vec<T>,
graph_state: Vec<ADNode<T>>,
id: usize,
memory_usage: usize,
}
impl<T: Float + Debug + Send + Sync + 'static + Default + Clone + scirs2_core::ndarray::ScalarOperand> AutodiffEngine<T> {
pub fn new(config: AutodiffConfig) -> Self {
Self {
config,
graph: Vec::new(),
variables: HashMap::new(),
tape: Vec::new(),
context: ComputationContext {
forward_pass_id: 0,
record_gradients: true,
higher_order: false,
},
hessian_state: None,
}
}
pub fn create_variable(&mut self, name: &str, value: T) -> usize {
let node = ADNode {
value,
gradient: vec![T::one()],
hessian: None,
operation: Operation::Input,
parents: Vec::new(),
id: self.graph.len(),
};
let id = self.graph.len();
self.graph.push(node);
self.variables.insert(name.to_string(), id);
id
}
pub fn backward(&mut self, outputid: usize) -> Result<Vec<T>> {
if outputid >= self.graph.len() {
return Err(OptimError::InvalidConfig(
"Invalid output node ID".to_string(),
));
}
let mut gradients = vec![T::zero(); self.graph.len()];
gradients[outputid] = T::one();
for entry in self.tape.iter().rev() {
let output_grad = gradients[entry.output];
for (i, &input_id) in entry.inputs.iter().enumerate() {
if i < entry.local_gradients.len() {
gradients[input_id] =
gradients[input_id] + output_grad * entry.local_gradients[i];
}
}
}
Ok(gradients)
}
pub fn compute_hessian(&mut self, outputid: usize) -> Result<Array2<T>> {
match self.config.hessian_approximation {
HessianApproximation::Exact => self.compute_exact_hessian(outputid),
HessianApproximation::BFGS => self.compute_bfgs_hessian(),
HessianApproximation::LBFGS => self.compute_lbfgs_hessian(),
HessianApproximation::Diagonal => self.compute_diagonal_hessian(outputid),
HessianApproximation::GaussNewton => self.compute_hessian(outputid),
HessianApproximation::Fisher => self.compute_diagonal_hessian(outputid),
}
}
fn compute_exact_hessian(&mut self, outputid: usize) -> Result<Array2<T>> {
let n_vars = self.variables.len();
let mut hessian = Array2::zeros((n_vars, n_vars));
for i in 0..n_vars {
for j in i..n_vars {
let second_deriv = self.compute_second_derivative(outputid, i, j)?;
hessian[[i, j]] = second_deriv;
hessian[[j, i]] = second_deriv; }
}
Ok(hessian)
}
fn compute_second_derivative(
&mut self,
outputid: usize,
var1: usize,
var2: usize,
) -> Result<T> {
if self.config.gradient_checkpointing {
return self.compute_second_derivative_checkpointed(outputid, var1, var2);
}
let eps = scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero());
let original_val1 = self.graph[var1].value;
let original_val2 = self.graph[var2].value;
self.graph[var1].value = original_val1 + eps;
self.graph[var2].value = original_val2 + eps;
let f_pp = self.evaluate_node(outputid)?;
self.graph[var2].value = original_val2 - eps;
let f_pm = self.evaluate_node(outputid)?;
self.graph[var1].value = original_val1 - eps;
self.graph[var2].value = original_val2 + eps;
let f_mp = self.evaluate_node(outputid)?;
self.graph[var2].value = original_val2 - eps;
let f_mm = self.evaluate_node(outputid)?;
self.graph[var1].value = original_val1;
self.graph[var2].value = original_val2;
let second_deriv = (f_pp - f_pm - f_mp + f_mm) / (scirs2_core::numeric::NumCast::from(4.0).unwrap_or_else(|| T::zero()) * eps * eps);
Ok(second_deriv)
}
fn compute_second_derivative_checkpointed(
&mut self,
outputid: usize,
var1: usize,
var2: usize,
) -> Result<T> {
let chunk_size = self.config.checkpoint_chunk_size;
let eps = scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero());
let checkpoint = self.create_checkpoint();
let mut results = Vec::new();
for chunk_start in (0..4).step_by(chunk_size) {
let chunk_end = (chunk_start + chunk_size).min(4);
self.restore_checkpoint(&checkpoint)?;
for i in chunk_start..chunk_end {
let point_values = match i {
0 => (eps, eps), 1 => (eps, -eps), 2 => (-eps, eps), 3 => (-eps, -eps), _ => continue,
};
self.graph[var1].value = checkpoint.values[var1] + point_values.0;
self.graph[var2].value = checkpoint.values[var2] + point_values.1;
let result = self.evaluate_node(outputid)?;
results.push(result);
}
}
self.restore_checkpoint(&checkpoint)?;
if results.len() >= 4 {
let second_deriv = (results[0] - results[1] - results[2] + results[3])
/ (scirs2_core::numeric::NumCast::from(4.0).unwrap_or_else(|| T::zero()) * eps * eps);
Ok(second_deriv)
} else {
Err(OptimError::InvalidConfig(
"Insufficient results for second derivative".to_string(),
))
}
}
fn evaluate_node(&self, nodeid: usize) -> Result<T> {
if nodeid >= self.graph.len() {
return Err(OptimError::InvalidConfig("Invalid node ID".to_string()));
}
Ok(self.graph[nodeid].value)
}
fn compute_bfgs_hessian(&self) -> Result<Array2<T>> {
if let Some(ref state) = self.hessian_state {
if let Some(ref inv_hessian) = state.inverse_hessian {
return Ok(inv_hessian.clone());
}
}
Err(OptimError::InvalidConfig(
"BFGS Hessian not initialized".to_string(),
))
}
fn compute_lbfgs_hessian(&self) -> Result<Array2<T>> {
if let Some(ref state) = self.hessian_state {
if let Some(ref lbfgs) = state.lbfgs_memory {
return self.lbfgs_two_loop_recursion(lbfgs);
}
}
Err(OptimError::InvalidConfig(
"L-BFGS memory not initialized".to_string(),
))
}
fn lbfgs_two_loop_recursion(&self, lbfgs: &LBFGSMemory<T>) -> Result<Array2<T>> {
let n = lbfgs
.s_history
.get(0)
.ok_or_else(|| OptimError::InvalidConfig("Empty L-BFGS history".to_string()))?
.len();
let result = Array2::eye(n);
Ok(result)
}
fn compute_diagonal_hessian(&mut self, outputid: usize) -> Result<Array2<T>> {
let n_vars = self.variables.len();
let mut hessian = Array2::zeros((n_vars, n_vars));
for i in 0..n_vars {
let second_deriv = self.compute_second_derivative(outputid, i, i)?;
hessian[[i, i]] = second_deriv;
}
Ok(hessian)
}
pub fn update_bfgs(
&mut self,
gradient_new: &Array1<T>,
gradient_old: &Array1<T>,
step: &Array1<T>,
) -> Result<()> {
if self.hessian_state.is_none() {
let n = gradient_new.len();
self.hessian_state = Some(HessianApproximationState {
method: HessianApproximation::BFGS,
gradient_history: Vec::new(),
update_history: Vec::new(),
inverse_hessian: Some(Array2::eye(n)),
lbfgs_memory: None,
max_history: 100,
});
}
let state = self.hessian_state.as_mut().expect("unwrap failed");
let y = gradient_new - gradient_old; let s = step;
let sy = s.dot(&y);
if sy.abs() < scirs2_core::numeric::NumCast::from(1e-10).unwrap_or_else(|| T::zero()) {
return Ok(()); }
if let Some(ref mut h_inv) = state.inverse_hessian {
let rho = T::one() / sy;
let n = s.len();
let mut sy_outer = Array2::zeros((n, n));
let mut ys_outer = Array2::zeros((n, n));
let mut ss_outer = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
sy_outer[[i, j]] = s[i] * y[j];
ys_outer[[i, j]] = y[i] * s[j];
ss_outer[[i, j]] = s[i] * s[j];
}
}
let identity = Array2::eye(n);
let term1 = &identity - &(sy_outer * rho);
let term2 = &identity - &(ys_outer * rho);
*h_inv = term1.dot(h_inv).dot(&term2) + ss_outer * rho;
}
state.gradient_history.push(gradient_new.clone());
state.update_history.push(step.clone());
if state.gradient_history.len() > state.max_history {
state.gradient_history.remove(0);
state.update_history.remove(0);
}
Ok(())
}
pub fn compute_meta_gradients(
&mut self,
_inner_steps: usize,
outer_objective_id: usize,
) -> Result<Vec<T>> {
if !self.config.enable_meta_gradients {
return Err(OptimError::InvalidConfig(
"Meta-gradients not enabled".to_string(),
));
}
let meta_grads = self.backward(outer_objective_id)?;
Ok(meta_grads)
}
pub fn enable_checkpointing(&mut self, enabled: bool) {
self.config.gradient_checkpointing = enabled;
}
pub fn get_graph_stats(&self) -> GraphStats {
GraphStats {
num_nodes: self.graph.len(),
num_variables: self.variables.len(),
tape_length: self.tape.len(),
memory_usage_estimate: self.estimate_memory_usage(),
}
}
fn estimate_memory_usage(&self) -> usize {
let node_size = std::mem::size_of::<ADNode<T>>();
let tape_size = std::mem::size_of::<TapeEntry<T>>();
self.graph.len() * node_size + self.tape.len() * tape_size
}
fn create_checkpoint(&self) -> ComputationCheckpoint<T> {
let values: Vec<T> = self.graph.iter().map(|node| node.value).collect();
let graph_state = self.graph.clone();
let memory_usage = values.len() * std::mem::size_of::<T>()
+ graph_state.len() * std::mem::size_of::<ADNode<T>>();
ComputationCheckpoint {
values,
graph_state,
id: self.context.forward_pass_id,
memory_usage,
}
}
fn restore_checkpoint(&mut self, checkpoint: &ComputationCheckpoint<T>) -> Result<()> {
if checkpoint.values.len() != self.graph.len() {
return Err(OptimError::InvalidConfig(
"Checkpoint size mismatch".to_string(),
));
}
for (i, &value) in checkpoint.values.iter().enumerate() {
if i < self.graph.len() {
self.graph[i].value = value;
}
}
if checkpoint.graph_state.len() == self.graph.len() {
self.graph = checkpoint.graph_state.clone();
}
Ok(())
}
pub fn integrate_with_candle(&mut self, enable: bool) -> Result<()> {
if enable {
self.context.higher_order = true;
self.config.enable_forward_mode = true;
self.config.enable_reverse_mode = true;
self.config.gradient_checkpointing = true;
self.setup_candle_compatibility()?;
}
Ok(())
}
fn setup_candle_compatibility(&mut self) -> Result<()> {
self.config.max_derivative_order = 3; self.config.enable_hessian = true;
self.config.sparse_hessian = true;
self.config.checkpoint_chunk_size = 500;
self.tape.clear();
self.tape.reserve(10000);
Ok(())
}
pub fn from_candle_gradients<D: Dimension>(
&mut self,
gradients: &Array<T, D>,
) -> Result<Vec<T>> {
if let Some(grad_slice) = gradients.as_slice() {
Ok(grad_slice.to_vec())
} else {
let flat_grads: Vec<T> = gradients.iter().cloned().collect();
Ok(flat_grads)
}
}
pub fn to_candle_gradients<D: Dimension>(
&self,
gradients: &[T],
shape: &[usize],
) -> Result<Array<T, D>> {
if gradients.len() != shape.iter().product::<usize>() {
return Err(OptimError::InvalidConfig(
"Gradient size mismatch with shape".to_string(),
));
}
let dyn_array = Array::from_shape_vec(shape, gradients.to_vec())
.map_err(|_| OptimError::InvalidConfig("Invalid shape for gradients".to_string()))?;
dyn_array
.into_dimensionality::<D>()
.map_err(|_| OptimError::InvalidConfig("Dimension conversion failed".to_string()))
}
pub fn compute_jvp(&mut self, outputid: usize, tangent: &[T]) -> Result<Vec<T>> {
if tangent.len() != self.variables.len() {
return Err(OptimError::InvalidConfig(
"Tangent vector size mismatch".to_string(),
));
}
let mut jvp_values = vec![T::zero(); self.graph.len()];
for (_var_name, &var_id) in &self.variables {
if var_id < tangent.len() {
jvp_values[var_id] = tangent[var_id];
}
}
for entry in &self.tape {
let mut output_tangent = T::zero();
for (i, &input_id) in entry.inputs.iter().enumerate() {
if i < entry.local_gradients.len() {
output_tangent =
output_tangent + entry.local_gradients[i] * jvp_values[input_id];
}
}
jvp_values[entry.output] = output_tangent;
}
Ok(vec![jvp_values[outputid]])
}
pub fn compute_vjp(&mut self, outputid: usize, cotangent: T) -> Result<Vec<T>> {
let mut vjp_values = vec![T::zero(); self.graph.len()];
vjp_values[outputid] = cotangent;
for entry in self.tape.iter().rev() {
let output_cotangent = vjp_values[entry.output];
for (i, &input_id) in entry.inputs.iter().enumerate() {
if i < entry.local_gradients.len() {
vjp_values[input_id] =
vjp_values[input_id] + output_cotangent * entry.local_gradients[i];
}
}
}
let mut result = Vec::new();
for (_var_name, &var_id) in &self.variables {
result.push(vjp_values[var_id]);
}
Ok(result)
}
pub fn register_candle_operation(
&mut self,
op_name: &str,
inputs: &[usize],
output_value: T,
local_grads: &[T],
) -> usize {
let outputid = self.graph.len();
let output_node = ADNode {
value: output_value,
gradient: vec![T::zero()],
hessian: None,
operation: Operation::Custom(op_name.to_string()),
parents: inputs.to_vec(),
id: outputid,
};
self.graph.push(output_node);
let tape_entry = TapeEntry {
operation: Operation::Custom(op_name.to_string()),
inputs: inputs.to_vec(),
output: outputid,
local_gradients: local_grads.to_vec(),
};
self.tape.push(tape_entry);
outputid
}
pub fn optimize_for_candle(&mut self) -> Result<()> {
self.fuse_operations()?;
self.eliminate_dead_code()?;
self.optimize_memory_layout()?;
Ok(())
}
fn fuse_operations(&mut self) -> Result<()> {
let mut fused_operations = Vec::new();
for i in 0..self.tape.len() {
if i + 1 < self.tape.len() {
let curr_op = &self.tape[i];
let next_op = &self.tape[i + 1];
if matches!(curr_op.operation, Operation::Add)
&& matches!(next_op.operation, Operation::Multiply)
&& next_op.inputs.contains(&curr_op.output)
{
fused_operations.push((i, i + 1, "fused_add_mul".to_string()));
}
}
}
for (start, end, fused_name) in fused_operations.iter().rev() {
let fused_entry = TapeEntry {
operation: Operation::Custom(fused_name.clone()),
inputs: self.tape[*start].inputs.clone(),
output: self.tape[*end].output,
local_gradients: vec![], };
self.tape[*start] = fused_entry;
self.tape.remove(*end);
}
Ok(())
}
fn eliminate_dead_code(&mut self) -> Result<()> {
let mut reachable = vec![false; self.graph.len()];
let mut stack = Vec::new();
for &var_id in self.variables.values() {
reachable[var_id] = true;
stack.push(var_id);
}
while let Some(nodeid) = stack.pop() {
for entry in &self.tape {
if entry.inputs.contains(&nodeid) && !reachable[entry.output] {
reachable[entry.output] = true;
stack.push(entry.output);
}
}
}
self.tape.retain(|entry| reachable[entry.output]);
Ok(())
}
fn optimize_memory_layout(&mut self) -> Result<()> {
self.tape.sort_by_key(|entry| entry.output);
let mut new_graph = Vec::new();
let mut id_mapping = HashMap::new();
for (new_id, node) in self.graph.iter().enumerate() {
id_mapping.insert(node.id, new_id);
let mut new_node = node.clone();
new_node.id = new_id;
new_graph.push(new_node);
}
for entry in &mut self.tape {
entry.output = *id_mapping.get(&entry.output).unwrap_or(&entry.output);
for input in &mut entry.inputs {
*input = *id_mapping.get(input).unwrap_or(input);
}
}
self.graph = new_graph;
Ok(())
}
pub fn set_pytorch_compatibility(&mut self, enable: bool) {
if enable {
self.context.record_gradients = true;
self.config.enable_meta_gradients = true;
}
}
pub fn enable_jax_transforms(&mut self, enable: bool) -> Result<()> {
if enable {
self.config.enable_forward_mode = true;
self.config.enable_reverse_mode = true;
self.config.gradient_checkpointing = true;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct GraphStats {
pub num_nodes: usize,
pub num_variables: usize,
pub tape_length: usize,
pub memory_usage_estimate: usize,
}
pub struct AutodiffUtils;
impl AutodiffUtils {
pub fn check_gradients<T: Float + Debug + Send + Sync + 'static>(
forward_fn: impl Fn(&[T]) -> T,
gradient_fn: impl Fn(&[T]) -> Vec<T>,
point: &[T],
epsilon: T,
) -> Result<GradientCheckResult<T>> {
let analytical_grad = gradient_fn(point);
let mut numerical_grad = vec![T::zero(); point.len()];
for i in 0..point.len() {
let mut point_plus = point.to_vec();
let mut point_minus = point.to_vec();
point_plus[i] = point_plus[i] + epsilon;
point_minus[i] = point_minus[i] - epsilon;
let f_plus = forward_fn(&point_plus);
let f_minus = forward_fn(&point_minus);
numerical_grad[i] = (f_plus - f_minus) / (scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero()) * epsilon);
}
let mut max_error = T::zero();
let mut total_error = T::zero();
for i in 0..point.len() {
let error = (analytical_grad[i] - numerical_grad[i]).abs();
let relative_error = if analytical_grad[i].abs() > scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero()) {
error / analytical_grad[i].abs()
} else {
error
};
if relative_error > max_error {
max_error = relative_error;
}
total_error = total_error + relative_error;
}
let avg_error = total_error / T::from(point.len()).expect("unwrap failed");
Ok(GradientCheckResult {
analytical_gradient: analytical_grad,
numerical_gradient: numerical_grad,
max_relative_error: max_error,
avg_relative_error: avg_error,
is_correct: max_error < scirs2_core::numeric::NumCast::from(1e-4).unwrap_or_else(|| T::zero()),
})
}
}
#[derive(Debug, Clone)]
pub struct GradientCheckResult<T: Float + Debug + Send + Sync + 'static> {
pub analytical_gradient: Vec<T>,
pub numerical_gradient: Vec<T>,
pub max_relative_error: T,
pub avg_relative_error: T,
pub is_correct: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_autodiff_config_default() {
let config = AutodiffConfig::default();
assert!(config.enable_forward_mode);
assert!(config.enable_reverse_mode);
assert_eq!(config.max_derivative_order, 2);
assert!(config.enable_hessian);
}
#[test]
fn test_autodiff_engine_creation() {
let config = AutodiffConfig::default();
let mut engine = AutodiffEngine::<f64>::new(config);
let var_id = engine.create_variable("x", 5.0);
assert_eq!(engine.graph.len(), 1);
assert_eq!(engine.graph[var_id].value, 5.0);
}
#[test]
fn test_gradient_check() {
let forward_fn = |x: &[f64]| x[0] * x[0] + 2.0 * x[0] * x[1] + x[1] * x[1];
let gradient_fn = |x: &[f64]| vec![2.0 * x[0] + 2.0 * x[1], 2.0 * x[0] + 2.0 * x[1]];
let point = [3.0, 4.0];
let result = AutodiffUtils::check_gradients(forward_fn, gradient_fn, &point, 1e-8).expect("unwrap failed");
assert!(result.is_correct);
assert!(result.max_relative_error < 1e-4);
}
#[test]
fn test_bfgs_update() {
let config = AutodiffConfig {
hessian_approximation: HessianApproximation::BFGS,
..Default::default()
};
let mut engine = AutodiffEngine::<f64>::new(config);
let grad_new = Array1::from_vec(vec![1.0, 2.0]);
let grad_old = Array1::from_vec(vec![0.5, 1.5]);
let step = Array1::from_vec(vec![0.1, 0.2]);
let result = engine.update_bfgs(&grad_new, &grad_old, &step);
assert!(result.is_ok());
}
}