#![allow(dead_code)]
use std::sync::Arc;
use oxicuda_blas::GpuFloat;
use oxicuda_driver::Module;
use oxicuda_launch::{Kernel, LaunchParams, grid_size_for};
use oxicuda_memory::DeviceBuffer;
use oxicuda_ptx::arch::SmVersion;
use oxicuda_ptx::builder::KernelBuilder;
use oxicuda_ptx::ir::PtxType;
use crate::error::{SparseError, SparseResult};
use crate::format::CsrMatrix;
use crate::handle::SparseHandle;
use crate::ptx_helpers::{load_global_float, ptx_suffix, store_global_float};
const ILU0_BLOCK_SIZE: u32 = 256;
const ILU0_STATUS_SINGULAR: u32 = 1;
pub fn ilu0<T: GpuFloat>(
handle: &SparseHandle,
a: &CsrMatrix<T>,
) -> SparseResult<(CsrMatrix<T>, CsrMatrix<T>)> {
let _ = handle;
if a.rows() != a.cols() {
return Err(SparseError::DimensionMismatch(format!(
"ILU(0) requires square matrix, got {}x{}",
a.rows(),
a.cols()
)));
}
let n = a.rows();
if n == 0 {
return Err(SparseError::InvalidArgument(
"cannot factor an empty matrix".to_string(),
));
}
let (h_row_ptr, h_col_idx, h_values) = a.to_host()?;
let levels = analyze_ilu0_levels(&h_row_ptr, &h_col_idx, n)?;
let mut work_values = h_values;
for level_rows in &levels {
for &row_u32 in level_rows {
factor_row_host(row_u32 as usize, &h_row_ptr, &h_col_idx, &mut work_values)?;
}
}
split_lu(&h_row_ptr, &h_col_idx, &work_values, n)
}
pub fn ilu0_gpu<T: GpuFloat>(
handle: &SparseHandle,
a: &CsrMatrix<T>,
) -> SparseResult<(CsrMatrix<T>, CsrMatrix<T>)> {
if a.rows() != a.cols() {
return Err(SparseError::DimensionMismatch(format!(
"ILU(0) requires square matrix, got {}x{}",
a.rows(),
a.cols()
)));
}
let n = a.rows();
if n == 0 {
return Err(SparseError::InvalidArgument(
"cannot factor an empty matrix".to_string(),
));
}
let (h_row_ptr, h_col_idx, h_values) = a.to_host()?;
let levels = analyze_ilu0_levels(&h_row_ptr, &h_col_idx, n)?;
let ptx = emit_ilu0_kernel::<T>(handle.sm_version())?;
let module = Arc::new(Module::from_ptx(&ptx)?);
let kernel = Kernel::from_module(module, "ilu0_level")?;
let d_values = DeviceBuffer::<T>::from_host(&h_values)?;
let row_ptr_dev = a.row_ptr().as_device_ptr();
let col_idx_dev = a.col_idx().as_device_ptr();
let d_status = DeviceBuffer::<u32>::from_host(&[0u32])?;
for level_rows in &levels {
if level_rows.is_empty() {
continue;
}
let d_level_rows = DeviceBuffer::<u32>::from_host(level_rows)?;
let num_rows_in_level = level_rows.len() as u32;
let block_size = ILU0_BLOCK_SIZE;
let grid_size = grid_size_for(num_rows_in_level, block_size);
let params = LaunchParams::new(grid_size, block_size);
kernel.launch(
¶ms,
handle.stream(),
&(
row_ptr_dev,
col_idx_dev,
d_values.as_device_ptr(),
d_level_rows.as_device_ptr(),
num_rows_in_level,
d_status.as_device_ptr(),
),
)?;
handle.stream().synchronize()?;
let mut status = [0u32];
d_status.copy_to_host(&mut status)?;
if status[0] & ILU0_STATUS_SINGULAR != 0 {
return Err(SparseError::SingularMatrix);
}
}
let mut factored = vec![T::gpu_zero(); h_values.len()];
d_values.copy_to_host(&mut factored)?;
split_lu(&h_row_ptr, &h_col_idx, &factored, n)
}
fn factor_row_host<T: GpuFloat>(
row: usize,
h_row_ptr: &[i32],
h_col_idx: &[i32],
work_values: &mut [T],
) -> SparseResult<()> {
let row_start = h_row_ptr[row] as usize;
let row_end = h_row_ptr[row + 1] as usize;
for nz in row_start..row_end {
let k = h_col_idx[nz] as usize;
if k >= row {
break; }
let k_start = h_row_ptr[k] as usize;
let k_end = h_row_ptr[k + 1] as usize;
let diag_pos = match find_col_in_row(&h_col_idx[k_start..k_end], k as i32) {
Some(pos) => k_start + pos,
None => return Err(SparseError::SingularMatrix),
};
let a_kk = work_values[diag_pos];
if a_kk == T::gpu_zero() {
return Err(SparseError::SingularMatrix);
}
let a_ik = work_values[nz];
let ratio = div_gpu_float(a_ik, a_kk);
work_values[nz] = ratio;
for k_nz in (diag_pos + 1)..k_end {
let j = h_col_idx[k_nz];
if let Some(ij_off) = find_col_in_row(&h_col_idx[row_start..row_end], j) {
let ij_pos = row_start + ij_off;
let a_kj = work_values[k_nz];
let update = mul_gpu_float(ratio, a_kj);
work_values[ij_pos] = sub_gpu_float(work_values[ij_pos], update);
}
}
}
Ok(())
}
fn find_col_in_row(col_slice: &[i32], target_col: i32) -> Option<usize> {
col_slice.iter().position(|&c| c == target_col)
}
fn analyze_ilu0_levels(row_ptr: &[i32], col_idx: &[i32], n: u32) -> SparseResult<Vec<Vec<u32>>> {
let n_usize = n as usize;
let mut depth = vec![0u32; n_usize];
let mut max_depth: u32 = 0;
for i in 0..n_usize {
let start = row_ptr[i] as usize;
let end = row_ptr[i + 1] as usize;
let mut max_dep = 0u32;
for &cj in &col_idx[start..end] {
let j = cj as usize;
if j < i {
let d = depth[j] + 1;
if d > max_dep {
max_dep = d;
}
}
}
depth[i] = max_dep;
if max_dep > max_depth {
max_depth = max_dep;
}
}
let num_levels = max_depth as usize + 1;
let mut levels: Vec<Vec<u32>> = vec![Vec::new(); num_levels];
for (i, &d) in depth.iter().enumerate() {
levels[d as usize].push(i as u32);
}
Ok(levels)
}
fn split_lu<T: GpuFloat>(
row_ptr: &[i32],
col_idx: &[i32],
values: &[T],
n: u32,
) -> SparseResult<(CsrMatrix<T>, CsrMatrix<T>)> {
let n_usize = n as usize;
let mut l_nnz = 0usize;
let mut u_nnz = 0usize;
for i in 0..n_usize {
let start = row_ptr[i] as usize;
let end = row_ptr[i + 1] as usize;
for &cj in &col_idx[start..end] {
let j = cj as usize;
if j < i {
l_nnz += 1;
} else {
u_nnz += 1;
}
}
l_nnz += 1; }
let mut l_row_ptr = vec![0i32; n_usize + 1];
let mut l_col_idx = Vec::with_capacity(l_nnz);
let mut l_values = Vec::with_capacity(l_nnz);
let mut u_row_ptr = vec![0i32; n_usize + 1];
let mut u_col_idx = Vec::with_capacity(u_nnz);
let mut u_values = Vec::with_capacity(u_nnz);
for i in 0..n_usize {
let start = row_ptr[i] as usize;
let end = row_ptr[i + 1] as usize;
for idx in start..end {
let j = col_idx[idx] as usize;
if j < i {
l_col_idx.push(col_idx[idx]);
l_values.push(values[idx]);
}
}
l_col_idx.push(i as i32);
l_values.push(T::gpu_one());
l_row_ptr[i + 1] = l_col_idx.len() as i32;
for idx in start..end {
let j = col_idx[idx] as usize;
if j >= i {
u_col_idx.push(col_idx[idx]);
u_values.push(values[idx]);
}
}
u_row_ptr[i + 1] = u_col_idx.len() as i32;
}
let l_mat = CsrMatrix::from_host(n, n, &l_row_ptr, &l_col_idx, &l_values)?;
let u_mat = CsrMatrix::from_host(n, n, &u_row_ptr, &u_col_idx, &u_values)?;
Ok((l_mat, u_mat))
}
fn emit_ilu0_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
let elem_bytes = T::size_u32();
let suffix = ptx_suffix::<T>();
KernelBuilder::new("ilu0_level")
.target(sm)
.param("row_ptr", PtxType::U64)
.param("col_idx", PtxType::U64)
.param("values", PtxType::U64)
.param("level_rows", PtxType::U64)
.param("num_level_rows", PtxType::U32)
.param("status", PtxType::U64)
.body(move |b| {
let gid = b.global_thread_id_x();
let num_level_rows = b.load_param_u32("num_level_rows");
let gid_inner = gid.clone();
b.if_lt_u32(gid, num_level_rows, move |b| {
let tid = gid_inner;
let level_rows_ptr = b.load_param_u64("level_rows");
let row_ptr_base = b.load_param_u64("row_ptr");
let col_idx_base = b.load_param_u64("col_idx");
let values_base = b.load_param_u64("values");
let status_ptr = b.load_param_u64("status");
let row_addr = b.byte_offset_addr(level_rows_ptr, tid, 4);
let row = b.load_global_u32(row_addr);
let rs_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
let rs_i32 = b.load_global_i32(rs_addr);
let row_start = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {row_start}, {rs_i32};"));
let row_p1 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {row_p1}, {row}, 1;"));
let re_addr = b.byte_offset_addr(row_ptr_base.clone(), row_p1, 4);
let re_i32 = b.load_global_i32(re_addr);
let row_end = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {row_end}, {re_i32};"));
let nz = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {nz}, {row_start};"));
let outer_top = b.fresh_label("ilu0_outer");
let outer_end = b.fresh_label("ilu0_outer_end");
b.label(&outer_top);
{
let cont = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.lo.u32 {cont}, {nz}, {row_end};"));
b.raw_ptx(&format!("@!{cont} bra ${outer_end};"));
let ci_addr = b.byte_offset_addr(col_idx_base.clone(), nz.clone(), 4);
let k_i32 = b.load_global_i32(ci_addr);
let k = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {k}, {k_i32};"));
let is_lower = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.lo.u32 {is_lower}, {k}, {row};"));
b.raw_ptx(&format!("@!{is_lower} bra ${outer_end};"));
let ks_addr = b.byte_offset_addr(row_ptr_base.clone(), k.clone(), 4);
let ks_i32 = b.load_global_i32(ks_addr);
let k_start = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {k_start}, {ks_i32};"));
let k_p1 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {k_p1}, {k}, 1;"));
let ke_addr = b.byte_offset_addr(row_ptr_base.clone(), k_p1, 4);
let ke_i32 = b.load_global_i32(ke_addr);
let k_end = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {k_end}, {ke_i32};"));
let diag_pos = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {diag_pos}, {k_end};"));
let scan = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {scan}, {k_start};"));
let diag_top = b.fresh_label("ilu0_diag");
let diag_end = b.fresh_label("ilu0_diag_end");
b.label(&diag_top);
{
let scan_ok = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.lo.u32 {scan_ok}, {scan}, {k_end};"));
b.raw_ptx(&format!("@!{scan_ok} bra ${diag_end};"));
let sc_addr = b.byte_offset_addr(col_idx_base.clone(), scan.clone(), 4);
let sc_i32 = b.load_global_i32(sc_addr);
let sc_col = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {sc_col}, {sc_i32};"));
let hit = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.eq.u32 {hit}, {sc_col}, {k};"));
b.raw_ptx(&format!("@{hit} mov.u32 {diag_pos}, {scan};"));
b.raw_ptx(&format!("@{hit} bra ${diag_end};"));
b.raw_ptx(&format!("add.u32 {scan}, {scan}, 1;"));
b.branch(&diag_top);
}
b.label(&diag_end);
let no_diag = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.ge.u32 {no_diag}, {diag_pos}, {k_end};"));
let after_singular = b.fresh_label("ilu0_after_sing");
b.raw_ptx(&format!("@!{no_diag} bra ${after_singular};"));
{
let one = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {one}, {ILU0_STATUS_SINGULAR};"));
let old = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("atom.global.or.b32 {old}, [{status_ptr}], {one};"));
b.branch(&outer_end);
}
b.label(&after_singular);
let akk_addr =
b.byte_offset_addr(values_base.clone(), diag_pos.clone(), elem_bytes);
let a_kk = load_global_float::<T>(b, akk_addr);
let zero = b.alloc_reg(T::PTX_TYPE);
if elem_bytes == 4 {
b.raw_ptx(&format!("mov.b32 {zero}, 0F00000000;"));
} else {
b.raw_ptx(&format!("mov.b64 {zero}, 0D0000000000000000;"));
}
let pivot_zero = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.eq.{suffix} {pivot_zero}, {a_kk}, {zero};"));
let after_zero = b.fresh_label("ilu0_after_zero");
b.raw_ptx(&format!("@!{pivot_zero} bra ${after_zero};"));
{
let one = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {one}, {ILU0_STATUS_SINGULAR};"));
let old = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("atom.global.or.b32 {old}, [{status_ptr}], {one};"));
b.branch(&outer_end);
}
b.label(&after_zero);
let aik_addr = b.byte_offset_addr(values_base.clone(), nz.clone(), elem_bytes);
let a_ik = load_global_float::<T>(b, aik_addr.clone());
let ratio = b.alloc_reg(T::PTX_TYPE);
b.raw_ptx(&format!("div.rn.{suffix} {ratio}, {a_ik}, {a_kk};"));
store_global_float::<T>(b, aik_addr, ratio.clone());
let k_nz = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {k_nz}, {diag_pos}, 1;"));
let inner_top = b.fresh_label("ilu0_inner");
let inner_end = b.fresh_label("ilu0_inner_end");
b.label(&inner_top);
{
let inner_ok = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.lo.u32 {inner_ok}, {k_nz}, {k_end};"));
b.raw_ptx(&format!("@!{inner_ok} bra ${inner_end};"));
let j_addr = b.byte_offset_addr(col_idx_base.clone(), k_nz.clone(), 4);
let j_i32 = b.load_global_i32(j_addr);
let j_col = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {j_col}, {j_i32};"));
let ij_pos = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {ij_pos}, {row_end};"));
let pscan = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {pscan}, {row_start};"));
let psc_top = b.fresh_label("ilu0_pat");
let psc_end = b.fresh_label("ilu0_pat_end");
b.label(&psc_top);
{
let psc_ok = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.lo.u32 {psc_ok}, {pscan}, {row_end};"));
b.raw_ptx(&format!("@!{psc_ok} bra ${psc_end};"));
let pc_addr =
b.byte_offset_addr(col_idx_base.clone(), pscan.clone(), 4);
let pc_i32 = b.load_global_i32(pc_addr);
let pc_col = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {pc_col}, {pc_i32};"));
let pat_hit = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.eq.u32 {pat_hit}, {pc_col}, {j_col};"));
b.raw_ptx(&format!("@{pat_hit} mov.u32 {ij_pos}, {pscan};"));
b.raw_ptx(&format!("@{pat_hit} bra ${psc_end};"));
b.raw_ptx(&format!("add.u32 {pscan}, {pscan}, 1;"));
b.branch(&psc_top);
}
b.label(&psc_end);
let found = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.lo.u32 {found}, {ij_pos}, {row_end};"));
let skip_update = b.fresh_label("ilu0_skip_upd");
b.raw_ptx(&format!("@!{found} bra ${skip_update};"));
{
let akj_addr =
b.byte_offset_addr(values_base.clone(), k_nz.clone(), elem_bytes);
let a_kj = load_global_float::<T>(b, akj_addr);
let ij_addr =
b.byte_offset_addr(values_base.clone(), ij_pos.clone(), elem_bytes);
let a_ij = load_global_float::<T>(b, ij_addr.clone());
let update = b.alloc_reg(T::PTX_TYPE);
b.raw_ptx(&format!("mul.rn.{suffix} {update}, {ratio}, {a_kj};"));
let new_ij = b.alloc_reg(T::PTX_TYPE);
b.raw_ptx(&format!("sub.rn.{suffix} {new_ij}, {a_ij}, {update};"));
store_global_float::<T>(b, ij_addr, new_ij);
}
b.label(&skip_update);
b.raw_ptx(&format!("add.u32 {k_nz}, {k_nz}, 1;"));
b.branch(&inner_top);
}
b.label(&inner_end);
b.raw_ptx(&format!("add.u32 {nz}, {nz}, 1;"));
b.branch(&outer_top);
}
b.label(&outer_end);
});
b.ret();
})
.build()
.map_err(|e| SparseError::PtxGeneration(e.to_string()))
}
fn div_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
let a_bits = a.to_bits_u64();
let b_bits = b.to_bits_u64();
if T::SIZE == 4 {
let fa = f32::from_bits(a_bits as u32);
let fb = f32::from_bits(b_bits as u32);
T::from_bits_u64(u64::from((fa / fb).to_bits()))
} else {
let fa = f64::from_bits(a_bits);
let fb = f64::from_bits(b_bits);
T::from_bits_u64((fa / fb).to_bits())
}
}
fn mul_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
let a_bits = a.to_bits_u64();
let b_bits = b.to_bits_u64();
if T::SIZE == 4 {
let fa = f32::from_bits(a_bits as u32);
let fb = f32::from_bits(b_bits as u32);
T::from_bits_u64(u64::from((fa * fb).to_bits()))
} else {
let fa = f64::from_bits(a_bits);
let fb = f64::from_bits(b_bits);
T::from_bits_u64((fa * fb).to_bits())
}
}
fn sub_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
let a_bits = a.to_bits_u64();
let b_bits = b.to_bits_u64();
if T::SIZE == 4 {
let fa = f32::from_bits(a_bits as u32);
let fb = f32::from_bits(b_bits as u32);
T::from_bits_u64(u64::from((fa - fb).to_bits()))
} else {
let fa = f64::from_bits(a_bits);
let fb = f64::from_bits(b_bits);
T::from_bits_u64((fa - fb).to_bits())
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxicuda_ptx::arch::SmVersion;
#[test]
fn ilu0_kernel_ptx_generates_f32() {
let ptx = emit_ilu0_kernel::<f32>(SmVersion::Sm80);
assert!(ptx.is_ok());
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry ilu0_level"));
}
#[test]
fn ilu0_kernel_ptx_generates_f64() {
let ptx = emit_ilu0_kernel::<f64>(SmVersion::Sm80);
assert!(ptx.is_ok());
}
#[test]
fn ilu0_kernel_body_performs_elimination() {
let ptx = emit_ilu0_kernel::<f32>(SmVersion::Sm80).expect("test: PTX gen should succeed");
assert!(
ptx.contains("div.rn.f32"),
"kernel must compute the elimination multiplier"
);
assert!(
ptx.contains("mul.rn.f32"),
"kernel must compute the fill update product"
);
assert!(
ptx.contains("sub.rn.f32"),
"kernel must apply the fill update"
);
assert!(
ptx.contains("st.global.f32"),
"kernel must store the factored values"
);
assert!(
ptx.contains("atom.global.or.b32"),
"kernel must report singular pivots"
);
}
#[test]
fn ilu0_kernel_body_has_no_dead_loads() {
let ptx = emit_ilu0_kernel::<f64>(SmVersion::Sm80).expect("test: PTX gen should succeed");
let global_loads = ptx.matches("ld.global").count();
let global_stores = ptx.matches("st.global").count();
assert!(
global_loads >= 8,
"elimination kernel must issue many global loads, got {global_loads}"
);
assert!(
global_stores >= 2,
"elimination kernel must write factored values, got {global_stores}"
);
}
#[test]
fn ilu0_levels_identity() {
let row_ptr = vec![0, 1, 2, 3];
let col_idx = vec![0, 1, 2];
let levels = analyze_ilu0_levels(&row_ptr, &col_idx, 3);
assert!(levels.is_ok());
let levels = levels.expect("test: levels should succeed");
assert_eq!(levels.len(), 1);
assert_eq!(levels[0].len(), 3);
}
#[test]
fn host_float_arithmetic() {
let a = 6.0_f32;
let b = 2.0_f32;
let result = div_gpu_float(a, b);
assert!((result - 3.0_f32).abs() < 1e-6);
let result = mul_gpu_float(a, b);
assert!((result - 12.0_f32).abs() < 1e-6);
let result = sub_gpu_float(a, b);
assert!((result - 4.0_f32).abs() < 1e-6);
}
#[test]
fn host_float_arithmetic_f64() {
let a = 6.0_f64;
let b = 2.0_f64;
let result = div_gpu_float(a, b);
assert!((result - 3.0_f64).abs() < 1e-12);
}
#[test]
fn find_col_works() {
let cols = [0, 2, 5, 7];
assert_eq!(find_col_in_row(&cols, 2), Some(1));
assert_eq!(find_col_in_row(&cols, 3), None);
assert_eq!(find_col_in_row(&cols, 7), Some(3));
}
#[test]
fn factor_row_host_lower_update() {
let row_ptr = vec![0, 2, 4];
let col_idx = vec![0, 1, 0, 1];
let mut values = vec![4.0_f64, 2.0, 1.0, 3.0];
factor_row_host(0, &row_ptr, &col_idx, &mut values)
.expect("test: row 0 factorization should succeed");
assert_eq!(values, vec![4.0, 2.0, 1.0, 3.0]);
factor_row_host(1, &row_ptr, &col_idx, &mut values)
.expect("test: row 1 factorization should succeed");
assert!((values[2] - 0.25).abs() < 1e-12, "multiplier l_10");
assert!((values[3] - 2.5).abs() < 1e-12, "updated u_11");
}
#[test]
fn factor_row_host_singular_pivot() {
let row_ptr = vec![0, 2, 4];
let col_idx = vec![0, 1, 0, 1];
let mut values = vec![0.0_f64, 2.0, 1.0, 3.0];
let result = factor_row_host(1, &row_ptr, &col_idx, &mut values);
assert!(matches!(result, Err(SparseError::SingularMatrix)));
}
#[cfg(feature = "gpu-tests")]
mod gpu {
use super::*;
fn gpu_context() -> Option<oxicuda_driver::Context> {
oxicuda_driver::init().ok()?;
if oxicuda_driver::Device::count().ok()? == 0 {
return None;
}
let dev = oxicuda_driver::Device::get(0).ok()?;
oxicuda_driver::Context::new(&dev).ok()
}
fn max_abs_diff(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).abs())
.fold(0.0, f64::max)
}
fn assert_gpu_matches_cpu(handle: &SparseHandle, a: &CsrMatrix<f64>) {
let (cpu_l, cpu_u) = ilu0(handle, a).expect("test: CPU ILU(0) should succeed");
let (gpu_l, gpu_u) = ilu0_gpu(handle, a).expect("test: GPU ILU(0) should succeed");
let (cpu_l_rp, cpu_l_ci, cpu_l_v) = cpu_l.to_host().expect("test: download CPU L");
let (gpu_l_rp, gpu_l_ci, gpu_l_v) = gpu_l.to_host().expect("test: download GPU L");
assert_eq!(cpu_l_rp, gpu_l_rp, "L row_ptr must match");
assert_eq!(cpu_l_ci, gpu_l_ci, "L col_idx must match");
assert!(
max_abs_diff(&cpu_l_v, &gpu_l_v) < 1e-9,
"L values diverge: cpu={cpu_l_v:?} gpu={gpu_l_v:?}"
);
let (cpu_u_rp, cpu_u_ci, cpu_u_v) = cpu_u.to_host().expect("test: download CPU U");
let (gpu_u_rp, gpu_u_ci, gpu_u_v) = gpu_u.to_host().expect("test: download GPU U");
assert_eq!(cpu_u_rp, gpu_u_rp, "U row_ptr must match");
assert_eq!(cpu_u_ci, gpu_u_ci, "U col_idx must match");
assert!(
max_abs_diff(&cpu_u_v, &gpu_u_v) < 1e-9,
"U values diverge: cpu={cpu_u_v:?} gpu={gpu_u_v:?}"
);
}
#[test]
fn gpu_ilu0_dense_2x2() {
let Some(ctx) = gpu_context() else {
return;
};
let ctx = Arc::new(ctx);
let handle = SparseHandle::new(&ctx).expect("test: handle creation");
let a =
CsrMatrix::<f64>::from_host(2, 2, &[0, 2, 4], &[0, 1, 0, 1], &[4.0, 2.0, 1.0, 3.0])
.expect("test: matrix creation");
assert_gpu_matches_cpu(&handle, &a);
}
#[test]
fn gpu_ilu0_dense_3x3() {
let Some(ctx) = gpu_context() else {
return;
};
let ctx = Arc::new(ctx);
let handle = SparseHandle::new(&ctx).expect("test: handle creation");
let a = CsrMatrix::<f64>::from_host(
3,
3,
&[0, 3, 6, 9],
&[0, 1, 2, 0, 1, 2, 0, 1, 2],
&[4.0, 1.0, 2.0, 1.0, 5.0, 1.0, 2.0, 1.0, 6.0],
)
.expect("test: matrix creation");
assert_gpu_matches_cpu(&handle, &a);
}
#[test]
fn gpu_ilu0_tridiagonal_5x5() {
let Some(ctx) = gpu_context() else {
return;
};
let ctx = Arc::new(ctx);
let handle = SparseHandle::new(&ctx).expect("test: handle creation");
let mut row_ptr = vec![0i32];
let mut col_idx = Vec::new();
let mut values = Vec::new();
for i in 0..5i32 {
if i > 0 {
col_idx.push(i - 1);
values.push(-1.0_f64);
}
col_idx.push(i);
values.push(4.0_f64);
if i < 4 {
col_idx.push(i + 1);
values.push(-1.0_f64);
}
row_ptr.push(col_idx.len() as i32);
}
let a = CsrMatrix::<f64>::from_host(5, 5, &row_ptr, &col_idx, &values)
.expect("test: matrix creation");
assert_gpu_matches_cpu(&handle, &a);
}
#[test]
fn gpu_ilu0_identity() {
let Some(ctx) = gpu_context() else {
return;
};
let ctx = Arc::new(ctx);
let handle = SparseHandle::new(&ctx).expect("test: handle creation");
let a = CsrMatrix::<f64>::from_host(
4,
4,
&[0, 1, 2, 3, 4],
&[0, 1, 2, 3],
&[1.0, 1.0, 1.0, 1.0],
)
.expect("test: matrix creation");
assert_gpu_matches_cpu(&handle, &a);
}
#[test]
fn gpu_ilu0_singular_pivot() {
let Some(ctx) = gpu_context() else {
return;
};
let ctx = Arc::new(ctx);
let handle = SparseHandle::new(&ctx).expect("test: handle creation");
let a =
CsrMatrix::<f64>::from_host(2, 2, &[0, 2, 4], &[0, 1, 0, 1], &[0.0, 2.0, 1.0, 3.0])
.expect("test: matrix creation");
let result = ilu0_gpu(&handle, &a);
assert!(matches!(result, Err(SparseError::SingularMatrix)));
}
}
}