use smallvec::SmallVec;
use crate::{DType, Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Target {
Cpu,
Gpu {
class: char,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MaskKind {
None,
Causal,
Array,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RouteKey {
pub(crate) b: usize,
pub(crate) hq: usize,
pub(crate) hkv: usize,
pub(crate) lq: usize,
pub(crate) lk: usize,
pub(crate) d: usize,
pub(crate) dv: usize,
pub(crate) dtype: DType,
pub(crate) mask: MaskKind,
pub(crate) bool_mask: bool,
pub(crate) sinks: bool,
pub(crate) window: Option<usize>,
pub(crate) cap: Option<usize>,
pub(crate) target: Target,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum GroupKind {
Vector {
passes: u8,
blocks: usize,
gqa_kernel: bool,
tk1: bool,
},
Composed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Group {
pub(crate) start: usize,
pub(crate) end: usize,
pub(crate) keys: usize,
pub(crate) kind: GroupKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Route {
Vector(SmallVec<[Group; 2]>),
Tiled,
Composed { rows: usize },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Plan {
pub(crate) route: Route,
pub(crate) scratch: usize,
pub(crate) max_intermediate: usize,
pub(crate) capped: bool,
}
pub(crate) fn vector_two_pass(class: char, lk: usize, grouped: bool) -> bool {
(matches!(class, 'd' | 's') && lk >= 1024) || (grouped && lk >= 4096)
}
pub(crate) fn vector_blocks(class: char, lk: usize, n_simds: usize) -> usize {
match class {
's' if lk > 1024 && n_simds > 4 => match lk {
..=8192 => 128,
8193..=32768 => 256,
32769..=65536 => 512,
_ => 1024,
},
's' => 64,
'd' if n_simds <= 2 && lk > 8192 => 256,
'd' if n_simds >= 6 && lk >= 65536 => 1024,
'd' if n_simds >= 6 && lk >= 16384 => 512,
'd' => 128,
_ if n_simds >= 4 => 64,
_ => 32,
}
}
pub(crate) fn vector_head(d: usize, dv: usize) -> bool {
(d == dv && matches!(d, 64 | 96 | 128 | 256 | 512))
|| (d, dv) == (192, 128)
|| (d, dv) == (96, 64)
}
pub(crate) fn tiled_head(d: usize, dv: usize) -> bool {
(d == dv && matches!(d, 64 | 72 | 80 | 96 | 128 | 192 | 256)) || (d, dv) == (96, 64)
}
pub(crate) fn tk1_variant(dtype: DType, d: usize, dv: usize) -> bool {
(dtype == DType::BF16 && d == 256 && dv == 256)
|| (dtype == DType::F32 && d == dv && (d == 192 || d == 256))
}
fn one_row(key: &RouteKey, class: char, lk: usize) -> Option<GroupKind> {
let (d, dv) = (key.d, key.dv);
let gqa = key.hq / key.hkv;
let array_mask = key.mask == MaskKind::Array;
let d512 = d != 512 || (gqa == 8 && !array_mask && lk >= 1024);
if !(vector_head(d, dv) && lk >= 1 && gqa <= 32 && d512) {
return None;
}
if !vector_two_pass(class, lk, key.hkv < key.hq) {
return Some(GroupKind::Vector {
passes: 1,
blocks: 1,
gqa_kernel: false,
tk1: false,
});
}
let blocks = vector_blocks(class, lk, gqa);
let gqa_kernel = !array_mask
&& !key.sinks
&& d == dv
&& lk >= 8192
&& ((gqa == 8 && (d == 64 || d == 128)) || ((gqa == 12 || gqa == 16) && d == 128));
let tk1 =
!gqa_kernel && tk1_variant(key.dtype, d, dv) && 32 * gqa > threads(key, blocks, false);
Some(GroupKind::Vector {
passes: 2,
blocks,
gqa_kernel,
tk1,
})
}
fn threads(key: &RouteKey, blocks: usize, causal_rows: bool) -> usize {
let array_mask = key.mask == MaskKind::Array;
crate::gpu::vector_threads(
key.dtype,
key.d,
key.dv,
crate::gpu::VectorFlags {
has_mask: array_mask,
causal: causal_rows && key.mask == MaskKind::Causal,
bool_mask: array_mask && key.bool_mask,
float_mask: array_mask && !key.bool_mask,
sinks: key.sinks,
},
blocks,
)
}
fn fits(key: &RouteKey, kind: GroupKind, rows: usize) -> bool {
match kind {
GroupKind::Composed => rows == 1,
GroupKind::Vector { passes: 1, .. } => true,
GroupKind::Vector {
gqa_kernel,
tk1,
blocks,
..
} => {
if rows == 1 {
return true;
}
!gqa_kernel
&& !tk1
&& key.d != 512
&& 32 * (key.hq / key.hkv) * rows <= threads(key, blocks, true)
}
}
}
fn vector_schedule(key: &RouteKey, class: char) -> Option<SmallVec<[Group; 2]>> {
let (lq, lk) = (key.lq, key.lk);
if lq > lk || !vector_head(key.d, key.dv) {
return None;
}
let keys = |row: usize| match key.mask {
MaskKind::Causal => lk - (lq - 1 - row),
_ => lk,
};
let kinds: SmallVec<[Option<GroupKind>; 8]> =
(0..lq).map(|row| one_row(key, class, keys(row))).collect();
if kinds.iter().all(Option::is_none) {
return None;
}
let gqa = key.hq / key.hkv;
let tk1 = |k: &Option<GroupKind>| matches!(k, Some(GroupKind::Vector { tk1: true, .. }));
if lq > 1 && lq * gqa > 32 && kinds.iter().any(tk1) {
return None;
}
let mut groups: SmallVec<[Group; 2]> = SmallVec::new();
for (row, kind) in kinds.into_iter().enumerate() {
let kind = kind.unwrap_or(GroupKind::Composed);
if let Some(last) = groups.last_mut()
&& last.kind == kind
&& fits(key, kind, last.end - last.start + 1)
{
last.end = row + 1;
last.keys = keys(row);
continue;
}
groups.push(Group {
start: row,
end: row + 1,
keys: keys(row),
kind,
});
}
Some(groups)
}
fn tiled_uncapped(key: &RouteKey) -> bool {
let (lq, lk) = (key.lq, key.lk);
let causal = key.mask == MaskKind::Causal;
let scores = key.b * key.hq * lq * lk * key.dtype.size();
let huge = scores >= 4 << 30;
let slow = match key.d {
192 => !huge,
256 => !(huge || (causal && lq >= 1024 && (lk <= 4096 || (lq >= 2048 && lk <= 8192)))),
_ => false,
};
!slow
}
fn score_size(key: &RouteKey) -> usize {
if key.sinks || key.dtype == DType::F32 {
4
} else {
key.dtype.size()
}
}
fn block_keys(key: &RouteKey, rows: usize) -> usize {
match key.window {
Some(w) => key.lk.min(w.saturating_add(rows - 1)),
None => key.lk,
}
}
fn block_intermediate(key: &RouteKey, rows: usize) -> usize {
key.b
.saturating_mul(key.hq)
.saturating_mul(rows)
.saturating_mul(block_keys(key, rows))
.saturating_mul(score_size(key))
}
fn composed_scratch(key: &RouteKey, rows: usize) -> usize {
let s = key.dtype.size();
let l = block_keys(key, rows);
let n = key
.b
.saturating_mul(key.hq)
.saturating_mul(rows)
.saturating_mul(l);
let scaled_q = key.b * key.hq * rows * key.d * s;
let f32_scores = n.saturating_mul(4);
let blocked = rows < key.lq;
let sinks = match key.sinks && key.dtype != DType::F32 {
false => 0,
true if blocked && f32_scores < 16 << 20 => f32_scores.saturating_mul(3),
true => f32_scores,
};
let mask = match (key.mask, key.window) {
(_, Some(_)) => 3 * rows * l + 4 * (rows + l),
(MaskKind::Causal, None) => rows * l + 4 * (rows + l),
_ => 0,
};
scaled_q
.max(sinks)
.saturating_add(n.saturating_mul(s))
.saturating_add(mask)
}
fn blocked_scratch(key: &RouteKey, rows: usize) -> usize {
let blocked = rows < key.lq;
let outputs = key.b * key.hq * key.lq * key.dv * key.dtype.size();
composed_scratch(key, rows).saturating_add(if blocked { outputs } else { 0 })
}
fn vector_scratch(key: &RouteKey, groups: &[Group]) -> usize {
let s = key.dtype.size();
let mut bytes = 0usize;
for g in groups {
let rows = g.end - g.start;
let group = match g.kind {
GroupKind::Vector {
passes: 2, blocks, ..
} => key.b * key.hq * rows * blocks * (key.dv * s + 8),
GroupKind::Composed => {
let one = RouteKey {
lq: 1,
lk: g.keys,
mask: MaskKind::None,
..*key
};
composed_scratch(&one, 1)
}
GroupKind::Vector { .. } => 0,
};
bytes = bytes.max(group);
}
if groups.len() > 1 {
bytes += key.b * key.hq * key.lq * (key.d + key.dv) * s;
}
bytes
}
fn capped_override() -> Option<bool> {
static CHOICE: std::sync::LazyLock<Option<bool>> = std::sync::LazyLock::new(|| {
match std::env::var("NUVEC_ATTENTION_CAPPED_KERNEL").as_deref() {
Ok("tiled") => Some(true),
Ok("blocks") => Some(false),
_ => None,
}
});
*CHOICE
}
fn capped_tiled(_key: &RouteKey, rows: usize) -> bool {
if let Some(tiled) = capped_override() {
return tiled;
}
rows < 192
}
pub(crate) fn plan(key: &RouteKey) -> Result<Plan> {
if key.cap == Some(0) {
return Err(Error::InvalidArgument(
"scaled_dot_product_attention: max_score_bytes must be positive".into(),
));
}
if key.window.is_some() && key.mask != MaskKind::Causal {
return Err(Error::InvalidArgument(
"scaled_dot_product_attention: a window needs the causal mask".into(),
));
}
if key.window == Some(0) {
return Err(Error::InvalidArgument(
"scaled_dot_product_attention: the window must be at least 1".into(),
));
}
let composed = |rows: usize, capped: bool| Plan {
route: Route::Composed { rows },
scratch: blocked_scratch(key, rows),
max_intermediate: block_intermediate(key, rows),
capped,
};
if key.lq == 0 || key.lk == 0 {
return Ok(Plan {
route: Route::Composed { rows: key.lq },
scratch: 0,
max_intermediate: 0,
capped: false,
});
}
let class = match key.target {
Target::Gpu { class } => Some(class),
Target::Cpu => None,
};
if let Some(class) = class
&& key.lq <= 8
&& key.window.is_none()
&& let Some(groups) = vector_schedule(key, class)
{
let scratch = vector_scratch(key, &groups);
return Ok(Plan {
route: Route::Vector(groups),
scratch,
max_intermediate: 0,
capped: false,
});
}
let tiled_ok = class.is_some()
&& key.window.is_none()
&& tiled_head(key.d, key.dv)
&& (key.mask != MaskKind::Causal || key.lq <= key.lk);
let tiled = |capped: bool| Plan {
route: Route::Tiled,
scratch: 0,
max_intermediate: 0,
capped,
};
let forced_blocks = key.cap.is_some() && capped_override() == Some(false);
if tiled_ok && key.lq > 8 && tiled_uncapped(key) && !forced_blocks {
return Ok(tiled(false));
}
let Some(cap) = key.cap else {
return Ok(composed(key.lq, false));
};
if block_intermediate(key, key.lq) <= cap {
return Ok(composed(key.lq, false));
}
let (mut lo, mut hi) = (0usize, key.lq);
while lo < hi {
let mid = (lo + hi).div_ceil(2);
if block_intermediate(key, mid) <= cap {
lo = mid;
} else {
hi = mid - 1;
}
}
let rows_max = lo;
if rows_max == 0 {
if tiled_ok {
return Ok(tiled(true));
}
return Err(Error::InvalidArgument(format!(
"scaled_dot_product_attention: max_score_bytes {cap} is below one query row: \
needs at least {} bytes",
block_intermediate(key, 1)
)));
}
let blocks = key.lq.div_ceil(rows_max);
let rows = key.lq.div_ceil(blocks);
if tiled_ok && capped_tiled(key, rows) {
return Ok(tiled(true));
}
Ok(composed(rows, true))
}