use candle_core::Device;
use super::*;
fn t(data: &[f32], shape: (usize, usize, usize, usize)) -> Tensor {
Tensor::from_slice(data, shape, &Device::Cpu).unwrap()
}
fn max_diff(a: &Tensor, b: &Tensor) -> f32 {
(a - b)
.unwrap()
.abs()
.unwrap()
.flatten_all()
.unwrap()
.max(0)
.unwrap()
.to_scalar::<f32>()
.unwrap()
}
fn values(t: &Tensor) -> Vec<f32> {
t.flatten_all().unwrap().to_vec1::<f32>().unwrap()
}
#[test]
fn attention_matches_hand_computation() {
let q = t(&[1.0, 0.0], (1, 1, 1, 2));
let k = t(&[0.0, 0.0, 3f32.ln(), 0.0], (1, 1, 2, 2));
let v = t(&[1.0, 0.0, 0.0, 1.0], (1, 1, 2, 2));
let out = values(&scaled_dot_product(&q, &k, &v, &AttentionMask::none(), 1.0).unwrap());
assert!(
(out[0] - 0.25).abs() < 1e-6 && (out[1] - 0.75).abs() < 1e-6,
"{out:?}"
);
}
#[test]
fn masked_keys_get_no_weight() {
let q = t(&[1.0, 0.0], (1, 1, 1, 2));
let k = t(&[0.0, 0.0, 3f32.ln(), 0.0], (1, 1, 2, 2));
let v = t(&[1.0, 0.0, 0.0, 1.0], (1, 1, 2, 2));
let attention_mask = Tensor::new(&[[1u32, 0]], &Device::Cpu).unwrap();
let mask = AttentionMask::padding(&attention_mask, &[1], 1, 2, DType::F32).unwrap();
let out = values(&scaled_dot_product(&q, &k, &v, &mask, 1.0).unwrap());
assert!(
(out[0] - 1.0).abs() < 1e-6 && out[1].abs() < 1e-6,
"{out:?}"
);
}
#[test]
fn the_additive_term_is_added_after_the_scale() {
let q = t(&[1.0, 0.0, 0.0, 0.0], (1, 1, 2, 2));
let k = t(&[0.0, 0.0, 2.0 * 3f32.ln(), 0.0], (1, 1, 2, 2));
let v = t(&[1.0, 0.0, 0.0, 1.0], (1, 1, 2, 2));
let bias = t(&[0.0, 3f32.ln(), 0.0, 0.0], (1, 1, 2, 2));
let attention_mask = Tensor::new(&[[1u32, 1]], &Device::Cpu).unwrap();
let mask =
AttentionMask::padding_with_bias(&attention_mask, &[2], &bias, 1, 2, DType::F32).unwrap();
let out = values(&scaled_dot_product(&q, &k, &v, &mask, 0.5).unwrap());
assert!(
(out[0] - 0.1).abs() < 1e-6 && (out[1] - 0.9).abs() < 1e-6,
"{out:?}"
);
}
#[test]
fn repeat_kv_tiles_each_kv_head_for_its_group() {
let k = t(&[1.0, 2.0], (1, 2, 1, 1));
assert_eq!(values(&repeat_kv(&k, 2).unwrap()), vec![1.0, 1.0, 2.0, 2.0]);
assert_eq!(repeat_kv(&k, 1).unwrap().dims(), &[1, 2, 1, 1]);
}
#[test]
fn metal_sdpa_applies_only_for_metal_and_listed_head_sizes() {
assert!(!metal_sdpa_applies(&Device::Cpu, 128, DType::F32));
assert!(!metal_sdpa_applies(&Device::Cpu, 256, DType::F16));
}
#[test]
fn the_kernel_takes_wide_heads_only_in_a_half_dtype() {
assert!(sdpa_accepts(256, DType::F16) && sdpa_accepts(256, DType::BF16));
assert!(!sdpa_accepts(256, DType::F32));
assert!(sdpa_accepts(128, DType::F32) && sdpa_accepts(64, DType::F32));
assert!(!sdpa_accepts(512, DType::BF16) && !sdpa_accepts(40, DType::F16));
}
#[test]
fn grouped_query_matches_explicitly_repeated_heads() {
let q = Tensor::randn(0f32, 1.0, (2, 4, 3, 8), &Device::Cpu).unwrap();
let k = Tensor::randn(0f32, 1.0, (2, 2, 3, 8), &Device::Cpu).unwrap();
let v = Tensor::randn(0f32, 1.0, (2, 2, 3, 8), &Device::Cpu).unwrap();
let grouped = scaled_dot_product(&q, &k, &v, &AttentionMask::none(), 0.5).unwrap();
let full = scaled_dot_product(
&q,
&repeat_kv(&k, 2).unwrap(),
&repeat_kv(&v, 2).unwrap(),
&AttentionMask::none(),
0.5,
)
.unwrap();
assert!(max_diff(&grouped, &full) < 1e-6);
assert_eq!(grouped.dims(), &[2, 4, 3, 8]);
}
#[test]
fn scaled_dot_product_rejects_head_counts_that_do_not_divide() {
let q = Tensor::randn(0f32, 1.0, (1, 4, 2, 8), &Device::Cpu).unwrap();
let k = Tensor::randn(0f32, 1.0, (1, 3, 2, 8), &Device::Cpu).unwrap();
let v = Tensor::randn(0f32, 1.0, (1, 3, 2, 8), &Device::Cpu).unwrap();
let err = scaled_dot_product(&q, &k, &v, &AttentionMask::none(), 1.0).unwrap_err();
let message = err.to_string();
assert!(
message.contains("4 query heads") && message.contains("3 kv heads"),
"{message}"
);
}
#[test]
fn causal_attention_sees_only_the_past() {
let q = Tensor::randn(0f32, 1.0, (1, 1, 3, 4), &Device::Cpu).unwrap();
let k = Tensor::randn(0f32, 1.0, (1, 1, 3, 4), &Device::Cpu).unwrap();
let v = t(
&[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
(1, 1, 3, 4),
);
let mask = AttentionMask::causal(3, 4, DType::F32, &Device::Cpu).unwrap();
let rows = scaled_dot_product(&q, &k, &v, &mask, 0.5)
.unwrap()
.squeeze(0)
.unwrap()
.squeeze(0)
.unwrap()
.to_vec2::<f32>()
.unwrap();
assert!(
(rows[0][0] - 1.0).abs() < 1e-6 && rows[0][1].abs() < 1e-6 && rows[0][2].abs() < 1e-6,
"position 0 attends to key 0 only: {:?}",
rows[0]
);
assert!(
rows[1][2].abs() < 1e-6,
"position 1 must not see key 2: {:?}",
rows[1]
);
}
#[test]
fn the_fused_projection_splits_into_query_key_value_along_its_rows() {
let fused = Tensor::from_slice(
&[1f32, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0, 3.0],
(6, 2),
&Device::Cpu,
)
.unwrap();
let (q, k, v) = split_fused_qkv(&fused, 2).unwrap();
assert_eq!(
q.to_vec2::<f32>().unwrap(),
vec![vec![1.0, 1.0], vec![1.0, 1.0]]
);
assert_eq!(
k.to_vec2::<f32>().unwrap(),
vec![vec![2.0, 2.0], vec![2.0, 2.0]]
);
assert_eq!(
v.to_vec2::<f32>().unwrap(),
vec![vec![3.0, 3.0], vec![3.0, 3.0]]
);
}
mod weights;