use super::Embedding;
use crate::autograd::{self, Tensor};
const FD_EPS: f32 = 1e-3;
const TOL: f32 = 2e-2;
fn coeff(n: usize) -> Vec<f32> {
(0..n).map(|i| 0.29 + 0.17 * (i as f32)).collect()
}
fn scalar_loss(output: &Tensor, c: &[f32]) -> Tensor {
let ct = Tensor::new(c, output.shape());
output.mul(&ct).sum()
}
fn perturbed_loss(
w_data: &[f32],
vocab: usize,
hidden: usize,
ids: &[u32],
flat_idx: usize,
delta: f32,
c: &[f32],
) -> f32 {
autograd::no_grad(|| {
let mut wd = w_data.to_vec();
wd[flat_idx] += delta;
let mut emb = Embedding::new(vocab, hidden);
emb.set_weight(Tensor::new(&wd, &[vocab, hidden]));
let y = emb.forward(ids);
scalar_loss(&y, c).item()
})
}
#[test]
fn embedding_backward_scatter_add_gradcheck() {
autograd::clear_graph();
let vocab = 6usize;
let hidden = 4usize;
let ids = [2u32, 5, 0, 2];
let w_data: Vec<f32> = (0..vocab * hidden)
.map(|i| 0.1 + 0.013 * (i as f32) - 0.002 * ((i * i) as f32))
.collect();
let mut emb = Embedding::new(vocab, hidden);
emb.set_weight(Tensor::new(&w_data, &[vocab, hidden]).requires_grad());
let wid = emb.weight().id();
let y = emb.forward(&ids);
let c = coeff(y.numel());
let loss = scalar_loss(&y, &c);
loss.backward();
let grad = autograd::get_grad(wid)
.expect("Embedding weight received NO gradient — autograd graph severed");
assert_eq!(grad.shape(), &[vocab, hidden]);
assert!(grad.data().iter().all(|v| v.is_finite()));
assert!(grad.data().iter().any(|&v| v.abs() > 1e-9));
for i in 0..w_data.len() {
let num = (perturbed_loss(&w_data, vocab, hidden, &ids, i, FD_EPS, &c)
- perturbed_loss(&w_data, vocab, hidden, &ids, i, -FD_EPS, &c))
/ (2.0 * FD_EPS);
let denom = grad.data()[i].abs().max(num.abs()).max(1.0);
let rel = (grad.data()[i] - num).abs() / denom;
assert!(
rel < TOL,
"Embedding dW[{i}]: analytic {} != finite-diff {num} (rel {rel})",
grad.data()[i]
);
}
for &unused in &[1usize, 3, 4] {
for j in 0..hidden {
assert!(
grad.data()[unused * hidden + j].abs() < 1e-9,
"Embedding: unused row {unused} got nonzero grad"
);
}
}
}
#[test]
fn embedding_backward_is_additive_for_repeated_index() {
autograd::clear_graph();
let vocab = 5usize;
let hidden = 3usize;
let ids = [4u32, 1, 4];
let w_data: Vec<f32> = (0..vocab * hidden).map(|i| 0.01 * (i as f32)).collect();
let mut emb = Embedding::new(vocab, hidden);
emb.set_weight(Tensor::new(&w_data, &[vocab, hidden]).requires_grad());
let wid = emb.weight().id();
let y = emb.forward(&ids);
let c = coeff(y.numel()); let loss = scalar_loss(&y, &c);
loss.backward();
let grad = autograd::get_grad(wid).expect("severed");
let g = grad.data();
for j in 0..hidden {
let c_pos0 = c[0 * hidden + j];
let c_pos2 = c[2 * hidden + j];
let expect = c_pos0 + c_pos2;
let got = g[4 * hidden + j];
assert!(
(got - expect).abs() < 1e-5,
"Embedding scatter must ADD: dW[4][{j}] = {got}, expected sum {expect} (overwrite would give {c_pos2})"
);
assert!(got > c_pos2 + 1e-6 && got > c_pos0 + 1e-6);
}
for j in 0..hidden {
let expect = c[1 * hidden + j];
assert!((g[1 * hidden + j] - expect).abs() < 1e-5);
}
}