use crate::inference::models::qwen35::Qwen35Config;
#[derive(Debug, Clone)]
pub struct FullAttnLayerWeights {
pub attn_norm: Vec<f32>,
pub post_attn_norm: Vec<f32>,
pub wq: Vec<f32>,
pub wk: Vec<f32>,
pub wv: Vec<f32>,
pub w_gate: Vec<f32>,
pub attn_q_norm: Vec<f32>,
pub attn_k_norm: Vec<f32>,
pub wo: Vec<f32>,
}
#[derive(Debug, Clone, Copy)]
pub struct FullAttnShape {
pub hidden_size: u32,
pub n_head: u32,
pub n_kv: u32,
pub head_dim: u32,
pub rotary_dim: u32,
pub rope_theta: f32,
pub mrope_section: [u32; 4],
pub rms_norm_eps: f32,
}
impl FullAttnShape {
pub fn from_config(cfg: &Qwen35Config) -> Self {
Self {
hidden_size: cfg.hidden_size,
n_head: cfg.num_attention_heads,
n_kv: cfg.num_key_value_heads,
head_dim: cfg.head_dim,
rotary_dim: cfg.rotary_dim,
rope_theta: cfg.rope_theta as f32,
mrope_section: cfg.mrope_section,
rms_norm_eps: cfg.rms_norm_eps,
}
}
}
fn rms_norm_row(x: &[f32], weight: &[f32], eps: f32) -> Vec<f32> {
let n = x.len() as f32;
let sum_sq: f32 = x.iter().map(|v| v * v).sum();
let inv = (sum_sq / n + eps).sqrt().recip();
x.iter()
.zip(weight.iter())
.map(|(xi, wi)| xi * inv * wi)
.collect()
}
fn matmul_a_by_bt(lhs: &[f32], rhs: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
let mut out = vec![0.0f32; m * n];
for i in 0..m {
for j in 0..n {
let mut acc = 0.0f32;
for kk in 0..k {
acc += lhs[i * k + kk] * rhs[j * k + kk];
}
out[i * n + j] = acc;
}
}
out
}
fn sigmoid(x: f32) -> f32 {
1.0 / (1.0 + (-x).exp())
}
fn imrope_inplace(
data: &mut [f32],
n_head: u32,
head_dim: u32,
rotary_dim: u32,
theta: f32,
positions: [i32; 4],
sections: [u32; 4],
) {
let head_dim = head_dim as usize;
let half_dim = head_dim / 2;
let rotary_dim = rotary_dim as usize;
let half_rope = rotary_dim / 2;
let sect_dims = sections.iter().sum::<u32>().max(1);
let pick_axis = |sector: u32| -> usize {
if sector % 3 == 0 && sector < 3 * sections[0] {
0
} else if sector % 3 == 1 && sector < 3 * sections[1] {
1
} else if sector % 3 == 2 && sector < 3 * sections[2] {
2
} else {
3
}
};
for h in 0..n_head as usize {
let base = h * head_dim;
for pair in 0..half_rope {
let sector = (pair as u32) % sect_dims;
let axis = pick_axis(sector);
let pos = positions[axis] as f32;
let dim_ratio = 2.0 * pair as f32 / rotary_dim as f32;
let freq = 1.0 / theta.powf(dim_ratio);
let angle = pos * freq;
let (ca, sa) = (angle.cos(), angle.sin());
let x0 = data[base + pair];
let x1 = data[base + pair + half_dim];
data[base + pair] = x0 * ca - x1 * sa;
data[base + pair + half_dim] = x0 * sa + x1 * ca;
}
}
}
pub fn gated_full_attention_cpu_ref(
x: &[f32],
positions: &[[i32; 4]],
weights: &FullAttnLayerWeights,
shape: FullAttnShape,
) -> Vec<f32> {
let seq_len = positions.len();
let h = shape.hidden_size as usize;
let nh = shape.n_head as usize;
let nkv = shape.n_kv as usize;
let d = shape.head_dim as usize;
let q_total = nh * d;
let kv_total = nkv * d;
assert_eq!(x.len(), seq_len * h, "x shape mismatch");
assert_eq!(weights.attn_norm.len(), h);
assert_eq!(weights.wq.len(), q_total * h);
assert_eq!(weights.wk.len(), kv_total * h);
assert_eq!(weights.wv.len(), kv_total * h);
assert_eq!(weights.w_gate.len(), q_total * h);
assert_eq!(weights.attn_q_norm.len(), d);
assert_eq!(weights.attn_k_norm.len(), d);
assert_eq!(weights.wo.len(), h * q_total);
assert!(nh % nkv == 0, "n_head must be a multiple of n_kv (GQA)");
let gqa_group = nh / nkv;
let mut x_norm = vec![0.0f32; seq_len * h];
for t in 0..seq_len {
let row = &x[t * h..(t + 1) * h];
let normed = rms_norm_row(row, &weights.attn_norm, shape.rms_norm_eps);
x_norm[t * h..(t + 1) * h].copy_from_slice(&normed);
}
let q_flat = matmul_a_by_bt(&x_norm, &weights.wq, seq_len, h, q_total);
let k_flat = matmul_a_by_bt(&x_norm, &weights.wk, seq_len, h, kv_total);
let v_flat = matmul_a_by_bt(&x_norm, &weights.wv, seq_len, h, kv_total);
let gate = matmul_a_by_bt(&x_norm, &weights.w_gate, seq_len, h, q_total);
let mut q = q_flat;
for t in 0..seq_len {
for hd in 0..nh {
let base = (t * nh + hd) * d;
let row = &q[base..base + d];
let normed = rms_norm_row(row, &weights.attn_q_norm, shape.rms_norm_eps);
q[base..base + d].copy_from_slice(&normed);
}
let tok_start = t * nh * d;
imrope_inplace(
&mut q[tok_start..tok_start + nh * d],
shape.n_head,
shape.head_dim,
shape.rotary_dim,
shape.rope_theta,
positions[t],
shape.mrope_section,
);
}
let mut k = k_flat;
for t in 0..seq_len {
for kh in 0..nkv {
let base = (t * nkv + kh) * d;
let row = &k[base..base + d];
let normed = rms_norm_row(row, &weights.attn_k_norm, shape.rms_norm_eps);
k[base..base + d].copy_from_slice(&normed);
}
let tok_start = t * nkv * d;
imrope_inplace(
&mut k[tok_start..tok_start + nkv * d],
shape.n_kv,
shape.head_dim,
shape.rotary_dim,
shape.rope_theta,
positions[t],
shape.mrope_section,
);
}
let scale = 1.0 / (d as f32).sqrt();
let mut attn_out = vec![0.0f32; seq_len * nh * d]; for t_q in 0..seq_len {
for hq in 0..nh {
let hkv = hq / gqa_group;
let n_keys = t_q + 1;
let mut logits = vec![0.0f32; n_keys];
for t_k in 0..n_keys {
let q_vec = &q[(t_q * nh + hq) * d..(t_q * nh + hq) * d + d];
let k_vec = &k[(t_k * nkv + hkv) * d..(t_k * nkv + hkv) * d + d];
let mut dot = 0.0f32;
for i in 0..d {
dot += q_vec[i] * k_vec[i];
}
logits[t_k] = dot * scale;
}
let max_logit = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let mut sum = 0.0f32;
for l in logits.iter_mut() {
*l = (*l - max_logit).exp();
sum += *l;
}
for l in logits.iter_mut() {
*l /= sum;
}
for t_k in 0..n_keys {
let v_vec = &v_flat[(t_k * nkv + hkv) * d..(t_k * nkv + hkv) * d + d];
let w = logits[t_k];
let out_off = (t_q * nh + hq) * d;
for i in 0..d {
attn_out[out_off + i] += w * v_vec[i];
}
}
}
}
for i in 0..attn_out.len() {
attn_out[i] *= sigmoid(gate[i]);
}
let out = matmul_a_by_bt(&attn_out, &weights.wo, seq_len, q_total, h);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_weights(shape: FullAttnShape, seed: u32) -> FullAttnLayerWeights {
let h = shape.hidden_size as usize;
let nh = shape.n_head as usize;
let nkv = shape.n_kv as usize;
let d = shape.head_dim as usize;
let q_total = nh * d;
let kv_total = nkv * d;
let mut seed = seed;
let step = |seed: &mut u32| {
*seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
((*seed as i32 as f32) / (i32::MAX as f32)) * 0.2
};
let mk = |seed: &mut u32, n: usize| -> Vec<f32> { (0..n).map(|_| step(seed)).collect() };
let mk_norm = |seed: &mut u32, n: usize| -> Vec<f32> {
(0..n).map(|_| 1.0 + (step(seed) * 0.1)).collect()
};
FullAttnLayerWeights {
attn_norm: mk_norm(&mut seed, h),
post_attn_norm: vec![1.0f32; h],
wq: mk(&mut seed, q_total * h),
wk: mk(&mut seed, kv_total * h),
wv: mk(&mut seed, kv_total * h),
w_gate: mk(&mut seed, q_total * h),
attn_q_norm: mk_norm(&mut seed, d),
attn_k_norm: mk_norm(&mut seed, d),
wo: mk(&mut seed, h * q_total),
}
}
fn spec_shape_small() -> FullAttnShape {
FullAttnShape {
hidden_size: 32,
n_head: 4,
n_kv: 2,
head_dim: 16,
rotary_dim: 8, rope_theta: 10000.0,
mrope_section: [2, 2, 0, 0], rms_norm_eps: 1e-6,
}
}
#[test]
fn acceptance_1seq_4tok_deterministic() {
let shape = spec_shape_small();
let weights = synthetic_weights(shape, 0x1234);
let seq_len = 4;
let h = shape.hidden_size as usize;
let mut x_seed = 0x4242u32;
let mut x_rand = || -> f32 {
x_seed = x_seed.wrapping_mul(1103515245).wrapping_add(12345);
((x_seed as i32 as f32) / (i32::MAX as f32)) * 0.5
};
let x: Vec<f32> = (0..seq_len * h).map(|_| x_rand()).collect();
let positions: Vec<[i32; 4]> = (0..seq_len as i32).map(|i| [i, i, i, i]).collect();
let out1 = gated_full_attention_cpu_ref(&x, &positions, &weights, shape);
let out2 = gated_full_attention_cpu_ref(&x, &positions, &weights, shape);
assert_eq!(out1.len(), seq_len * h);
for i in 0..out1.len() {
assert_eq!(
out1[i].to_bits(),
out2[i].to_bits(),
"non-deterministic at {}",
i
);
}
let sum_abs: f32 = out1.iter().map(|v| v.abs()).sum();
assert!(sum_abs > 0.0, "output is all zeros — something is broken");
}
#[test]
fn causal_mask_future_inputs_dont_leak() {
let shape = spec_shape_small();
let weights = synthetic_weights(shape, 0xABCD);
let seq_len = 4;
let h = shape.hidden_size as usize;
let mut x = vec![0.1f32; seq_len * h];
for (i, v) in x.iter_mut().enumerate() {
*v = 0.01 * (i as f32);
}
let positions: Vec<[i32; 4]> = (0..seq_len as i32).map(|i| [i, i, i, i]).collect();
let out_base = gated_full_attention_cpu_ref(&x, &positions, &weights, shape);
let mut x_pert = x.clone();
for j in 0..h {
x_pert[3 * h + j] += 5.0;
}
let out_pert = gated_full_attention_cpu_ref(&x_pert, &positions, &weights, shape);
for t in 0..3 {
for j in 0..h {
let d = (out_base[t * h + j] - out_pert[t * h + j]).abs();
assert!(
d < 1e-5,
"causal violation at token {}, dim {}: base={}, pert={}",
t,
j,
out_base[t * h + j],
out_pert[t * h + j]
);
}
}
let mut any_diff = false;
for j in 0..h {
if (out_base[3 * h + j] - out_pert[3 * h + j]).abs() > 1e-5 {
any_diff = true;
break;
}
}
assert!(
any_diff,
"perturbation at token 3 had no effect on token 3 output"
);
}
#[test]
fn gate_zero_gives_half_output() {
let shape = FullAttnShape {
hidden_size: 8,
n_head: 2,
n_kv: 1,
head_dim: 4,
rotary_dim: 2,
rope_theta: 10000.0,
mrope_section: [1, 0, 0, 0],
rms_norm_eps: 1e-6,
};
let mut weights = synthetic_weights(shape, 0x777);
for v in weights.w_gate.iter_mut() {
*v = 0.0;
}
let seq_len = 2;
let h = shape.hidden_size as usize;
let x: Vec<f32> = (0..seq_len * h).map(|i| (i as f32) * 0.1).collect();
let positions: Vec<[i32; 4]> = (0..seq_len as i32).map(|i| [i, i, i, i]).collect();
let out_zero_gate = gated_full_attention_cpu_ref(&x, &positions, &weights, shape);
for v in &out_zero_gate {
assert!(v.is_finite(), "non-finite output with gate=0");
}
let sum_abs: f32 = out_zero_gate.iter().map(|v| v.abs()).sum();
assert!(sum_abs > 1e-3, "output at gate=0 is too small");
let mut weights2 = weights.clone();
for v in weights2.w_gate.iter_mut() {
*v = 10.0; }
let out_big_gate = gated_full_attention_cpu_ref(&x, &positions, &weights2, shape);
for i in 0..out_zero_gate.len() {
let zero = out_zero_gate[i];
let big = out_big_gate[i];
if zero.abs() > 1e-5 {
let ratio = big / zero;
assert!(
ratio.abs() > 1.5 && ratio.abs() < 2.5,
"gate-scaling ratio at {} = {} (zero={}, big={})",
i,
ratio,
zero,
big
);
}
}
}
#[test]
fn gqa_ratio_4_2_runs_without_panic() {
let shape = spec_shape_small(); let weights = synthetic_weights(shape, 0xBEEF);
let seq_len = 3;
let h = shape.hidden_size as usize;
let x: Vec<f32> = (0..seq_len * h).map(|i| 0.01 * i as f32).collect();
let positions: Vec<[i32; 4]> = (0..seq_len as i32).map(|i| [i, i, i, i]).collect();
let out = gated_full_attention_cpu_ref(&x, &positions, &weights, shape);
assert_eq!(out.len(), seq_len * h);
assert!(out.iter().all(|v| v.is_finite()));
}
#[test]
fn rope_makes_output_position_dependent() {
let shape = spec_shape_small();
let weights = synthetic_weights(shape, 0x1111);
let seq_len = 2;
let h = shape.hidden_size as usize;
let x: Vec<f32> = (0..seq_len * h).map(|i| 0.1 * (i as f32)).collect();
let pos_near: Vec<[i32; 4]> = vec![[0, 0, 0, 0], [1, 1, 1, 1]];
let pos_far: Vec<[i32; 4]> = vec![[0, 0, 0, 0], [100, 100, 100, 100]];
let out_near = gated_full_attention_cpu_ref(&x, &pos_near, &weights, shape);
let out_far = gated_full_attention_cpu_ref(&x, &pos_far, &weights, shape);
let mut any_diff = false;
for i in 0..h {
let base = h + i; if (out_near[base] - out_far[base]).abs() > 1e-5 {
any_diff = true;
break;
}
}
assert!(any_diff, "RoPE did not make the output position-dependent");
}
#[test]
fn shape_from_config() {
use crate::inference::models::qwen35::{
default_layer_types, Qwen35MoeConfig, Qwen35Variant,
};
let cfg = Qwen35Config {
variant: Qwen35Variant::Moe,
hidden_size: 2048,
num_hidden_layers: 40,
num_attention_heads: 16,
num_key_value_heads: 2,
head_dim: 256,
linear_num_key_heads: 16,
linear_num_value_heads: 32,
linear_key_head_dim: 128,
linear_value_head_dim: 128,
linear_conv_kernel_dim: 4,
full_attention_interval: 4,
layer_types: default_layer_types(40, 4),
partial_rotary_factor: 0.25,
rope_theta: 1e7,
rotary_dim: 64,
mrope_section: [11, 11, 10, 0],
mrope_interleaved: true,
rms_norm_eps: 1e-6,
max_position_embeddings: 262144,
vocab_size: 248320,
attn_output_gate: true,
mtp_num_hidden_layers: 0,
mtp_use_dedicated_embeddings: true,
intermediate_size: None,
moe: Some(Qwen35MoeConfig {
moe_intermediate_size: 512,
num_experts: 256,
num_experts_per_tok: 8,
shared_expert_intermediate_size: 512,
}),
};
let s = FullAttnShape::from_config(&cfg);
assert_eq!(s.hidden_size, 2048);
assert_eq!(s.n_head, 16);
assert_eq!(s.n_kv, 2);
assert_eq!(s.head_dim, 256);
assert_eq!(s.rotary_dim, 64);
assert_eq!(s.rope_theta, 1e7);
assert_eq!(s.mrope_section, [11, 11, 10, 0]);
}
#[test]
fn single_token_seq() {
let shape = spec_shape_small();
let weights = synthetic_weights(shape, 0x9999);
let h = shape.hidden_size as usize;
let x: Vec<f32> = (0..h).map(|i| 0.1 * (i as f32)).collect();
let positions = vec![[0, 0, 0, 0]];
let out = gated_full_attention_cpu_ref(&x, &positions, &weights, shape);
assert_eq!(out.len(), h);
assert!(out.iter().all(|v| v.is_finite()));
}
}