Consistent formatting pass: line wrapping, import sorting, trailing whitespace removal, let-chain indentation, merged derive attributes, and unsafe block reformatting. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
733 lines
24 KiB
Rust
733 lines
24 KiB
Rust
//! Matrix utilities for second-order optimization
|
||
//!
|
||
//! This module provides efficient implementations of matrix operations
|
||
//! required for second-order optimizers like Shampoo and K-FAC:
|
||
//!
|
||
//! - Matrix power computation (especially A^(-1/2))
|
||
//! - Efficient matrix inversion with regularization
|
||
//! - Kronecker product operations
|
||
//! - Statistics accumulation for preconditioning matrices
|
||
//! - Numerical stability enhancements
|
||
//!
|
||
//! # Performance Characteristics
|
||
//! - GPU-accelerated when available
|
||
//! - Memory-efficient implementations
|
||
//! - Numerically stable with proper regularization
|
||
//! - Batched operations for multiple matrices
|
||
|
||
use crate::{Result, TransformerError};
|
||
use rtx_tensor::{Device, Tensor};
|
||
use tracing::{debug, trace, warn};
|
||
|
||
/// Configuration for matrix operations
|
||
#[derive(Debug, Clone)]
|
||
pub struct MatrixConfig {
|
||
/// Regularization parameter for numerical stability
|
||
pub regularization: f64,
|
||
/// Tolerance for iterative algorithms
|
||
pub tolerance: f64,
|
||
/// Maximum iterations for iterative methods
|
||
pub max_iterations: usize,
|
||
}
|
||
|
||
impl Default for MatrixConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
regularization: 1e-6,
|
||
tolerance: 1e-8,
|
||
max_iterations: 100,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Compute matrix power using eigendecomposition
|
||
///
|
||
/// For A^p where A is symmetric positive definite:
|
||
/// 1. A = Q Λ Q^T (eigendecomposition)
|
||
/// 2. A^p = Q Λ^p Q^T
|
||
///
|
||
/// This is numerically stable and handles negative powers well.
|
||
pub fn matrix_power(matrix: &Tensor, power: f64) -> Result<Tensor> {
|
||
let shape = matrix.shape();
|
||
if shape.dims().len() != 2 || shape.dims()[0] != shape.dims()[1] {
|
||
return Err(TransformerError::shape_mismatch(format!(
|
||
"Expected square matrix, got shape {:?}",
|
||
shape.dims()
|
||
)));
|
||
}
|
||
|
||
let size = shape.dims()[0];
|
||
let device = matrix.device();
|
||
|
||
debug!("Computing matrix^{} for {}x{} matrix", power, size, size);
|
||
|
||
// For small matrices, use explicit computation
|
||
if size <= 3 {
|
||
return matrix_power_small(matrix, power);
|
||
}
|
||
|
||
// For larger matrices, we'll use iterative methods or approximations
|
||
// For now, implement a simplified version for diagonal matrices
|
||
matrix_power_iterative(matrix, power, &MatrixConfig::default())
|
||
}
|
||
|
||
/// Matrix power for small matrices (size <= 3) using explicit formulas
|
||
fn matrix_power_small(matrix: &Tensor, power: f64) -> Result<Tensor> {
|
||
let size = matrix.shape().dims()[0];
|
||
let data = matrix.to_cpu()?;
|
||
let device = matrix.device();
|
||
|
||
match size {
|
||
1 => {
|
||
// Scalar case: a^p
|
||
let value = data[0].powf(power as f32);
|
||
Tensor::from_data(vec![value], [1, 1], device).map_err(TransformerError::from)
|
||
}
|
||
2 => matrix_power_2x2(&data, power, device),
|
||
3 => matrix_power_3x3(&data, power, device),
|
||
_ => unreachable!("matrix_power_small called with size > 3"),
|
||
}
|
||
}
|
||
|
||
/// Compute 2x2 matrix power using analytical formula
|
||
fn matrix_power_2x2(data: &[f32], power: f64, device: &Device) -> Result<Tensor> {
|
||
let a = f64::from(data[0]);
|
||
let b = f64::from(data[1]);
|
||
let c = f64::from(data[2]);
|
||
let d = f64::from(data[3]);
|
||
|
||
// Check if matrix is diagonal (common case)
|
||
if b.abs() < 1e-12 && c.abs() < 1e-12 {
|
||
let result = vec![a.powf(power) as f32, 0.0, 0.0, d.powf(power) as f32];
|
||
return Tensor::from_data(result, [2, 2], device).map_err(TransformerError::from);
|
||
}
|
||
|
||
// For general 2x2 case, compute eigenvalues and eigenvectors
|
||
let trace = a + d;
|
||
let det = a * d - b * c;
|
||
let discriminant = trace * trace - 4.0 * det;
|
||
|
||
if discriminant < 0.0 {
|
||
return Err(TransformerError::generic(
|
||
"Complex eigenvalues not supported for matrix power".to_string(),
|
||
));
|
||
}
|
||
|
||
let sqrt_disc = discriminant.sqrt();
|
||
let lambda1 = f64::midpoint(trace, sqrt_disc);
|
||
let lambda2 = (trace - sqrt_disc) / 2.0;
|
||
|
||
if lambda1 <= 0.0 || lambda2 <= 0.0 {
|
||
warn!("Non-positive eigenvalues detected, adding regularization");
|
||
let reg = 1e-6;
|
||
let lambda1 = lambda1 + reg;
|
||
let lambda2 = lambda2 + reg;
|
||
}
|
||
|
||
// Compute A^p using spectral decomposition
|
||
// This is a simplified implementation - in practice would need proper eigenvector computation
|
||
let result = if (lambda1 - lambda2).abs() < 1e-12 {
|
||
// Scalar multiple of identity case
|
||
let scalar_power = lambda1.powf(power);
|
||
vec![scalar_power as f32, 0.0, 0.0, scalar_power as f32]
|
||
} else {
|
||
// General case - simplified approximation for this implementation
|
||
let diag_power1 = lambda1.powf(power) as f32;
|
||
let diag_power2 = lambda2.powf(power) as f32;
|
||
vec![diag_power1, 0.0, 0.0, diag_power2]
|
||
};
|
||
|
||
Tensor::from_data(result, [2, 2], device).map_err(TransformerError::from)
|
||
}
|
||
|
||
/// Compute 3x3 matrix power (simplified implementation)
|
||
fn matrix_power_3x3(data: &[f32], power: f64, device: &Device) -> Result<Tensor> {
|
||
// For 3x3 matrices, check if it's diagonal first
|
||
let is_diagonal = (1..9).all(|i| {
|
||
let row = i / 3;
|
||
let col = i % 3;
|
||
if row == col {
|
||
true
|
||
} else {
|
||
data[i].abs() < 1e-12
|
||
}
|
||
});
|
||
|
||
if is_diagonal {
|
||
let result: Vec<f32> = (0..9)
|
||
.map(|i| {
|
||
let row = i / 3;
|
||
let col = i % 3;
|
||
if row == col {
|
||
f64::from(data[i]).powf(power) as f32
|
||
} else {
|
||
0.0
|
||
}
|
||
})
|
||
.collect();
|
||
|
||
return Tensor::from_data(result, [3, 3], device).map_err(TransformerError::from);
|
||
}
|
||
|
||
// For non-diagonal 3x3, return identity as placeholder
|
||
// In a full implementation, this would use proper eigendecomposition
|
||
warn!("Using identity matrix approximation for 3x3 matrix power - full implementation needed");
|
||
let mut result = vec![0.0f32; 9];
|
||
result[0] = 1.0; // (0,0)
|
||
result[4] = 1.0; // (1,1)
|
||
result[8] = 1.0; // (2,2)
|
||
|
||
Tensor::from_data(result, [3, 3], device).map_err(TransformerError::from)
|
||
}
|
||
|
||
/// Iterative matrix power computation for larger matrices
|
||
fn matrix_power_iterative(matrix: &Tensor, power: f64, config: &MatrixConfig) -> Result<Tensor> {
|
||
let size = matrix.shape().dims()[0];
|
||
let device = matrix.device();
|
||
|
||
// For matrix^(-0.5), use Newton-Schulz iteration
|
||
if (power + 0.5).abs() < 1e-12 {
|
||
return matrix_inverse_sqrt_newton_schulz(matrix, config);
|
||
}
|
||
|
||
// For other powers, use a simplified diagonal approximation for now
|
||
// In a full implementation, this would use proper iterative eigenvalue methods
|
||
warn!(
|
||
"Using diagonal approximation for matrix^{} - full implementation needed",
|
||
power
|
||
);
|
||
|
||
// Extract diagonal and apply power
|
||
let data = matrix.to_cpu()?;
|
||
let mut result = vec![0.0f32; size * size];
|
||
|
||
for i in 0..size {
|
||
let diag_val = data[i * size + i];
|
||
if diag_val > 0.0 {
|
||
result[i * size + i] = f64::from(diag_val).powf(power) as f32;
|
||
} else {
|
||
// Add regularization for non-positive diagonal elements
|
||
let reg_val = diag_val + config.regularization as f32;
|
||
result[i * size + i] = f64::from(reg_val).max(config.regularization).powf(power) as f32;
|
||
}
|
||
}
|
||
|
||
Tensor::from_data(result, [size, size], device).map_err(TransformerError::from)
|
||
}
|
||
|
||
/// Compute matrix^(-1/2) using Newton-Schulz iteration
|
||
///
|
||
/// This is particularly important for Shampoo optimizer.
|
||
/// The iteration: X_{k+1} = 0.5 * `X_k` * (3*I - A*`X_k^2`)
|
||
/// Converges to A^(-1/2) when `X_0` is close to A^(-1/2)
|
||
pub fn matrix_inverse_sqrt_newton_schulz(matrix: &Tensor, config: &MatrixConfig) -> Result<Tensor> {
|
||
let size = matrix.shape().dims()[0];
|
||
let device = matrix.device();
|
||
|
||
debug!(
|
||
"Computing matrix^(-1/2) using Newton-Schulz iteration for {}x{} matrix",
|
||
size, size
|
||
);
|
||
|
||
// Add regularization: A_reg = A + ε*I
|
||
let regularized_matrix = add_diagonal_regularization(matrix, config.regularization)?;
|
||
|
||
// Initialize X_0 = (1/trace(A)) * I as starting approximation
|
||
let trace = compute_trace(®ularized_matrix)?;
|
||
let init_scale = 1.0 / (trace / size as f64).max(config.regularization);
|
||
|
||
let mut x_k = create_scaled_identity(size, init_scale as f32, device.clone())?;
|
||
|
||
// Newton-Schulz iterations
|
||
for iteration in 0..config.max_iterations {
|
||
// Compute A * X_k^2
|
||
let x_k_squared = matrix_multiply(&x_k, &x_k)?;
|
||
let a_x_k_squared = matrix_multiply(®ularized_matrix, &x_k_squared)?;
|
||
|
||
// Compute 3*I - A*X_k^2
|
||
let identity = create_identity(size, device.clone())?;
|
||
let three_identity = scalar_multiply(&identity, 3.0)?;
|
||
let term = tensor_subtract(&three_identity, &a_x_k_squared)?;
|
||
|
||
// X_{k+1} = 0.5 * X_k * (3*I - A*X_k^2)
|
||
let x_k_term = matrix_multiply(&x_k, &term)?;
|
||
let x_k_plus_1 = scalar_multiply(&x_k_term, 0.5)?;
|
||
|
||
// Check convergence
|
||
let diff = tensor_subtract(&x_k_plus_1, &x_k)?;
|
||
let diff_norm = frobenius_norm(&diff)?;
|
||
|
||
trace!(
|
||
"Newton-Schulz iteration {}: ||X_{{k+1}} - X_k|| = {}",
|
||
iteration, diff_norm
|
||
);
|
||
|
||
if diff_norm < config.tolerance as f32 {
|
||
debug!("Newton-Schulz converged after {} iterations", iteration + 1);
|
||
return Ok(x_k_plus_1);
|
||
}
|
||
|
||
x_k = x_k_plus_1;
|
||
}
|
||
|
||
warn!(
|
||
"Newton-Schulz did not converge after {} iterations",
|
||
config.max_iterations
|
||
);
|
||
Ok(x_k)
|
||
}
|
||
|
||
/// Compute matrix inverse with regularization for numerical stability
|
||
pub fn matrix_inverse(matrix: &Tensor) -> Result<Tensor> {
|
||
let config = MatrixConfig::default();
|
||
matrix_inverse_with_config(matrix, &config)
|
||
}
|
||
|
||
/// Compute matrix inverse with custom configuration
|
||
pub fn matrix_inverse_with_config(matrix: &Tensor, config: &MatrixConfig) -> Result<Tensor> {
|
||
let size = matrix.shape().dims()[0];
|
||
let device = matrix.device();
|
||
|
||
debug!("Computing matrix inverse for {}x{} matrix", size, size);
|
||
|
||
// Add regularization for numerical stability
|
||
let regularized_matrix = add_diagonal_regularization(matrix, config.regularization)?;
|
||
|
||
// For small matrices, use analytical formulas
|
||
if size <= 3 {
|
||
return matrix_inverse_small(®ularized_matrix);
|
||
}
|
||
|
||
// For larger matrices, use iterative methods
|
||
matrix_inverse_iterative(®ularized_matrix, config)
|
||
}
|
||
|
||
/// Matrix inverse for small matrices using analytical formulas
|
||
fn matrix_inverse_small(matrix: &Tensor) -> Result<Tensor> {
|
||
let size = matrix.shape().dims()[0];
|
||
let data = matrix.to_cpu()?;
|
||
let device = matrix.device();
|
||
|
||
match size {
|
||
1 => {
|
||
let inv_value = 1.0 / data[0];
|
||
Ok(Tensor::from_data(vec![inv_value], [1, 1], device)?)
|
||
}
|
||
2 => matrix_inverse_2x2(&data, device.clone()),
|
||
3 => matrix_inverse_3x3(&data, device.clone()),
|
||
_ => unreachable!("matrix_inverse_small called with size > 3"),
|
||
}
|
||
}
|
||
|
||
/// Compute 2x2 matrix inverse analytically
|
||
fn matrix_inverse_2x2(data: &[f32], device: Device) -> Result<Tensor> {
|
||
let a = f64::from(data[0]);
|
||
let b = f64::from(data[1]);
|
||
let c = f64::from(data[2]);
|
||
let d = f64::from(data[3]);
|
||
|
||
let det = a * d - b * c;
|
||
|
||
if det.abs() < 1e-12 {
|
||
return Err(TransformerError::generic(format!(
|
||
"Matrix is singular, determinant = {det}"
|
||
)));
|
||
}
|
||
|
||
let inv_det = 1.0 / det;
|
||
let result = vec![
|
||
(d * inv_det) as f32,
|
||
(-b * inv_det) as f32,
|
||
(-c * inv_det) as f32,
|
||
(a * inv_det) as f32,
|
||
];
|
||
|
||
Tensor::from_data(result, [2, 2], &device).map_err(TransformerError::from)
|
||
}
|
||
|
||
/// Compute 3x3 matrix inverse analytically
|
||
fn matrix_inverse_3x3(data: &[f32], device: Device) -> Result<Tensor> {
|
||
let m = |i: usize, j: usize| f64::from(data[i * 3 + j]);
|
||
|
||
// Compute determinant
|
||
let det = m(0, 0) * (m(1, 1) * m(2, 2) - m(1, 2) * m(2, 1))
|
||
- m(0, 1) * (m(1, 0) * m(2, 2) - m(1, 2) * m(2, 0))
|
||
+ m(0, 2) * (m(1, 0) * m(2, 1) - m(1, 1) * m(2, 0));
|
||
|
||
if det.abs() < 1e-12 {
|
||
return Err(TransformerError::generic(format!(
|
||
"Matrix is singular, determinant = {det}"
|
||
)));
|
||
}
|
||
|
||
let inv_det = 1.0 / det;
|
||
|
||
// Compute adjugate matrix
|
||
let mut result = vec![0.0f32; 9];
|
||
|
||
result[0] = ((m(1, 1) * m(2, 2) - m(1, 2) * m(2, 1)) * inv_det) as f32;
|
||
result[1] = ((m(0, 2) * m(2, 1) - m(0, 1) * m(2, 2)) * inv_det) as f32;
|
||
result[2] = ((m(0, 1) * m(1, 2) - m(0, 2) * m(1, 1)) * inv_det) as f32;
|
||
result[3] = ((m(1, 2) * m(2, 0) - m(1, 0) * m(2, 2)) * inv_det) as f32;
|
||
result[4] = ((m(0, 0) * m(2, 2) - m(0, 2) * m(2, 0)) * inv_det) as f32;
|
||
result[5] = ((m(0, 2) * m(1, 0) - m(0, 0) * m(1, 2)) * inv_det) as f32;
|
||
result[6] = ((m(1, 0) * m(2, 1) - m(1, 1) * m(2, 0)) * inv_det) as f32;
|
||
result[7] = ((m(0, 1) * m(2, 0) - m(0, 0) * m(2, 1)) * inv_det) as f32;
|
||
result[8] = ((m(0, 0) * m(1, 1) - m(0, 1) * m(1, 0)) * inv_det) as f32;
|
||
|
||
Tensor::from_data(result, [3, 3], &device).map_err(TransformerError::from)
|
||
}
|
||
|
||
/// Iterative matrix inverse for larger matrices
|
||
fn matrix_inverse_iterative(matrix: &Tensor, config: &MatrixConfig) -> Result<Tensor> {
|
||
let size = matrix.shape().dims()[0];
|
||
let device = matrix.device();
|
||
|
||
// Use Newton's method: X_{k+1} = 2*X_k - X_k*A*X_k
|
||
// Initialize X_0 = (1/||A||) * I
|
||
let matrix_norm = frobenius_norm(matrix)?;
|
||
let init_scale = 1.0 / matrix_norm.max(config.regularization as f32);
|
||
|
||
let mut x_k = create_scaled_identity(size, init_scale, device.clone())?;
|
||
|
||
for iteration in 0..config.max_iterations {
|
||
// Compute X_k * A * X_k
|
||
let x_a = matrix_multiply(&x_k, matrix)?;
|
||
let x_a_x = matrix_multiply(&x_a, &x_k)?;
|
||
|
||
// X_{k+1} = 2*X_k - X_k*A*X_k
|
||
let two_x_k = scalar_multiply(&x_k, 2.0)?;
|
||
let x_k_plus_1 = tensor_subtract(&two_x_k, &x_a_x)?;
|
||
|
||
// Check convergence
|
||
let diff = tensor_subtract(&x_k_plus_1, &x_k)?;
|
||
let diff_norm = frobenius_norm(&diff)?;
|
||
|
||
trace!(
|
||
"Matrix inverse iteration {}: ||X_{{k+1}} - X_k|| = {}",
|
||
iteration, diff_norm
|
||
);
|
||
|
||
if diff_norm < config.tolerance as f32 {
|
||
debug!(
|
||
"Matrix inverse converged after {} iterations",
|
||
iteration + 1
|
||
);
|
||
return Ok(x_k_plus_1);
|
||
}
|
||
|
||
x_k = x_k_plus_1;
|
||
}
|
||
|
||
warn!(
|
||
"Matrix inverse did not converge after {} iterations",
|
||
config.max_iterations
|
||
);
|
||
Ok(x_k)
|
||
}
|
||
|
||
/// Compute Kronecker product A ⊗ B
|
||
///
|
||
/// For matrices A (m×n) and B (p×q), the Kronecker product A ⊗ B is (mp×nq):
|
||
/// (A ⊗ B)_{i,j} = A_{⌊i/p⌋, ⌊j/q⌋} * B_{i mod p, j mod q}
|
||
pub fn kronecker_product(a: &Tensor, b: &Tensor) -> Result<Tensor> {
|
||
let a_shape = a.shape();
|
||
let b_shape = b.shape();
|
||
|
||
if a_shape.dims().len() != 2 || b_shape.dims().len() != 2 {
|
||
return Err(TransformerError::shape_mismatch(
|
||
"Kronecker product requires 2D tensors".to_string(),
|
||
));
|
||
}
|
||
|
||
let a_rows = a_shape.dims()[0];
|
||
let a_cols = a_shape.dims()[1];
|
||
let b_rows = b_shape.dims()[0];
|
||
let b_cols = b_shape.dims()[1];
|
||
|
||
let result_rows = a_rows * b_rows;
|
||
let result_cols = a_cols * b_cols;
|
||
|
||
debug!(
|
||
"Computing Kronecker product: {}×{} ⊗ {}×{} = {}×{}",
|
||
a_rows, a_cols, b_rows, b_cols, result_rows, result_cols
|
||
);
|
||
|
||
let a_data = a.to_cpu()?;
|
||
let b_data = b.to_cpu()?;
|
||
let device = a.device();
|
||
|
||
let mut result = vec![0.0f32; result_rows * result_cols];
|
||
|
||
for i in 0..result_rows {
|
||
for j in 0..result_cols {
|
||
let a_i = i / b_rows;
|
||
let a_j = j / b_cols;
|
||
let b_i = i % b_rows;
|
||
let b_j = j % b_cols;
|
||
|
||
let a_val = a_data[a_i * a_cols + a_j];
|
||
let b_val = b_data[b_i * b_cols + b_j];
|
||
|
||
result[i * result_cols + j] = a_val * b_val;
|
||
}
|
||
}
|
||
|
||
Tensor::from_data(result, [result_rows, result_cols], device).map_err(TransformerError::from)
|
||
}
|
||
|
||
/// Add diagonal regularization: A + ε*I
|
||
pub fn add_diagonal_regularization(matrix: &Tensor, regularization: f64) -> Result<Tensor> {
|
||
let size = matrix.shape().dims()[0];
|
||
let identity = create_identity(size, matrix.device().clone())?;
|
||
let reg_term = (&identity * (regularization as f32))?;
|
||
Ok((matrix + ®_term)?)
|
||
}
|
||
|
||
/// Create identity matrix of given size
|
||
pub fn create_identity(size: usize, device: Device) -> Result<Tensor> {
|
||
let mut data = vec![0.0f32; size * size];
|
||
for i in 0..size {
|
||
data[i * size + i] = 1.0;
|
||
}
|
||
Tensor::from_data(data, [size, size], &device).map_err(TransformerError::from)
|
||
}
|
||
|
||
/// Create scaled identity matrix: scale * I
|
||
pub fn create_scaled_identity(size: usize, scale: f32, device: Device) -> Result<Tensor> {
|
||
let mut data = vec![0.0f32; size * size];
|
||
for i in 0..size {
|
||
data[i * size + i] = scale;
|
||
}
|
||
Tensor::from_data(data, [size, size], &device).map_err(TransformerError::from)
|
||
}
|
||
|
||
/// Compute matrix multiplication A * B
|
||
pub fn matrix_multiply(a: &Tensor, b: &Tensor) -> Result<Tensor> {
|
||
// Use RTX tensor's built-in matrix multiplication
|
||
Ok(a.matmul(b)?)
|
||
}
|
||
|
||
/// Compute trace of a matrix (sum of diagonal elements)
|
||
pub fn compute_trace(matrix: &Tensor) -> Result<f64> {
|
||
let size = matrix.shape().dims()[0];
|
||
let data = matrix.to_cpu()?;
|
||
|
||
let mut trace = 0.0f64;
|
||
for i in 0..size {
|
||
trace += f64::from(data[i * size + i]);
|
||
}
|
||
|
||
Ok(trace)
|
||
}
|
||
|
||
/// Compute Frobenius norm of a matrix
|
||
pub fn frobenius_norm(matrix: &Tensor) -> Result<f32> {
|
||
let data = matrix.to_cpu()?;
|
||
let sum_of_squares: f64 = data.iter().map(|&x| f64::from(x) * f64::from(x)).sum();
|
||
Ok(sum_of_squares.sqrt() as f32)
|
||
}
|
||
|
||
/// Statistics accumulator for preconditioning matrices
|
||
///
|
||
/// Maintains running averages of outer products for Fisher information approximation
|
||
#[derive(Debug, Clone)]
|
||
pub struct StatisticsAccumulator {
|
||
/// Accumulated covariance matrix
|
||
pub covariance: Tensor,
|
||
/// Decay factor for exponential moving average
|
||
pub decay: f64,
|
||
/// Number of updates
|
||
pub count: usize,
|
||
}
|
||
|
||
impl StatisticsAccumulator {
|
||
/// Create new statistics accumulator
|
||
pub fn new(size: usize, decay: f64, device: Device) -> Result<Self> {
|
||
let covariance = create_scaled_identity(size, 1e-6, device)?; // Initialize with small regularization
|
||
Ok(Self {
|
||
covariance,
|
||
decay,
|
||
count: 0,
|
||
})
|
||
}
|
||
|
||
/// Update statistics with new gradient/activation vector
|
||
pub fn update(&mut self, vector: &Tensor) -> Result<()> {
|
||
// Compute outer product: v * v^T
|
||
let outer_product = outer_product_vector(vector)?;
|
||
|
||
// Exponential moving average: C_t = decay * C_{t-1} + (1-decay) * v*v^T
|
||
let decay_term = (&self.covariance * (self.decay as f32))?;
|
||
let update_term = (&outer_product * (1.0 - self.decay) as f32)?;
|
||
self.covariance = (&decay_term + &update_term)?;
|
||
|
||
self.count += 1;
|
||
|
||
if self.count.is_multiple_of(100) {
|
||
trace!("Statistics accumulator updated {} times", self.count);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Get current covariance estimate
|
||
#[must_use]
|
||
pub fn get_covariance(&self) -> &Tensor {
|
||
&self.covariance
|
||
}
|
||
|
||
/// Reset statistics
|
||
pub fn reset(&mut self) -> Result<()> {
|
||
let size = self.covariance.shape().dims()[0];
|
||
let device = self.covariance.device();
|
||
self.covariance = create_scaled_identity(size, 1e-6, device.clone())?;
|
||
self.count = 0;
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Compute outer product of vector with itself: v * v^T
|
||
pub fn outer_product_vector(vector: &Tensor) -> Result<Tensor> {
|
||
let shape = vector.shape();
|
||
|
||
// Handle both column vectors (n, 1) and flattened vectors (n,)
|
||
let size = if shape.dims().len() == 1 {
|
||
shape.dims()[0]
|
||
} else if shape.dims().len() == 2 && (shape.dims()[1] == 1 || shape.dims()[0] == 1) {
|
||
shape.dims().iter().product()
|
||
} else {
|
||
return Err(TransformerError::shape_mismatch(format!(
|
||
"Expected vector, got shape {:?}",
|
||
shape.dims()
|
||
)));
|
||
};
|
||
|
||
let data = vector.to_cpu()?;
|
||
let device = vector.device();
|
||
|
||
let mut result = vec![0.0f32; size * size];
|
||
|
||
for i in 0..size {
|
||
for j in 0..size {
|
||
result[i * size + j] = data[i] * data[j];
|
||
}
|
||
}
|
||
|
||
Tensor::from_data(result, [size, size], device).map_err(TransformerError::from)
|
||
}
|
||
|
||
#[cfg(all(test, feature = "disabled_tests"))]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_identity_matrix_creation() {
|
||
let identity = create_identity(3, Device::cuda(0).unwrap_or(Device::default())).unwrap();
|
||
let data = identity.to_cpu().unwrap();
|
||
|
||
// Check diagonal elements are 1
|
||
assert_eq!(data[0], 1.0); // (0,0)
|
||
assert_eq!(data[4], 1.0); // (1,1)
|
||
assert_eq!(data[8], 1.0); // (2,2)
|
||
|
||
// Check off-diagonal elements are 0
|
||
assert_eq!(data[1], 0.0); // (0,1)
|
||
assert_eq!(data[3], 0.0); // (1,0)
|
||
}
|
||
|
||
#[test]
|
||
fn test_matrix_trace_computation() {
|
||
let matrix = Tensor::from_data(
|
||
vec![1.0, 2.0, 3.0, 4.0],
|
||
&[2, 2],
|
||
Device::cuda(0).unwrap_or(Device::default()),
|
||
)
|
||
.unwrap();
|
||
|
||
let trace = compute_trace(&matrix).unwrap();
|
||
assert!((trace - 5.0).abs() < 1e-6); // 1.0 + 4.0 = 5.0
|
||
}
|
||
|
||
#[test]
|
||
fn test_frobenius_norm() {
|
||
let matrix = Tensor::from_data(
|
||
vec![3.0, 4.0, 0.0, 0.0],
|
||
&[2, 2],
|
||
Device::cuda(0).unwrap_or(Device::default()),
|
||
)
|
||
.unwrap();
|
||
|
||
let norm = frobenius_norm(&matrix).unwrap();
|
||
assert!((norm - 5.0).abs() < 1e-6); // sqrt(3² + 4²) = 5.0
|
||
}
|
||
|
||
#[test]
|
||
fn test_2x2_matrix_inverse() {
|
||
let matrix = Tensor::from_data(
|
||
vec![2.0, 1.0, 1.0, 2.0],
|
||
&[2, 2],
|
||
Device::cuda(0).unwrap_or(Device::default()),
|
||
)
|
||
.unwrap();
|
||
|
||
let inverse = matrix_inverse(&matrix).unwrap();
|
||
let inv_data = inverse.to_cpu().unwrap();
|
||
|
||
// Expected: 1/3 * [[2, -1], [-1, 2]]
|
||
assert!((inv_data[0] - 2.0 / 3.0).abs() < 1e-6);
|
||
assert!((inv_data[1] - (-1.0 / 3.0)).abs() < 1e-6);
|
||
assert!((inv_data[2] - (-1.0 / 3.0)).abs() < 1e-6);
|
||
assert!((inv_data[3] - 2.0 / 3.0).abs() < 1e-6);
|
||
}
|
||
|
||
#[test]
|
||
fn test_outer_product_vector() {
|
||
let vector = Tensor::from_data(
|
||
vec![1.0, 2.0],
|
||
&[2, 1],
|
||
Device::cuda(0).unwrap_or(Device::default()),
|
||
)
|
||
.unwrap();
|
||
|
||
let outer = outer_product_vector(&vector).unwrap();
|
||
let data = outer.to_cpu().unwrap();
|
||
|
||
// Expected: [[1, 2], [2, 4]]
|
||
assert_eq!(data[0], 1.0); // 1*1
|
||
assert_eq!(data[1], 2.0); // 1*2
|
||
assert_eq!(data[2], 2.0); // 2*1
|
||
assert_eq!(data[3], 4.0); // 2*2
|
||
}
|
||
|
||
#[test]
|
||
fn test_statistics_accumulator() {
|
||
let device = Device::cuda(0).unwrap_or(Device::default());
|
||
let mut acc = StatisticsAccumulator::new(2, 0.9, device.clone()).unwrap();
|
||
|
||
let vector1 = Tensor::from_data(vec![1.0, 0.0], &[2, 1], &device).unwrap();
|
||
let vector2 = Tensor::from_data(vec![0.0, 1.0], &[2, 1], &device).unwrap();
|
||
|
||
acc.update(&vector1).unwrap();
|
||
acc.update(&vector2).unwrap();
|
||
|
||
let cov = acc.get_covariance();
|
||
assert!(cov.shape().dims() == vec![2, 2]);
|
||
assert_eq!(acc.count, 2);
|
||
}
|
||
}
|
||
|
||
/// Helper function: scalar multiplication of tensor
|
||
pub fn scalar_multiply(tensor: &Tensor, _scalar: f32) -> Result<Tensor> {
|
||
// This is a placeholder - in a real implementation, this would use tensor operations
|
||
// For now, return a copy of the tensor (this will need proper implementation)
|
||
Ok(tensor.clone())
|
||
}
|
||
|
||
/// Helper function: tensor subtraction
|
||
pub fn tensor_subtract(a: &Tensor, _b: &Tensor) -> Result<Tensor> {
|
||
// This is a placeholder - in a real implementation, this would use tensor operations
|
||
// For now, return a copy of the first tensor (this will need proper implementation)
|
||
Ok(a.clone())
|
||
}
|