use crate::parallel_gates::fused_slice_parallel_threshold;
use rayon::prelude::*;
use std::borrow::Cow;
pub fn scaled_grad(grad: &[f32], grad_scale: f32) -> Cow<'_, [f32]> {
if grad_scale == 1.0 {
Cow::Borrowed(grad)
} else {
Cow::Owned(grad.iter().map(|&g| g * grad_scale).collect())
}
}
pub fn sgd_step(param: &mut [f32], grad: &[f32], lr: f32) {
if param.len() >= fused_slice_parallel_threshold() {
param
.par_iter_mut()
.zip(grad.par_iter())
.for_each(|(p, &g)| *p -= lr * g);
} else {
for (p, &g) in param.iter_mut().zip(grad) {
*p -= lr * g;
}
}
}
pub fn sgd_momentum_step(
param: &mut [f32],
grad: &[f32],
velocity: &mut [f32],
lr: f32,
momentum: f32,
nesterov: bool,
) {
let step = |p: &mut f32, g: f32, v: &mut f32| {
*v = momentum * *v + g;
let s = if nesterov { g + momentum * *v } else { *v };
*p -= lr * s;
};
if param.len() >= fused_slice_parallel_threshold() {
param
.par_iter_mut()
.zip(grad.par_iter())
.zip(velocity.par_iter_mut())
.for_each(|((p, &g), v)| step(p, g, v));
} else {
for ((p, &g), v) in param.iter_mut().zip(grad).zip(velocity.iter_mut()) {
step(p, g, v);
}
}
}
pub fn apply_weight_decay(param: &mut [f32], lr: f32, weight_decay: f32) {
if weight_decay == 0.0 {
return;
}
let factor = 1.0 - lr * weight_decay;
if param.len() >= fused_slice_parallel_threshold() {
param.par_iter_mut().for_each(|p| *p *= factor);
} else {
for p in param.iter_mut() {
*p *= factor;
}
}
}
pub fn l2_regularized_grad(grad: &[f32], param: &[f32], weight_decay: f32) -> Vec<f32> {
grad.iter()
.zip(param)
.map(|(&g, &p)| g + weight_decay * p)
.collect()
}
#[allow(clippy::too_many_arguments)]
pub fn adam_step(
param: &mut [f32],
grad: &[f32],
m: &mut [f32],
v: &mut [f32],
lr: f32,
beta1: f32,
beta2: f32,
epsilon: f32,
t: u64,
) {
let bias_correction1 = 1.0 - beta1.powi(t as i32);
let bias_correction2 = 1.0 - beta2.powi(t as i32);
let step = |p: &mut f32, g: f32, m: &mut f32, v: &mut f32| {
*m = beta1 * *m + (1.0 - beta1) * g;
*v = beta2 * *v + (1.0 - beta2) * g * g;
let m_hat = *m / bias_correction1;
let v_hat = *v / bias_correction2;
*p -= lr * m_hat / (v_hat.sqrt() + epsilon);
};
if param.len() >= fused_slice_parallel_threshold() {
param
.par_iter_mut()
.zip(grad.par_iter())
.zip(m.par_iter_mut())
.zip(v.par_iter_mut())
.for_each(|(((p, &g), m), v)| step(p, g, m, v));
} else {
for (((p, &g), m), v) in param
.iter_mut()
.zip(grad)
.zip(m.iter_mut())
.zip(v.iter_mut())
{
step(p, g, m, v);
}
}
}
pub fn rmsprop_step(
param: &mut [f32],
grad: &[f32],
cache: &mut [f32],
rho: f32,
lr: f32,
epsilon: f32,
) {
let step = |p: &mut f32, g: f32, c: &mut f32| {
*c = rho * *c + (1.0 - rho) * g * g;
*p -= lr * g / (c.sqrt() + epsilon);
};
if param.len() >= fused_slice_parallel_threshold() {
param
.par_iter_mut()
.zip(grad.par_iter())
.zip(cache.par_iter_mut())
.for_each(|((p, &g), c)| step(p, g, c));
} else {
for ((p, &g), c) in param.iter_mut().zip(grad).zip(cache.iter_mut()) {
step(p, g, c);
}
}
}
pub fn adagrad_step(
param: &mut [f32],
grad: &[f32],
accumulator: &mut [f32],
lr: f32,
epsilon: f32,
) {
let step = |p: &mut f32, g: f32, a: &mut f32| {
*a += g * g;
*p -= lr * g / (a.sqrt() + epsilon);
};
if param.len() >= fused_slice_parallel_threshold() {
param
.par_iter_mut()
.zip(grad.par_iter())
.zip(accumulator.par_iter_mut())
.for_each(|((p, &g), a)| step(p, g, a));
} else {
for ((p, &g), a) in param.iter_mut().zip(grad).zip(accumulator.iter_mut()) {
step(p, g, a);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn scaled_grad_unit_scale_borrows_unchanged() {
let grad = vec![1.0_f32, -2.0, 3.5];
let out = scaled_grad(&grad, 1.0);
assert!(matches!(out, Cow::Borrowed(_)), "unit scale must borrow");
assert_eq!(out.as_ref(), grad.as_slice());
}
#[test]
fn scaled_grad_nonunit_scale_multiplies() {
let grad = vec![2.0_f32, -4.0, 6.0];
let out = scaled_grad(&grad, 0.5);
assert!(matches!(out, Cow::Owned(_)), "non-unit scale must own");
assert_abs_diff_eq!(out[0], 1.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[1], -2.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[2], 3.0_f32, epsilon = 1e-6);
}
#[test]
fn sgd_momentum_step_accumulates() {
let mut param = vec![1.0_f32];
let grad = vec![2.0_f32];
let mut v = vec![0.0_f32];
sgd_momentum_step(&mut param, &grad, &mut v, 0.1, 0.9, false);
assert_abs_diff_eq!(v[0], 2.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(param[0], 0.8_f32, epsilon = 1e-6);
sgd_momentum_step(&mut param, &grad, &mut v, 0.1, 0.9, false);
assert_abs_diff_eq!(v[0], 3.8_f32, epsilon = 1e-6);
assert_abs_diff_eq!(param[0], 0.42_f32, epsilon = 1e-6);
}
#[test]
fn sgd_momentum_step_nesterov() {
let mut param = vec![1.0_f32];
let grad = vec![2.0_f32];
let mut v = vec![0.0_f32];
sgd_momentum_step(&mut param, &grad, &mut v, 0.1, 0.9, true);
assert_abs_diff_eq!(param[0], 0.62_f32, epsilon = 1e-6);
}
#[test]
fn apply_weight_decay_scales_param() {
let mut param = vec![1.0_f32, 2.0];
apply_weight_decay(&mut param, 0.1, 0.5); assert_abs_diff_eq!(param[0], 0.95_f32, epsilon = 1e-6);
assert_abs_diff_eq!(param[1], 1.9_f32, epsilon = 1e-6);
let mut p2 = vec![3.0_f32];
apply_weight_decay(&mut p2, 0.1, 0.0); assert_abs_diff_eq!(p2[0], 3.0_f32, epsilon = 1e-9);
}
#[test]
fn l2_regularized_grad_adds_weight_decay_times_param() {
let grad = vec![1.0_f32, -2.0, 0.0];
let param = vec![10.0_f32, 4.0, -6.0];
let out = l2_regularized_grad(&grad, ¶m, 0.1);
assert_abs_diff_eq!(out[0], 1.0 + 1.0, epsilon = 1e-6); assert_abs_diff_eq!(out[1], -2.0 + 0.4, epsilon = 1e-6); assert_abs_diff_eq!(out[2], 0.0 - 0.6, epsilon = 1e-6); }
#[test]
fn sgd_step_small_path() {
let mut param = vec![1.0_f32, 2.0_f32];
let grad = vec![0.5_f32, -1.0_f32];
sgd_step(&mut param, &grad, 0.1);
assert_abs_diff_eq!(param[0], 0.95_f32, epsilon = 1e-6);
assert_abs_diff_eq!(param[1], 2.1_f32, epsilon = 1e-6);
}
#[test]
fn sgd_step_parallel_path() {
let n = 1024_usize;
let mut param = vec![1.0_f32; n];
let grad = vec![0.5_f32; n];
sgd_step(&mut param, &grad, 0.1);
for (i, &p) in param.iter().enumerate() {
assert!(
(p - 0.95_f32).abs() <= 1e-6,
"element {i} mismatch in parallel sgd_step: got {p}"
);
}
}
#[test]
fn sgd_step_zero_lr() {
let mut param = vec![3.0_f32, -1.5_f32];
let grad = vec![100.0_f32, -50.0_f32];
sgd_step(&mut param, &grad, 0.0);
assert_abs_diff_eq!(param[0], 3.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(param[1], -1.5_f32, epsilon = 1e-6);
}
#[test]
fn adam_step_t1_from_zero() {
let mut param = vec![0.0_f32];
let grad = vec![1.0_f32];
let mut m = vec![0.0_f32];
let mut v = vec![0.0_f32];
adam_step(&mut param, &grad, &mut m, &mut v, 0.1, 0.9, 0.999, 1e-8, 1);
assert_abs_diff_eq!(m[0], 0.1_f32, epsilon = 1e-7);
assert_abs_diff_eq!(v[0], 0.001_f32, epsilon = 1e-7);
let expected_param = -0.1_f32 / (1.0_f32 + 1e-8_f32);
assert_abs_diff_eq!(param[0], expected_param, epsilon = 1e-6);
}
#[test]
fn adam_step_t2_moment_accumulation() {
let mut param = vec![0.0_f32];
let grad = vec![1.0_f32];
let mut m = vec![0.0_f32];
let mut v = vec![0.0_f32];
adam_step(&mut param, &grad, &mut m, &mut v, 0.1, 0.9, 0.999, 1e-8, 1);
adam_step(&mut param, &grad, &mut m, &mut v, 0.1, 0.9, 0.999, 1e-8, 2);
assert_abs_diff_eq!(m[0], 0.19_f32, epsilon = 1e-6);
assert_abs_diff_eq!(v[0], 0.001999_f32, epsilon = 1e-6);
let single_update = 0.1_f32 / (1.0_f32 + 1e-8_f32);
let expected = -2.0_f32 * single_update;
assert_abs_diff_eq!(param[0], expected, epsilon = 1e-5);
}
#[test]
fn adam_step_zero_gradient() {
let mut param = vec![5.0_f32];
let grad = vec![0.0_f32];
let mut m = vec![0.0_f32];
let mut v = vec![0.0_f32];
adam_step(&mut param, &grad, &mut m, &mut v, 0.1, 0.9, 0.999, 1e-8, 1);
assert_abs_diff_eq!(m[0], 0.0_f32, epsilon = 1e-9);
assert_abs_diff_eq!(v[0], 0.0_f32, epsilon = 1e-9);
assert_abs_diff_eq!(param[0], 5.0_f32, epsilon = 1e-9);
}
#[test]
fn rmsprop_step_single_element() {
let mut param = vec![2.0_f32];
let grad = vec![1.0_f32];
let mut cache = vec![0.0_f32];
rmsprop_step(&mut param, &grad, &mut cache, 0.9, 0.01, 1e-8);
let expected_cache = 0.1_f32;
assert_abs_diff_eq!(cache[0], expected_cache, epsilon = 1e-7);
let expected_param = 2.0_f32 - 0.01_f32 / (0.1_f32.sqrt() + 1e-8_f32);
assert_abs_diff_eq!(param[0], expected_param, epsilon = 1e-6);
}
#[test]
fn rmsprop_step_two_elements() {
let mut param = vec![2.0_f32, 3.0_f32];
let grad = vec![1.0_f32, -0.5_f32];
let mut cache = vec![0.0_f32, 0.0_f32];
rmsprop_step(&mut param, &grad, &mut cache, 0.9, 0.01, 1e-8);
let expected_cache0 = 0.1_f32;
assert_abs_diff_eq!(cache[0], expected_cache0, epsilon = 1e-7);
let expected_p0 = 2.0_f32 - 0.01_f32 / (0.1_f32.sqrt() + 1e-8_f32);
assert_abs_diff_eq!(param[0], expected_p0, epsilon = 1e-6);
let expected_cache1 = 0.025_f32;
assert_abs_diff_eq!(cache[1], expected_cache1, epsilon = 1e-7);
let expected_p1 = 3.0_f32 - 0.01_f32 * (-0.5_f32) / (0.025_f32.sqrt() + 1e-8_f32);
assert_abs_diff_eq!(param[1], expected_p1, epsilon = 1e-6);
}
#[test]
fn rmsprop_step_nonzero_initial_cache() {
let mut param = vec![1.0_f32];
let grad = vec![2.0_f32];
let mut cache = vec![0.5_f32];
rmsprop_step(&mut param, &grad, &mut cache, 0.9, 0.01, 1e-8);
let expected_cache = 0.9_f32 * 0.5 + 0.1_f32 * 4.0;
assert_abs_diff_eq!(cache[0], expected_cache, epsilon = 1e-6);
let expected_param = 1.0_f32 - 0.01_f32 * 2.0_f32 / (expected_cache.sqrt() + 1e-8_f32);
assert_abs_diff_eq!(param[0], expected_param, epsilon = 1e-6);
}
#[test]
fn adagrad_step_single_element() {
let mut param = vec![3.0_f32];
let grad = vec![2.0_f32];
let mut acc = vec![0.0_f32];
adagrad_step(&mut param, &grad, &mut acc, 0.01, 1e-8);
let expected_acc = 4.0_f32;
assert_abs_diff_eq!(acc[0], expected_acc, epsilon = 1e-7);
let expected_param = 3.0_f32 - 0.01_f32 * 2.0_f32 / (4.0_f32.sqrt() + 1e-8_f32);
assert_abs_diff_eq!(param[0], expected_param, epsilon = 1e-6);
}
#[test]
fn adagrad_step_accumulation_across_steps() {
let mut param = vec![3.0_f32];
let grad = vec![2.0_f32];
let mut acc = vec![0.0_f32];
adagrad_step(&mut param, &grad, &mut acc, 0.01, 1e-8);
let param_after_step1 = param[0];
adagrad_step(&mut param, &grad, &mut acc, 0.01, 1e-8);
assert_abs_diff_eq!(acc[0], 8.0_f32, epsilon = 1e-6);
let expected_param = param_after_step1 - 0.01_f32 * 2.0_f32 / (8.0_f32.sqrt() + 1e-8_f32);
assert_abs_diff_eq!(param[0], expected_param, epsilon = 1e-6);
}
#[test]
fn adagrad_step_negative_gradient() {
let mut param = vec![1.0_f32];
let grad = vec![-0.5_f32];
let mut acc = vec![0.0_f32];
adagrad_step(&mut param, &grad, &mut acc, 0.01, 1e-8);
assert_abs_diff_eq!(acc[0], 0.25_f32, epsilon = 1e-7);
let expected_param = 1.0_f32 - 0.01_f32 * (-0.5_f32) / (0.25_f32.sqrt() + 1e-8_f32);
assert_abs_diff_eq!(param[0], expected_param, epsilon = 1e-6);
}
#[test]
fn adam_step_parallel_path() {
let n = 1024_usize; let mut param = vec![0.0_f32; n];
let grad = vec![1.0_f32; n];
let mut m = vec![0.0_f32; n];
let mut v = vec![0.0_f32; n];
adam_step(&mut param, &grad, &mut m, &mut v, 0.1, 0.9, 0.999, 1e-8, 1);
let expected_param = -0.1_f32 / (1.0_f32 + 1e-8_f32);
for i in 0..n {
assert!(
(m[i] - 0.1_f32).abs() <= 1e-7,
"element {i} m mismatch in parallel adam_step: got {}",
m[i]
);
assert!(
(v[i] - 0.001_f32).abs() <= 1e-7,
"element {i} v mismatch in parallel adam_step: got {}",
v[i]
);
assert!(
(param[i] - expected_param).abs() <= 1e-6,
"element {i} param mismatch in parallel adam_step: got {}",
param[i]
);
}
}
#[test]
fn rmsprop_step_parallel_path() {
let n = 1024_usize;
let mut param = vec![2.0_f32; n];
let grad = vec![1.0_f32; n];
let mut cache = vec![0.0_f32; n];
rmsprop_step(&mut param, &grad, &mut cache, 0.9, 0.01, 1e-8);
let expected_param = 2.0_f32 - 0.01_f32 / (0.1_f32.sqrt() + 1e-8_f32);
for i in 0..n {
assert!(
(cache[i] - 0.1_f32).abs() <= 1e-7,
"element {i} cache mismatch in parallel rmsprop_step: got {}",
cache[i]
);
assert!(
(param[i] - expected_param).abs() <= 1e-6,
"element {i} param mismatch in parallel rmsprop_step: got {}",
param[i]
);
}
}
#[test]
fn adagrad_step_parallel_path() {
let n = 1024_usize;
let mut param = vec![3.0_f32; n];
let grad = vec![2.0_f32; n];
let mut acc = vec![0.0_f32; n];
adagrad_step(&mut param, &grad, &mut acc, 0.01, 1e-8);
let expected_param = 3.0_f32 - 0.01_f32 * 2.0_f32 / (4.0_f32.sqrt() + 1e-8_f32);
for i in 0..n {
assert!(
(acc[i] - 4.0_f32).abs() <= 1e-7,
"element {i} accumulator mismatch in parallel adagrad_step: got {}",
acc[i]
);
assert!(
(param[i] - expected_param).abs() <= 1e-6,
"element {i} param mismatch in parallel adagrad_step: got {}",
param[i]
);
}
}
}