#![allow(dead_code)]
use oxicuda_blas::GpuFloat;
use oxicuda_driver::ffi::CUdeviceptr;
use crate::error::SparseResult;
use crate::format::CsrMatrix;
use crate::handle::SparseHandle;
use crate::ops::spmv::{SpMVAlgo, spmv};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RecommendedFormat {
Csr,
Ell,
Bsr,
Csr5,
}
impl std::fmt::Display for RecommendedFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Csr => write!(f, "CSR"),
Self::Ell => write!(f, "ELL"),
Self::Bsr => write!(f, "BSR"),
Self::Csr5 => write!(f, "CSR5"),
}
}
}
#[derive(Debug, Clone)]
pub struct SparsityStats {
pub rows: usize,
pub nnz: usize,
pub avg_nnz_per_row: f64,
pub max_nnz_per_row: usize,
pub min_nnz_per_row: usize,
pub std_nnz_per_row: f64,
pub cov_nnz_per_row: f64,
pub max_avg_ratio: f64,
pub block_detected: bool,
pub block_size: usize,
}
pub fn analyze_sparsity<T: GpuFloat>(matrix: &CsrMatrix<T>) -> SparseResult<SparsityStats> {
let n = matrix.rows() as usize;
if n == 0 {
return Ok(SparsityStats {
rows: 0,
nnz: 0,
avg_nnz_per_row: 0.0,
max_nnz_per_row: 0,
min_nnz_per_row: 0,
std_nnz_per_row: 0.0,
cov_nnz_per_row: 0.0,
max_avg_ratio: 0.0,
block_detected: false,
block_size: 1,
});
}
let (h_row_ptr, h_col_idx, _) = matrix.to_host()?;
let row_nnz: Vec<usize> = (0..n)
.map(|i| (h_row_ptr[i + 1] - h_row_ptr[i]) as usize)
.collect();
let total_nnz = matrix.nnz() as usize;
let avg = total_nnz as f64 / n as f64;
let max_nnz = row_nnz.iter().copied().max().unwrap_or(0);
let min_nnz = row_nnz.iter().copied().min().unwrap_or(0);
let variance: f64 = row_nnz
.iter()
.map(|&x| {
let diff = x as f64 - avg;
diff * diff
})
.sum::<f64>()
/ n as f64;
let std_dev = variance.sqrt();
let cov = if avg > 0.0 { std_dev / avg } else { 0.0 };
let max_avg_ratio = if avg > 0.0 { max_nnz as f64 / avg } else { 0.0 };
let (block_detected, block_size) =
detect_blocks(&h_row_ptr, &h_col_idx, n, matrix.cols() as usize);
Ok(SparsityStats {
rows: n,
nnz: total_nnz,
avg_nnz_per_row: avg,
max_nnz_per_row: max_nnz,
min_nnz_per_row: min_nnz,
std_nnz_per_row: std_dev,
cov_nnz_per_row: cov,
max_avg_ratio,
block_detected,
block_size,
})
}
fn detect_blocks(row_ptr: &[i32], col_idx: &[i32], n: usize, _cols: usize) -> (bool, usize) {
if n < 4 {
return (false, 1);
}
for bs in [4usize, 3, 2] {
if n % bs != 0 {
continue;
}
let num_block_rows = n / bs;
let sample_count = num_block_rows.min(16);
let step = if num_block_rows > sample_count {
num_block_rows / sample_count
} else {
1
};
let mut aligned_count = 0usize;
let mut total_checked = 0usize;
for block_row in (0..num_block_rows).step_by(step) {
let base_row = block_row * bs;
if base_row + bs > n {
break;
}
let r0_start = row_ptr[base_row] as usize;
let r0_end = row_ptr[base_row + 1] as usize;
let r0_nnz = r0_end - r0_start;
if r0_nnz < bs || r0_nnz % bs != 0 {
total_checked += 1;
continue;
}
let num_blocks_in_row = r0_nnz / bs;
let base_cols_grouped = (0..num_blocks_in_row).all(|blk| {
let group_start = r0_start + blk * bs;
let first_col = col_idx[group_start] as usize;
if first_col % bs != 0 {
return false;
}
(1..bs).all(|offset| col_idx[group_start + offset] as usize == first_col + offset)
});
if !base_cols_grouped {
total_checked += 1;
continue;
}
let mut block_aligned = true;
for sub_row in 1..bs {
let ri_start = row_ptr[base_row + sub_row] as usize;
let ri_end = row_ptr[base_row + sub_row + 1] as usize;
let ri_nnz = ri_end - ri_start;
if ri_nnz != r0_nnz {
block_aligned = false;
break;
}
let cols_match =
(0..r0_nnz).all(|k| col_idx[ri_start + k] == col_idx[r0_start + k]);
if !cols_match {
block_aligned = false;
break;
}
}
total_checked += 1;
if block_aligned {
aligned_count += 1;
}
}
if total_checked > 0 && aligned_count * 10 > total_checked * 6 {
return (true, bs);
}
}
(false, 1)
}
const COV_ELL_THRESHOLD: f64 = 0.3;
const MAX_AVG_CSR5_THRESHOLD: f64 = 10.0;
pub fn recommend_format<T: GpuFloat>(matrix: &CsrMatrix<T>) -> SparseResult<RecommendedFormat> {
let stats = analyze_sparsity(matrix)?;
if stats.rows == 0 {
return Ok(RecommendedFormat::Csr);
}
if stats.block_detected && stats.block_size > 1 {
return Ok(RecommendedFormat::Bsr);
}
if stats.cov_nnz_per_row < COV_ELL_THRESHOLD && stats.avg_nnz_per_row > 1.0 {
return Ok(RecommendedFormat::Ell);
}
if stats.max_avg_ratio > MAX_AVG_CSR5_THRESHOLD {
return Ok(RecommendedFormat::Csr5);
}
Ok(RecommendedFormat::Csr)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpMatFormat {
Csr,
Ell,
Hyb,
Csr5,
}
#[must_use]
pub fn select_format(avg_nnz_per_row: f64, n_rows: usize) -> SpMatFormat {
if avg_nnz_per_row <= 2.0 {
SpMatFormat::Csr
} else if avg_nnz_per_row <= 32.0 {
SpMatFormat::Ell
} else if avg_nnz_per_row > 128.0 && n_rows >= 1024 {
SpMatFormat::Csr5
} else {
SpMatFormat::Hyb
}
}
#[allow(clippy::too_many_arguments)]
pub fn auto_spmv<T: GpuFloat>(
handle: &SparseHandle,
matrix: &CsrMatrix<T>,
x: CUdeviceptr,
y: CUdeviceptr,
alpha: T,
beta: T,
) -> SparseResult<()> {
spmv(handle, SpMVAlgo::Adaptive, alpha, matrix, x, beta, y)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recommended_format_display() {
assert_eq!(format!("{}", RecommendedFormat::Csr), "CSR");
assert_eq!(format!("{}", RecommendedFormat::Ell), "ELL");
assert_eq!(format!("{}", RecommendedFormat::Bsr), "BSR");
assert_eq!(format!("{}", RecommendedFormat::Csr5), "CSR5");
}
#[test]
fn detect_blocks_too_small() {
let row_ptr = vec![0, 1, 2, 3];
let col_idx = vec![0, 1, 2];
let (detected, size) = detect_blocks(&row_ptr, &col_idx, 3, 3);
assert!(!detected);
assert_eq!(size, 1);
}
#[test]
fn detect_blocks_diagonal_4x4() {
let row_ptr = vec![0, 1, 2, 3, 4];
let col_idx = vec![0, 1, 2, 3];
let (detected, _) = detect_blocks(&row_ptr, &col_idx, 4, 4);
assert!(!detected);
}
#[test]
fn detect_blocks_2x2_block_diagonal() {
let row_ptr = vec![0, 2, 4, 6, 8];
let col_idx = vec![0, 1, 0, 1, 2, 3, 2, 3];
let (detected, size) = detect_blocks(&row_ptr, &col_idx, 4, 4);
assert!(detected);
assert_eq!(size, 2);
}
#[test]
fn cov_threshold_sanity() {
const { assert!(COV_ELL_THRESHOLD > 0.0) };
const { assert!(COV_ELL_THRESHOLD < 1.0) };
}
#[test]
fn max_avg_threshold_sanity() {
const { assert!(MAX_AVG_CSR5_THRESHOLD > 1.0) };
}
#[test]
fn sparsity_stats_empty() {
let stats = SparsityStats {
rows: 0,
nnz: 0,
avg_nnz_per_row: 0.0,
max_nnz_per_row: 0,
min_nnz_per_row: 0,
std_nnz_per_row: 0.0,
cov_nnz_per_row: 0.0,
max_avg_ratio: 0.0,
block_detected: false,
block_size: 1,
};
assert_eq!(stats.rows, 0);
}
#[test]
fn detect_blocks_non_block() {
let row_ptr = vec![0, 3, 5, 6, 8];
let col_idx = vec![0, 1, 3, 1, 2, 2, 0, 3];
let (detected, _) = detect_blocks(&row_ptr, &col_idx, 4, 4);
assert!(!detected);
}
#[test]
fn format_select_very_sparse_uses_csr() {
assert_eq!(select_format(1.0, 1000), SpMatFormat::Csr);
}
#[test]
fn format_select_boundary_2_uses_csr() {
assert_eq!(select_format(2.0, 1000), SpMatFormat::Csr);
}
#[test]
fn format_select_regular_uses_ell() {
assert_eq!(select_format(8.0, 1000), SpMatFormat::Ell);
}
#[test]
fn format_select_boundary_32_uses_ell() {
assert_eq!(select_format(32.0, 1000), SpMatFormat::Ell);
}
#[test]
fn format_select_irregular_uses_hyb() {
assert_eq!(select_format(50.0, 100), SpMatFormat::Hyb);
}
#[test]
fn format_select_large_but_insufficient_rows_uses_hyb() {
assert_eq!(select_format(200.0, 500), SpMatFormat::Hyb);
}
#[test]
fn format_select_large_dense_uses_csr5() {
assert_eq!(select_format(200.0, 2048), SpMatFormat::Csr5);
}
#[test]
fn format_select_exactly_128_avg_uses_hyb() {
assert_eq!(select_format(128.0, 2048), SpMatFormat::Hyb);
}
}