use super::common::{AttentionConfig, AttentionProjections, AttentionUtils};
use super::flash_kernel::{flash_attention, FlashParams};
use crate::errors::Result;
use crate::tensor::Tensor;
use crate::traits::Layer;
#[derive(Debug, Clone)]
pub struct FlashAttention {
config: AttentionConfig,
projections: AttentionProjections,
block_size: usize,
causal: bool,
use_flash_attention_2: bool,
}
impl FlashAttention {
pub fn new(
hidden_size: usize,
num_heads: usize,
dropout_prob: f32,
bias: bool,
block_size: Option<usize>,
causal: bool,
) -> Result<Self> {
let config = AttentionConfig::new(hidden_size, num_heads, dropout_prob, bias)?;
let projections = AttentionProjections::new(&config);
let block_size = block_size.unwrap_or(AttentionUtils::compute_block_size(
1024,
config.head_dim,
None,
));
Ok(Self {
config,
projections,
block_size,
causal,
use_flash_attention_2: true,
})
}
pub fn new_with_version(
hidden_size: usize,
num_heads: usize,
dropout_prob: f32,
bias: bool,
block_size: Option<usize>,
causal: bool,
use_flash_attention_2: bool,
) -> Result<Self> {
let mut flash_attention = Self::new(
hidden_size,
num_heads,
dropout_prob,
bias,
block_size,
causal,
)?;
flash_attention.use_flash_attention_2 = use_flash_attention_2;
Ok(flash_attention)
}
pub fn config(&self) -> &AttentionConfig {
&self.config
}
pub fn projections(&self) -> &AttentionProjections {
&self.projections
}
pub fn projections_mut(&mut self) -> &mut AttentionProjections {
&mut self.projections
}
pub fn block_size(&self) -> usize {
self.block_size
}
pub fn set_block_size(&mut self, block_size: usize) {
self.block_size = block_size;
}
pub fn is_using_flash_attention_2(&self) -> bool {
self.use_flash_attention_2
}
pub fn set_flash_attention_2(&mut self, enabled: bool) {
self.use_flash_attention_2 = enabled;
}
pub fn is_training(&self) -> bool {
self.config.training
}
pub fn set_training(&mut self, training: bool) {
self.config.training = training;
}
pub fn parameter_count(&self) -> usize {
self.projections.query.parameter_count()
+ self.projections.key.parameter_count()
+ self.projections.value.parameter_count()
+ self.projections.out_proj.parameter_count()
}
pub fn set_query_weight(&mut self, weight: Tensor) -> Result<()> {
self.projections.query.set_weight(weight)
}
pub fn set_query_bias(&mut self, bias: Tensor) -> Result<()> {
self.projections.query.set_bias(bias)
}
pub fn set_key_weight(&mut self, weight: Tensor) -> Result<()> {
self.projections.key.set_weight(weight)
}
pub fn set_key_bias(&mut self, bias: Tensor) -> Result<()> {
self.projections.key.set_bias(bias)
}
pub fn set_value_weight(&mut self, weight: Tensor) -> Result<()> {
self.projections.value.set_weight(weight)
}
pub fn set_value_bias(&mut self, bias: Tensor) -> Result<()> {
self.projections.value.set_bias(bias)
}
pub fn set_out_proj_weight(&mut self, weight: Tensor) -> Result<()> {
self.projections.out_proj.set_weight(weight)
}
pub fn set_out_proj_bias(&mut self, bias: Tensor) -> Result<()> {
self.projections.out_proj.set_bias(bias)
}
pub fn forward_self_attention(
&self,
input: &Tensor,
attention_mask: Option<&Tensor>,
) -> Result<Tensor> {
self.forward_attention(input, input, input, attention_mask)
}
pub fn forward_attention(
&self,
query_input: &Tensor,
key_input: &Tensor,
value_input: &Tensor,
attention_mask: Option<&Tensor>,
) -> Result<Tensor> {
let query = self.projections.query.forward_ref(query_input)?;
let key = self.projections.key.forward_ref(key_input)?;
let value = self.projections.value.forward_ref(value_input)?;
let q = AttentionUtils::split_heads(&query, self.config.num_heads, self.config.head_dim)?;
let k = AttentionUtils::split_heads(&key, self.config.num_heads, self.config.head_dim)?;
let v = AttentionUtils::split_heads(&value, self.config.num_heads, self.config.head_dim)?;
AttentionUtils::validate_attention_dims(
&q,
&k,
&v,
self.config.num_heads,
self.config.head_dim,
)?;
let attention_output = if self.use_flash_attention_2 {
self.compute_flash_attention_2(&q, &k, &v, attention_mask)?
} else {
self.compute_flash_attention_1(&q, &k, &v, attention_mask)?
};
let combined = AttentionUtils::combine_heads(
&attention_output,
self.config.num_heads,
self.config.head_dim,
)?;
self.projections.out_proj.forward(combined)
}
fn compute_flash_attention_1(
&self,
q: &Tensor,
k: &Tensor,
v: &Tensor,
attention_mask: Option<&Tensor>,
) -> Result<Tensor> {
let params = FlashParams::new(self.config.head_dim, self.causal, self.block_size)
.with_dropout(self.active_dropout())?;
flash_attention(q, k, v, attention_mask, ¶ms)
}
fn compute_flash_attention_2(
&self,
q: &Tensor,
k: &Tensor,
v: &Tensor,
attention_mask: Option<&Tensor>,
) -> Result<Tensor> {
let q_shape = q.shape();
let seq_q = q_shape[q_shape.len() - 2];
let seq_k = {
let k_shape = k.shape();
k_shape[k_shape.len() - 2]
};
let block_size = self.compute_adaptive_block_size(seq_q, seq_k);
let params = FlashParams::new(self.config.head_dim, self.causal, block_size)
.with_dropout(self.active_dropout())?;
flash_attention(q, k, v, attention_mask, ¶ms)
}
fn active_dropout(&self) -> Option<f32> {
if self.config.training {
Some(self.config.dropout_prob)
} else {
None
}
}
pub fn estimate_memory_usage(&self, batch_size: usize, seq_len: usize) -> usize {
let projection_memory = batch_size * seq_len * self.config.hidden_size * 4; let block_memory = batch_size * self.config.num_heads * self.block_size * self.block_size;
let intermediate_memory =
batch_size * self.config.num_heads * seq_len * self.config.head_dim * 3;
(projection_memory + block_memory + intermediate_memory) * 4 }
pub fn compute_optimal_block_size(
&self,
seq_len: usize,
available_memory_mb: Option<usize>,
) -> usize {
AttentionUtils::compute_block_size(seq_len, self.config.head_dim, available_memory_mb)
}
pub fn update_block_size(&mut self, seq_len: usize, available_memory_mb: Option<usize>) {
self.block_size = self.compute_optimal_block_size(seq_len, available_memory_mb);
}
fn compute_adaptive_block_size(&self, seq_q: usize, seq_k: usize) -> usize {
let base_block_size = self.block_size.max(1);
let adaptive_size = if seq_q > 4096 || seq_k > 4096 {
(base_block_size * 2).min(1024)
} else if seq_q < 128 && seq_k < 128 {
(base_block_size / 2).max(1)
} else {
base_block_size
};
adaptive_size.clamp(1, seq_q.max(seq_k).max(1))
}
}
impl Layer for FlashAttention {
type Input = Tensor;
type Output = Tensor;
fn forward(&self, input: Self::Input) -> Result<Self::Output> {
self.forward_self_attention(&input, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layers::attention::multi_head::MultiHeadAttention;
use crate::tensor::Tensor;
fn max_abs_difference(a: &[f32], b: &[f32]) -> f32 {
assert_eq!(a.len(), b.len(), "output length mismatch");
a.iter().zip(b.iter()).fold(0.0f32, |acc, (x, y)| acc.max((x - y).abs()))
}
fn deterministic(shape: &[usize], seed: u32) -> Tensor {
let count: usize = shape.iter().product();
let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(12_345);
let mut data = Vec::with_capacity(count);
for _ in 0..count {
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
let unit = (state >> 8) as f32 / (1u32 << 24) as f32;
data.push(unit * 2.0 - 1.0);
}
Tensor::from_vec(data, shape).expect("test tensor shape must be valid")
}
fn projection_parameters(hidden_size: usize, seed: u32) -> ([Tensor; 4], [Tensor; 4]) {
let weights = [
deterministic(&[hidden_size, hidden_size], seed),
deterministic(&[hidden_size, hidden_size], seed + 1),
deterministic(&[hidden_size, hidden_size], seed + 2),
deterministic(&[hidden_size, hidden_size], seed + 3),
];
let biases = [
deterministic(&[hidden_size], seed + 4),
deterministic(&[hidden_size], seed + 5),
deterministic(&[hidden_size], seed + 6),
deterministic(&[hidden_size], seed + 7),
];
(weights, biases)
}
fn apply_parameters(
attention: &mut FlashAttention,
weights: &[Tensor; 4],
biases: &[Tensor; 4],
) {
attention.set_query_weight(weights[0].clone()).expect("q weight");
attention.set_key_weight(weights[1].clone()).expect("k weight");
attention.set_value_weight(weights[2].clone()).expect("v weight");
attention.set_out_proj_weight(weights[3].clone()).expect("o weight");
attention.set_query_bias(biases[0].clone()).expect("q bias");
attention.set_key_bias(biases[1].clone()).expect("k bias");
attention.set_value_bias(biases[2].clone()).expect("v bias");
attention.set_out_proj_bias(biases[3].clone()).expect("o bias");
}
#[test]
fn test_flash_attention_creation() {
let attention =
FlashAttention::new(512, 8, 0.1, true, None, false).expect("operation failed in test");
assert_eq!(attention.config.hidden_size, 512);
assert_eq!(attention.config.num_heads, 8);
assert_eq!(attention.config.head_dim, 64);
}
#[test]
fn test_flash_attention_with_custom_block_size() {
let attention = FlashAttention::new(512, 8, 0.1, true, Some(128), false)
.expect("operation failed in test");
assert_eq!(attention.block_size(), 128);
}
#[test]
fn test_flash_attention_2_version() {
let attention = FlashAttention::new_with_version(512, 8, 0.1, true, None, false, true)
.expect("operation failed in test");
assert!(attention.is_using_flash_attention_2());
}
#[test]
fn test_flash_attention_forward() {
let attention =
FlashAttention::new(512, 8, 0.1, true, None, false).expect("operation failed in test");
let input = deterministic(&[2, 10, 512], 1);
let output = attention.forward(input).expect("Forward pass failed");
assert_eq!(output.shape(), vec![2, 10, 512]);
}
#[test]
fn test_memory_estimation() {
let attention =
FlashAttention::new(512, 8, 0.1, true, None, false).expect("operation failed in test");
let memory_usage = attention.estimate_memory_usage(2, 1000);
assert!(memory_usage > 0);
}
#[test]
fn test_optimal_block_size_computation() {
let attention =
FlashAttention::new(512, 8, 0.1, true, None, false).expect("operation failed in test");
let block_size = attention.compute_optimal_block_size(2048, Some(1024));
assert!(block_size > 0);
assert!(block_size <= 512);
}
#[test]
fn test_causal_attention() {
let attention =
FlashAttention::new(512, 8, 0.1, true, None, true).expect("operation failed in test");
let input = deterministic(&[2, 10, 512], 2);
let output = attention.forward(input).expect("Forward pass failed");
assert_eq!(output.shape(), vec![2, 10, 512]);
}
#[test]
fn forward_output_is_not_constant() {
let attention =
FlashAttention::new(64, 4, 0.0, true, Some(8), false).expect("construction failed");
let first =
attention.forward(deterministic(&[1, 12, 64], 3)).expect("first forward failed");
let second = attention
.forward(deterministic(&[1, 12, 64], 4))
.expect("second forward failed");
let first_data = first.data().expect("data");
let second_data = second.data().expect("data");
assert!(
max_abs_difference(&first_data, &second_data) > 1e-3,
"FlashAttention output must depend on its input"
);
let row0 = &first_data[0..64];
let row1 = &first_data[64..128];
assert!(
max_abs_difference(row0, row1) > 1e-4,
"FlashAttention output must vary across sequence positions"
);
}
#[test]
fn flash_attention_matches_standard_attention() {
let hidden_size = 64;
let num_heads = 4;
let mut flash = FlashAttention::new(hidden_size, num_heads, 0.0, true, Some(5), false)
.expect("flash construction failed");
let mut standard = MultiHeadAttention::new(hidden_size, num_heads, 0.0, true)
.expect("standard construction failed");
let (weights, biases) = projection_parameters(hidden_size, 5);
apply_parameters(&mut flash, &weights, &biases);
standard.set_query_weight(weights[0].clone()).expect("q weight");
standard.set_key_weight(weights[1].clone()).expect("k weight");
standard.set_value_weight(weights[2].clone()).expect("v weight");
standard.set_out_proj_weight(weights[3].clone()).expect("o weight");
standard.set_query_bias(biases[0].clone()).expect("q bias");
standard.set_key_bias(biases[1].clone()).expect("k bias");
standard.set_value_bias(biases[2].clone()).expect("v bias");
standard.set_out_proj_bias(biases[3].clone()).expect("o bias");
let input = deterministic(&[2, 13, hidden_size], 13);
let flash_output =
flash.forward_self_attention(&input, None).expect("flash forward failed");
let standard_output = standard
.forward_self_attention(&input, None, false)
.expect("standard forward failed");
let difference = max_abs_difference(
&flash_output.data().expect("flash data"),
&standard_output.data().expect("standard data"),
);
assert!(
difference < 1e-4,
"FlashAttention disagrees with standard attention by {difference}"
);
}
#[test]
fn flash_attention_1_and_2_agree() {
for causal in [false, true] {
let hidden_size = 32;
let num_heads = 4;
let mut version_1 = FlashAttention::new_with_version(
hidden_size,
num_heads,
0.0,
true,
Some(4),
causal,
false,
)
.expect("v1 construction failed");
let mut version_2 = FlashAttention::new_with_version(
hidden_size,
num_heads,
0.0,
true,
Some(4),
causal,
true,
)
.expect("v2 construction failed");
let (weights, biases) = projection_parameters(hidden_size, 14);
apply_parameters(&mut version_1, &weights, &biases);
apply_parameters(&mut version_2, &weights, &biases);
let input = deterministic(&[1, 17, hidden_size], 22);
let output_1 =
version_1.forward_self_attention(&input, None).expect("v1 forward failed");
let output_2 =
version_2.forward_self_attention(&input, None).expect("v2 forward failed");
let difference = max_abs_difference(
&output_1.data().expect("v1 data"),
&output_2.data().expect("v2 data"),
);
assert!(
difference < 1e-4,
"FlashAttention-1 and -2 disagree by {difference} (causal={causal})"
);
}
}
#[test]
fn causal_flash_attention_ignores_future_tokens() {
let hidden_size = 32;
let attention = FlashAttention::new(hidden_size, 4, 0.0, false, Some(3), true)
.expect("construction failed");
let base_input = deterministic(&[1, 10, hidden_size], 23);
let mut perturbed_data = base_input.data().expect("input data");
for position in 5..10 {
for feature in 0..hidden_size {
perturbed_data[position * hidden_size + feature] += 2.0;
}
}
let perturbed_input = Tensor::from_vec(perturbed_data, &[1, 10, hidden_size])
.expect("perturbed tensor shape");
let base = attention
.forward_self_attention(&base_input, None)
.expect("base forward failed");
let perturbed = attention
.forward_self_attention(&perturbed_input, None)
.expect("perturbed forward failed");
let base_data = base.data().expect("base data");
let perturbed_data = perturbed.data().expect("perturbed data");
let prefix = 5 * hidden_size;
assert!(
max_abs_difference(&base_data[..prefix], &perturbed_data[..prefix]) < 1e-4,
"causal FlashAttention leaked information from future tokens"
);
assert!(
max_abs_difference(&base_data[prefix..], &perturbed_data[prefix..]) > 1e-3,
"perturbation had no effect at all"
);
}
}