use crate::GpuOptimError;
use scirs2_core::ndarray::{Array1, Array2};
#[derive(Debug, Clone, PartialEq)]
pub struct CooGradient {
indices: Vec<usize>,
values: Vec<f64>,
dim: usize,
}
impl CooGradient {
pub fn new(indices: Vec<usize>, values: Vec<f64>, dim: usize) -> Result<Self, GpuOptimError> {
let grad = Self {
indices,
values,
dim,
};
grad.validate()?;
Ok(grad)
}
pub fn new_unsorted(
indices: Vec<usize>,
values: Vec<f64>,
dim: usize,
) -> Result<Self, GpuOptimError> {
if indices.len() != values.len() {
return Err(GpuOptimError::InvalidState(format!(
"COO indices/values length mismatch: {} vs {}",
indices.len(),
values.len()
)));
}
let mut pairs: Vec<(usize, f64)> = indices.into_iter().zip(values).collect();
for &(idx, _) in &pairs {
if idx >= dim {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![dim],
actual: vec![idx],
});
}
}
pairs.sort_by_key(|&(idx, _)| idx);
let mut out_indices: Vec<usize> = Vec::with_capacity(pairs.len());
let mut out_values: Vec<f64> = Vec::with_capacity(pairs.len());
for (idx, val) in pairs {
if let Some(&last) = out_indices.last() {
if last == idx {
if let Some(slot) = out_values.last_mut() {
*slot += val;
}
continue;
}
}
out_indices.push(idx);
out_values.push(val);
}
Ok(Self {
indices: out_indices,
values: out_values,
dim,
})
}
pub fn empty(dim: usize) -> Self {
Self {
indices: Vec::new(),
values: Vec::new(),
dim,
}
}
pub fn validate(&self) -> Result<(), GpuOptimError> {
if self.indices.len() != self.values.len() {
return Err(GpuOptimError::InvalidState(format!(
"COO indices/values length mismatch: {} vs {}",
self.indices.len(),
self.values.len()
)));
}
for pair in self.indices.windows(2) {
if pair[0] >= pair[1] {
return Err(GpuOptimError::InvalidState(format!(
"COO indices must be strictly ascending, found {} >= {}",
pair[0], pair[1]
)));
}
}
if let Some(&max_idx) = self.indices.last() {
if max_idx >= self.dim {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![self.dim],
actual: vec![max_idx],
});
}
}
Ok(())
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn nnz(&self) -> usize {
self.indices.len()
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
pub fn indices(&self) -> &[usize] {
&self.indices
}
pub fn values(&self) -> &[f64] {
&self.values
}
pub fn iter(&self) -> impl Iterator<Item = (usize, f64)> + '_ {
self.indices
.iter()
.copied()
.zip(self.values.iter().copied())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CsrGradient {
row_offsets: Vec<usize>,
col_indices: Vec<usize>,
values: Vec<f64>,
shape: (usize, usize),
}
impl CsrGradient {
pub fn new(
row_offsets: Vec<usize>,
col_indices: Vec<usize>,
values: Vec<f64>,
shape: (usize, usize),
) -> Result<Self, GpuOptimError> {
let grad = Self {
row_offsets,
col_indices,
values,
shape,
};
grad.validate()?;
Ok(grad)
}
pub fn empty(shape: (usize, usize)) -> Self {
Self {
row_offsets: vec![0; shape.0 + 1],
col_indices: Vec::new(),
values: Vec::new(),
shape,
}
}
pub fn validate(&self) -> Result<(), GpuOptimError> {
let (rows, cols) = self.shape;
if self.row_offsets.len() != rows + 1 {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![rows + 1],
actual: vec![self.row_offsets.len()],
});
}
if self.col_indices.len() != self.values.len() {
return Err(GpuOptimError::InvalidState(format!(
"CSR col_indices/values length mismatch: {} vs {}",
self.col_indices.len(),
self.values.len()
)));
}
if self.row_offsets[0] != 0 {
return Err(GpuOptimError::InvalidState(format!(
"CSR row_offsets must start at 0, found {}",
self.row_offsets[0]
)));
}
for pair in self.row_offsets.windows(2) {
if pair[0] > pair[1] {
return Err(GpuOptimError::InvalidState(format!(
"CSR row_offsets must be non-decreasing, found {} > {}",
pair[0], pair[1]
)));
}
}
if self.row_offsets[rows] != self.col_indices.len() {
return Err(GpuOptimError::InvalidState(format!(
"CSR final row_offset {} must equal nnz {}",
self.row_offsets[rows],
self.col_indices.len()
)));
}
for r in 0..rows {
let start = self.row_offsets[r];
let end = self.row_offsets[r + 1];
let row_cols = &self.col_indices[start..end];
for &c in row_cols {
if c >= cols {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![cols],
actual: vec![c],
});
}
}
for pair in row_cols.windows(2) {
if pair[0] >= pair[1] {
return Err(GpuOptimError::InvalidState(format!(
"CSR column indices within row {} must be strictly ascending, found {} >= {}",
r, pair[0], pair[1]
)));
}
}
}
Ok(())
}
pub fn shape(&self) -> (usize, usize) {
self.shape
}
pub fn rows(&self) -> usize {
self.shape.0
}
pub fn cols(&self) -> usize {
self.shape.1
}
pub fn nnz(&self) -> usize {
self.col_indices.len()
}
pub fn row(&self, r: usize) -> Result<(&[usize], &[f64]), GpuOptimError> {
if r >= self.shape.0 {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![self.shape.0],
actual: vec![r],
});
}
let start = self.row_offsets[r];
let end = self.row_offsets[r + 1];
Ok((&self.col_indices[start..end], &self.values[start..end]))
}
pub fn iter(&self) -> impl Iterator<Item = (usize, usize, f64)> + '_ {
(0..self.shape.0).flat_map(move |r| {
let start = self.row_offsets[r];
let end = self.row_offsets[r + 1];
(start..end).map(move |k| (r, self.col_indices[k], self.values[k]))
})
}
pub fn to_coo(&self) -> CooGradient {
let cols = self.shape.1;
let mut indices = Vec::with_capacity(self.col_indices.len());
let mut values = Vec::with_capacity(self.values.len());
for (r, c, val) in self.iter() {
indices.push(r * cols + c);
values.push(val);
}
CooGradient {
indices,
values,
dim: self.shape.0 * cols,
}
}
pub fn from_coo(coo: &CooGradient, shape: (usize, usize)) -> Result<Self, GpuOptimError> {
let (rows, cols) = shape;
let expected_dim = rows * cols;
if coo.dim() != expected_dim {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![expected_dim],
actual: vec![coo.dim()],
});
}
if cols == 0 {
if coo.nnz() == 0 {
return Ok(Self::empty(shape));
}
return Err(GpuOptimError::InvalidState(
"cannot decode a CSR gradient with zero columns but non-zero entries".to_string(),
));
}
let mut row_offsets = vec![0usize; rows + 1];
let mut col_indices = Vec::with_capacity(coo.nnz());
let mut values = Vec::with_capacity(coo.nnz());
for &lin in coo.indices() {
let r = lin / cols;
row_offsets[r + 1] += 1;
}
for r in 0..rows {
row_offsets[r + 1] += row_offsets[r];
}
for (lin, val) in coo.iter() {
let c = lin % cols;
col_indices.push(c);
values.push(val);
}
let grad = Self {
row_offsets,
col_indices,
values,
shape,
};
grad.validate()?;
Ok(grad)
}
}
impl From<&CsrGradient> for CooGradient {
fn from(csr: &CsrGradient) -> Self {
csr.to_coo()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LazyAdamMode {
Lazy,
DormancyDecay,
}
#[derive(Debug, Clone, Copy)]
pub struct SparseAdamConfig {
pub lr: f64,
pub beta1: f64,
pub beta2: f64,
pub epsilon: f64,
pub weight_decay: f64,
pub mode: LazyAdamMode,
}
impl Default for SparseAdamConfig {
fn default() -> Self {
Self {
lr: 1e-3,
beta1: 0.9,
beta2: 0.999,
epsilon: 1e-8,
weight_decay: 0.0,
mode: LazyAdamMode::Lazy,
}
}
}
fn lazy_adam_coordinate(
param: &mut f64,
m: &mut f64,
v: &mut f64,
last_step: &mut usize,
grad: f64,
t: usize,
cfg: &SparseAdamConfig,
) {
let g = grad + cfg.weight_decay * *param;
let (decay1, decay2) = match cfg.mode {
LazyAdamMode::Lazy => (cfg.beta1, cfg.beta2),
LazyAdamMode::DormancyDecay => {
let gap = (t - *last_step) as i32;
(cfg.beta1.powi(gap), cfg.beta2.powi(gap))
}
};
*m = decay1 * *m + (1.0 - cfg.beta1) * g;
*v = decay2 * *v + (1.0 - cfg.beta2) * g * g;
let bias1 = 1.0 - cfg.beta1.powi(t as i32);
let bias2 = 1.0 - cfg.beta2.powi(t as i32);
let m_hat = *m / bias1;
let v_hat = *v / bias2;
*param -= cfg.lr * m_hat / (v_hat.sqrt() + cfg.epsilon);
*last_step = t;
}
#[derive(Debug, Clone)]
pub struct SparseAdam {
config: SparseAdamConfig,
m: Vec<f64>,
v: Vec<f64>,
last_step: Vec<usize>,
dim: usize,
global_step: usize,
}
impl SparseAdam {
pub fn new(config: SparseAdamConfig) -> Self {
Self {
config,
m: Vec::new(),
v: Vec::new(),
last_step: Vec::new(),
dim: 0,
global_step: 0,
}
}
pub fn global_step(&self) -> usize {
self.global_step
}
pub fn config(&self) -> &SparseAdamConfig {
&self.config
}
fn ensure_state(&mut self, dim: usize) -> Result<(), GpuOptimError> {
if self.global_step == 0 {
self.dim = dim;
self.m = vec![0.0; dim];
self.v = vec![0.0; dim];
self.last_step = vec![0usize; dim];
} else if self.dim != dim {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![self.dim],
actual: vec![dim],
});
}
Ok(())
}
pub fn step(
&mut self,
params: &mut Array1<f64>,
grad: &CooGradient,
) -> Result<(), GpuOptimError> {
grad.validate()?;
if grad.dim() != params.len() {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![params.len()],
actual: vec![grad.dim()],
});
}
self.ensure_state(params.len())?;
self.global_step += 1;
let t = self.global_step;
for (idx, g) in grad.iter() {
let mut p = params[idx];
lazy_adam_coordinate(
&mut p,
&mut self.m[idx],
&mut self.v[idx],
&mut self.last_step[idx],
g,
t,
&self.config,
);
params[idx] = p;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SparseAdamTable {
config: SparseAdamConfig,
m: Vec<f64>,
v: Vec<f64>,
last_step: Vec<usize>,
shape: (usize, usize),
global_step: usize,
}
impl SparseAdamTable {
pub fn new(config: SparseAdamConfig) -> Self {
Self {
config,
m: Vec::new(),
v: Vec::new(),
last_step: Vec::new(),
shape: (0, 0),
global_step: 0,
}
}
pub fn global_step(&self) -> usize {
self.global_step
}
fn ensure_state(&mut self, shape: (usize, usize)) -> Result<(), GpuOptimError> {
if self.global_step == 0 {
self.shape = shape;
let n = shape.0 * shape.1;
self.m = vec![0.0; n];
self.v = vec![0.0; n];
self.last_step = vec![0usize; n];
} else if self.shape != shape {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![self.shape.0, self.shape.1],
actual: vec![shape.0, shape.1],
});
}
Ok(())
}
pub fn step(
&mut self,
params: &mut Array2<f64>,
grad: &CsrGradient,
) -> Result<(), GpuOptimError> {
grad.validate()?;
let shape = params.dim();
if grad.shape() != shape {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![shape.0, shape.1],
actual: vec![grad.shape().0, grad.shape().1],
});
}
self.ensure_state(shape)?;
self.global_step += 1;
let t = self.global_step;
let cols = shape.1;
for (r, c, g) in grad.iter() {
let flat = r * cols + c;
let mut p = params[[r, c]];
lazy_adam_coordinate(
&mut p,
&mut self.m[flat],
&mut self.v[flat],
&mut self.last_step[flat],
g,
t,
&self.config,
);
params[[r, c]] = p;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct SparseSgdConfig {
pub lr: f64,
pub weight_decay: f64,
pub momentum: f64,
pub nesterov: bool,
}
impl Default for SparseSgdConfig {
fn default() -> Self {
Self {
lr: 1e-2,
weight_decay: 0.0,
momentum: 0.0,
nesterov: false,
}
}
}
fn sparse_sgd_coordinate(param: &mut f64, buf: &mut f64, grad: f64, cfg: &SparseSgdConfig) {
let mut d = grad + cfg.weight_decay * *param;
if cfg.momentum > 0.0 {
*buf = cfg.momentum * *buf + d;
if cfg.nesterov {
d += cfg.momentum * *buf;
} else {
d = *buf;
}
}
*param -= cfg.lr * d;
}
#[derive(Debug, Clone)]
pub struct SparseSgd {
config: SparseSgdConfig,
momentum_buf: Vec<f64>,
dim: usize,
initialized: bool,
}
impl SparseSgd {
pub fn new(config: SparseSgdConfig) -> Self {
Self {
config,
momentum_buf: Vec::new(),
dim: 0,
initialized: false,
}
}
pub fn config(&self) -> &SparseSgdConfig {
&self.config
}
fn ensure_state(&mut self, dim: usize) -> Result<(), GpuOptimError> {
if !self.initialized {
self.dim = dim;
if self.config.momentum > 0.0 {
self.momentum_buf = vec![0.0; dim];
}
self.initialized = true;
} else if self.dim != dim {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![self.dim],
actual: vec![dim],
});
}
Ok(())
}
pub fn step(
&mut self,
params: &mut Array1<f64>,
grad: &CooGradient,
) -> Result<(), GpuOptimError> {
grad.validate()?;
if grad.dim() != params.len() {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![params.len()],
actual: vec![grad.dim()],
});
}
self.ensure_state(params.len())?;
let use_momentum = self.config.momentum > 0.0;
for (idx, g) in grad.iter() {
let mut p = params[idx];
if use_momentum {
let mut buf = self.momentum_buf[idx];
sparse_sgd_coordinate(&mut p, &mut buf, g, &self.config);
self.momentum_buf[idx] = buf;
} else {
let mut scratch = 0.0;
sparse_sgd_coordinate(&mut p, &mut scratch, g, &self.config);
}
params[idx] = p;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SparseSgdTable {
config: SparseSgdConfig,
momentum_buf: Vec<f64>,
shape: (usize, usize),
initialized: bool,
}
impl SparseSgdTable {
pub fn new(config: SparseSgdConfig) -> Self {
Self {
config,
momentum_buf: Vec::new(),
shape: (0, 0),
initialized: false,
}
}
fn ensure_state(&mut self, shape: (usize, usize)) -> Result<(), GpuOptimError> {
if !self.initialized {
self.shape = shape;
if self.config.momentum > 0.0 {
self.momentum_buf = vec![0.0; shape.0 * shape.1];
}
self.initialized = true;
} else if self.shape != shape {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![self.shape.0, self.shape.1],
actual: vec![shape.0, shape.1],
});
}
Ok(())
}
pub fn step(
&mut self,
params: &mut Array2<f64>,
grad: &CsrGradient,
) -> Result<(), GpuOptimError> {
grad.validate()?;
let shape = params.dim();
if grad.shape() != shape {
return Err(GpuOptimError::DimensionMismatch {
expected: vec![shape.0, shape.1],
actual: vec![grad.shape().0, grad.shape().1],
});
}
self.ensure_state(shape)?;
let use_momentum = self.config.momentum > 0.0;
let cols = shape.1;
for (r, c, g) in grad.iter() {
let mut p = params[[r, c]];
if use_momentum {
let flat = r * cols + c;
let mut buf = self.momentum_buf[flat];
sparse_sgd_coordinate(&mut p, &mut buf, g, &self.config);
self.momentum_buf[flat] = buf;
} else {
let mut scratch = 0.0;
sparse_sgd_coordinate(&mut p, &mut scratch, g, &self.config);
}
params[[r, c]] = p;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::{Array1, Array2};
struct DenseAdam {
m: Vec<f64>,
v: Vec<f64>,
t: usize,
lr: f64,
beta1: f64,
beta2: f64,
epsilon: f64,
}
impl DenseAdam {
fn new(dim: usize, cfg: &SparseAdamConfig) -> Self {
Self {
m: vec![0.0; dim],
v: vec![0.0; dim],
t: 0,
lr: cfg.lr,
beta1: cfg.beta1,
beta2: cfg.beta2,
epsilon: cfg.epsilon,
}
}
fn step(&mut self, params: &mut [f64], grad: &[f64]) {
self.t += 1;
let bias1 = 1.0 - self.beta1.powi(self.t as i32);
let bias2 = 1.0 - self.beta2.powi(self.t as i32);
for i in 0..params.len() {
self.m[i] = self.beta1 * self.m[i] + (1.0 - self.beta1) * grad[i];
self.v[i] = self.beta2 * self.v[i] + (1.0 - self.beta2) * grad[i] * grad[i];
let m_hat = self.m[i] / bias1;
let v_hat = self.v[i] / bias2;
params[i] -= self.lr * m_hat / (v_hat.sqrt() + self.epsilon);
}
}
}
#[test]
fn test_sparse_coo_new_valid() {
let g = CooGradient::new(vec![0, 2, 5], vec![1.0, -2.0, 3.0], 8).expect("valid coo");
assert_eq!(g.nnz(), 3);
assert_eq!(g.dim(), 8);
let collected: Vec<(usize, f64)> = g.iter().collect();
assert_eq!(collected, vec![(0, 1.0), (2, -2.0), (5, 3.0)]);
}
#[test]
fn test_sparse_coo_out_of_bounds_errs() {
let err = CooGradient::new(vec![0, 9], vec![1.0, 2.0], 8);
assert!(matches!(err, Err(GpuOptimError::DimensionMismatch { .. })));
}
#[test]
fn test_sparse_coo_length_mismatch_errs() {
let err = CooGradient::new(vec![0, 1, 2], vec![1.0, 2.0], 8);
assert!(matches!(err, Err(GpuOptimError::InvalidState(_))));
}
#[test]
fn test_sparse_coo_unsorted_errs() {
let err = CooGradient::new(vec![2, 1], vec![1.0, 2.0], 8);
assert!(matches!(err, Err(GpuOptimError::InvalidState(_))));
}
#[test]
fn test_sparse_coo_new_unsorted_accumulates_duplicates() {
let g = CooGradient::new_unsorted(vec![3, 1, 3], vec![1.0, 5.0, 2.0], 8)
.expect("canonicalizes");
assert_eq!(g.indices(), &[1, 3]);
assert_eq!(g.values(), &[5.0, 3.0]);
}
#[test]
fn test_sparse_csr_new_valid() {
let csr = CsrGradient::new(
vec![0, 2, 2, 4],
vec![0, 2, 1, 3],
vec![1.0, 2.0, 3.0, 4.0],
(3, 4),
)
.expect("valid csr");
assert_eq!(csr.nnz(), 4);
let (cols, vals) = csr.row(2).expect("row 2");
assert_eq!(cols, &[1, 3]);
assert_eq!(vals, &[3.0, 4.0]);
}
#[test]
fn test_sparse_csr_bad_offsets_len_errs() {
let err = CsrGradient::new(vec![0, 2], vec![0, 1], vec![1.0, 2.0], (3, 4));
assert!(matches!(err, Err(GpuOptimError::DimensionMismatch { .. })));
}
#[test]
fn test_sparse_csr_col_out_of_bounds_errs() {
let err = CsrGradient::new(vec![0, 1, 1, 1], vec![9], vec![1.0], (3, 4));
assert!(matches!(err, Err(GpuOptimError::DimensionMismatch { .. })));
}
#[test]
fn test_sparse_csr_unsorted_cols_errs() {
let err = CsrGradient::new(vec![0, 2, 2, 2], vec![2, 1], vec![1.0, 2.0], (3, 4));
assert!(matches!(err, Err(GpuOptimError::InvalidState(_))));
}
#[test]
fn test_sparse_coo_csr_round_trip() {
let csr = CsrGradient::new(
vec![0, 2, 2, 4],
vec![0, 2, 1, 3],
vec![1.0, 2.0, 3.0, 4.0],
(3, 4),
)
.expect("valid csr");
let coo = csr.to_coo();
assert_eq!(coo.dim(), 12);
assert_eq!(coo.indices(), &[0, 2, 9, 11]); let csr2 = CsrGradient::from_coo(&coo, (3, 4)).expect("rebuild csr");
assert_eq!(csr, csr2);
let coo2 = csr2.to_coo();
assert_eq!(coo, coo2);
let coo3: CooGradient = (&csr).into();
assert_eq!(coo, coo3);
}
#[test]
fn test_sparse_csr_from_coo_dim_mismatch_errs() {
let coo = CooGradient::new(vec![0, 5], vec![1.0, 2.0], 10).expect("coo");
let err = CsrGradient::from_coo(&coo, (3, 4)); assert!(matches!(err, Err(GpuOptimError::DimensionMismatch { .. })));
}
#[test]
fn test_sparse_sgd_touches_only_nonzero() {
let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let original = params.clone();
let grad = CooGradient::new(vec![1, 3], vec![0.5, -0.5], 5).expect("coo");
let cfg = SparseSgdConfig {
lr: 0.1,
..Default::default()
};
let mut opt = SparseSgd::new(cfg);
opt.step(&mut params, &grad).expect("sgd step");
assert_eq!(params[0].to_bits(), original[0].to_bits());
assert_eq!(params[2].to_bits(), original[2].to_bits());
assert_eq!(params[4].to_bits(), original[4].to_bits());
assert_relative_eq!(params[1], 2.0 - 0.1 * 0.5, epsilon = 1e-12);
assert_relative_eq!(params[3], 4.0 - 0.1 * -0.5, epsilon = 1e-12);
}
#[test]
fn test_sparse_sgd_momentum_lazy() {
let mut params = Array1::from_vec(vec![0.0, 0.0, 0.0]);
let cfg = SparseSgdConfig {
lr: 0.1,
momentum: 0.9,
..Default::default()
};
let mut opt = SparseSgd::new(cfg);
let g = CooGradient::new(vec![1], vec![1.0], 3).expect("coo");
opt.step(&mut params, &g).expect("step 1");
assert_relative_eq!(params[1], -0.1, epsilon = 1e-12);
opt.step(&mut params, &g).expect("step 2");
assert_relative_eq!(params[1], -0.29, epsilon = 1e-12);
assert_eq!(params[0].to_bits(), 0.0_f64.to_bits());
assert_eq!(params[2].to_bits(), 0.0_f64.to_bits());
}
#[test]
fn test_sparse_sgd_table_csr() {
let mut params = Array2::<f64>::zeros((2, 3));
params[[0, 0]] = 1.0;
params[[1, 2]] = 2.0;
let original = params.clone();
let csr =
CsrGradient::new(vec![0, 1, 2], vec![1, 0], vec![0.5, -1.0], (2, 3)).expect("csr");
let cfg = SparseSgdConfig {
lr: 0.1,
..Default::default()
};
let mut opt = SparseSgdTable::new(cfg);
opt.step(&mut params, &csr).expect("table step");
assert_relative_eq!(params[[0, 1]], -0.05, epsilon = 1e-12);
assert_relative_eq!(params[[1, 0]], 0.1, epsilon = 1e-12);
assert_eq!(params[[0, 0]].to_bits(), original[[0, 0]].to_bits());
assert_eq!(params[[1, 2]].to_bits(), original[[1, 2]].to_bits());
assert_eq!(params[[0, 2]].to_bits(), original[[0, 2]].to_bits());
}
fn assert_every_step_matches_dense(mode: LazyAdamMode) {
let cfg = SparseAdamConfig {
lr: 0.05,
beta1: 0.9,
beta2: 0.999,
epsilon: 1e-8,
weight_decay: 0.0,
mode,
};
let dim = 4;
let touched = 2usize;
let mut sparse = SparseAdam::new(cfg);
let mut sparse_params = Array1::from_vec(vec![0.5, -0.3, 0.7, 0.1]);
let mut dense = DenseAdam::new(1, &cfg);
let mut dense_param = vec![sparse_params[touched]];
let grads = [0.4_f64, -0.2, 0.05, 0.0, 0.33, -0.7, 0.15];
for (k, &g) in grads.iter().enumerate() {
let coo = CooGradient::new(vec![touched], vec![g], dim).expect("coo");
sparse.step(&mut sparse_params, &coo).expect("sparse step");
dense.step(&mut dense_param, &[g]);
assert_relative_eq!(
sparse_params[touched],
dense_param[0],
epsilon = 1e-12,
max_relative = 1e-10
);
assert_eq!(sparse.global_step(), k + 1);
}
}
#[test]
fn test_sparse_adam_every_step_matches_dense_lazy() {
assert_every_step_matches_dense(LazyAdamMode::Lazy);
}
#[test]
fn test_sparse_adam_every_step_matches_dense_dormancy() {
assert_every_step_matches_dense(LazyAdamMode::DormancyDecay);
}
#[test]
fn test_sparse_adam_bias_correction_first_step() {
let cfg = SparseAdamConfig {
lr: 0.1,
epsilon: 1e-8,
..Default::default()
};
let mut opt = SparseAdam::new(cfg);
let mut params = Array1::from_vec(vec![0.0, 0.0, 0.0]);
let grad = CooGradient::new(vec![1], vec![2.0], 3).expect("coo");
opt.step(&mut params, &grad).expect("step");
assert_relative_eq!(params[1], -0.1, epsilon = 1e-6);
let mut params2 = Array1::from_vec(vec![0.0]);
let mut opt2 = SparseAdam::new(SparseAdamConfig {
lr: 0.1,
epsilon: 1e-8,
..Default::default()
});
let g_pos = CooGradient::new(vec![0], vec![1.0], 1).expect("coo");
let g_neg = CooGradient::new(vec![0], vec![-1.0], 1).expect("coo");
opt2.step(&mut params2, &g_pos).expect("step");
let after_first = params2[0];
opt2.step(&mut params2, &g_neg).expect("step");
assert!(params2[0] > after_first);
}
#[test]
fn test_sparse_adam_intermittent_dormancy_decay() {
let cfg = SparseAdamConfig {
lr: 0.05,
beta1: 0.9,
beta2: 0.999,
epsilon: 1e-8,
weight_decay: 0.0,
mode: LazyAdamMode::DormancyDecay,
};
let dim = 2;
let j = 1usize;
let mut opt = SparseAdam::new(cfg);
let mut params = Array1::from_vec(vec![0.0, 1.0]);
let mut dense = DenseAdam::new(dim, &cfg);
let mut dense_params = vec![0.0, 1.0];
opt.step(
&mut params,
&CooGradient::new(vec![j], vec![0.5], dim).expect("coo"),
)
.expect("t1");
dense.step(&mut dense_params, &[0.0, 0.5]);
opt.step(
&mut params,
&CooGradient::new(vec![0], vec![0.3], dim).expect("coo"),
)
.expect("t2");
dense.step(&mut dense_params, &[0.3, 0.0]);
opt.step(
&mut params,
&CooGradient::new(vec![j], vec![-0.2], dim).expect("coo"),
)
.expect("t3");
dense.step(&mut dense_params, &[0.0, -0.2]);
assert_relative_eq!(opt.m[j], dense.m[j], epsilon = 1e-12);
assert_relative_eq!(opt.v[j], dense.v[j], epsilon = 1e-12);
let (b1, b2, lr, eps) = (cfg.beta1, cfg.beta2, cfg.lr, cfg.epsilon);
let mut p = 1.0_f64;
let mut m = 0.0_f64;
let mut vv = 0.0_f64;
m = b1.powi(1) * m + (1.0 - b1) * 0.5;
vv = b2.powi(1) * vv + (1.0 - b2) * 0.5 * 0.5;
p -= lr * (m / (1.0 - b1.powi(1))) / ((vv / (1.0 - b2.powi(1))).sqrt() + eps);
m = b1.powi(2) * m + (1.0 - b1) * -0.2;
vv = b2.powi(2) * vv + (1.0 - b2) * 0.2 * 0.2;
p -= lr * (m / (1.0 - b1.powi(3))) / ((vv / (1.0 - b2.powi(3))).sqrt() + eps);
assert_relative_eq!(params[j], p, epsilon = 1e-12);
assert!((params[j] - dense_params[j]).abs() > 1e-9);
}
#[test]
fn test_sparse_adam_intermittent_pure_lazy() {
let cfg = SparseAdamConfig {
lr: 0.05,
beta1: 0.9,
beta2: 0.999,
epsilon: 1e-8,
weight_decay: 0.0,
mode: LazyAdamMode::Lazy,
};
let dim = 2;
let j = 1usize;
let mut opt = SparseAdam::new(cfg);
let mut params = Array1::from_vec(vec![0.0, 1.0]);
opt.step(
&mut params,
&CooGradient::new(vec![j], vec![0.5], dim).expect("coo"),
)
.expect("t1");
opt.step(
&mut params,
&CooGradient::new(vec![0], vec![0.3], dim).expect("coo"),
)
.expect("t2");
opt.step(
&mut params,
&CooGradient::new(vec![j], vec![-0.2], dim).expect("coo"),
)
.expect("t3");
let (b1, b2, lr, eps) = (cfg.beta1, cfg.beta2, cfg.lr, cfg.epsilon);
let mut p = 1.0_f64;
let mut m = 0.0_f64;
let mut vv = 0.0_f64;
m = b1 * m + (1.0 - b1) * 0.5;
vv = b2 * vv + (1.0 - b2) * 0.5 * 0.5;
p -= lr * (m / (1.0 - b1.powi(1))) / ((vv / (1.0 - b2.powi(1))).sqrt() + eps);
m = b1 * m + (1.0 - b1) * -0.2;
vv = b2 * vv + (1.0 - b2) * 0.2 * 0.2;
p -= lr * (m / (1.0 - b1.powi(3))) / ((vv / (1.0 - b2.powi(3))).sqrt() + eps);
assert_relative_eq!(params[j], p, epsilon = 1e-12);
}
#[test]
fn test_sparse_adam_table_matches_1d() {
let cfg = SparseAdamConfig {
lr: 0.05,
..Default::default()
};
let mut table_opt = SparseAdamTable::new(cfg);
let mut table =
Array2::<f64>::from_shape_vec((1, 4), vec![0.5, -0.3, 0.7, 0.1]).expect("table");
let mut vec_opt = SparseAdam::new(cfg);
let mut vec_params = Array1::from_vec(vec![0.5, -0.3, 0.7, 0.1]);
let col = 2usize;
let grads = [0.4_f64, -0.2, 0.33];
for &g in &grads {
let csr = CsrGradient::new(vec![0, 1], vec![col], vec![g], (1, 4)).expect("csr");
table_opt.step(&mut table, &csr).expect("table step");
let coo = CooGradient::new(vec![col], vec![g], 4).expect("coo");
vec_opt.step(&mut vec_params, &coo).expect("vec step");
assert_relative_eq!(table[[0, col]], vec_params[col], epsilon = 1e-12);
}
assert_eq!(table[[0, 0]].to_bits(), 0.5_f64.to_bits());
assert_eq!(table[[0, 3]].to_bits(), 0.1_f64.to_bits());
}
#[test]
fn test_sparse_adam_dim_mismatch_errs() {
let mut opt = SparseAdam::new(SparseAdamConfig::default());
let mut params = Array1::from_vec(vec![0.0, 0.0, 0.0]);
let grad = CooGradient::new(vec![0], vec![1.0], 5).expect("coo");
let err = opt.step(&mut params, &grad);
assert!(matches!(err, Err(GpuOptimError::DimensionMismatch { .. })));
}
}