use crate::chunked::{sp_matmul_topn_chunked, AccumMode, BProjection};
use crate::csr::{matmul_topn_short_circuit, CsrMatrix, CsrView};
use crate::index::Index;
use crate::matmul::sp_matmul;
use crate::maxheap::MaxHeap;
use crate::scalar::Scalar;
const UNVISITED: usize = usize::MAX;
const HEAD_NIL: usize = usize::MAX - 1;
#[derive(Debug, Clone, Copy, Default)]
pub enum SortMode {
#[default]
ByColumn,
ByValueDesc,
}
#[derive(Debug, Clone, Copy)]
pub struct TopNOptions<V: Scalar> {
pub threshold: Option<V>,
pub sort: SortMode,
pub density_hint: Option<f64>,
pub chunk_cols: Option<usize>,
pub n_threads: Option<usize>,
pub accum_mode: AccumMode,
pub projection: Option<BProjection>,
pub row_block: Option<usize>,
pub tile_b: bool,
}
impl<V: Scalar> Default for TopNOptions<V> {
fn default() -> Self {
Self {
threshold: None,
sort: SortMode::ByColumn,
density_hint: None,
chunk_cols: None,
n_threads: None,
accum_mode: AccumMode::Adaptive,
projection: None,
row_block: None,
tile_b: false,
}
}
}
pub fn sp_matmul_topn<V: Scalar, I: Index>(
a: CsrView<'_, V, I>,
b: CsrView<'_, V, I>,
top_n: usize,
opts: TopNOptions<V>,
) -> CsrMatrix<V, I> {
assert_eq!(
a.ncols, b.nrows,
"sp_matmul_topn: A.ncols ({}) must equal B.nrows ({})",
a.ncols, b.nrows,
);
if let Some(out) = matmul_topn_short_circuit(a, b, top_n) {
return out;
}
if let Some(out) = maybe_parallel(a, b, top_n, opts) {
return out;
}
if top_n >= b.ncols && opts.threshold.is_none() && matches!(opts.sort, SortMode::ByColumn) {
return sp_matmul(a, b);
}
sp_matmul_topn_chunked(a, b, top_n, opts)
}
#[cfg(feature = "rayon")]
fn maybe_parallel<V: Scalar, I: Index>(
a: CsrView<'_, V, I>,
b: CsrView<'_, V, I>,
top_n: usize,
opts: TopNOptions<V>,
) -> Option<CsrMatrix<V, I>> {
let n_threads = opts.n_threads.unwrap_or(1);
if n_threads > 1 {
Some(crate::parallel::sp_matmul_topn_parallel(a, b, top_n, opts))
} else {
None
}
}
#[cfg(not(feature = "rayon"))]
fn maybe_parallel<V: Scalar, I: Index>(
_a: CsrView<'_, V, I>,
_b: CsrView<'_, V, I>,
_top_n: usize,
_opts: TopNOptions<V>,
) -> Option<CsrMatrix<V, I>> {
None
}
#[doc(hidden)]
pub fn sp_matmul_topn_unchunked_for_tests<V: Scalar, I: Index>(
a: CsrView<'_, V, I>,
b: CsrView<'_, V, I>,
top_n: usize,
opts: TopNOptions<V>,
) -> CsrMatrix<V, I> {
assert_eq!(
a.ncols, b.nrows,
"sp_matmul_topn_unchunked_for_tests: A.ncols ({}) must equal B.nrows ({})",
a.ncols, b.nrows,
);
if let Some(out) = matmul_topn_short_circuit(a, b, top_n) {
return out;
}
let nrows = a.nrows;
let ncols = b.ncols;
let threshold = opts.threshold.unwrap_or_else(V::min_value);
let density = opts.density_hint.unwrap_or(1.0);
let cap_hint = ((nrows as f64) * (top_n as f64) * density).ceil() as usize;
let mut sums: Vec<V> = vec![V::default(); ncols];
let mut next: Vec<usize> = vec![UNVISITED; ncols];
let mut heap = MaxHeap::<V, I>::new(top_n, threshold);
let mut c_indptr: Vec<I> = Vec::with_capacity(nrows + 1);
let mut c_indices: Vec<I> = Vec::with_capacity(cap_hint);
let mut c_data: Vec<V> = Vec::with_capacity(cap_hint);
c_indptr.push(I::zero());
let mut nnz_total: usize = 0;
for i in 0..nrows {
let mut head: usize = HEAD_NIL;
let mut length: usize = 0;
let mut min = heap.reset();
let jj_start = a.indptr[i].to_usize();
let jj_end = a.indptr[i + 1].to_usize();
for jj in jj_start..jj_end {
let j = a.indices[jj].to_usize();
let v = a.data[jj];
let kk_start = b.indptr[j].to_usize();
let kk_end = b.indptr[j + 1].to_usize();
for kk in kk_start..kk_end {
let k = b.indices[kk].to_usize();
sums[k] += v * b.data[kk];
if next[k] == UNVISITED {
next[k] = head;
head = k;
length += 1;
}
}
}
for _ in 0..length {
if sums[head].partial_cmp(&min) == Some(std::cmp::Ordering::Greater) {
min = heap.push_pop(I::from_usize(head), sums[head]);
}
let temp = head;
head = next[head];
next[temp] = UNVISITED;
sums[temp] = V::default();
}
match opts.sort {
SortMode::ByColumn => heap.sort_by_insertion_order(),
SortMode::ByValueDesc => heap.sort_by_value_desc(),
}
let n_set = heap.n_set();
for entry in heap.entries() {
c_indices.push(entry.idx);
c_data.push(entry.val);
}
nnz_total += n_set;
c_indptr.push(I::from_usize(nnz_total));
}
CsrMatrix {
nrows,
ncols,
indptr: c_indptr,
indices: c_indices,
data: c_data,
}
}
#[cfg(test)]
mod tests {
use super::*;
type CsrParts = (Vec<i32>, Vec<i32>, Vec<f64>, Vec<i32>, Vec<i32>, Vec<f64>);
fn make_a_b() -> CsrParts {
let a_indptr = vec![0i32, 2, 4];
let a_indices = vec![0i32, 2, 1, 2];
let a_data = vec![1.0f64, 2.0, 3.0, 4.0];
let b_indptr = vec![0i32, 2, 4, 6];
let b_indices = vec![0i32, 3, 1, 3, 2, 3];
let b_data = vec![1.0f64, 5.0, 1.0, 6.0, 1.0, 7.0];
(a_indptr, a_indices, a_data, b_indptr, b_indices, b_data)
}
#[test]
fn top2_by_value_desc() {
let (a_ip, a_idx, a_d, b_ip, b_idx, b_d) = make_a_b();
let a = CsrView::new(2, 3, &a_ip, &a_idx, &a_d).unwrap();
let b = CsrView::new(3, 4, &b_ip, &b_idx, &b_d).unwrap();
let opts = TopNOptions {
sort: SortMode::ByValueDesc,
..Default::default()
};
let c = sp_matmul_topn(a, b, 2, opts);
assert_eq!(c.indptr, vec![0, 2, 2 + 2]);
assert_eq!(c.data[0], 19.0);
assert_eq!(c.indices[0], 3);
assert_eq!(c.data[1], 2.0);
assert_eq!(c.indices[1], 2);
assert_eq!(c.data[2], 46.0);
assert_eq!(c.indices[2], 3);
assert_eq!(c.data[3], 4.0);
assert_eq!(c.indices[3], 2);
}
#[test]
fn top2_by_column_order() {
let (a_ip, a_idx, a_d, b_ip, b_idx, b_d) = make_a_b();
let a = CsrView::new(2, 3, &a_ip, &a_idx, &a_d).unwrap();
let b = CsrView::new(3, 4, &b_ip, &b_idx, &b_d).unwrap();
let opts = TopNOptions {
sort: SortMode::ByColumn,
..Default::default()
};
let c = sp_matmul_topn(a, b, 2, opts);
let row0: Vec<(i32, f64)> = (c.indptr[0] as usize..c.indptr[1] as usize)
.map(|k| (c.indices[k], c.data[k]))
.collect();
assert_eq!(row0, vec![(2, 2.0), (3, 19.0)]);
let row1: Vec<(i32, f64)> = (c.indptr[1] as usize..c.indptr[2] as usize)
.map(|k| (c.indices[k], c.data[k]))
.collect();
assert_eq!(row1, vec![(2, 4.0), (3, 46.0)]);
}
#[test]
fn threshold_filters() {
let (a_ip, a_idx, a_d, b_ip, b_idx, b_d) = make_a_b();
let a = CsrView::new(2, 3, &a_ip, &a_idx, &a_d).unwrap();
let b = CsrView::new(3, 4, &b_ip, &b_idx, &b_d).unwrap();
let opts = TopNOptions {
threshold: Some(10.0),
sort: SortMode::ByValueDesc,
..Default::default()
};
let c = sp_matmul_topn(a, b, 4, opts);
assert_eq!(c.indptr, vec![0, 1, 2]);
assert_eq!(c.data, vec![19.0, 46.0]);
assert_eq!(c.indices, vec![3, 3]);
}
#[test]
fn top_n_zero_returns_zero_matrix() {
let (a_ip, a_idx, a_d, b_ip, b_idx, b_d) = make_a_b();
let a = CsrView::new(2, 3, &a_ip, &a_idx, &a_d).unwrap();
let b = CsrView::new(3, 4, &b_ip, &b_idx, &b_d).unwrap();
let c = sp_matmul_topn(a, b, 0, TopNOptions::default());
assert_eq!(c.nnz(), 0);
assert_eq!(c.indptr, vec![0, 0, 0]);
}
#[test]
fn top_n_equals_ncols_matches_sp_matmul() {
let (a_ip, a_idx, a_d, b_ip, b_idx, b_d) = make_a_b();
let a = CsrView::new(2, 3, &a_ip, &a_idx, &a_d).unwrap();
let b = CsrView::new(3, 4, &b_ip, &b_idx, &b_d).unwrap();
let c_topn = sp_matmul_topn(a, b, 4, TopNOptions::default());
let c_plain = sp_matmul(a, b);
assert_eq!(c_topn.indptr, c_plain.indptr);
let sort_rows = |c: &CsrMatrix<f64, i32>| -> Vec<(i32, f64)> {
let mut out = Vec::new();
for i in 0..c.nrows {
let mut row: Vec<(i32, f64)> = (c.indptr[i] as usize..c.indptr[i + 1] as usize)
.map(|k| (c.indices[k], c.data[k]))
.collect();
row.sort_by_key(|(idx, _)| *idx);
out.extend(row);
}
out
};
assert_eq!(sort_rows(&c_topn), sort_rows(&c_plain));
}
#[test]
fn unchunked_oracle_matches_chunked_one_chunk() {
let (a_ip, a_idx, a_d, b_ip, b_idx, b_d) = make_a_b();
let a = CsrView::new(2, 3, &a_ip, &a_idx, &a_d).unwrap();
let b = CsrView::new(3, 4, &b_ip, &b_idx, &b_d).unwrap();
let opts = TopNOptions {
sort: SortMode::ByValueDesc,
chunk_cols: Some(usize::MAX),
..Default::default()
};
let chunked = sp_matmul_topn(a, b, 2, opts);
let oracle = sp_matmul_topn_unchunked_for_tests(a, b, 2, opts);
assert_eq!(chunked.indptr, oracle.indptr);
assert_eq!(chunked.indices, oracle.indices);
assert_eq!(chunked.data, oracle.data);
}
#[test]
fn n_threads_none_and_one_agree() {
let (a_ip, a_idx, a_d, b_ip, b_idx, b_d) = make_a_b();
let a = CsrView::new(2, 3, &a_ip, &a_idx, &a_d).unwrap();
let b = CsrView::new(3, 4, &b_ip, &b_idx, &b_d).unwrap();
let opts_none = TopNOptions {
sort: SortMode::ByValueDesc,
..Default::default()
};
let opts_one = TopNOptions {
n_threads: Some(1),
..opts_none
};
let c_none = sp_matmul_topn(a, b, 2, opts_none);
let c_one = sp_matmul_topn(a, b, 2, opts_one);
assert_eq!(c_none.indptr, c_one.indptr);
assert_eq!(c_none.indices, c_one.indices);
assert_eq!(c_none.data, c_one.data);
}
#[cfg(feature = "rayon")]
#[test]
fn n_threads_many_matches_sequential() {
let (a_ip, a_idx, a_d, b_ip, b_idx, b_d) = make_a_b();
let a = CsrView::new(2, 3, &a_ip, &a_idx, &a_d).unwrap();
let b = CsrView::new(3, 4, &b_ip, &b_idx, &b_d).unwrap();
let base = TopNOptions {
sort: SortMode::ByValueDesc,
..Default::default()
};
let seq = sp_matmul_topn(a, b, 2, base);
let par = sp_matmul_topn(
a,
b,
2,
TopNOptions {
n_threads: Some(4),
..base
},
);
assert_eq!(par.indptr, seq.indptr);
assert_eq!(par.indices, seq.indices);
assert_eq!(par.data, seq.data);
}
}