use super::*;
use crate::autograd::cuda_training::CudaTrainer;
use crate::transformer::{Transformer, TransformerConfig};
fn rel_l2(a: &[f32], b: &[f32]) -> f32 {
let mut num = 0.0f64;
let mut den = 0.0f64;
for (&x, &y) in a.iter().zip(b.iter()) {
num += f64::from(x - y) * f64::from(x - y);
den += f64::from(y) * f64::from(y);
}
((num / den.max(1e-30)) as f32).sqrt()
}
fn download(buf: &GpuBuffer<f32>, n: usize) -> Vec<f32> {
let mut host = vec![0.0f32; buf.len()];
buf.copy_to_host(&mut host).expect("download");
host.truncate(n);
host
}
fn matmul_nt(x: &[f32], w: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
crate::autograd::ops::matmul::matmul_nt_compute(x, w, m, k, n)
}
fn add_bias(x: &mut [f32], bias: &[f32], rows: usize) {
let dim = bias.len();
for r in 0..rows {
for (i, b) in bias.iter().enumerate() {
x[r * dim + i] += b;
}
}
}
fn rms_norm(x: &[f32], weight: &[f32], rows: usize, dim: usize, eps: f32) -> Vec<f32> {
let mut out = vec![0.0f32; rows * dim];
for r in 0..rows {
let row = &x[r * dim..(r + 1) * dim];
let ms: f32 = row.iter().map(|v| v * v).sum::<f32>() / dim as f32;
let inv = 1.0 / (ms + eps).sqrt();
for i in 0..dim {
out[r * dim + i] = row[i] * inv * weight[i];
}
}
out
}
fn rope(x: &mut [f32], seq_len: usize, num_heads: usize, head_dim: usize, theta: f32) {
let total = num_heads * head_dim;
let half = head_dim / 2;
let inv_freq: Vec<f32> =
(0..half).map(|i| 1.0 / theta.powf(2.0 * i as f32 / head_dim as f32)).collect();
for pos in 0..seq_len {
for h in 0..num_heads {
let off = pos * total + h * head_dim;
for i in 0..half {
let f = pos as f32 * inv_freq[i];
let (s, c) = f.sin_cos();
let a = x[off + i];
let b = x[off + i + half];
x[off + i] = a * c - b * s;
x[off + i + half] = b * c + a * s;
}
}
}
}
fn attention(
q: &[f32],
k: &[f32],
v: &[f32],
seq_len: usize,
num_heads: usize,
num_kv_heads: usize,
head_dim: usize,
) -> Vec<f32> {
let q_dim = num_heads * head_dim;
let kv_dim = num_kv_heads * head_dim;
let heads_per_kv = num_heads / num_kv_heads;
let scale = 1.0 / (head_dim as f32).sqrt();
let mut out = vec![0.0f32; seq_len * q_dim];
for h in 0..num_heads {
let kv_h = h / heads_per_kv;
for i in 0..seq_len {
let mut scores = vec![0.0f32; i + 1];
for (j, sc) in scores.iter_mut().enumerate() {
let mut dot = 0.0f32;
for d in 0..head_dim {
dot += q[i * q_dim + h * head_dim + d] * k[j * kv_dim + kv_h * head_dim + d];
}
*sc = dot * scale;
}
let m = scores.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let mut denom = 0.0f32;
for sc in &mut scores {
*sc = (*sc - m).exp();
denom += *sc;
}
for d in 0..head_dim {
let mut acc = 0.0f32;
for (j, sc) in scores.iter().enumerate() {
acc += sc * v[j * kv_dim + kv_h * head_dim + d];
}
out[i * q_dim + h * head_dim + d] = acc / denom;
}
}
}
out
}
#[test]
#[ignore = "requires CUDA GPU + APR_PARITY_MODEL pointing at a Qwen2-family .apr"]
fn parity_probe_layer0_per_op() {
let Ok(model_path) = std::env::var("APR_PARITY_MODEL") else {
eprintln!("[op-probe] SKIP: APR_PARITY_MODEL unset");
return;
};
let config = TransformerConfig::from_apr_metadata(
Some(1536),
Some(12),
Some(2),
Some(8960),
Some(28),
Some(151_936),
Some(32_768),
Some(1e-6),
Some(1_000_000.0),
Some("qwen2"),
)
.expect("config");
let model = Transformer::from_apr(&model_path, &config).expect("model");
let layer = &model.layers[0];
let seq_len = 15usize;
let max_seq = 16usize;
let hidden = config.hidden_size;
let q_dim = config.q_dim();
let kv_dim = config.num_kv_heads * config.head_dim();
let inter = config.intermediate_size;
let num_heads = config.num_attention_heads;
let num_kv = config.num_kv_heads;
let head_dim = config.head_dim();
let eps = config.rms_norm_eps;
let theta = config.rope_theta;
let token_ids: Vec<u32> =
vec![3838, 374, 220, 17, 10, 17, 30, 21806, 448, 1101, 279, 1372, 13, 198, 19];
let embed = model.embed_tokens.forward(&token_ids);
let x = embed.data().as_slice().expect("contiguous").to_vec();
assert_eq!(x.len(), seq_len * hidden);
let trainer = CudaTrainer::new().expect("CUDA trainer");
let ctx = std::sync::Arc::clone(trainer.context());
let stream = trainer.stream();
let g = |t: &crate::Tensor| -> Vec<f32> { t.data().as_slice().expect("contiguous").to_vec() };
let input_norm_w = g(&layer.input_norm.weight);
let post_norm_w = g(&layer.post_attn_norm.weight);
let w_q = g(&layer.self_attn.w_q);
let w_k = g(&layer.self_attn.w_k);
let w_v = g(&layer.self_attn.w_v);
let w_o = g(&layer.self_attn.w_o);
let w_gate = g(&layer.ffn.w_gate);
let w_up = g(&layer.ffn.w_up);
let w_down = g(&layer.ffn.w_down);
let b_q = layer.self_attn.b_q.as_ref().map(|t| g(t));
let b_k = layer.self_attn.b_k.as_ref().map(|t| g(t));
let b_v = layer.self_attn.b_v.as_ref().map(|t| g(t));
let block = CudaNf4TransformerBlock::new(
&config,
0,
std::sync::Arc::clone(&ctx),
&input_norm_w,
&post_norm_w,
&w_q,
&w_k,
&w_v,
&w_o,
&w_gate,
&w_up,
&w_down,
max_seq,
None,
None,
1.0,
8,
None,
None,
b_q.as_deref(),
b_k.as_deref(),
b_v.as_deref(),
)
.expect("NF4 block");
let mut scratch = CudaBlockScratch::new(&config, max_seq, &ctx, 8).expect("scratch");
scratch.zero_forward_buffers(stream);
let gpu_in = trainer.upload(&x).expect("upload");
let mut gpu_out = trainer.zeros(seq_len * hidden).expect("out");
block.forward(&gpu_in, &mut gpu_out, seq_len, stream, &mut scratch).expect("forward");
stream.synchronize().expect("sync");
let g_norm1 = download(&scratch.norm1_out, seq_len * hidden);
let g_q = download(&scratch.q, seq_len * q_dim);
let g_k = download(&scratch.k, seq_len * kv_dim);
let g_v = download(&scratch.v, seq_len * kv_dim);
let g_attn = download(&scratch.attn_out, seq_len * q_dim);
let g_oproj = download(&scratch.o_proj_out, seq_len * hidden);
let g_res1 = download(&scratch.residual1, seq_len * hidden);
let g_norm2 = download(&scratch.norm2_out, seq_len * hidden);
let g_gate = download(&scratch.gate_out, seq_len * inter);
let g_up = download(&scratch.up_out, seq_len * inter);
let g_swiglu = download(&scratch.swiglu_out, seq_len * inter);
let g_ffn = download(&scratch.ffn_out, seq_len * hidden);
let g_out = download(&gpu_out, seq_len * hidden);
let norm1 = rms_norm(&x, &input_norm_w, seq_len, hidden, eps);
eprintln!("[op-probe] norm1: relL2={:.4}", rel_l2(&g_norm1, &norm1));
let mut q_nb = matmul_nt(&norm1, &w_q, seq_len, hidden, q_dim);
let mut k_nb = matmul_nt(&norm1, &w_k, seq_len, hidden, kv_dim);
let v_nb = matmul_nt(&norm1, &w_v, seq_len, hidden, kv_dim);
let mut q_b = q_nb.clone();
let mut k_b = k_nb.clone();
let mut v_b = v_nb.clone();
if let Some(ref b) = b_q {
add_bias(&mut q_b, b, seq_len);
}
if let Some(ref b) = b_k {
add_bias(&mut k_b, b, seq_len);
}
if let Some(ref b) = b_v {
add_bias(&mut v_b, b, seq_len);
}
rope(&mut q_nb, seq_len, num_heads, head_dim, theta);
rope(&mut k_nb, seq_len, num_kv, head_dim, theta);
rope(&mut q_b, seq_len, num_heads, head_dim, theta);
rope(&mut k_b, seq_len, num_kv, head_dim, theta);
eprintln!(
"[op-probe] q (post-rope): relL2(gpu,nobias)={:.4} relL2(gpu,bias)={:.4}",
rel_l2(&g_q, &q_nb),
rel_l2(&g_q, &q_b)
);
eprintln!(
"[op-probe] k (post-rope): relL2(gpu,nobias)={:.4} relL2(gpu,bias)={:.4}",
rel_l2(&g_k, &k_nb),
rel_l2(&g_k, &k_b)
);
eprintln!(
"[op-probe] v: relL2(gpu,nobias)={:.4} relL2(gpu,bias)={:.4}",
rel_l2(&g_v, &v_nb),
rel_l2(&g_v, &v_b)
);
let attn_nb = attention(&q_nb, &k_nb, &v_nb, seq_len, num_heads, num_kv, head_dim);
let attn_b = attention(&q_b, &k_b, &v_b, seq_len, num_heads, num_kv, head_dim);
eprintln!(
"[op-probe] attn_out: relL2(gpu,nobias)={:.4} relL2(gpu,bias)={:.4}",
rel_l2(&g_attn, &attn_nb),
rel_l2(&g_attn, &attn_b)
);
let oproj = matmul_nt(&attn_b, &w_o, seq_len, q_dim, hidden);
eprintln!("[op-probe] o_proj: relL2={:.4}", rel_l2(&g_oproj, &oproj));
let res1: Vec<f32> = x.iter().zip(oproj.iter()).map(|(a, b)| a + b).collect();
eprintln!("[op-probe] residual1: relL2={:.4}", rel_l2(&g_res1, &res1));
let norm2 = rms_norm(&res1, &post_norm_w, seq_len, hidden, eps);
eprintln!("[op-probe] norm2: relL2={:.4}", rel_l2(&g_norm2, &norm2));
let gate = matmul_nt(&norm2, &w_gate, seq_len, hidden, inter);
let up = matmul_nt(&norm2, &w_up, seq_len, hidden, inter);
eprintln!("[op-probe] gate: relL2={:.4}", rel_l2(&g_gate, &gate));
eprintln!("[op-probe] up: relL2={:.4}", rel_l2(&g_up, &up));
let swiglu: Vec<f32> =
gate.iter().zip(up.iter()).map(|(&gv, &uv)| (gv / (1.0 + (-gv).exp())) * uv).collect();
eprintln!("[op-probe] swiglu: relL2={:.4}", rel_l2(&g_swiglu, &swiglu));
let ffn = matmul_nt(&swiglu, &w_down, seq_len, inter, hidden);
eprintln!("[op-probe] ffn_down: relL2={:.4}", rel_l2(&g_ffn, &ffn));
let out: Vec<f32> = res1.iter().zip(ffn.iter()).map(|(a, b)| a + b).collect();
eprintln!("[op-probe] block_out: relL2={:.4}", rel_l2(&g_out, &out));
}