use super::types::KvOuterIndex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WorkSplit {
pub head: usize,
pub compact_slot: usize,
pub edge_start: usize,
pub edge_end: usize,
}
#[must_use]
pub fn enumerate_work_units(idx: &KvOuterIndex) -> Vec<WorkSplit> {
let mut out = Vec::new();
for h in 0..idx.hkv {
let off_base = h * (idx.nbs + 1);
let num_sel = idx.num_sel_of(h);
for j in 0..num_sel {
let head_local_start = idx.sel_offsets[off_base + j] as usize;
let head_local_end = idx.sel_offsets[off_base + j + 1] as usize;
if head_local_end <= head_local_start {
continue;
}
let base = idx.edge_head_base[h];
out.push(WorkSplit {
head: h,
compact_slot: j,
edge_start: base + head_local_start,
edge_end: base + head_local_end,
});
}
}
out
}
#[must_use]
pub fn build_fixed_schedule(idx: &KvOuterIndex, num_splits: usize) -> Vec<WorkSplit> {
let units = enumerate_work_units(idx);
if num_splits == 0 {
return units;
}
let mut out = Vec::with_capacity(num_splits.max(units.len()));
if units.is_empty() {
return sentinel_vec(num_splits);
}
let n = units.len();
let base = n / num_splits.max(1);
let rem = n % num_splits.max(1);
let mut cursor = 0usize;
for s in 0..num_splits.max(1) {
let count = base + usize::from(s < rem);
if count == 0 {
out.push(sentinel_split());
continue;
}
for u in &units[cursor..cursor + count] {
out.push(*u);
}
cursor += count;
}
while out.len() < num_splits {
out.push(sentinel_split());
}
out
}
const fn sentinel_split() -> WorkSplit {
WorkSplit {
head: 0,
compact_slot: 0,
edge_start: 0,
edge_end: 0,
}
}
fn sentinel_vec(n: usize) -> Vec<WorkSplit> {
vec![sentinel_split(); n]
}
#[cfg(test)]
mod tests {
use super::super::{
build_kvouter_index,
types::{BlockTables, CuSeqlensQ, SparseSelection},
};
use super::*;
fn make_multi_head_idx() -> super::super::types::KvOuterIndex {
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();
build_kvouter_index(&sel, &tbl, &cu, None, 128, 128).unwrap()
}
#[test]
fn enumerate_work_units_covers_every_edge() {
let idx = make_multi_head_idx();
let units = enumerate_work_units(&idx);
let total_edges: usize = units.iter().map(|w| w.edge_end - w.edge_start).sum();
assert_eq!(total_edges, idx.edges.len());
assert_eq!(units.len(), 7);
}
#[test]
fn enumerate_work_units_stays_within_head_offsets() {
let idx = make_multi_head_idx();
for u in enumerate_work_units(&idx) {
let base = idx.edge_head_base[u.head];
let cap = idx.edge_head_base[u.head + 1];
assert!(u.edge_start >= base);
assert!(u.edge_end <= cap);
}
}
#[test]
fn build_fixed_schedule_pads_with_sentinels() {
let idx = make_multi_head_idx();
let schedule = build_fixed_schedule(&idx, 32);
assert!(schedule.len() >= 32);
let sentinel_count = schedule
.iter()
.filter(|w| w.edge_start == w.edge_end)
.count();
assert!(sentinel_count >= 32 - 7);
}
#[test]
fn build_fixed_schedule_num_splits_zero_returns_units() {
let idx = make_multi_head_idx();
assert_eq!(build_fixed_schedule(&idx, 0), enumerate_work_units(&idx));
}
}