use crate::autograd::Variable;
use crate::tensor::{Device, Result, Tensor};
use super::init;
use super::parameter::Parameter;
use super::rope::RotaryEmbedding;
use super::Module;
pub struct MultiheadAttention {
q_proj: Linear,
k_proj: Linear,
v_proj: Linear,
out_proj: Linear,
num_heads: i64,
head_dim: i64,
scale: f64,
rotary: Option<RotaryEmbedding>,
}
struct Linear {
weight: Parameter,
bias: Parameter,
}
impl Linear {
fn on_device(in_features: i64, out_features: i64, device: Device) -> Result<Self> {
let w = init::xavier_uniform(
&[out_features, in_features], in_features, out_features, device,
)?;
let b = Tensor::zeros(
&[out_features],
crate::tensor::TensorOptions { dtype: crate::tensor::DType::Float32, device },
)?;
Ok(Linear {
weight: Parameter::new(w, "weight"),
bias: Parameter::new(b, "bias"),
})
}
fn forward(&self, input: &Variable) -> Result<Variable> {
crate::autograd::linear(
input,
&self.weight.variable,
Some(&self.bias.variable),
)
}
fn parameters(&self, prefix: &str) -> Vec<Parameter> {
vec![
Parameter {
variable: self.weight.variable.clone(),
name: format!("{prefix}.weight"),
},
Parameter {
variable: self.bias.variable.clone(),
name: format!("{prefix}.bias"),
},
]
}
}
impl MultiheadAttention {
pub fn new(embed_dim: i64, num_heads: i64) -> Result<Self> {
Self::on_device(embed_dim, num_heads, Device::CPU)
}
pub fn on_device(embed_dim: i64, num_heads: i64, device: Device) -> Result<Self> {
if num_heads <= 0 || embed_dim % num_heads != 0 {
return Err(crate::tensor::TensorError::new(&format!(
"MultiheadAttention: embed_dim ({embed_dim}) must be divisible by \
a positive num_heads ({num_heads})"
)));
}
let head_dim = embed_dim / num_heads;
Ok(MultiheadAttention {
q_proj: Linear::on_device(embed_dim, embed_dim, device)?,
k_proj: Linear::on_device(embed_dim, embed_dim, device)?,
v_proj: Linear::on_device(embed_dim, embed_dim, device)?,
out_proj: Linear::on_device(embed_dim, embed_dim, device)?,
num_heads,
head_dim,
scale: 1.0 / (head_dim as f64).sqrt(),
rotary: None,
})
}
pub fn rotary(mut self, rope: RotaryEmbedding) -> Result<Self> {
if rope.head_dim() != self.head_dim {
return Err(crate::tensor::TensorError::new(&format!(
"MultiheadAttention::rotary: RotaryEmbedding head_dim ({}) \
does not match per-head dim ({})",
rope.head_dim(),
self.head_dim
)));
}
self.rotary = Some(rope);
Ok(self)
}
pub fn forward_ext(
&self,
query: &Variable,
key: &Variable,
value: &Variable,
mask: Option<&Tensor>,
) -> Result<Variable> {
let q_shape = query.shape();
let k_shape = key.shape();
if q_shape.len() < 3 || k_shape.len() < 3 {
return Err(crate::tensor::TensorError::new(&format!(
"MultiheadAttention::forward_ext expects 3-D [batch, seq, embed] \
query/key, got query {q_shape:?}, key {k_shape:?}"
)));
}
let batch = q_shape[0];
let seq_q = q_shape[1];
let seq_k = k_shape[1];
let q = self.q_proj.forward(query)?;
let k = self.k_proj.forward(key)?;
let v = self.v_proj.forward(value)?;
let q = q.reshape(&[batch, seq_q, self.num_heads, self.head_dim])?
.transpose(1, 2)?;
let k = k.reshape(&[batch, seq_k, self.num_heads, self.head_dim])?
.transpose(1, 2)?;
let v = v.reshape(&[batch, seq_k, self.num_heads, self.head_dim])?
.transpose(1, 2)?;
let (q, k) = match &self.rotary {
Some(rope) => rope.apply(&q, &k)?,
None => (q, k),
};
let add_mask = match mask {
Some(m) => Some(
Tensor::zeros(
&m.shape(),
crate::tensor::TensorOptions {
dtype: crate::tensor::DType::Float32,
device: m.device(),
},
)?
.masked_fill(m, f64::NEG_INFINITY)?,
),
None => None,
};
let out = crate::autograd::scaled_dot_product_attention(
&q, &k, &v,
add_mask.as_ref(),
0.0,
false,
Some(self.scale),
)?;
let out = out.transpose(1, 2)?
.reshape(&[batch, seq_q, self.num_heads * self.head_dim])?;
self.out_proj.forward(&out)
}
}
impl Module for MultiheadAttention {
fn name(&self) -> &str { "multihead_attention" }
fn forward(&self, input: &Variable) -> Result<Variable> {
self.forward_ext(input, input, input, None)
}
fn parameters(&self) -> Vec<Parameter> {
let mut params = Vec::new();
params.extend(self.q_proj.parameters("q_proj"));
params.extend(self.k_proj.parameters("k_proj"));
params.extend(self.v_proj.parameters("v_proj"));
params.extend(self.out_proj.parameters("out_proj"));
params
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tensor::test_device;
#[test]
fn test_mha_indivisible_heads_is_err_not_panic() {
let device = test_device();
let err = MultiheadAttention::on_device(10, 3, device).map(|_| ()).unwrap_err();
assert!(err.to_string().contains("divisible"), "unexpected: {err}");
assert!(MultiheadAttention::on_device(8, 0, device).is_err());
}
#[test]
fn test_mha_self_attention() {
let device = test_device();
let mha = MultiheadAttention::on_device(8, 2, device).unwrap();
let opts = crate::tensor::test_opts();
let x = Variable::new(
Tensor::randn(&[2, 4, 8], opts).unwrap(), false,
);
let y = mha.forward(&x).unwrap();
assert_eq!(y.shape(), vec![2, 4, 8]);
}
#[test]
fn test_mha_cross_attention() {
let device = test_device();
let mha = MultiheadAttention::on_device(8, 2, device).unwrap();
let opts = crate::tensor::test_opts();
let q = Variable::new(Tensor::randn(&[1, 3, 8], opts).unwrap(), false);
let kv = Variable::new(Tensor::randn(&[1, 5, 8], opts).unwrap(), false);
let y = mha.forward_ext(&q, &kv, &kv, None).unwrap();
assert_eq!(y.shape(), vec![1, 3, 8]); }
#[test]
fn test_mha_causal_mask() {
let device = test_device();
let mha = MultiheadAttention::on_device(8, 2, device).unwrap();
let opts = crate::tensor::test_opts();
let x = Variable::new(Tensor::randn(&[1, 4, 8], opts).unwrap(), false);
let mask = Tensor::ones(&[4, 4], opts).unwrap().triu(1).unwrap();
let y = mha.forward_ext(&x, &x, &x, Some(&mask)).unwrap();
assert_eq!(y.shape(), vec![1, 4, 8]);
}
#[test]
fn test_mha_gradient() {
let device = test_device();
let mha = MultiheadAttention::on_device(8, 2, device).unwrap();
let opts = crate::tensor::test_opts();
let x = Variable::new(Tensor::randn(&[1, 3, 8], opts).unwrap(), true);
let y = mha.forward(&x).unwrap();
let loss = y.sum().unwrap();
loss.backward().unwrap();
let grad = x.grad().unwrap();
assert_eq!(grad.shape(), vec![1, 3, 8]);
}
#[test]
fn test_mha_parameters() {
let mha = MultiheadAttention::new(16, 4).unwrap();
let params = mha.parameters();
assert_eq!(params.len(), 8);
}
#[test]
fn test_mha_rotary_head_dim_mismatch_is_err() {
let device = test_device();
let rope = RotaryEmbedding::on_device(8, 16, device).unwrap();
let err = MultiheadAttention::on_device(8, 2, device)
.unwrap()
.rotary(rope)
.map(|_| ())
.unwrap_err();
assert!(err.to_string().contains("head_dim"), "unexpected: {err}");
}
#[test]
fn test_mha_rotary_forward_and_gradient() {
let device = test_device();
let rope = RotaryEmbedding::on_device(4, 16, device).unwrap();
let mha = MultiheadAttention::on_device(8, 2, device)
.unwrap()
.rotary(rope)
.unwrap();
let opts = crate::tensor::test_opts();
let x = Variable::new(Tensor::randn(&[2, 5, 8], opts).unwrap(), true);
let y = mha.forward(&x).unwrap();
assert_eq!(y.shape(), vec![2, 5, 8]);
y.sum().unwrap().backward().unwrap();
assert_eq!(x.grad().unwrap().shape(), vec![2, 5, 8]);
}
#[test]
fn test_sdpa_core_matches_explicit_score_chain() {
let opts = crate::tensor::test_opts();
let (b, h, lq, lk, e) = (2, 3, 5, 6, 4);
let q = Tensor::randn(&[b, h, lq, e], opts).unwrap();
let k = Tensor::randn(&[b, h, lk, e], opts).unwrap();
let v = Tensor::randn(&[b, h, lk, e], opts).unwrap();
let scale = 1.0 / (e as f64).sqrt();
let mask = Tensor::ones(&[lq, lk], opts).unwrap().triu(2).unwrap();
for m in [None, Some(&mask)] {
let mut scores = q
.matmul(&k.transpose(2, 3).unwrap()).unwrap()
.mul_scalar(scale).unwrap();
if let Some(m) = m {
scores = scores.masked_fill(m, f64::NEG_INFINITY).unwrap();
}
let reference = scores.softmax(-1).unwrap().matmul(&v).unwrap();
let add_mask = m.map(|m| {
Tensor::zeros(&m.shape(), opts).unwrap()
.masked_fill(m, f64::NEG_INFINITY).unwrap()
});
let fused = Tensor::scaled_dot_product_attention(
&q, &k, &v, add_mask.as_ref(), 0.0, false, Some(scale),
).unwrap();
let diff = fused.sub(&reference).unwrap().abs().unwrap()
.max().unwrap().item().unwrap();
assert!(
diff < 1e-5,
"SDPA diverged from the explicit chain (masked={}): max |Δ| = {diff}",
m.is_some(),
);
}
}
#[test]
fn test_mha_rotary_with_causal_mask() {
let device = test_device();
let rope = RotaryEmbedding::on_device(4, 8, device).unwrap();
let mha = MultiheadAttention::on_device(8, 2, device)
.unwrap()
.rotary(rope)
.unwrap();
let opts = crate::tensor::test_opts();
let x = Variable::new(Tensor::randn(&[1, 4, 8], opts).unwrap(), false);
let mask = Tensor::ones(&[4, 4], opts).unwrap().triu(1).unwrap();
let y = mha.forward_ext(&x, &x, &x, Some(&mask)).unwrap();
assert_eq!(y.shape(), vec![1, 4, 8]);
}
}