#[cfg(test)]
mod tests {
use ferrotensor_backend::{ComputeEngine, UnifiedTensor, WgpuContext};
use ferrotensor_graph::FlashAttention;
use std::sync::Arc;
fn gpu_tests_enabled() -> bool {
std::env::var("FERROTENSOR_RUN_GPU_TESTS").as_deref() == Ok("1")
}
fn setup() -> Option<(Arc<WgpuContext>, ComputeEngine)> {
if !gpu_tests_enabled() {
eprintln!(
"skipping flash attention correctness tests; set FERROTENSOR_RUN_GPU_TESTS=1"
);
return None;
}
let ctx = Arc::new(pollster::block_on(async { WgpuContext::new().await }));
let engine = ComputeEngine::new(&ctx);
Some((ctx, engine))
}
#[test]
fn test_flash_attention_shape() {
let Some((ctx, engine)) = setup() else { return };
let flash_attn = FlashAttention::new(768, 12, true);
let seq_len = 128;
let hidden_dim = 768;
let q_data: Vec<f32> = (0..seq_len * hidden_dim)
.map(|i| (i as f32 % 100.0) / 100.0)
.collect();
let q = UnifiedTensor::new(&ctx, &q_data, &[1, seq_len, hidden_dim]);
let k = UnifiedTensor::new(&ctx, &q_data, &[1, seq_len, hidden_dim]);
let v = UnifiedTensor::new(&ctx, &q_data, &[1, seq_len, hidden_dim]);
let output = flash_attn.forward(&q, &k, &v, &ctx, &engine);
assert_eq!(output.shape, vec![1, seq_len, hidden_dim]);
}
#[test]
fn test_causal_masking() {
let Some((ctx, engine)) = setup() else { return };
let flash_attn = FlashAttention::new(64, 1, true);
let seq_len = 8;
let hidden_dim = 64;
let q_data: Vec<f32> = vec![1.0; seq_len * hidden_dim];
let k_data: Vec<f32> = (0..seq_len * hidden_dim)
.map(|i| (i / hidden_dim) as f32)
.collect();
let v_data = k_data.clone();
let q = UnifiedTensor::new(&ctx, &q_data, &[seq_len, hidden_dim]);
let k = UnifiedTensor::new(&ctx, &k_data, &[seq_len, hidden_dim]);
let v = UnifiedTensor::new(&ctx, &v_data, &[seq_len, hidden_dim]);
let output = flash_attn.forward(&q, &k, &v, &ctx, &engine);
let result = pollster::block_on(output.to_vec(&ctx));
assert!(result.iter().any(|&x| x.abs() > 1e-6));
assert!(result[..hidden_dim].iter().all(|&x| x.abs() < 1e-6));
for i in 0..seq_len {
for &x in &result[i * hidden_dim..(i + 1) * hidden_dim] {
assert!((-1e-4..=i as f32 + 1e-4).contains(&x), "row {i}: {x}");
}
}
}
#[test]
fn test_attention_normalization() {
let Some((ctx, engine)) = setup() else { return };
let flash_attn = FlashAttention::new(64, 1, false);
let seq_len = 16;
let hidden_dim = 64;
let q_data: Vec<f32> = (0..seq_len * hidden_dim)
.map(|i| ((i % 100) as f32 - 50.0) / 100.0)
.collect();
let q = UnifiedTensor::new(&ctx, &q_data, &[seq_len, hidden_dim]);
let k = UnifiedTensor::new(&ctx, &q_data, &[seq_len, hidden_dim]);
let v = UnifiedTensor::new(&ctx, &q_data, &[seq_len, hidden_dim]);
let output = flash_attn.forward(&q, &k, &v, &ctx, &engine);
let result = pollster::block_on(output.to_vec(&ctx));
for &val in &result {
assert!(val.is_finite(), "Output contains non-finite values");
}
let sum: f32 = result.iter().sum();
assert!(sum.abs() > 1e-4, "Output is all zeros");
}
#[test]
fn test_backward_pass_shapes() {
let Some((ctx, engine)) = setup() else { return };
let flash_attn = FlashAttention::new(64, 1, true);
let seq_len = 64;
let head_dim = 64;
let q_data: Vec<f32> = (0..seq_len * head_dim)
.map(|i| (i as f32 % 100.0) / 100.0)
.collect();
let q = UnifiedTensor::new(&ctx, &q_data, &[seq_len, head_dim]);
let k = UnifiedTensor::new(&ctx, &q_data, &[seq_len, head_dim]);
let v = UnifiedTensor::new(&ctx, &q_data, &[seq_len, head_dim]);
let output = flash_attn.forward(&q, &k, &v, &ctx, &engine);
let grad_output = UnifiedTensor::new(&ctx, &q_data, &[seq_len, head_dim]);
let grads = flash_attn.backward(&q, &k, &v, &output, &grad_output, &ctx, &engine);
assert_eq!(grads.grad_q.shape, q.shape);
assert_eq!(grads.grad_k.shape, k.shape);
assert_eq!(grads.grad_v.shape, v.shape);
}
#[test]
fn test_memory_savings() {
let flash_attn = FlashAttention::new(768, 12, true);
let savings = flash_attn.memory_savings(2048);
assert!(savings.contains("x less memory"));
assert!(savings.contains("2048"));
}
#[test]
fn test_multi_head_creation() {
let flash_attn = FlashAttention::new(768, 12, true);
assert!(!flash_attn.memory_savings(1024).is_empty());
}
#[test]
fn test_different_sequence_lengths() {
let Some((ctx, engine)) = setup() else { return };
let flash_attn = FlashAttention::new(256, 4, true);
let test_lengths = vec![16, 32, 64, 128, 256];
for seq_len in test_lengths {
let hidden_dim = 256;
let q_data: Vec<f32> = (0..seq_len * hidden_dim)
.map(|i| (i as f32 % 100.0) / 100.0)
.collect();
let q = UnifiedTensor::new(&ctx, &q_data, &[1, seq_len, hidden_dim]);
let k = UnifiedTensor::new(&ctx, &q_data, &[1, seq_len, hidden_dim]);
let v = UnifiedTensor::new(&ctx, &q_data, &[1, seq_len, hidden_dim]);
let output = flash_attn.forward(&q, &k, &v, &ctx, &engine);
assert_eq!(output.shape, vec![1, seq_len, hidden_dim]);
let result = pollster::block_on(output.to_vec(&ctx));
assert!(result.iter().all(|&x| x.is_finite()));
}
}
}