#[test]
fn test_imp_101c_attention_with_cache_softmax_normalized() {
let config = GGUFConfig {
architecture: "test".to_string(),
constraints: crate::gguf::ArchConstraints::from_architecture("test"),
hidden_dim: 4,
intermediate_dim: 16,
num_layers: 1,
num_heads: 1,
num_kv_heads: 1,
vocab_size: 100,
context_length: 2048,
eps: 1e-5,
rope_type: 0,
rope_theta: 10000.0,
explicit_head_dim: None,
query_pre_attn_scalar: None,
bos_token_id: None,
eos_token_id: None,
};
let model = OwnedQuantizedModel {
config,
token_embedding: vec![],
position_embedding: None,
layers: vec![],
encoder_layers: vec![],
encoder_output_norm_weight: None,
encoder_output_norm_bias: None,
output_norm_weight: vec![],
output_norm_bias: None,
lm_head_weight: OwnedQuantizedTensor {
data: vec![],
in_dim: 4,
out_dim: 100,
qtype: 0,
},
lm_head_bias: None,
#[cfg(feature = "cuda")]
cuda_executor: None,
#[cfg(feature = "cuda")]
cuda_kernel_count: std::sync::atomic::AtomicU64::new(0),
#[cfg(feature = "cuda")]
cached_weight_names: std::sync::Mutex::new(std::collections::HashSet::new()),
};
let q = vec![1.0, 0.0, 0.0, 0.0];
let k_cache = vec![1.0, 0.0, 0.0, 0.0]; let v_cache = vec![1.0, 0.0, 0.0, 0.0];
let current_k = vec![1.0, 0.0, 0.0, 0.0]; let current_v = vec![0.0, 1.0, 0.0, 0.0];
let output = model.attention_with_cache(&q, &k_cache, &v_cache, ¤t_k, ¤t_v);
assert_eq!(
output.len(),
4,
"IMP-101c: Output should have hidden_dim elements"
);
let sum: f32 = output.iter().sum();
assert!(
(sum - 1.0).abs() < 0.1,
"IMP-101c: Attention output should be normalized weighted sum"
);
}
#[test]
fn test_imp_101c_kv_cache_multiple_positions() {
let mut cache = OwnedQuantizedKVCache::new(1, 4, 100);
for i in 0..3 {
let k = vec![i as f32; 4];
let v = vec![(i as f32) * 0.1; 4];
cache.append(0, &k, &v);
cache.advance();
}
assert_eq!(cache.len(), 3, "IMP-101c: Cache should have 3 positions");
let k_data = cache.get_k(0);
assert_eq!(
k_data.len(),
12,
"IMP-101c: K cache should have 3 * 4 = 12 elements"
);
assert!(
(k_data[0] - 0.0).abs() < 1e-6,
"IMP-101c: First K should be 0"
);
assert!(
(k_data[4] - 1.0).abs() < 1e-6,
"IMP-101c: Second K should be 1"
);
assert!(
(k_data[8] - 2.0).abs() < 1e-6,
"IMP-101c: Third K should be 2"
);
}
#[test]
fn test_imp_105_gqa_attention_multiple_q_per_kv() {
let config = GGUFConfig {
architecture: "test".to_string(),
constraints: crate::gguf::ArchConstraints::from_architecture("test"),
hidden_dim: 32, intermediate_dim: 128,
num_layers: 1,
num_heads: 8, num_kv_heads: 2, vocab_size: 100,
context_length: 1024,
rope_theta: 10000.0,
eps: 1e-5,
rope_type: 0,
explicit_head_dim: None,
query_pre_attn_scalar: None,
bos_token_id: None,
eos_token_id: None,
};
let hidden_dim = config.hidden_dim;
let head_dim = hidden_dim / config.num_heads; let kv_dim = config.num_kv_heads * head_dim;
let q = vec![1.0f32; hidden_dim];
let current_k = vec![1.0f32; kv_dim];
let current_v = vec![1.0f32; kv_dim];
let k_cache: Vec<f32> = vec![];
let v_cache: Vec<f32> = vec![];
let model = create_test_model_with_config(&config);
let output = model.attention_with_cache_gqa(&q, &k_cache, &v_cache, ¤t_k, ¤t_v);
assert_eq!(
output.len(),
hidden_dim,
"IMP-105: GQA output should have hidden_dim={hidden_dim} elements"
);
for head in 0..config.num_heads {
let head_start = head * head_dim;
let head_sum: f32 = output[head_start..head_start + head_dim].iter().sum();
assert!(
head_sum.abs() > 1e-6,
"IMP-105: GQA head {head} output should be non-zero"
);
}
}
#[test]
fn test_imp_105_gqa_kv_head_sharing() {
let config = GGUFConfig {
architecture: "test".to_string(),
constraints: crate::gguf::ArchConstraints::from_architecture("test"),
hidden_dim: 16, intermediate_dim: 64,
num_layers: 1,
num_heads: 4, num_kv_heads: 2, vocab_size: 100,
context_length: 1024,
rope_theta: 10000.0,
eps: 1e-5,
rope_type: 0,
explicit_head_dim: None,
query_pre_attn_scalar: None,
bos_token_id: None,
eos_token_id: None,
};
let hidden_dim = config.hidden_dim;
let head_dim = hidden_dim / config.num_heads; let kv_dim = config.num_kv_heads * head_dim;
let mut q = vec![0.0f32; hidden_dim];
for head in 0..config.num_heads {
for d in 0..head_dim {
q[head * head_dim + d] = (head + 1) as f32;
}
}
let mut current_k = vec![0.0f32; kv_dim];
for kv_head in 0..config.num_kv_heads {
for d in 0..head_dim {
current_k[kv_head * head_dim + d] = (kv_head + 1) as f32 * 0.5;
}
}
let mut current_v = vec![0.0f32; kv_dim];
for kv_head in 0..config.num_kv_heads {
for d in 0..head_dim {
current_v[kv_head * head_dim + d] = (kv_head + 1) as f32;
}
}
let k_cache: Vec<f32> = vec![];
let v_cache: Vec<f32> = vec![];
let model = create_test_model_with_config(&config);
let output = model.attention_with_cache_gqa(&q, &k_cache, &v_cache, ¤t_k, ¤t_v);
let eps = 1e-5;
let head0_sum: f32 = output[0..head_dim].iter().sum();
let head1_sum: f32 = output[head_dim..2 * head_dim].iter().sum();
let head2_sum: f32 = output[2 * head_dim..3 * head_dim].iter().sum();
let head3_sum: f32 = output[3 * head_dim..4 * head_dim].iter().sum();
assert!(
(head0_sum - head1_sum).abs() < eps,
"IMP-105: Heads 0,1 should produce same output (share KV head 0)"
);
assert!(
(head2_sum - head3_sum).abs() < eps,
"IMP-105: Heads 2,3 should produce same output (share KV head 1)"
);
assert!(
(head0_sum - head2_sum).abs() > eps,
"IMP-105: Heads using different KV heads should have different outputs"
);
}