use crate::ops::exp_neg;
use crate::par::{self, Shared};
pub const HEAD: usize = 64;
pub const QB: usize = 32;
pub fn attention(
qkv: &[f32],
heads: usize,
cu: &[usize],
window: Option<usize>,
out: &mut [f32],
threads: usize,
) {
let d = heads * HEAD;
let stride = 3 * d;
let t = out.len() / d.max(1);
assert_eq!(out.len(), t * d);
assert_eq!(qkv.len(), t * stride);
assert!(cu.first() == Some(&0) && cu.last() == Some(&t), "cu does not cover the batch");
assert!(cu.windows(2).all(|p| p[0] <= p[1]), "cu is not sorted");
if t == 0 || heads == 0 {
return;
}
let mut tasks = Vec::new();
for s in 0..cu.len() - 1 {
for q0 in (cu[s]..cu[s + 1]).step_by(QB) {
for h in 0..heads {
tasks.push((s, h, q0));
}
}
}
let shared = Shared::new(out);
par::for_each(tasks.len(), threads, |task| {
let (s, h, q0) = tasks[task];
unsafe { block(qkv, heads, (cu[s], cu[s + 1]), q0, h, window, &mut Vec::new(), &shared) };
});
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub unsafe fn block(
qkv: &[f32],
heads: usize,
(lo, hi): (usize, usize),
q0: usize,
h: usize,
window: Option<usize>,
p: &mut Vec<f32>,
out: &Shared<'_>,
) {
let d = heads * HEAD;
let stride = 3 * d;
let scale = 1.0 / (HEAD as f32).sqrt();
let q1 = (q0 + QB).min(hi);
if q0 >= q1 {
return;
}
let span = |i: usize| match window {
Some(w) => (i.saturating_sub(w).max(lo), (i + w + 1).min(hi)),
None => (lo, hi),
};
let (ka, kb) = (span(q0).0, span(q1 - 1).1);
let np = (kb - ka).next_multiple_of(KT);
let key = |j: usize| (ka + j).min(kb - 1) * stride + h * HEAD;
p.clear();
p.resize(QT * np, 0.0);
let s = &mut p[..];
for i0 in (q0..q1).step_by(QT) {
let m = (q1 - i0).min(QT);
let t0 = (span(i0).0 - ka) / KT * KT;
let t1 = (span(i0 + m - 1).1 - ka).next_multiple_of(KT);
let mut qs = [[0f32; HEAD]; QT];
for (r, q) in qs.iter_mut().enumerate().take(m) {
let src = &qkv[(i0 + r) * stride + h * HEAD..][..HEAD];
q.iter_mut().zip(src).for_each(|(q, &x)| *q = x * scale);
}
for j in (t0..t1).step_by(KT) {
let ks = std::array::from_fn(|t| qkv[d + key(j + t)..][..HEAD].try_into().unwrap());
let t = scores(&qs, ks);
for (r, t) in t.iter().enumerate() {
s[r * np + j..][..KT].copy_from_slice(t);
}
}
let mut inv = [0f32; QT];
for (r, inv) in inv.iter_mut().enumerate() {
let row = &mut s[r * np + t0..r * np + t1];
if r >= m {
row.fill(0.0);
continue;
}
let (a, b) = span(i0 + r);
let (a, b) = (a - ka - t0, b - ka - t0);
row[..a].fill(0.0);
row[b..].fill(0.0);
let row = &mut row[a..b];
let mx = lanes(row, f32::NEG_INFINITY, f32::max)
.into_iter()
.fold(f32::NEG_INFINITY, f32::max);
row.iter_mut().for_each(|x| *x = exp_neg(*x - mx));
*inv = 1.0 / lanes(row, 0.0, |a, b| a + b).iter().sum::<f32>();
}
for c in (0..HEAD).step_by(CT) {
let mut o = [[0f32; CT]; QT];
for j in t0..t1 {
let v: &[f32; CT] = qkv[2 * d + key(j) + c..][..CT].try_into().unwrap();
for (r, or) in o.iter_mut().enumerate() {
let e = s[r * np + j];
or.iter_mut().zip(v).for_each(|(o, &v)| *o = madd(*o, e, v));
}
}
for (r, (o, inv)) in o.iter().zip(inv).enumerate().take(m) {
for (x, &v) in o.iter().enumerate() {
unsafe { out.set((i0 + r) * d + h * HEAD + c + x, v * inv) };
}
}
}
}
}
#[must_use]
pub fn scratch_len(tokens: usize) -> usize {
QT * tokens.next_multiple_of(KT)
}
const QT: usize = 4;
const KT: usize = 4;
const CT: usize = 16;
#[inline(always)]
fn scores(qs: &[[f32; HEAD]; QT], ks: [&[f32; HEAD]; KT]) -> [[f32; KT]; QT] {
let mut t = [[[0f32; 4]; KT]; QT];
for c in (0..HEAD).step_by(4) {
for (tr, q) in t.iter_mut().zip(qs) {
for (tk, k) in tr.iter_mut().zip(ks) {
for l in 0..4 {
tk[l] = madd(tk[l], q[c + l], k[c + l]);
}
}
}
}
t.map(|tr| tr.map(|[a, b, c, d]| (a + c) + (b + d)))
}
#[inline(always)]
fn lanes(x: &[f32], init: f32, f: impl Fn(f32, f32) -> f32) -> [f32; 8] {
let mut acc = [init; 8];
let (chunks, rest) = x.as_chunks::<8>();
for c in chunks {
acc.iter_mut().zip(c).for_each(|(a, &v)| *a = f(*a, v));
}
for (a, &v) in acc.iter_mut().zip(rest) {
*a = f(*a, v);
}
acc
}
#[inline(always)]
fn madd(c: f32, a: f32, b: f32) -> f32 {
#[cfg(target_arch = "aarch64")]
return a.mul_add(b, c);
#[cfg(not(target_arch = "aarch64"))]
return c + a * b;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::{Rng, close};
fn naive(qkv: &[f32], heads: usize, cu: &[usize], window: Option<usize>) -> Vec<f32> {
let d = heads * HEAD;
let t = *cu.last().unwrap();
let mut out = vec![0f32; t * d];
for s in 0..cu.len() - 1 {
for i in cu[s]..cu[s + 1] {
for h in 0..heads {
let keys: Vec<usize> = (cu[s]..cu[s + 1])
.filter(|&j| window.is_none_or(|w| i.abs_diff(j) <= w))
.collect();
let sc: Vec<f64> = keys
.iter()
.map(|&j| {
(0..HEAD)
.map(|c| {
f64::from(qkv[i * 3 * d + h * HEAD + c])
* f64::from(qkv[j * 3 * d + d + h * HEAD + c])
})
.sum::<f64>()
/ 8.0
})
.collect();
let mx = sc.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let z: f64 = sc.iter().map(|x| (x - mx).exp()).sum();
for c in 0..HEAD {
let v: f64 = keys
.iter()
.zip(&sc)
.map(|(&j, x)| {
(x - mx).exp() / z
* f64::from(qkv[j * 3 * d + 2 * d + h * HEAD + c])
})
.sum();
out[i * d + h * HEAD + c] = v as f32;
}
}
}
}
out
}
#[test]
fn matches_naive_with_empty_and_single_sequences() {
let mut rng = Rng(5);
let heads = 2;
let d = heads * HEAD;
let cu = [0, 0, 1, 4, 4, 74, 75];
let t = 75;
let qkv: Vec<f32> = rng.vec(t * 3 * d).iter().map(|x| x * 3.0).collect();
for window in [None, Some(0), Some(1), Some(64)] {
let want = naive(&qkv, heads, &cu, window);
for threads in [1, 4] {
let mut out = vec![f32::NAN; t * d];
attention(&qkv, heads, &cu, window, &mut out, threads);
close(&out, &want, 1e-5, &format!("{window:?}"));
}
}
let mut none: Vec<f32> = Vec::new();
attention(&[], heads, &[0], None, &mut none, 2);
}
}