use super::RowAdagrad;
use candle_core::{DType, Device, Tensor, Var};
fn approx(a: f32, b: f32, tol: f32) -> bool {
(a - b).abs() <= tol
}
#[test]
fn one_row_adagrad_step_matches_the_closed_form() {
let dev = Device::Cpu;
let var =
Var::from_tensor(&Tensor::from_vec(vec![1f32, 2., 3., 4., 5., 6.], (3, 2), &dev).unwrap())
.unwrap();
let grad = Tensor::from_vec(vec![1f32, -1., 0., 0., 2., 2.], (3, 2), &dev).unwrap();
let mut opt = RowAdagrad::new(3, 0.1, &dev).unwrap();
opt.step(&var, &grad).unwrap();
let p = var.as_tensor().to_vec2::<f32>().unwrap();
assert!(approx(p[0][0], 0.9, 1e-6) && approx(p[0][1], 2.1, 1e-6));
assert!(approx(p[2][0], 4.9, 1e-6) && approx(p[2][1], 5.9, 1e-6));
let acc = opt.accumulator().to_vec1::<f32>().unwrap();
assert!(approx(acc[0], 1.0, 1e-6) && approx(acc[2], 4.0, 1e-6));
opt.step(&var, &grad).unwrap();
let p = var.as_tensor().to_vec2::<f32>().unwrap();
assert!(approx(p[0][0], 0.9 - 0.1 / 2f32.sqrt(), 1e-6));
assert!(approx(p[2][1], 5.9 - 0.1 * 2.0 / 8f32.sqrt(), 1e-6));
}
#[test]
fn rows_with_zero_gradient_are_left_unchanged_and_keep_a_zero_accumulator() {
let dev = Device::Cpu;
let var = Var::from_tensor(
&Tensor::from_vec(vec![1f32, 2., 3.3333, 4.25, 5., 6.], (3, 2), &dev).unwrap(),
)
.unwrap();
let before = var.as_tensor().to_vec2::<f32>().unwrap();
let grad = Tensor::from_vec(vec![1f32, -1., 0., 0., 2., 2.], (3, 2), &dev).unwrap();
let mut opt = RowAdagrad::new(3, 0.1, &dev).unwrap();
opt.step(&var, &grad).unwrap();
opt.step(&var, &grad).unwrap();
let after = var.as_tensor().to_vec2::<f32>().unwrap();
assert_eq!(after[1], before[1], "bit-identical untouched row");
assert_eq!(opt.accumulator().to_vec1::<f32>().unwrap()[1], 0.0);
assert_ne!(after[0], before[0]);
}
#[test]
fn the_dense_gradient_of_index_select_sums_duplicate_rows_like_a_coalesced_sparse_update() {
let dev = Device::Cpu;
let var = Var::from_tensor(&Tensor::ones((6, 2), DType::F32, &dev).unwrap()).unwrap();
let idx = Tensor::from_vec(vec![2u32, 2, 5], 3, &dev).unwrap();
let w = Tensor::from_vec(vec![1f32, 1., 2., 2., 3., 3.], (3, 2), &dev).unwrap();
let loss = var
.as_tensor()
.index_select(&idx, 0)
.unwrap()
.mul(&w)
.unwrap()
.sum_all()
.unwrap();
let grads = loss.backward().unwrap();
let g = grads
.get(&var)
.expect("dense gradient on the table")
.to_vec2::<f32>()
.unwrap();
assert_eq!(g.len(), 6);
assert_eq!(g[2], vec![3.0, 3.0], "row 2 appears twice: 1 + 2");
assert_eq!(g[5], vec![3.0, 3.0]);
for r in [0, 1, 3, 4] {
assert_eq!(g[r], vec![0.0, 0.0], "untouched row {r} is exactly zero");
}
}
#[test]
fn the_accumulator_does_not_retain_the_gradients_autograd_graph() {
let dev = Device::Cpu;
let var =
Var::from_tensor(&Tensor::from_vec(vec![1f32, 2., 3., 4., 5., 6.], (3, 2), &dev).unwrap())
.unwrap();
let source = Var::from_tensor(
&Tensor::from_vec(vec![0.5f32, -1., 0., 0., 2., 1.], (3, 2), &dev).unwrap(),
)
.unwrap();
let g = source.as_tensor().affine(2.0, 0.0).unwrap();
assert!(
g.sum_all()
.unwrap()
.backward()
.unwrap()
.get(&source)
.is_some(),
"premise: the fake gradient is graph-attached"
);
let mut opt = RowAdagrad::new(3, 0.1, &dev).unwrap();
opt.step(&var, &g).unwrap();
let reach = opt.accumulator().sum_all().unwrap().backward().unwrap();
assert!(
reach.get(&source).is_none(),
"the accumulator retains the gradient's graph"
);
}
#[test]
fn a_row_and_its_bias_share_the_accumulator_and_a_mask_pins_the_row_alone() {
let dev = Device::Cpu;
let row =
Var::from_tensor(&Tensor::from_vec(vec![1f32, 2., 3., 4., 5., 6.], (3, 2), &dev).unwrap())
.unwrap();
let bias = Var::from_tensor(&Tensor::from_vec(vec![0f32, 0., 0.], 3, &dev).unwrap()).unwrap();
let g_row = Tensor::from_vec(vec![1f32, -1., 0., 0., 2., 2.], (3, 2), &dev).unwrap();
let g_bias = Tensor::from_vec(vec![1f32, 0., 3.], 3, &dev).unwrap();
let mask = Tensor::from_vec(vec![1f32, 1., 0.], (3, 1), &dev).unwrap(); let mut opt = RowAdagrad::new(3, 0.1, &dev).unwrap();
opt.step_with_bias(&row, &bias, &g_row, &g_bias, Some(&mask), 0.5)
.unwrap();
let p = row.as_tensor().to_vec2::<f32>().unwrap();
let b = bias.as_tensor().to_vec1::<f32>().unwrap();
assert!(approx(p[0][0], 0.5 - 0.1, 1e-6) && approx(p[0][1], 1.0 + 0.1, 1e-6));
assert!(approx(b[0], -0.1, 1e-6));
assert_eq!(p[1], vec![3., 4.]);
assert_eq!(b[1], 0.0);
assert_eq!(p[2], vec![5., 6.]);
assert!(approx(b[2], -0.1, 1e-6));
let acc = opt.accumulator().to_vec1::<f32>().unwrap();
assert!(approx(acc[0], 1.0, 1e-6) && acc[1] == 0.0 && approx(acc[2], 9.0, 1e-6));
}
#[test]
fn a_bias_only_step_matches_the_closed_form() {
let dev = Device::Cpu;
let bias = Var::from_tensor(&Tensor::from_vec(vec![1f32, 2., 3.], 3, &dev).unwrap()).unwrap();
let grad = Tensor::from_vec(vec![2f32, 0., -1.], 3, &dev).unwrap();
let mut opt = RowAdagrad::new(3, 0.1, &dev).unwrap();
opt.step_bias(&bias, &grad).unwrap();
let b = bias.as_tensor().to_vec1::<f32>().unwrap();
assert!(approx(b[0], 0.9, 1e-6), "{b:?}");
assert!(approx(b[1], 2.0, 1e-6), "{b:?}");
assert!(approx(b[2], 3.1, 1e-6), "{b:?}");
let acc = opt.accumulator().to_vec1::<f32>().unwrap();
assert!(approx(acc[0], 4.0, 1e-6) && approx(acc[1], 0.0, 1e-6) && approx(acc[2], 1.0, 1e-6));
}