#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SparseAttentionError {
HeadCountMismatch { hq: usize, hkv: usize },
BlockPageMismatch { block_size: usize, page_size: usize },
CuSeqlensInvalid,
ShapeMismatch {
what: &'static str,
expected: usize,
got: usize,
},
SelectedOutOfRange { got: i32, msb: usize },
HeadDimMismatch,
EmptyInput,
GpuInitFailed { what: &'static str },
GpuRuntimeError { what: &'static str },
}
impl core::fmt::Display for SparseAttentionError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::HeadCountMismatch { hq, hkv } => {
write!(f, "Hq ({hq}) must be a positive multiple of Hkv ({hkv})")
}
Self::BlockPageMismatch {
block_size,
page_size,
} => {
write!(
f,
"block_size ({block_size}) must be a positive multiple of page_size ({page_size})"
)
}
Self::CuSeqlensInvalid => f.write_str("cu_seqlens_q invalid (empty or not monotonic)"),
Self::ShapeMismatch {
what,
expected,
got,
} => {
write!(
f,
"shape mismatch for {what}: expected {expected}, got {got}"
)
}
Self::SelectedOutOfRange { got, msb } => {
write!(f, "selected block id {got} out of range [-1, {msb})")
}
Self::HeadDimMismatch => f.write_str("head dim mismatch across q/k_cache/v_cache"),
Self::EmptyInput => f.write_str("empty input"),
Self::GpuInitFailed { what } => write!(f, "GPU init failed: {what}"),
Self::GpuRuntimeError { what } => write!(f, "GPU runtime error: {what}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for SparseAttentionError {}
#[derive(Debug, Clone)]
pub struct SparseSelection {
pub selected: Vec<i32>,
pub tq: usize,
pub hkv: usize,
pub topk: usize,
}
impl SparseSelection {
pub fn new(
selected: Vec<i32>,
tq: usize,
hkv: usize,
topk: usize,
) -> Result<Self, SparseAttentionError> {
let expected = tq
.checked_mul(hkv)
.and_then(|v| v.checked_mul(topk))
.ok_or(SparseAttentionError::ShapeMismatch {
what: "selected (overflow)",
expected: usize::MAX,
got: selected.len(),
})?;
if selected.len() != expected {
return Err(SparseAttentionError::ShapeMismatch {
what: "selected",
expected,
got: selected.len(),
});
}
Ok(Self {
selected,
tq,
hkv,
topk,
})
}
#[inline]
#[must_use]
pub unsafe fn get_unchecked(&self, tq_idx: usize, hkv_idx: usize, rank: usize) -> i32 {
let idx = (tq_idx * self.hkv + hkv_idx) * self.topk + rank;
*self.selected.get_unchecked(idx)
}
#[inline]
#[must_use]
pub fn get(&self, tq_idx: usize, hkv_idx: usize, rank: usize) -> i32 {
self.selected[(tq_idx * self.hkv + hkv_idx) * self.topk + rank]
}
}
#[derive(Debug, Clone)]
pub struct BlockTables {
pub table: Vec<i32>,
pub batch_size: usize,
pub max_pages: usize,
}
impl BlockTables {
pub fn new(
table: Vec<i32>,
batch_size: usize,
max_pages: usize,
) -> Result<Self, SparseAttentionError> {
let expected = batch_size.saturating_mul(max_pages);
if table.len() != expected {
return Err(SparseAttentionError::ShapeMismatch {
what: "block_tables",
expected,
got: table.len(),
});
}
Ok(Self {
table,
batch_size,
max_pages,
})
}
#[inline]
#[must_use]
pub fn get(&self, batch_idx: usize, page_idx: usize) -> i32 {
self.table[batch_idx * self.max_pages + page_idx]
}
}
#[derive(Debug, Clone)]
pub struct CuSeqlensQ {
pub prefix: Vec<i64>,
}
impl CuSeqlensQ {
pub fn new(prefix: Vec<i64>) -> Result<Self, SparseAttentionError> {
if prefix.len() < 2 || prefix[0] != 0 {
return Err(SparseAttentionError::CuSeqlensInvalid);
}
for w in prefix.windows(2) {
if w[1] < w[0] {
return Err(SparseAttentionError::CuSeqlensInvalid);
}
}
Ok(Self { prefix })
}
#[inline]
#[must_use]
pub fn batch_size(&self) -> usize {
self.prefix.len() - 1
}
#[inline]
#[must_use]
pub fn total_tq(&self) -> usize {
self.prefix[self.prefix.len() - 1] as usize
}
#[inline]
#[must_use]
pub fn batch_of(&self, tq_idx: usize) -> usize {
let idx = self.prefix.partition_point(|&p| p <= tq_idx as i64);
idx.saturating_sub(1)
}
}
#[derive(Debug, Clone)]
pub struct KvOuterIndex {
pub sel_slots: Vec<i32>,
pub sel_offsets: Vec<i32>,
pub num_sel: Vec<i32>,
pub edges: Vec<(i32, i32)>,
pub edge_head_base: Vec<usize>,
pub inv: Vec<i32>,
pub hkv: usize,
pub tq: usize,
pub topk: usize,
pub nbs: usize,
pub msb: usize,
pub batch_size: usize,
pub pages_per_block: usize,
}
impl KvOuterIndex {
#[inline]
#[must_use]
pub fn num_sel_of(&self, h: usize) -> usize {
self.num_sel[h] as usize
}
#[inline]
#[must_use]
pub fn edge_range(&self, h: usize, j: usize) -> (usize, usize) {
let base = h * (self.nbs + 1);
let start = self.sel_offsets[base + j] as usize;
let end = self.sel_offsets[base + j + 1] as usize;
let head_base = self.edge_head_base[h];
(head_base + start, head_base + end)
}
#[inline]
#[must_use]
pub fn raw_slot(&self, h: usize, j: usize) -> i32 {
self.sel_slots[h * self.nbs + j]
}
#[inline]
#[must_use]
pub fn inv_of(&self, h: usize, tq_idx: usize, rank: usize) -> i32 {
self.inv[(h * self.tq + tq_idx) * self.topk + rank]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sparse_selection_shape_mismatch() {
let err = SparseSelection::new(vec![0, 1, 2], 2, 2, 2).unwrap_err();
matches!(err, SparseAttentionError::ShapeMismatch { .. });
}
#[test]
fn sparse_selection_get_indexes_row_major() {
let sel = SparseSelection::new(vec![0, 1, 2, 3, 10, 55, 20, 21], 2, 2, 2).unwrap();
assert_eq!(sel.get(1, 0, 1), 55);
assert_eq!(sel.get(0, 1, 0), 2);
}
#[test]
fn block_tables_shape_mismatch() {
let err = BlockTables::new(vec![0, 1, 2], 2, 2).unwrap_err();
matches!(err, SparseAttentionError::ShapeMismatch { .. });
}
#[test]
fn cu_seqlens_validation() {
assert!(CuSeqlensQ::new(vec![]).is_err());
assert!(CuSeqlensQ::new(vec![1, 2, 3]).is_err());
assert!(CuSeqlensQ::new(vec![0, 3, 2]).is_err());
let c = CuSeqlensQ::new(vec![0, 3, 8]).unwrap();
assert_eq!(c.batch_size(), 2);
assert_eq!(c.total_tq(), 8);
}
#[test]
fn batch_of_maps_packed_query_indexes() {
let c = CuSeqlensQ::new(vec![0, 3, 8]).unwrap();
assert_eq!(c.batch_of(0), 0);
assert_eq!(c.batch_of(2), 0);
assert_eq!(c.batch_of(3), 1);
assert_eq!(c.batch_of(7), 1);
}
}