use crate::apr_transformer::AprTransformer;
use crate::apr_transformer::{AprKVCache, AprTransformerConfig, AprTransformerLayer};
fn make_qknorm_model(qk_norm: Option<Vec<f32>>) -> AprTransformer {
let hidden_dim = 8;
let num_heads = 2;
let num_kv_heads = 2;
let vocab_size = 16;
let intermediate_dim = 16;
let head_dim = hidden_dim / num_heads; let kv_dim = num_kv_heads * head_dim; let qkv_out_dim = hidden_dim + 2 * kv_dim;
let config = AprTransformerConfig {
architecture: "qwen3".to_string(),
hidden_dim,
num_layers: 1,
num_heads,
num_kv_heads,
vocab_size,
intermediate_dim,
context_length: 64,
rope_theta: 10000.0,
eps: 1e-6,
..Default::default()
};
let mut token_embedding = vec![0.0f32; vocab_size * hidden_dim];
for tok in 0..vocab_size {
for d in 0..hidden_dim {
token_embedding[tok * hidden_dim + d] = ((tok + 1) as f32 * 0.13 + d as f32 * 0.07).sin();
}
}
let qkv_weight: Vec<f32> = (0..qkv_out_dim * hidden_dim)
.map(|i| ((i % 13) as f32 - 6.0) * 0.05)
.collect();
let attn_output_weight: Vec<f32> = (0..hidden_dim * hidden_dim)
.map(|i| ((i % 7) as f32 - 3.0) * 0.02)
.collect();
let ffn_gate_weight: Vec<f32> = (0..intermediate_dim * hidden_dim)
.map(|i| ((i % 5) as f32 - 2.0) * 0.01)
.collect();
let ffn_up_weight: Vec<f32> = (0..intermediate_dim * hidden_dim)
.map(|i| ((i % 3) as f32 - 1.0) * 0.01)
.collect();
let ffn_down_weight: Vec<f32> = (0..hidden_dim * intermediate_dim)
.map(|i| ((i % 4) as f32 - 1.5) * 0.01)
.collect();
let layer = AprTransformerLayer {
attn_norm_weight: vec![1.0; hidden_dim],
attn_norm_bias: None,
qkv_weight,
qkv_bias: None,
attn_output_weight,
attn_output_bias: None,
ffn_gate_weight: Some(ffn_gate_weight),
ffn_gate_bias: None,
ffn_up_weight,
ffn_up_bias: None,
ffn_down_weight,
ffn_down_bias: None,
ffn_norm_weight: Some(vec![1.0; hidden_dim]),
ffn_norm_bias: None,
attn_q_norm_weight: qk_norm.clone(),
attn_k_norm_weight: qk_norm,
linear_attn_z_weight: None,
linear_attn_b_weight: None,
linear_attn_a_weight: None,
linear_attn_conv1d_weight: None,
linear_attn_a_log: None,
linear_attn_dt_bias: None,
linear_attn_norm_weight: None,
moe_gate_weight: None,
moe_expert_gate_up: None,
moe_expert_down: None,
moe_shared_gate: None,
moe_shared_up: None,
moe_shared_down: None,
moe_shared_expert_gate_weight: None,
};
let lm_head_weight: Vec<f32> = (0..hidden_dim * vocab_size)
.map(|i| ((i % 11) as f32 - 5.0) * 0.01)
.collect();
AprTransformer {
config,
token_embedding,
layers: vec![layer],
output_norm_weight: vec![1.0; hidden_dim],
output_norm_bias: None,
lm_head_weight,
lm_head_bias: None,
q4k_layers: None,
lm_head_weight_q6k: None,
lm_head_weight_q4k: None,
lm_head_tied: false,
}
}
#[test]
fn pmat799_qk_norm_applied_in_forward_with_cache() {
let qk_weight = vec![0.1f32, 4.0, 8.0, 0.05];
let model_with_norm = make_qknorm_model(Some(qk_weight));
let model_without_norm = make_qknorm_model(None);
let mut cache_with = AprKVCache::new(&model_with_norm.config);
let mut cache_without = AprKVCache::new(&model_without_norm.config);
let prompt = [3u32, 7u32];
let mut logits_with = Vec::new();
let mut logits_without = Vec::new();
for (pos, &tok) in prompt.iter().enumerate() {
logits_with = model_with_norm
.forward_with_cache(tok, &mut cache_with, pos)
.expect("forward_with_cache (with qk-norm) should succeed");
logits_without = model_without_norm
.forward_with_cache(tok, &mut cache_without, pos)
.expect("forward_with_cache (without qk-norm) should succeed");
}
assert_eq!(logits_with.len(), logits_without.len());
assert!(logits_with.iter().all(|v| v.is_finite()));
let max_abs_diff = logits_with
.iter()
.zip(logits_without.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0f32, f32::max);
assert!(
max_abs_diff > 1e-5,
"PMAT-799 REGRESSION: per-head QK RMSNorm was ignored on the cached \
decode path. Non-identity attn_q/k_norm_weight produced (near-)IDENTICAL \
logits (max_abs_diff={max_abs_diff}). forward_with_cache must apply \
apply_per_head_rms_norm after projection+bias, before RoPE."
);
}
#[test]
fn pmat799_qk_norm_matches_independent_reference() {
let head_dim = 4usize;
let qk_weight = vec![0.5f32, 1.5, 2.0, 0.25];
let eps = 1e-6f32;
let reference_head = |head: &[f32]| -> Vec<f32> {
let sum_sq: f32 = head.iter().map(|v| v * v).sum();
let inv_rms = 1.0 / (sum_sq / head_dim as f32 + eps).sqrt();
head.iter()
.enumerate()
.map(|(j, &v)| v * inv_rms * qk_weight[j])
.collect()
};
let head = [0.3f32, -1.2, 0.7, 2.1];
let mut buf = head.to_vec();
crate::gguf::ops::apply_per_head_rms_norm(&mut buf, &qk_weight, 1, eps);
let expected = reference_head(&head);
for (i, (a, b)) in buf.iter().zip(expected.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-6,
"per-head RMSNorm mismatch at {i}: got {a}, expected {b}"
);
}
}