use super::KvCache;
use crate::flash_attention::{AttentionVariant, FlashAttention, FlashAttentionConfig};
use candle_core::{Result as CandleResult, Tensor, D};
#[allow(dead_code)]
const FLASH_ATTENTION_THRESHOLD: usize = 2048;
pub fn attention_with_cache(
q: &Tensor,
k: &Tensor,
v: &Tensor,
cache: &mut dyn KvCache,
num_heads: usize,
num_kv_heads: usize,
mask: Option<&Tensor>,
) -> CandleResult<Tensor> {
cache.append(k, v)?;
if cache.supports_fused_attention() {
let head_dim = q.dims()[3];
let scale = 1.0 / (head_dim as f32).sqrt();
cache.forward_attention(q, num_heads, scale)
} else {
let (k_full, v_full) = cache
.get_kv()?
.ok_or_else(|| candle_core::Error::Msg("Cache returned empty K/V".to_string()))?;
let k_full = repeat_kv(k_full, num_heads / num_kv_heads)?;
let v_full = repeat_kv(v_full, num_heads / num_kv_heads)?;
let seq_len = k_full.dims()[2];
let mode = AttentionVariant::recommended_for_seq_len(seq_len);
match mode {
AttentionVariant::Flash => {
attention_flash(q, &k_full, &v_full, mask)
},
_ => {
attention_standard(q, &k_full, &v_full, mask)
},
}
}
}
pub fn attention_with_cache_mode(
q: &Tensor,
k: &Tensor,
v: &Tensor,
cache: &mut dyn KvCache,
num_heads: usize,
num_kv_heads: usize,
mask: Option<&Tensor>,
mode: AttentionVariant,
) -> CandleResult<Tensor> {
cache.append(k, v)?;
if cache.supports_fused_attention() {
let head_dim = q.dims()[3];
let scale = 1.0 / (head_dim as f32).sqrt();
cache.forward_attention(q, num_heads, scale)
} else {
let (k_full, v_full) = cache
.get_kv()?
.ok_or_else(|| candle_core::Error::Msg("Cache returned empty K/V".to_string()))?;
let k_full = repeat_kv(k_full, num_heads / num_kv_heads)?;
let v_full = repeat_kv(v_full, num_heads / num_kv_heads)?;
match mode {
AttentionVariant::Flash => attention_flash(q, &k_full, &v_full, mask),
_ => attention_standard(q, &k_full, &v_full, mask),
}
}
}
fn attention_standard(
q: &Tensor,
k: &Tensor,
v: &Tensor,
mask: Option<&Tensor>,
) -> CandleResult<Tensor> {
let head_dim = q.dims()[3];
let scale = 1.0 / (head_dim as f32).sqrt();
let attn_scores = q.matmul(&k.transpose(D::Minus2, D::Minus1)?)?;
let attn_scores = (attn_scores * (scale as f64))?;
let attn_scores = match mask {
Some(m) => attn_scores.broadcast_add(m)?,
None => attn_scores,
};
let attn_weights = candle_nn::ops::softmax_last_dim(&attn_scores)?;
attn_weights.matmul(v)
}
fn attention_flash(
q: &Tensor,
k: &Tensor,
v: &Tensor,
mask: Option<&Tensor>,
) -> CandleResult<Tensor> {
let flash_attn = FlashAttention::new(FlashAttentionConfig::default());
flash_attn.forward(q, k, v, mask, Some(true))
}
pub fn repeat_kv(x: Tensor, n_rep: usize) -> CandleResult<Tensor> {
if n_rep == 1 {
return Ok(x);
}
let (batch, num_kv_heads, seq_len, head_dim) = x.dims4()?;
x.unsqueeze(2)?
.expand((batch, num_kv_heads, n_rep, seq_len, head_dim))?
.reshape((batch, num_kv_heads * n_rep, seq_len, head_dim))
}
pub fn create_causal_mask(
seq_len: usize,
dtype: candle_core::DType,
device: &candle_core::Device,
) -> CandleResult<Tensor> {
let mask: Vec<f32> = (0..seq_len)
.flat_map(|i| (0..seq_len).map(move |j| if j > i { f32::NEG_INFINITY } else { 0.0 }))
.collect();
Tensor::from_vec(mask, (1, 1, seq_len, seq_len), device)?.to_dtype(dtype)
}
#[derive(Debug, Clone)]
pub struct AttentionConfig {
pub num_heads: usize,
pub num_kv_heads: usize,
pub head_dim: usize,
pub causal: bool,
}
impl AttentionConfig {
pub fn new(num_heads: usize, num_kv_heads: usize, head_dim: usize) -> Self {
Self {
num_heads,
num_kv_heads,
head_dim,
causal: true,
}
}
pub fn with_causal(mut self, causal: bool) -> Self {
self.causal = causal;
self
}
pub fn scale(&self) -> f32 {
1.0 / (self.head_dim as f32).sqrt()
}
pub fn gqa_rep(&self) -> usize {
self.num_heads / self.num_kv_heads
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::attention_cache::StandardCache;
use candle_core::{DType, Device};
#[test]
fn test_repeat_kv_no_repeat() -> CandleResult<()> {
let device = Device::Cpu;
let x = Tensor::zeros((1, 8, 10, 64), DType::F32, &device)?;
let y = repeat_kv(x.clone(), 1)?;
assert_eq!(x.dims(), y.dims());
Ok(())
}
#[test]
fn test_repeat_kv_gqa() -> CandleResult<()> {
let device = Device::Cpu;
let x = Tensor::zeros((1, 2, 10, 64), DType::F32, &device)?;
let y = repeat_kv(x, 4)?;
assert_eq!(y.dims(), &[1, 8, 10, 64]);
Ok(())
}
#[test]
fn test_causal_mask() -> CandleResult<()> {
let device = Device::Cpu;
let mask = create_causal_mask(4, DType::F32, &device)?;
assert_eq!(mask.dims(), &[1, 1, 4, 4]);
let values: Vec<f32> = mask.flatten_all()?.to_vec1()?;
assert_eq!(values[0], 0.0); assert_eq!(values[4], 0.0); assert_eq!(values[5], 0.0);
assert!(values[1].is_infinite() && values[1] < 0.0); assert!(values[2].is_infinite() && values[2] < 0.0);
Ok(())
}
#[test]
fn test_attention_with_standard_cache() -> CandleResult<()> {
let device = Device::Cpu;
let batch = 1;
let num_heads = 8;
let num_kv_heads = 2;
let seq_len = 5;
let head_dim = 64;
let mut cache = StandardCache::new();
let q = Tensor::randn(0.0f32, 0.1, (batch, num_heads, seq_len, head_dim), &device)?;
let k = Tensor::randn(
0.0f32,
0.1,
(batch, num_kv_heads, seq_len, head_dim),
&device,
)?;
let v = Tensor::randn(
0.0f32,
0.1,
(batch, num_kv_heads, seq_len, head_dim),
&device,
)?;
let output = attention_with_cache(&q, &k, &v, &mut cache, num_heads, num_kv_heads, None)?;
assert_eq!(output.dims(), &[batch, num_heads, seq_len, head_dim]);
assert_eq!(cache.seq_len(), seq_len);
Ok(())
}
#[test]
fn test_attention_config() {
let config = AttentionConfig::new(32, 8, 128);
assert_eq!(config.num_heads, 32);
assert_eq!(config.num_kv_heads, 8);
assert_eq!(config.gqa_rep(), 4);
assert!((config.scale() - 0.0884).abs() < 0.001);
}
}