use super::types::{BlockTables, CuSeqlensQ, KvOuterIndex, SparseAttentionError, SparseSelection};
pub fn build_kvouter_index(
selected: &SparseSelection,
block_tables: &BlockTables,
cu_seqlens_q: &CuSeqlensQ,
used_kv_lens: Option<&[i32]>,
block_size: usize,
page_size: usize,
) -> Result<KvOuterIndex, SparseAttentionError> {
if selected.tq == 0 || selected.hkv == 0 || selected.topk == 0 {
return Err(SparseAttentionError::EmptyInput);
}
if selected.tq != cu_seqlens_q.total_tq() {
return Err(SparseAttentionError::ShapeMismatch {
what: "cu_seqlens_q.total_tq vs selected.tq",
expected: selected.tq,
got: cu_seqlens_q.total_tq(),
});
}
if cu_seqlens_q.batch_size() != block_tables.batch_size {
return Err(SparseAttentionError::ShapeMismatch {
what: "cu_seqlens_q.batch_size vs block_tables.batch_size",
expected: block_tables.batch_size,
got: cu_seqlens_q.batch_size(),
});
}
if page_size == 0 || block_size == 0 || !block_size.is_multiple_of(page_size) {
return Err(SparseAttentionError::BlockPageMismatch {
block_size,
page_size,
});
}
if let Some(used) = used_kv_lens {
if used.len() != cu_seqlens_q.batch_size() {
return Err(SparseAttentionError::ShapeMismatch {
what: "used_kv_lens",
expected: cu_seqlens_q.batch_size(),
got: used.len(),
});
}
}
let pages_per_block = block_size / page_size;
let msb = block_tables.max_pages / pages_per_block;
if msb == 0 {
return Err(SparseAttentionError::BlockPageMismatch {
block_size,
page_size,
});
}
let batch_size = cu_seqlens_q.batch_size();
let hkv = selected.hkv;
let tq = selected.tq;
let topk = selected.topk;
let nbs = batch_size * msb;
let per_batch_valid_slots: Vec<usize> = (0..batch_size)
.map(|b| {
used_kv_lens.map_or(msb, |used| {
let lk = used[b].max(0) as usize;
lk.div_ceil(block_size).min(msb)
})
})
.collect();
let mut slot_counts = vec![0i32; hkv * nbs];
let mut batch_of_tq: Vec<usize> = vec![0; tq];
{
let mut b = 0usize;
for i in 0..tq {
while b + 1 < batch_size && (i as i64) >= cu_seqlens_q.prefix[b + 1] {
b += 1;
}
batch_of_tq[i] = b;
}
}
for i in 0..tq {
let b = batch_of_tq[i];
let valid_slots_b = per_batch_valid_slots[b];
for h in 0..hkv {
for r in 0..topk {
let blk = selected.get(i, h, r);
if blk < 0 {
continue;
}
let blk_u = blk as usize;
if blk_u >= msb {
return Err(SparseAttentionError::SelectedOutOfRange { got: blk, msb });
}
if blk_u >= valid_slots_b {
continue;
}
let real_slot = b * msb + blk_u;
slot_counts[h * nbs + real_slot] += 1;
}
}
}
let sel_offsets_len = hkv * (nbs + 1);
let mut sel_offsets = vec![0i32; sel_offsets_len];
let mut sel_slots = vec![0i32; hkv * nbs];
let mut num_sel = vec![0i32; hkv];
let mut edge_head_base = vec![0usize; hkv + 1];
for h in 0..hkv {
let off_base = h * (nbs + 1);
let mut compact_j: usize = 0;
let mut cum: i32 = 0;
sel_offsets[off_base] = 0;
for raw_slot in 0..nbs {
let cnt = slot_counts[h * nbs + raw_slot];
if cnt > 0 {
sel_slots[h * nbs + compact_j] = raw_slot as i32;
cum += cnt;
sel_offsets[off_base + compact_j + 1] = cum;
compact_j += 1;
}
}
num_sel[h] = compact_j as i32;
for j in (compact_j + 1)..=nbs {
sel_offsets[off_base + j] = cum;
}
edge_head_base[h + 1] = edge_head_base[h] + cum as usize;
}
let total_edges = edge_head_base[hkv];
let mut edges = vec![(0i32, 0i32); total_edges];
let mut inv = vec![-1i32; hkv * tq * topk];
let mut write_cursor: Vec<i32> = Vec::with_capacity(hkv * nbs);
for h in 0..hkv {
let off_base = h * (nbs + 1);
for j in 0..nbs {
write_cursor.push(sel_offsets[off_base + j]);
}
}
let mut raw_to_compact: Vec<i32> = vec![-1; hkv * nbs];
for h in 0..hkv {
for j in 0..(num_sel[h] as usize) {
let raw = sel_slots[h * nbs + j] as usize;
raw_to_compact[h * nbs + raw] = j as i32;
}
}
for i in 0..tq {
let b = batch_of_tq[i];
let valid_slots_b = per_batch_valid_slots[b];
for h in 0..hkv {
for r in 0..topk {
let blk = selected.get(i, h, r);
if blk < 0 {
continue;
}
let blk_u = blk as usize;
if blk_u >= valid_slots_b {
continue;
}
let real_slot = b * msb + blk_u;
let compact_j = raw_to_compact[h * nbs + real_slot];
if compact_j < 0 {
continue;
}
let cj = compact_j as usize;
let cursor_idx = h * nbs + cj;
let write_pos = write_cursor[cursor_idx] as usize;
edges[edge_head_base[h] + write_pos] = (i as i32, r as i32);
write_cursor[cursor_idx] += 1;
inv[(h * tq + i) * topk + r] = write_pos as i32;
}
}
}
Ok(KvOuterIndex {
sel_slots,
sel_offsets,
num_sel,
edges,
edge_head_base,
inv,
hkv,
tq,
topk,
nbs,
msb,
batch_size,
pages_per_block,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn small_selection() -> (SparseSelection, BlockTables, CuSeqlensQ) {
let sel = SparseSelection::new(vec![0, 2, 2, -1, 3, 0], 3, 1, 2).unwrap();
let tbl = BlockTables::new(vec![0, 1, 2, 3], 1, 4).unwrap();
let cu = CuSeqlensQ::new(vec![0, 3]).unwrap();
(sel, tbl, cu)
}
#[test]
fn build_index_matches_hand_computed_csr() {
let (sel, tbl, cu) = small_selection();
let idx = build_kvouter_index(&sel, &tbl, &cu, None, 128, 128).unwrap();
assert_eq!(idx.hkv, 1);
assert_eq!(idx.tq, 3);
assert_eq!(idx.topk, 2);
assert_eq!(idx.nbs, 4);
assert_eq!(idx.msb, 4);
assert_eq!(idx.pages_per_block, 1);
assert_eq!(idx.num_sel_of(0), 3);
assert_eq!(idx.raw_slot(0, 0), 0);
assert_eq!(idx.raw_slot(0, 1), 2);
assert_eq!(idx.raw_slot(0, 2), 3);
assert_eq!(&idx.sel_offsets[0..5], &[0, 2, 4, 5, 5]);
let (s0, e0) = idx.edge_range(0, 0);
assert_eq!(&idx.edges[s0..e0], &[(0, 0), (2, 1)]);
let (s1, e1) = idx.edge_range(0, 1);
assert_eq!(&idx.edges[s1..e1], &[(0, 1), (1, 0)]);
let (s2, e2) = idx.edge_range(0, 2);
assert_eq!(&idx.edges[s2..e2], &[(2, 0)]);
assert_eq!(idx.inv_of(0, 0, 0), 0);
assert_eq!(idx.inv_of(0, 0, 1), 2);
assert_eq!(idx.inv_of(0, 2, 1), 1);
assert_eq!(idx.inv_of(0, 1, 1), -1);
}
#[test]
fn build_index_drops_padding_ranks() {
let sel = SparseSelection::new(vec![-1, -1, -1, -1], 2, 1, 2).unwrap();
let tbl = BlockTables::new(vec![0, 1], 1, 2).unwrap();
let cu = CuSeqlensQ::new(vec![0, 2]).unwrap();
let idx = build_kvouter_index(&sel, &tbl, &cu, None, 64, 64).unwrap();
assert_eq!(idx.num_sel_of(0), 0);
assert_eq!(idx.edges.len(), 0);
assert!(idx.inv.iter().all(|&v| v == -1));
}
#[test]
fn build_index_respects_used_kv_lens() {
let sel = SparseSelection::new(vec![0, 1, 3], 1, 1, 3).unwrap();
let tbl = BlockTables::new(vec![10, 11, 12, 13], 1, 4).unwrap();
let cu = CuSeqlensQ::new(vec![0, 1]).unwrap();
let idx = build_kvouter_index(&sel, &tbl, &cu, Some(&[128]), 64, 64).unwrap();
assert_eq!(idx.num_sel_of(0), 2);
assert_eq!(idx.inv_of(0, 0, 2), -1);
assert_eq!(idx.inv_of(0, 0, 0), 0);
assert_eq!(idx.inv_of(0, 0, 1), 1);
}
#[test]
fn multi_batch_multi_head_index() {
let sel = SparseSelection::new(
vec![
0, 1, 1, -1, 1, 0, 0, 1, 0, -1, 1, 0, ],
3,
2,
2,
)
.unwrap();
let tbl = BlockTables::new(vec![0, 1, 2, 3], 2, 2).unwrap();
let cu = CuSeqlensQ::new(vec![0, 2, 3]).unwrap();
let idx = build_kvouter_index(&sel, &tbl, &cu, None, 128, 128).unwrap();
assert_eq!(idx.nbs, 4);
assert_eq!(idx.num_sel_of(0), 3);
assert_eq!(idx.raw_slot(0, 0), 0);
assert_eq!(idx.raw_slot(0, 1), 1);
assert_eq!(idx.raw_slot(0, 2), 2);
assert_eq!(idx.num_sel_of(1), 4);
assert_eq!(idx.raw_slot(1, 0), 0);
assert_eq!(idx.raw_slot(1, 1), 1);
assert_eq!(idx.raw_slot(1, 2), 2);
assert_eq!(idx.raw_slot(1, 3), 3);
let (s, e) = idx.edge_range(0, 0);
assert_eq!(&idx.edges[s..e], &[(0, 0), (1, 1)]);
let (s, e) = idx.edge_range(1, 3);
assert_eq!(&idx.edges[s..e], &[(2, 0)]);
}
#[test]
fn build_index_rejects_out_of_range_selection() {
let sel = SparseSelection::new(vec![5], 1, 1, 1).unwrap();
let tbl = BlockTables::new(vec![0, 1], 1, 2).unwrap();
let cu = CuSeqlensQ::new(vec![0, 1]).unwrap();
let err = build_kvouter_index(&sel, &tbl, &cu, None, 128, 128).unwrap_err();
matches!(err, SparseAttentionError::SelectedOutOfRange { got: 5, .. });
}
#[test]
fn build_index_rejects_bad_block_page_ratio() {
let sel = SparseSelection::new(vec![0], 1, 1, 1).unwrap();
let tbl = BlockTables::new(vec![0], 1, 1).unwrap();
let cu = CuSeqlensQ::new(vec![0, 1]).unwrap();
let err = build_kvouter_index(&sel, &tbl, &cu, None, 100, 64).unwrap_err();
matches!(err, SparseAttentionError::BlockPageMismatch { .. });
}
}