use ferrotensor_backend::{AttentionOptions, ComputeEngine, UnifiedTensor, WgpuContext};
use std::sync::Arc;
pub struct FlashAttention {
dim: usize,
num_heads: usize,
head_dim: usize,
causal: bool,
#[allow(dead_code)]
scale: f32,
#[allow(dead_code)]
use_multi_head_kernel: bool,
}
pub struct KVCache {
pub k_cache: Vec<Arc<UnifiedTensor>>,
pub v_cache: Vec<Arc<UnifiedTensor>>,
pub max_seq_len: usize,
pub current_pos: usize,
}
impl FlashAttention {
pub fn new(dim: usize, num_heads: usize, causal: bool) -> Self {
let head_dim = dim / num_heads;
let scale = 1.0 / (head_dim as f32).sqrt();
Self {
dim,
num_heads,
head_dim,
causal,
scale,
use_multi_head_kernel: num_heads > 1,
}
}
fn heads_view(&self, t: &UnifiedTensor) -> UnifiedTensor {
let (batch, seq) = match t.shape.len() {
2 => (1, t.shape[0]),
3 | 4 => (t.shape[0], t.shape[1]),
n => panic!("FlashAttention expects a 2-D, 3-D or 4-D tensor, got {n}-D"),
};
assert_eq!(
t.shape.iter().product::<usize>(),
batch * seq * self.dim,
"tensor {:?} does not have dim {} per token",
t.shape,
self.dim
);
t.reshaped(&[batch, seq, self.num_heads, self.head_dim])
}
pub fn forward(
&self,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
ctx: &WgpuContext,
engine: &ComputeEngine,
) -> UnifiedTensor {
let output = UnifiedTensor::empty(ctx, &q.shape);
engine.flash_attention_v2(
ctx,
&self.heads_view(q),
&self.heads_view(k),
&self.heads_view(v),
&self.heads_view(&output),
self.causal,
0.0,
0,
None,
);
output
}
pub fn forward_multi_head(
&self,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
ctx: &WgpuContext,
engine: &ComputeEngine,
) -> UnifiedTensor {
self.forward(q, k, v, ctx, engine)
}
pub fn multi_head_forward(
&self,
hidden: &UnifiedTensor,
ctx: &WgpuContext,
engine: &ComputeEngine,
) -> UnifiedTensor {
self.forward(hidden, hidden, hidden, ctx, engine)
}
pub fn memory_savings(&self, seq_len: usize) -> String {
let standard_memory = seq_len * seq_len * 4; let flash_memory = seq_len * self.head_dim * 4; let savings = standard_memory as f32 / flash_memory as f32;
format!(
"{:.1}x less memory ({}MB vs {}MB for seq={})",
savings,
flash_memory / 1_000_000,
standard_memory / 1_000_000,
seq_len
)
}
#[allow(clippy::too_many_arguments)]
pub fn backward(
&self,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
output: &UnifiedTensor,
grad_output: &UnifiedTensor,
ctx: &WgpuContext,
engine: &ComputeEngine,
) -> FlashAttentionGrads {
let grad_q = UnifiedTensor::empty(ctx, &q.shape);
let grad_k = UnifiedTensor::empty(ctx, &k.shape);
let grad_v = UnifiedTensor::empty(ctx, &v.shape);
engine.flash_attention_v2_backward(
ctx,
&self.heads_view(q),
&self.heads_view(k),
&self.heads_view(v),
&self.heads_view(output),
&self.heads_view(grad_output),
&self.heads_view(&grad_q),
&self.heads_view(&grad_k),
&self.heads_view(&grad_v),
self.causal,
);
FlashAttentionGrads {
grad_q,
grad_k,
grad_v,
}
}
}
#[derive(Debug)]
pub struct FlashAttentionGrads {
pub grad_q: UnifiedTensor,
pub grad_k: UnifiedTensor,
pub grad_v: UnifiedTensor,
}
impl std::fmt::Debug for FlashAttention {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlashAttention")
.field("dim", &self.dim)
.field("num_heads", &self.num_heads)
.field("head_dim", &self.head_dim)
.field("causal", &self.causal)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_savings() {
let attn = FlashAttention::new(512, 8, true);
let savings = attn.memory_savings(2048);
assert!(savings.contains("x less memory"));
}
#[test]
fn test_creation() {
let attn = FlashAttention::new(768, 12, true);
assert_eq!(attn.dim, 768);
assert_eq!(attn.num_heads, 12);
assert_eq!(attn.head_dim, 64);
assert!(attn.causal);
}
}
pub struct GroupedQueryAttention {
pub dim: usize,
pub num_heads: usize,
pub num_kv_heads: usize,
pub head_dim: usize,
pub num_groups: usize,
pub causal: bool,
#[allow(dead_code)]
scale: f32,
pub dropout_prob: f32,
}
impl GroupedQueryAttention {
pub fn new(
dim: usize,
num_heads: usize,
num_kv_heads: usize,
causal: bool,
dropout_prob: f32,
) -> Self {
assert!(
num_heads.is_multiple_of(num_kv_heads),
"num_heads ({}) must be divisible by num_kv_heads ({})",
num_heads,
num_kv_heads
);
let head_dim = dim / num_heads;
let num_groups = num_heads / num_kv_heads;
let scale = 1.0 / (head_dim as f32).sqrt();
Self {
dim,
num_heads,
num_kv_heads,
head_dim,
num_groups,
causal,
scale,
dropout_prob,
}
}
pub fn forward(
&self,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
mask: Option<&UnifiedTensor>,
ctx: &WgpuContext,
engine: &ComputeEngine,
) -> UnifiedTensor {
self.forward_at(q, k, v, mask, 0, ctx, engine)
}
#[allow(clippy::too_many_arguments)]
pub fn forward_at(
&self,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
mask: Option<&UnifiedTensor>,
pos: usize,
ctx: &WgpuContext,
engine: &ComputeEngine,
) -> UnifiedTensor {
if q.shape.len() != 4 || k.shape.len() != 4 || v.shape.len() != 4 {
panic!("GroupedQueryAttention expects 4D tensors (batch, seq, heads, dim)");
}
let batch_size = q.shape[0];
let seq_q = q.shape[1];
let output = UnifiedTensor::empty(ctx, &[batch_size, seq_q, self.num_heads, self.head_dim]);
let seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
let opts = AttentionOptions {
causal: self.causal,
causal_offset: pos as u32,
dropout_prob: self.dropout_prob,
seed,
..AttentionOptions::default()
};
engine.flash_attention_v2_with(ctx, q, k, v, &output, &opts, mask);
output
}
pub fn memory_savings(&self) -> String {
let mha_kv_params = 2 * self.num_heads * self.head_dim;
let gqa_kv_params = 2 * self.num_kv_heads * self.head_dim;
let savings = mha_kv_params as f32 / gqa_kv_params as f32;
format!(
"{:.1}x KV memory savings ({} vs {} KV heads)",
savings, self.num_kv_heads, self.num_heads
)
}
}
pub struct MultiQueryAttention {
pub dim: usize,
pub num_heads: usize,
pub head_dim: usize,
pub causal: bool,
gqa: GroupedQueryAttention,
}
impl MultiQueryAttention {
pub fn new(dim: usize, num_heads: usize, causal: bool, dropout_prob: f32) -> Self {
let head_dim = dim / num_heads;
let gqa = GroupedQueryAttention::new(dim, num_heads, 1, causal, dropout_prob);
Self {
dim,
num_heads,
head_dim,
causal,
gqa,
}
}
pub fn forward(
&self,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
mask: Option<&UnifiedTensor>,
ctx: &WgpuContext,
engine: &ComputeEngine,
) -> UnifiedTensor {
self.gqa.forward(q, k, v, mask, ctx, engine)
}
pub fn memory_savings(&self) -> String {
format!(
"{}x KV memory savings (1 vs {} KV heads)",
self.num_heads, self.num_heads
)
}
}
pub struct SlidingWindowAttention {
pub dim: usize,
pub num_heads: usize,
pub head_dim: usize,
pub window_size: usize,
pub causal: bool,
#[allow(dead_code)]
scale: f32,
}
impl SlidingWindowAttention {
pub fn new(dim: usize, num_heads: usize, window_size: usize, causal: bool) -> Self {
let head_dim = dim / num_heads;
let scale = 1.0 / (head_dim as f32).sqrt();
Self {
dim,
num_heads,
head_dim,
window_size,
causal,
scale,
}
}
pub fn forward(
&self,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
ctx: &WgpuContext,
engine: &ComputeEngine,
) -> UnifiedTensor {
let view = |t: &UnifiedTensor| {
t.reshaped(&[t.shape[0], t.shape[1], self.num_heads, self.head_dim])
};
let output = UnifiedTensor::empty(ctx, &q.shape);
let opts = AttentionOptions {
causal: self.causal,
window: self.window_size as u32,
..AttentionOptions::default()
};
engine.flash_attention_v2_with(
ctx,
&view(q),
&view(k),
&view(v),
&view(&output),
&opts,
None,
);
output
}
pub fn memory_savings(&self, seq_len: usize) -> String {
let full_attention = seq_len * seq_len;
let window_attention = seq_len * self.window_size.min(seq_len);
let savings = full_attention as f32 / window_attention as f32;
format!(
"{:.1}x attention savings (window={}, seq={})",
savings, self.window_size, seq_len
)
}
}
#[derive(Debug, Clone)]
pub enum AttentionMask {
None,
Causal,
Padding(Vec<usize>), SlidingWindow(usize),
Custom(Arc<UnifiedTensor>),
}
impl AttentionMask {
pub fn causal() -> Self {
Self::Causal
}
pub fn from_lengths(lengths: Vec<usize>) -> Self {
Self::Padding(lengths)
}
pub fn sliding_window(window_size: usize) -> Self {
Self::SlidingWindow(window_size)
}
}
impl std::fmt::Debug for GroupedQueryAttention {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GroupedQueryAttention")
.field("dim", &self.dim)
.field("num_heads", &self.num_heads)
.field("num_kv_heads", &self.num_kv_heads)
.field("num_groups", &self.num_groups)
.field("head_dim", &self.head_dim)
.field("causal", &self.causal)
.field("dropout", &self.dropout_prob)
.finish()
}
}
impl std::fmt::Debug for MultiQueryAttention {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MultiQueryAttention")
.field("dim", &self.dim)
.field("num_heads", &self.num_heads)
.field("head_dim", &self.head_dim)
.field("causal", &self.causal)
.finish()
}
}
impl std::fmt::Debug for SlidingWindowAttention {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SlidingWindowAttention")
.field("dim", &self.dim)
.field("num_heads", &self.num_heads)
.field("head_dim", &self.head_dim)
.field("window_size", &self.window_size)
.field("causal", &self.causal)
.finish()
}
}