use super::scheduler::{enumerate_work_units, WorkSplit};
use super::simd;
use super::types::{BlockTables, CuSeqlensQ, KvOuterIndex, SparseAttentionError};
type WorkUnitTask<'a> = (WorkSplit, &'a mut [f32], &'a mut [f32], &'a mut [f32]);
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[derive(Debug, Clone)]
pub struct ForwardPartials {
pub o_partial: Vec<f32>,
pub m_partial: Vec<f32>,
pub l_partial: Vec<f32>,
pub total_edges: usize,
pub qhead: usize,
pub head_dim: usize,
}
impl ForwardPartials {
#[inline]
#[must_use]
pub fn partial_index(&self, abs_edge: usize, qh: usize) -> usize {
abs_edge * self.qhead + qh
}
}
struct WorkCtx<'a> {
q: &'a [f32],
k_pages: &'a [f32],
v_pages: &'a [f32],
idx: &'a KvOuterIndex,
block_tables: &'a BlockTables,
cu_seqlens_q: &'a CuSeqlensQ,
used_kv_lens: Option<&'a [i32]>,
q_local_pos: &'a [usize],
hq: usize,
qhead: usize,
head_dim: usize,
block_size: usize,
page_size: usize,
page_stride: usize,
num_pages: usize,
softmax_scale: f32,
causal: bool,
}
pub fn kvouter_forward(
q: &[f32],
k_pages: &[f32],
v_pages: &[f32],
idx: &KvOuterIndex,
block_tables: &BlockTables,
cu_seqlens_q: &CuSeqlensQ,
used_kv_lens: Option<&[i32]>,
hq: usize,
hkv: usize,
head_dim: usize,
block_size: usize,
page_size: usize,
softmax_scale: f32,
causal: bool,
) -> Result<ForwardPartials, SparseAttentionError> {
if hkv == 0 || hq == 0 || !hq.is_multiple_of(hkv) {
return Err(SparseAttentionError::HeadCountMismatch { hq, hkv });
}
let qhead = hq / hkv;
if head_dim == 0 || block_size == 0 || page_size == 0 || !block_size.is_multiple_of(page_size) {
return Err(SparseAttentionError::BlockPageMismatch {
block_size,
page_size,
});
}
if idx.hkv != hkv || idx.pages_per_block != block_size / page_size {
return Err(SparseAttentionError::ShapeMismatch {
what: "idx (hkv or pages_per_block)",
expected: hkv,
got: idx.hkv,
});
}
let tq = cu_seqlens_q.total_tq();
let expected_q = tq * hq * head_dim;
if q.len() != expected_q {
return Err(SparseAttentionError::ShapeMismatch {
what: "q",
expected: expected_q,
got: q.len(),
});
}
let page_stride = hkv * page_size * head_dim;
if !k_pages.len().is_multiple_of(page_stride) || v_pages.len() != k_pages.len() {
return Err(SparseAttentionError::ShapeMismatch {
what: "k_pages / v_pages page stride",
expected: page_stride,
got: k_pages.len(),
});
}
let num_pages = k_pages.len() / page_stride;
let batch_size = cu_seqlens_q.batch_size();
let mut q_local_pos: 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;
}
q_local_pos[i] = i - cu_seqlens_q.prefix[b] as usize;
}
}
let total_edges = idx.edges.len();
let mut o_partial = vec![0.0f32; total_edges * qhead * head_dim];
let mut m_partial = vec![f32::NEG_INFINITY; total_edges * qhead];
let mut l_partial = vec![0.0f32; total_edges * qhead];
let work_units = enumerate_work_units(idx);
let mut o_rest: &mut [f32] = o_partial.as_mut_slice();
let mut m_rest: &mut [f32] = m_partial.as_mut_slice();
let mut l_rest: &mut [f32] = l_partial.as_mut_slice();
let mut tasks: Vec<WorkUnitTask<'_>> = Vec::with_capacity(work_units.len());
for unit in &work_units {
let n = unit.edge_end - unit.edge_start;
let (o_head, o_tail) = std::mem::take(&mut o_rest).split_at_mut(n * qhead * head_dim);
let (m_head, m_tail) = std::mem::take(&mut m_rest).split_at_mut(n * qhead);
let (l_head, l_tail) = std::mem::take(&mut l_rest).split_at_mut(n * qhead);
tasks.push((*unit, o_head, m_head, l_head));
o_rest = o_tail;
m_rest = m_tail;
l_rest = l_tail;
}
let ctx = WorkCtx {
q,
k_pages,
v_pages,
idx,
block_tables,
cu_seqlens_q,
used_kv_lens,
q_local_pos: &q_local_pos,
hq,
qhead,
head_dim,
block_size,
page_size,
page_stride,
num_pages,
softmax_scale,
causal,
};
#[cfg(feature = "parallel")]
let result: Result<(), SparseAttentionError> =
tasks
.par_iter_mut()
.try_for_each(|(unit, o_slice, m_slice, l_slice)| {
process_work_unit(&ctx, unit, o_slice, m_slice, l_slice)
});
#[cfg(not(feature = "parallel"))]
let result: Result<(), SparseAttentionError> =
tasks
.iter_mut()
.try_for_each(|(unit, o_slice, m_slice, l_slice)| {
process_work_unit(&ctx, unit, o_slice, m_slice, l_slice)
});
result?;
Ok(ForwardPartials {
o_partial,
m_partial,
l_partial,
total_edges,
qhead,
head_dim,
})
}
fn process_work_unit(
ctx: &WorkCtx<'_>,
unit: &WorkSplit,
o_slice: &mut [f32],
m_slice: &mut [f32],
l_slice: &mut [f32],
) -> Result<(), SparseAttentionError> {
let h = unit.head;
let j = unit.compact_slot;
let raw_slot = ctx.idx.raw_slot(h, j) as usize;
let b = raw_slot / ctx.idx.msb;
let blk = raw_slot % ctx.idx.msb;
let lk = ctx
.used_kv_lens
.map_or(ctx.idx.msb * ctx.block_size, |u| u[b].max(0) as usize);
let block_start = blk * ctx.block_size;
let block_end = ((blk + 1) * ctx.block_size).min(lk);
if block_end <= block_start {
return Ok(());
}
let seq_len_b = lk;
let b_q_start = ctx.cu_seqlens_q.prefix[b] as usize;
let b_q_end = ctx.cu_seqlens_q.prefix[b + 1] as usize;
let qo_len_b = b_q_end - b_q_start;
let mut k_block: Vec<f32> = Vec::with_capacity(ctx.block_size * ctx.head_dim);
let mut v_block: Vec<f32> = Vec::with_capacity(ctx.block_size * ctx.head_dim);
let mut block_positions: Vec<usize> = Vec::with_capacity(ctx.block_size);
for page_local in 0..ctx.idx.pages_per_block {
let page_seq_start = blk * ctx.block_size + page_local * ctx.page_size;
let page_seq_end = page_seq_start + ctx.page_size;
let page_kv_start = block_start.max(page_seq_start);
let page_kv_end = block_end.min(page_seq_end);
if page_kv_end <= page_kv_start {
continue;
}
let page_slot = blk * ctx.idx.pages_per_block + page_local;
let page_id = ctx.block_tables.get(b, page_slot);
if page_id < 0 {
continue;
}
let page_id_u = page_id as usize;
if page_id_u >= ctx.num_pages {
return Err(SparseAttentionError::ShapeMismatch {
what: "block_tables page id",
expected: ctx.num_pages,
got: page_id_u,
});
}
let base_k = page_id_u * ctx.page_stride + h * ctx.page_size * ctx.head_dim;
let base_v = base_k;
let pos_start = page_kv_start - page_seq_start;
let pos_end = page_kv_end - page_seq_start;
for pos in pos_start..pos_end {
let k_off = base_k + pos * ctx.head_dim;
let v_off = base_v + pos * ctx.head_dim;
k_block.extend_from_slice(&ctx.k_pages[k_off..k_off + ctx.head_dim]);
v_block.extend_from_slice(&ctx.v_pages[v_off..v_off + ctx.head_dim]);
block_positions.push(page_seq_start + pos);
}
}
let block_len = block_positions.len();
if block_len == 0 {
return Ok(());
}
let mut scores: Vec<f32> = Vec::with_capacity(block_len);
for local_edge in 0..(unit.edge_end - unit.edge_start) {
let abs_edge = unit.edge_start + local_edge;
let (q_idx_i32, _rank) = ctx.idx.edges[abs_edge];
let q_idx = q_idx_i32 as usize;
let causal_limit = if ctx.causal {
let q_local = ctx.q_local_pos[q_idx];
if seq_len_b >= qo_len_b {
Some(seq_len_b - qo_len_b + q_local)
} else {
Some(q_local)
}
} else {
None
};
for qh in 0..ctx.qhead {
let hq_abs = h * ctx.qhead + qh;
let q_base = (q_idx * ctx.hq + hq_abs) * ctx.head_dim;
let q_vec = &ctx.q[q_base..q_base + ctx.head_dim];
scores.clear();
let mut m = f32::NEG_INFINITY;
for (pos, &kv_pos_abs) in block_positions.iter().enumerate() {
let alive = causal_limit.map_or(true, |limit| kv_pos_abs <= limit);
let s = if alive {
let k_off = pos * ctx.head_dim;
let k_row = &k_block[k_off..k_off + ctx.head_dim];
simd::dot(q_vec, k_row) * ctx.softmax_scale
} else {
f32::NEG_INFINITY
};
if s > m {
m = s;
}
scores.push(s);
}
let local_partial = local_edge * ctx.qhead + qh;
let o_off = local_partial * ctx.head_dim;
if !m.is_finite() {
m_slice[local_partial] = f32::NEG_INFINITY;
l_slice[local_partial] = 0.0;
o_slice[o_off..o_off + ctx.head_dim].fill(0.0);
continue;
}
let mut l = 0.0f32;
for (pos, &s) in scores.iter().enumerate() {
let p = if s.is_finite() { (s - m).exp() } else { 0.0 };
l += p;
let v_off = pos * ctx.head_dim;
let v_row = &v_block[v_off..v_off + ctx.head_dim];
simd::axpy(&mut o_slice[o_off..o_off + ctx.head_dim], p, v_row);
}
m_slice[local_partial] = m;
l_slice[local_partial] = l;
}
}
Ok(())
}