#![cfg(any(target_os = "macos", target_os = "ios"))]
use rlx_ir::op::{Activation, ReduceOp};
use rlx_ir::{DType, Graph, NodeId, Op, Shape};
use rlx_runtime::{Device, Session};
fn scalar_sum(g: &mut Graph, x: NodeId, shape: &Shape) -> NodeId {
let rank = shape.rank();
let axes: Vec<usize> = (0..rank).collect();
g.add_node(
Op::Reduce {
op: ReduceOp::Sum,
axes,
keep_dim: false,
},
vec![x],
Shape::from_dims(&[], DType::F32),
)
}
fn grad_parity(
name: &str,
bwd: &Graph,
params: &[(&str, Vec<f32>)],
inputs: &[(&str, &[f32])],
grad_out_index: usize,
) {
if !rlx_runtime::is_available(Device::Ane) {
eprintln!("skip: Device::Ane not available");
return;
}
let cpu_g = rlx_opt::rlx_autodiff::decompose_backward_ops_except(bwd.clone(), &[]);
let mut feeds: Vec<(&str, &[f32])> = inputs.to_vec();
let seed = [1.0f32];
feeds.push(("d_output", &seed));
let run = |device: Device, graph: &Graph| -> Vec<f32> {
let mut c = Session::new(device).compile(graph.clone());
for (n, d) in params {
c.set_param(n, d);
}
c.run(&feeds).remove(grad_out_index)
};
let ane = run(Device::Ane, bwd);
let cpu = run(Device::Cpu, &cpu_g);
assert_eq!(ane.len(), cpu.len(), "{name}: grad length mismatch");
assert!(
ane.iter().all(|v| v.is_finite()),
"{name}: non-finite ANE grad: {ane:?}"
);
let (mut dot, mut na, mut nc) = (0.0f32, 0.0f32, 0.0f32);
for (&a, &c) in ane.iter().zip(&cpu) {
dot += a * c;
na += a * a;
nc += c * c;
}
let cosine = if na > 0.0 && nc > 0.0 {
dot / (na.sqrt() * nc.sqrt())
} else {
1.0
};
assert!(
cosine > 0.999,
"{name}: ANE vs CPU grad diverged (cosine={cosine})\n ane={ane:?}\n cpu={cpu:?}"
);
eprintln!("OK {name}: cosine={cosine:.5}");
}
#[test]
fn grad_layer_norm() {
let (rows, h) = (2usize, 4usize);
let mut g = Graph::new("ln");
let x = g.input("x", Shape::new(&[rows, h], DType::F32));
let gamma = g.param("gamma", Shape::new(&[h], DType::F32));
let beta = g.param("beta", Shape::new(&[h], DType::F32));
let y = g.layer_norm(x, gamma, beta, -1, 1e-5, Shape::new(&[rows, h], DType::F32));
let loss = scalar_sum(&mut g, y, &Shape::new(&[rows, h], DType::F32));
g.set_outputs(vec![loss]);
let bwd = rlx_opt::grad_with_loss(&g, &[gamma]);
let x_data: Vec<f32> = (0..rows * h).map(|i| i as f32 * 0.2 - 0.4).collect();
grad_parity(
"layer_norm dgamma",
&bwd,
&[("gamma", vec![1.0; h]), ("beta", vec![0.0; h])],
&[("x", &x_data)],
1,
);
}
#[test]
fn grad_group_norm() {
let (n, c, hh, w) = (1usize, 4usize, 2usize, 2usize);
let mut g = Graph::new("gn");
let x = g.input("x", Shape::new(&[n, c, hh, w], DType::F32));
let gamma = g.param("gamma", Shape::new(&[c], DType::F32));
let beta = g.param("beta", Shape::new(&[c], DType::F32));
let y = g.group_norm(x, gamma, beta, 2, 1e-5);
let loss = scalar_sum(&mut g, y, &Shape::new(&[n, c, hh, w], DType::F32));
g.set_outputs(vec![loss]);
let bwd = rlx_opt::grad_with_loss(&g, &[gamma]);
let x_data: Vec<f32> = (0..n * c * hh * w).map(|i| i as f32 * 0.1 - 0.5).collect();
grad_parity(
"group_norm dgamma",
&bwd,
&[("gamma", vec![1.0; c]), ("beta", vec![0.0; c])],
&[("x", &x_data)],
1,
);
}
#[test]
fn grad_softmax() {
let (rows, h) = (2usize, 4usize);
let mut g = Graph::new("sm");
let x = g.input("x", Shape::new(&[rows, h], DType::F32));
let w = g.param("W", Shape::new(&[h, h], DType::F32));
let z = g.matmul(x, w, Shape::new(&[rows, h], DType::F32));
let y = g.softmax(z, -1, Shape::new(&[rows, h], DType::F32));
let loss = scalar_sum(&mut g, y, &Shape::new(&[rows, h], DType::F32));
g.set_outputs(vec![loss]);
let bwd = rlx_opt::grad_with_loss(&g, &[w]);
let x_data: Vec<f32> = (0..rows * h).map(|i| i as f32 * 0.1).collect();
grad_parity(
"softmax dW",
&bwd,
&[("W", vec![0.1; h * h])],
&[("x", &x_data)],
1,
);
}
#[test]
fn grad_conv2d_input_native_matches_analytic() {
if !rlx_runtime::is_available(Device::Ane) {
eprintln!("skip: Device::Ane not available");
return;
}
let (n, c, hh, w) = (1usize, 1usize, 4usize, 4usize);
let (co, kh, kw) = (2usize, 3usize, 3usize);
let mut g = Graph::new("conv_in");
let x = g.input("x", Shape::new(&[n, c, hh, w], DType::F32));
let s = g.param("S", Shape::new(&[1, c, 1, 1], DType::F32));
let scaled = g.binary(
rlx_ir::op::BinaryOp::Mul,
x,
s,
Shape::new(&[n, c, hh, w], DType::F32),
);
let weight = g.param("Wc", Shape::new(&[co, c, kh, kw], DType::F32));
let y = g.conv2d(scaled, weight, [kh, kw], [1, 1], [0, 0], [1, 1], 1);
let y_shape = g.node(y).shape.clone();
let loss = scalar_sum(&mut g, y, &y_shape);
g.set_outputs(vec![loss]);
let bwd = rlx_opt::grad_with_loss(&g, &[s]);
let x_data: Vec<f32> = (0..n * c * hh * w).map(|i| i as f32 * 0.1 - 0.5).collect();
let wdata: Vec<f32> = (0..co * c * kh * kw)
.map(|i| (i as f32 * 0.03).sin())
.collect();
let mut comp = Session::new(Device::Ane).compile(bwd);
comp.set_param("S", &[1.0]);
comp.set_param("Wc", &wdata);
let out = comp.run(&[("x", &x_data), ("d_output", &[1.0f32])]);
let (loss_v, ds) = (out[0][0], out[1][0]);
assert!(ds.is_finite(), "conv dInput non-finite: {ds}");
assert!(
(ds - loss_v).abs() < 1e-3,
"conv dInput grad wrong: dS={ds} but analytic truth (=loss) is {loss_v}"
);
}
#[test]
fn grad_relu_activation() {
let (rows, h) = (2usize, 4usize);
let mut g = Graph::new("relu");
let x = g.input("x", Shape::new(&[rows, h], DType::F32));
let w = g.param("W", Shape::new(&[h, h], DType::F32));
let z = g.matmul(x, w, Shape::new(&[rows, h], DType::F32));
let a = g.activation(Activation::Relu, z, Shape::new(&[rows, h], DType::F32));
let loss = scalar_sum(&mut g, a, &Shape::new(&[rows, h], DType::F32));
g.set_outputs(vec![loss]);
let bwd = rlx_opt::grad_with_loss(&g, &[w]);
let x_data: Vec<f32> = (0..rows * h).map(|i| i as f32 * 0.1 - 0.3).collect();
grad_parity(
"relu dW",
&bwd,
&[("W", vec![0.2; h * h])],
&[("x", &x_data)],
1,
);
}
#[test]
fn grad_conv2d_weight_native_matches_fd() {
if !rlx_runtime::is_available(Device::Ane) {
eprintln!("skip: Device::Ane not available");
return;
}
let (n, c, hh, w) = (1usize, 1usize, 4usize, 4usize);
let (co, kh, kw) = (2usize, 1usize, 2usize);
let build = || -> (Graph, NodeId) {
let mut g = Graph::new("conv_w");
let x = g.input("x", Shape::new(&[n, c, hh, w], DType::F32));
let weight = g.param("W", Shape::new(&[co, c, kh, kw], DType::F32));
let y = g.conv2d(x, weight, [kh, kw], [1, 1], [0, 0], [1, 1], 1);
let ys = g.node(y).shape.clone();
let loss = scalar_sum(&mut g, y, &ys);
g.set_outputs(vec![loss]);
(g, weight)
};
let x_data: Vec<f32> = (0..n * c * hh * w)
.map(|i| (i as f32 * 0.21).cos())
.collect();
let w_base: Vec<f32> = (0..co * c * kh * kw)
.map(|i| i as f32 * 0.1 - 0.15)
.collect();
let (fwd, _) = build();
let loss_at = |wv: &[f32]| -> f32 {
let mut cc = Session::new(Device::Cpu).compile(fwd.clone());
cc.set_param("W", wv);
cc.run(&[("x", &x_data)])[0][0]
};
let base = loss_at(&w_base);
let eps = 5e-3f32;
let fd: Vec<f32> = (0..w_base.len())
.map(|i| {
let mut wp = w_base.clone();
wp[i] += eps;
(loss_at(&wp) - base) / eps
})
.collect();
let (g, wnode) = build();
let bwd = rlx_opt::grad_with_loss(&g, &[wnode]);
let mut comp = Session::new(Device::Ane).compile(bwd);
comp.set_param("W", &w_base);
let dw = comp
.run(&[("x", &x_data), ("d_output", &[1.0f32])])
.remove(1);
assert_eq!(dw.len(), fd.len(), "dW length mismatch");
assert!(dw.iter().all(|v| v.is_finite()), "non-finite dW: {dw:?}");
let (mut dot, mut na, mut nc) = (0.0f32, 0.0f32, 0.0f32);
let mut max_abs = 0.0f32;
for (&a, &f) in dw.iter().zip(&fd) {
dot += a * f;
na += a * a;
nc += f * f;
max_abs = max_abs.max((a - f).abs());
}
let cosine = dot / (na.sqrt() * nc.sqrt());
assert!(
cosine > 0.999 && max_abs < 5e-2,
"conv dW vs finite-diff: cosine={cosine}, max_abs={max_abs}\n dW={dw:?}\n fd={fd:?}"
);
eprintln!("OK conv dWeight: cosine={cosine:.5}, max_abs={max_abs:.4}");
}