#![allow(dead_code)]
use std::collections::HashMap;
use oxicuda_blas::GpuFloat;
use crate::error::{SparseError, SparseResult};
use crate::format::CsrMatrix;
use crate::handle::SparseHandle;
fn to_f64<T: GpuFloat>(val: T) -> f64 {
if T::SIZE == 4 {
f32::from_bits(val.to_bits_u64() as u32) as f64
} else {
f64::from_bits(val.to_bits_u64())
}
}
fn from_f64<T: GpuFloat>(val: f64) -> T {
if T::SIZE == 4 {
T::from_bits_u64(u64::from((val as f32).to_bits()))
} else {
T::from_bits_u64(val.to_bits())
}
}
fn add_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
from_f64::<T>(to_f64(a) + to_f64(b))
}
fn mul_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
from_f64::<T>(to_f64(a) * to_f64(b))
}
struct RowWork {
row: usize,
total_products: usize,
}
fn compute_row_work(
a_row_ptr: &[i32],
a_col_idx: &[i32],
b_row_ptr: &[i32],
m: usize,
) -> Vec<RowWork> {
let mut work = Vec::with_capacity(m);
for row in 0..m {
let a_start = a_row_ptr[row] as usize;
let a_end = a_row_ptr[row + 1] as usize;
let mut total = 0usize;
for &a_col in &a_col_idx[a_start..a_end] {
let k = a_col as usize;
let b_nnz_row_k = (b_row_ptr[k + 1] - b_row_ptr[k]) as usize;
total += b_nnz_row_k;
}
work.push(RowWork {
row,
total_products: total,
});
}
work
}
fn merge_path_partition(row_work: &[RowWork], num_partitions: usize) -> Vec<usize> {
let m = row_work.len();
if m == 0 || num_partitions == 0 {
return vec![0; num_partitions + 1];
}
let total_work: usize = row_work.iter().map(|w| w.total_products).sum();
let work_per_partition = total_work.div_ceil(num_partitions);
let mut partitions = Vec::with_capacity(num_partitions + 1);
partitions.push(0);
let mut cumulative = 0usize;
let mut row_idx = 0;
for p in 1..num_partitions {
let target = p * work_per_partition;
while row_idx < m && cumulative + row_work[row_idx].total_products <= target {
cumulative += row_work[row_idx].total_products;
row_idx += 1;
}
partitions.push(row_idx);
}
partitions.push(m);
partitions
}
pub fn spgemm_merge<T: GpuFloat>(
_handle: &SparseHandle,
a: &CsrMatrix<T>,
b: &CsrMatrix<T>,
) -> SparseResult<CsrMatrix<T>> {
if a.cols() != b.rows() {
return Err(SparseError::DimensionMismatch(format!(
"A.cols ({}) != B.rows ({})",
a.cols(),
b.rows()
)));
}
let m = a.rows() as usize;
let n = b.cols();
if m == 0 {
let row_ptr = vec![0i32; 2];
let col_idx = vec![0i32];
let values = vec![T::gpu_zero()];
return CsrMatrix::from_host(1, n, &row_ptr, &col_idx, &values);
}
let (a_row_ptr, a_col_idx, a_values) = a.to_host()?;
let (b_row_ptr, b_col_idx, b_values) = b.to_host()?;
let row_work = compute_row_work(&a_row_ptr, &a_col_idx, &b_row_ptr, m);
let num_partitions = m.min(256);
let partitions = merge_path_partition(&row_work, num_partitions);
let mut c_row_ptr = vec![0i32; m + 1];
let mut c_col_idx = Vec::new();
let mut c_values = Vec::new();
for p in 0..num_partitions {
let row_start = partitions[p];
let row_end = partitions[p + 1];
for row in row_start..row_end {
let a_start = a_row_ptr[row] as usize;
let a_end = a_row_ptr[row + 1] as usize;
let mut accum: HashMap<i32, T> = HashMap::new();
for a_nz in a_start..a_end {
let k = a_col_idx[a_nz] as usize;
let a_val = a_values[a_nz];
let b_start = b_row_ptr[k] as usize;
let b_end = b_row_ptr[k + 1] as usize;
for b_nz in b_start..b_end {
let j = b_col_idx[b_nz];
let product = mul_gpu_float(a_val, b_values[b_nz]);
let entry = accum.entry(j).or_insert(T::gpu_zero());
*entry = add_gpu_float(*entry, product);
}
}
let mut sorted_entries: Vec<(i32, T)> = accum.into_iter().collect();
sorted_entries.sort_by_key(|&(col, _)| col);
for (col, val) in &sorted_entries {
c_col_idx.push(*col);
c_values.push(*val);
}
c_row_ptr[row + 1] = c_col_idx.len() as i32;
}
}
if c_values.is_empty() {
return Err(SparseError::ZeroNnz);
}
CsrMatrix::from_host(a.rows(), n, &c_row_ptr, &c_col_idx, &c_values)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn row_work_identity() {
let a_row_ptr = vec![0, 1, 2, 3];
let a_col_idx = vec![0, 1, 2];
let b_row_ptr = vec![0, 1, 2, 3];
let work = compute_row_work(&a_row_ptr, &a_col_idx, &b_row_ptr, 3);
assert_eq!(work.len(), 3);
for w in &work {
assert_eq!(w.total_products, 1);
}
}
#[test]
fn row_work_skewed() {
let a_row_ptr = vec![0, 3, 4];
let a_col_idx = vec![0, 1, 2, 0];
let b_row_ptr = vec![0, 1, 2, 3];
let work = compute_row_work(&a_row_ptr, &a_col_idx, &b_row_ptr, 2);
assert_eq!(work[0].total_products, 3);
assert_eq!(work[1].total_products, 1);
}
#[test]
fn merge_path_partition_balanced() {
let work = vec![
RowWork {
row: 0,
total_products: 10,
},
RowWork {
row: 1,
total_products: 10,
},
RowWork {
row: 2,
total_products: 10,
},
RowWork {
row: 3,
total_products: 10,
},
];
let parts = merge_path_partition(&work, 2);
assert_eq!(parts.len(), 3);
assert_eq!(parts[0], 0);
assert_eq!(parts[2], 4);
let p1_work: usize = work[parts[0]..parts[1]]
.iter()
.map(|w| w.total_products)
.sum();
let p2_work: usize = work[parts[1]..parts[2]]
.iter()
.map(|w| w.total_products)
.sum();
assert!(p1_work > 0);
assert!(p2_work > 0);
}
#[test]
fn merge_path_partition_empty() {
let work: Vec<RowWork> = Vec::new();
let parts = merge_path_partition(&work, 4);
assert_eq!(parts.len(), 5);
assert!(parts.iter().all(|&p| p == 0));
}
#[test]
fn merge_path_partition_single_row() {
let work = vec![RowWork {
row: 0,
total_products: 100,
}];
let parts = merge_path_partition(&work, 4);
assert_eq!(parts[0], 0);
assert_eq!(*parts.last().expect("test: non-empty"), 1);
}
#[test]
fn gpu_float_arithmetic_f32() {
let a = 3.0_f32;
let b = 4.0_f32;
let result = mul_gpu_float(a, b);
assert!((result - 12.0_f32).abs() < 1e-6);
let result = add_gpu_float(a, b);
assert!((result - 7.0_f32).abs() < 1e-6);
}
#[test]
fn gpu_float_arithmetic_f64() {
let a = 3.0_f64;
let b = 4.0_f64;
let result = mul_gpu_float(a, b);
assert!((result - 12.0_f64).abs() < 1e-12);
let result = add_gpu_float(a, b);
assert!((result - 7.0_f64).abs() < 1e-12);
}
#[test]
fn merge_path_partition_skewed() {
let mut work = Vec::new();
work.push(RowWork {
row: 0,
total_products: 1000,
});
for i in 1..10 {
work.push(RowWork {
row: i,
total_products: 1,
});
}
let parts = merge_path_partition(&work, 4);
assert_eq!(parts[0], 0);
assert_eq!(*parts.last().expect("test: non-empty"), 10);
}
}