use crate::{Device, Result, Tensor};
const HEAD_DIM: usize = 512;
fn fattn_decode_cpu_f32(
q: &[f32], k: &[f32], v: &[f32], sinks: Option<&[f32]>, n_head: usize,
n_kv_head: usize,
kv_len: usize,
q_len: usize,
window: usize,
scale: f32,
) -> Vec<f32> {
let group = n_head / n_kv_head;
let mut out = vec![0f32; n_head * q_len * HEAD_DIM];
for h in 0..n_head {
let kv_head = h / group;
for s in 0..q_len {
let qpos = kv_len - q_len + s;
let end = qpos + 1;
let start = if window != 0 && end > window {
end - window
} else {
0
};
let qbase = (h * q_len + s) * HEAD_DIM;
let qh = &q[qbase..qbase + HEAD_DIM];
let mut scores = Vec::with_capacity(end - start);
let mut m = f32::NEG_INFINITY;
for j in start..end {
let base = (kv_head * kv_len + j) * HEAD_DIM;
let krow = &k[base..base + HEAD_DIM];
let mut dot = 0f32;
for d in 0..HEAD_DIM {
dot += qh[d] * krow[d];
}
let sc = dot * scale;
m = m.max(sc);
scores.push(sc);
}
if let Some(sk) = sinks {
m = m.max(sk[h]);
}
let mut denom = 0f32;
let mut acc = vec![0f32; HEAD_DIM];
for (idx, j) in (start..end).enumerate() {
let e = (scores[idx] - m).exp();
denom += e;
let base = (kv_head * kv_len + j) * HEAD_DIM;
let vrow = &v[base..base + HEAD_DIM];
for d in 0..HEAD_DIM {
acc[d] += e * vrow[d];
}
}
if let Some(sk) = sinks {
denom += (sk[h] - m).exp();
}
let inv = if denom == 0.0 { 0.0 } else { 1.0 / denom };
for d in 0..HEAD_DIM {
out[qbase + d] = acc[d] * inv;
}
}
}
out
}
fn dims(q: &Tensor, k: &Tensor, v: &Tensor) -> Result<(usize, usize, usize, usize)> {
if k.dims().last().copied() != Some(HEAD_DIM) {
crate::bail!(
"fattn_decode: k last dim must be {HEAD_DIM}, got {:?}",
k.dims()
);
}
if k.dims() != v.dims() {
crate::bail!(
"fattn_decode: k {:?} and v {:?} must have equal shape",
k.dims(),
v.dims()
);
}
let (n_head, q_len) = match q.dims() {
[n_head, hd] if *hd == HEAD_DIM => (*n_head, 1usize),
[n_head, q_len, hd] if *hd == HEAD_DIM => (*n_head, *q_len),
other => crate::bail!(
"fattn_decode: q must be [n_head, 512] or [n_head, q_len, 512], got {other:?}"
),
};
let (n_kv_head, kv_len) = match k.dims() {
[kv_len, _] => (1usize, *kv_len),
[n_kv_head, kv_len, _] => (*n_kv_head, *kv_len),
other => crate::bail!("fattn_decode: k rank must be 2 or 3, got {other:?}"),
};
if n_head == 0 || n_kv_head == 0 || n_head % n_kv_head != 0 {
crate::bail!(
"fattn_decode: n_head {n_head} must be a nonzero multiple of n_kv_head {n_kv_head}"
);
}
if q_len == 0 || !(1..=8).contains(&q_len) {
crate::bail!("fattn_decode: q_len {q_len} must be in 1..=8");
}
if kv_len < q_len {
crate::bail!("fattn_decode: kv_len {kv_len} must be >= q_len {q_len}");
}
Ok((n_head, n_kv_head, kv_len, q_len))
}
pub fn fattn_decode_f32_hd512(
q: &Tensor,
k: &Tensor,
v: &Tensor,
sinks: Option<&Tensor>,
window: usize,
scale: f32,
) -> Result<Tensor> {
let (n_head, n_kv_head, kv_len, q_len) = dims(q, k, v)?;
let out_shape = q.shape().clone();
let q = q.to_dtype(crate::DType::F32)?.contiguous()?;
let k = k.to_dtype(crate::DType::F32)?.contiguous()?;
let v = v.to_dtype(crate::DType::F32)?.contiguous()?;
let sinks = match sinks {
Some(s) => {
if s.elem_count() != n_head {
crate::bail!(
"fattn_decode: sinks must have {n_head} elems, got {}",
s.elem_count()
);
}
Some(s.to_dtype(crate::DType::F32)?.contiguous()?)
}
None => None,
};
match q.device() {
Device::Cpu => {
let qv = q.flatten_all()?.to_vec1::<f32>()?;
let kv = k.flatten_all()?.to_vec1::<f32>()?;
let vv = v.flatten_all()?.to_vec1::<f32>()?;
let sv = match &sinks {
Some(s) => Some(s.flatten_all()?.to_vec1::<f32>()?),
None => None,
};
let out = fattn_decode_cpu_f32(
&qv,
&kv,
&vv,
sv.as_deref(),
n_head,
n_kv_head,
kv_len,
q_len,
window,
scale,
);
Tensor::from_vec(out, out_shape, &Device::Cpu)
}
#[cfg(feature = "cuda")]
Device::Cuda(dev) => cuda_impl(
dev,
&q,
&k,
&v,
sinks.as_ref(),
n_head,
n_kv_head,
kv_len,
q_len,
window,
scale,
out_shape,
),
_ => crate::bail!("fattn_decode_f32_hd512: unsupported device"),
}
}
#[cfg(feature = "cuda")]
#[allow(clippy::too_many_arguments)]
fn cuda_impl(
dev: &crate::CudaDevice,
q: &Tensor,
k: &Tensor,
v: &Tensor,
sinks: Option<&Tensor>,
n_head: usize,
n_kv_head: usize,
kv_len: usize,
q_len: usize,
window: usize,
scale: f32,
out_shape: crate::Shape,
) -> Result<Tensor> {
use crate::op::BackpropOp;
use crate::Storage;
use cudarc::driver::DevicePtr;
use std::ffi::c_void;
let stream = dev.cuda_stream();
let stream_ptr = stream.cu_stream() as *mut c_void;
let (qg, _) = q.storage_and_layout();
let (kg, _) = k.storage_and_layout();
let (vg, _) = v.storage_and_layout();
let q_ptr = match &*qg {
Storage::Cuda(s) => s.as_cuda_slice::<f32>()?.device_ptr(&stream).0 as *const f32,
_ => crate::bail!("fattn_decode: q not on cuda"),
};
let k_ptr = match &*kg {
Storage::Cuda(s) => s.as_cuda_slice::<f32>()?.device_ptr(&stream).0 as *const f32,
_ => crate::bail!("fattn_decode: k not on cuda"),
};
let v_ptr = match &*vg {
Storage::Cuda(s) => s.as_cuda_slice::<f32>()?.device_ptr(&stream).0 as *const f32,
_ => crate::bail!("fattn_decode: v not on cuda"),
};
let sink_guard = sinks.map(|s| s.storage_and_layout());
let sinks_ptr = match &sink_guard {
Some((g, _)) => match &**g {
Storage::Cuda(s) => s.as_cuda_slice::<f32>()?.device_ptr(&stream).0 as *const f32,
_ => crate::bail!("fattn_decode: sinks not on cuda"),
},
None => std::ptr::null(),
};
let out = unsafe { dev.alloc::<f32>(n_head * q_len * HEAD_DIM)? };
let out_ptr = out.device_ptr(&stream).0 as *mut f32;
unsafe {
hanzo_kernels::ffi::hanzo_fattn_decode_f32_hd512(
stream_ptr,
q_ptr,
k_ptr,
v_ptr,
sinks_ptr,
out_ptr,
n_head as i32,
n_kv_head as i32,
kv_len as i32,
q_len as i32,
window as i32,
scale,
);
}
let storage = crate::CudaStorage::wrap_cuda_slice(out, dev.clone());
Ok(crate::tensor::from_storage(
Storage::Cuda(storage),
out_shape,
BackpropOp::none(),
false,
))
}
#[cfg(all(test, feature = "cuda"))]
mod tests {
use super::*;
use crate::{Device, Tensor};
use rand::prelude::*;
fn max_abs_diff(kv_len: usize, q_len: usize, n_kv_head: usize, with_sink: bool) -> Result<f32> {
let n_head = 64usize;
let window = 128usize;
let scale = 1.0f32 / (HEAD_DIM as f32).sqrt();
let mut rng = StdRng::seed_from_u64(
0x00d5_4f10 ^ (kv_len as u64) ^ ((n_kv_head as u64) << 20) ^ ((q_len as u64) << 40),
);
let q: Vec<f32> = (0..n_head * q_len * HEAD_DIM)
.map(|_| rng.random::<f32>() - 0.5)
.collect();
let k: Vec<f32> = (0..n_kv_head * kv_len * HEAD_DIM)
.map(|_| rng.random::<f32>() - 0.5)
.collect();
let v: Vec<f32> = (0..n_kv_head * kv_len * HEAD_DIM)
.map(|_| rng.random::<f32>() - 0.5)
.collect();
let sinks: Option<Vec<f32>> = if with_sink {
Some((0..n_head).map(|_| rng.random::<f32>() - 0.5).collect())
} else {
None
};
let dev = Device::new_cuda(0)?;
let qt = Tensor::from_vec(q.clone(), (n_head, q_len, HEAD_DIM), &dev)?;
let kt = if n_kv_head == 1 {
Tensor::from_vec(k.clone(), (kv_len, HEAD_DIM), &dev)?
} else {
Tensor::from_vec(k.clone(), (n_kv_head, kv_len, HEAD_DIM), &dev)?
};
let vt = if n_kv_head == 1 {
Tensor::from_vec(v.clone(), (kv_len, HEAD_DIM), &dev)?
} else {
Tensor::from_vec(v.clone(), (n_kv_head, kv_len, HEAD_DIM), &dev)?
};
let st = match &sinks {
Some(s) => Some(Tensor::from_vec(s.clone(), (n_head,), &dev)?),
None => None,
};
let out = fattn_decode_f32_hd512(&qt, &kt, &vt, st.as_ref(), window, scale)?;
assert_eq!(out.dims(), &[n_head, q_len, HEAD_DIM]);
let got = out.flatten_all()?.to_vec1::<f32>()?;
let want = fattn_decode_cpu_f32(
&q,
&k,
&v,
sinks.as_deref(),
n_head,
n_kv_head,
kv_len,
q_len,
window,
scale,
);
assert_eq!(got.len(), want.len());
let mut m = 0f32;
for (a, b) in got.iter().zip(want.iter()) {
m = m.max((a - b).abs());
}
Ok(m)
}
#[test]
fn fattn_decode_matches_cpu_reference() -> Result<()> {
let cases = [
(130usize, 1usize, 1usize, true), (130, 2, 1, true), (130, 3, 1, false), (130, 8, 1, true), (40, 8, 1, true), (40, 3, 8, false), (7, 1, 1, true), (7, 2, 1, true), (7, 3, 8, true), (130, 8, 8, true), ];
for (kv_len, q_len, n_kv_head, with_sink) in cases {
let d = max_abs_diff(kv_len, q_len, n_kv_head, with_sink)?;
println!(
"fattn_decode kv_len={kv_len} q_len={q_len} n_kv_head={n_kv_head} sink={with_sink} max|Δ|={d:e}"
);
assert!(
d < 1e-4,
"kv_len={kv_len} q_len={q_len} n_kv_head={n_kv_head} sink={with_sink}: max|Δ|={d} >= 1e-4"
);
}
Ok(())
}
}