use oxicuda_ptx::prelude::*;
use crate::error::{SparseError, SparseResult};
use crate::ptx_helpers::{
emit_warp_reduce_sum, load_float_imm, load_global_float, mul_float, store_global_float,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EigenTarget {
LargestMagnitude,
SmallestMagnitude,
LargestAlgebraic,
SmallestAlgebraic,
}
pub const KRYLOV_BLOCK_SIZE: u32 = 256;
#[derive(Debug, Clone)]
pub struct LanczosConfig {
pub max_iterations: usize,
pub tolerance: f64,
pub num_eigenvalues: usize,
pub which: EigenTarget,
}
#[derive(Debug, Clone)]
pub struct LanczosResult {
pub eigenvalues: Vec<f64>,
pub alpha: Vec<f64>,
pub beta: Vec<f64>,
pub iterations: usize,
pub converged: bool,
}
#[derive(Debug)]
pub struct LanczosPlan {
config: LanczosConfig,
n: usize,
}
impl LanczosPlan {
pub fn new(config: LanczosConfig, n: usize) -> SparseResult<Self> {
if n == 0 {
return Err(SparseError::InvalidArgument(
"matrix dimension n must be positive".to_string(),
));
}
if config.num_eigenvalues == 0 {
return Err(SparseError::InvalidArgument(
"num_eigenvalues must be positive".to_string(),
));
}
if config.max_iterations < config.num_eigenvalues {
return Err(SparseError::InvalidArgument(format!(
"max_iterations ({}) must be >= num_eigenvalues ({})",
config.max_iterations, config.num_eigenvalues
)));
}
if config.max_iterations > n {
return Err(SparseError::InvalidArgument(format!(
"max_iterations ({}) must be <= matrix dimension n ({})",
config.max_iterations, n
)));
}
if config.tolerance <= 0.0 {
return Err(SparseError::InvalidArgument(
"tolerance must be positive".to_string(),
));
}
Ok(Self { config, n })
}
#[inline]
pub fn config(&self) -> &LanczosConfig {
&self.config
}
#[inline]
pub fn dimension(&self) -> usize {
self.n
}
pub fn workspace_bytes_f64(&self) -> usize {
let k = self.config.max_iterations;
let n = self.n;
let vectors = (k + 2) * n * 8; let scalars = (k + k) * 8; vectors + scalars
}
pub fn workspace_bytes_f32(&self) -> usize {
let k = self.config.max_iterations;
let n = self.n;
let vectors = (k + 2) * n * 4;
let scalars = (k + k) * 4;
vectors + scalars
}
pub fn generate_lanczos_step_ptx(&self) -> SparseResult<String> {
emit_lanczos_step_f64(self.n)
}
pub fn generate_lanczos_step_ptx_f32(&self) -> SparseResult<String> {
emit_lanczos_step_f32(self.n)
}
pub fn generate_reorthogonalize_ptx(&self) -> SparseResult<String> {
emit_reorthogonalize_f64(self.n)
}
pub fn generate_reorthogonalize_ptx_f32(&self) -> SparseResult<String> {
emit_reorthogonalize_f32(self.n)
}
pub fn generate_dot_product_ptx(&self) -> SparseResult<String> {
emit_dot_product_reduce_f64(self.n)
}
pub fn generate_dot_product_ptx_f32(&self) -> SparseResult<String> {
emit_dot_product_reduce_f32(self.n)
}
pub fn generate_norm_sq_ptx(&self) -> SparseResult<String> {
emit_norm_sq_reduce_f64(self.n)
}
pub fn generate_norm_sq_ptx_f32(&self) -> SparseResult<String> {
emit_norm_sq_reduce_f32(self.n)
}
}
#[derive(Debug, Clone)]
pub struct ArnoldiConfig {
pub max_iterations: usize,
pub tolerance: f64,
pub num_eigenvalues: usize,
pub which: EigenTarget,
}
#[derive(Debug, Clone)]
pub struct ArnoldiResult {
pub eigenvalues: Vec<(f64, f64)>,
pub hessenberg: Vec<Vec<f64>>,
pub iterations: usize,
pub converged: bool,
}
#[derive(Debug)]
pub struct ArnoldiPlan {
config: ArnoldiConfig,
n: usize,
}
impl ArnoldiPlan {
pub fn new(config: ArnoldiConfig, n: usize) -> SparseResult<Self> {
if n == 0 {
return Err(SparseError::InvalidArgument(
"matrix dimension n must be positive".to_string(),
));
}
if config.num_eigenvalues == 0 {
return Err(SparseError::InvalidArgument(
"num_eigenvalues must be positive".to_string(),
));
}
if config.max_iterations < config.num_eigenvalues {
return Err(SparseError::InvalidArgument(format!(
"max_iterations ({}) must be >= num_eigenvalues ({})",
config.max_iterations, config.num_eigenvalues
)));
}
if config.max_iterations > n {
return Err(SparseError::InvalidArgument(format!(
"max_iterations ({}) must be <= matrix dimension n ({})",
config.max_iterations, n
)));
}
if config.tolerance <= 0.0 {
return Err(SparseError::InvalidArgument(
"tolerance must be positive".to_string(),
));
}
Ok(Self { config, n })
}
#[inline]
pub fn config(&self) -> &ArnoldiConfig {
&self.config
}
#[inline]
pub fn dimension(&self) -> usize {
self.n
}
pub fn workspace_bytes_f64(&self) -> usize {
let k = self.config.max_iterations;
let n = self.n;
let vectors = (k + 2) * n * 8; let hessenberg = (k + 1) * k * 8; vectors + hessenberg
}
pub fn workspace_bytes_f32(&self) -> usize {
let k = self.config.max_iterations;
let n = self.n;
let vectors = (k + 2) * n * 4;
let hessenberg = (k + 1) * k * 4;
vectors + hessenberg
}
pub fn generate_arnoldi_step_ptx(&self) -> SparseResult<String> {
emit_arnoldi_step_f64(self.n)
}
pub fn generate_arnoldi_step_ptx_f32(&self) -> SparseResult<String> {
emit_arnoldi_step_f32(self.n)
}
pub fn generate_gram_schmidt_ptx(&self) -> SparseResult<String> {
emit_gram_schmidt_f64(self.n)
}
pub fn generate_gram_schmidt_ptx_f32(&self) -> SparseResult<String> {
emit_gram_schmidt_f32(self.n)
}
pub fn generate_dot_product_ptx(&self) -> SparseResult<String> {
emit_dot_product_reduce_f64(self.n)
}
pub fn generate_dot_product_ptx_f32(&self) -> SparseResult<String> {
emit_dot_product_reduce_f32(self.n)
}
pub fn generate_norm_sq_ptx(&self) -> SparseResult<String> {
emit_norm_sq_reduce_f64(self.n)
}
pub fn generate_norm_sq_ptx_f32(&self) -> SparseResult<String> {
emit_norm_sq_reduce_f32(self.n)
}
}
fn emit_lanczos_step_f64(n: usize) -> SparseResult<String> {
emit_lanczos_step_typed::<f64>(n, "lanczos_step_f64")
}
fn emit_lanczos_step_f32(n: usize) -> SparseResult<String> {
emit_lanczos_step_typed::<f32>(n, "lanczos_step_f32")
}
fn emit_lanczos_step_typed<T: oxicuda_blas::GpuFloat>(
_n: usize,
kernel_name: &str,
) -> SparseResult<String> {
let is_f64 = T::SIZE == 8;
let elem_bytes = T::size_u32();
let mov_suffix = if is_f64 { "f64" } else { "f32" };
KernelBuilder::new(kernel_name)
.target(SmVersion::Sm80)
.param("w_ptr", PtxType::U64)
.param("v_j_ptr", PtxType::U64)
.param("v_jm1_ptr", PtxType::U64)
.param("v_jp1_ptr", PtxType::U64)
.param("alpha_bits", PtxType::U64)
.param("beta_prev_bits", PtxType::U64)
.param("beta_j_bits", PtxType::U64)
.param("n", PtxType::U32)
.body(move |b| {
let gid = b.global_thread_id_x();
let n_param = b.load_param_u32("n");
let gid_inner = gid.clone();
b.if_lt_u32(gid, n_param, move |b| {
let tid = gid_inner;
let w_ptr = b.load_param_u64("w_ptr");
let v_j_ptr = b.load_param_u64("v_j_ptr");
let v_jm1_ptr = b.load_param_u64("v_jm1_ptr");
let v_jp1_ptr = b.load_param_u64("v_jp1_ptr");
let alpha_bits = b.load_param_u64("alpha_bits");
let beta_prev_bits = b.load_param_u64("beta_prev_bits");
let beta_j_bits = b.load_param_u64("beta_j_bits");
let alpha = reinterpret_bits::<T>(b, alpha_bits);
let beta_prev = reinterpret_bits::<T>(b, beta_prev_bits);
let beta_j = reinterpret_bits::<T>(b, beta_j_bits);
let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
let w_val = load_global_float::<T>(b, w_addr.clone());
let vj_addr = b.byte_offset_addr(v_j_ptr, tid.clone(), elem_bytes);
let vj_val = load_global_float::<T>(b, vj_addr);
let vjm1_addr = b.byte_offset_addr(v_jm1_ptr, tid.clone(), elem_bytes);
let vjm1_val = load_global_float::<T>(b, vjm1_addr);
let alpha_vj = mul_float::<T>(b, alpha, vj_val);
let beta_vjm1 = mul_float::<T>(b, beta_prev, vjm1_val);
let sub1 = sub_float::<T>(b, w_val, alpha_vj);
let w_orth = sub_float::<T>(b, sub1, beta_vjm1);
store_global_float::<T>(b, w_addr, w_orth.clone());
let v_jp1_val = div_float::<T>(b, w_orth, beta_j);
let vjp1_addr = b.byte_offset_addr(v_jp1_ptr, tid, elem_bytes);
store_global_float::<T>(b, vjp1_addr, v_jp1_val);
});
let _ = mov_suffix;
b.ret();
})
.build()
.map_err(|e| SparseError::PtxGeneration(e.to_string()))
}
fn emit_reorthogonalize_f64(n: usize) -> SparseResult<String> {
emit_reorthogonalize_typed::<f64>(n, "reorthogonalize_f64")
}
fn emit_reorthogonalize_f32(n: usize) -> SparseResult<String> {
emit_reorthogonalize_typed::<f32>(n, "reorthogonalize_f32")
}
fn emit_reorthogonalize_typed<T: oxicuda_blas::GpuFloat>(
_n: usize,
kernel_name: &str,
) -> SparseResult<String> {
let elem_bytes = T::size_u32();
KernelBuilder::new(kernel_name)
.target(SmVersion::Sm80)
.param("w_ptr", PtxType::U64)
.param("v_i_ptr", PtxType::U64)
.param("coeff_bits", PtxType::U64)
.param("n", PtxType::U32)
.body(move |b| {
let gid = b.global_thread_id_x();
let n_param = b.load_param_u32("n");
let gid_inner = gid.clone();
b.if_lt_u32(gid, n_param, move |b| {
let tid = gid_inner;
let w_ptr = b.load_param_u64("w_ptr");
let v_i_ptr = b.load_param_u64("v_i_ptr");
let coeff_bits = b.load_param_u64("coeff_bits");
let coeff = reinterpret_bits::<T>(b, coeff_bits);
let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
let w_val = load_global_float::<T>(b, w_addr.clone());
let vi_addr = b.byte_offset_addr(v_i_ptr, tid, elem_bytes);
let vi_val = load_global_float::<T>(b, vi_addr);
let proj = mul_float::<T>(b, coeff, vi_val);
let w_new = sub_float::<T>(b, w_val, proj);
store_global_float::<T>(b, w_addr, w_new);
});
b.ret();
})
.build()
.map_err(|e| SparseError::PtxGeneration(e.to_string()))
}
fn emit_arnoldi_step_f64(n: usize) -> SparseResult<String> {
emit_arnoldi_step_typed::<f64>(n, "arnoldi_step_f64")
}
fn emit_arnoldi_step_f32(n: usize) -> SparseResult<String> {
emit_arnoldi_step_typed::<f32>(n, "arnoldi_step_f32")
}
fn emit_arnoldi_step_typed<T: oxicuda_blas::GpuFloat>(
_n: usize,
kernel_name: &str,
) -> SparseResult<String> {
let elem_bytes = T::size_u32();
KernelBuilder::new(kernel_name)
.target(SmVersion::Sm80)
.param("w_ptr", PtxType::U64)
.param("v_jp1_ptr", PtxType::U64)
.param("h_jp1_j_bits", PtxType::U64)
.param("n", PtxType::U32)
.body(move |b| {
let gid = b.global_thread_id_x();
let n_param = b.load_param_u32("n");
let gid_inner = gid.clone();
b.if_lt_u32(gid, n_param, move |b| {
let tid = gid_inner;
let w_ptr = b.load_param_u64("w_ptr");
let v_jp1_ptr = b.load_param_u64("v_jp1_ptr");
let h_bits = b.load_param_u64("h_jp1_j_bits");
let h_jp1_j = reinterpret_bits::<T>(b, h_bits);
let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
let w_val = load_global_float::<T>(b, w_addr);
let v_new = div_float::<T>(b, w_val, h_jp1_j);
let vjp1_addr = b.byte_offset_addr(v_jp1_ptr, tid, elem_bytes);
store_global_float::<T>(b, vjp1_addr, v_new);
});
b.ret();
})
.build()
.map_err(|e| SparseError::PtxGeneration(e.to_string()))
}
fn emit_gram_schmidt_f64(n: usize) -> SparseResult<String> {
emit_gram_schmidt_typed::<f64>(n, "gram_schmidt_f64")
}
fn emit_gram_schmidt_f32(n: usize) -> SparseResult<String> {
emit_gram_schmidt_typed::<f32>(n, "gram_schmidt_f32")
}
fn emit_gram_schmidt_typed<T: oxicuda_blas::GpuFloat>(
_n: usize,
kernel_name: &str,
) -> SparseResult<String> {
let elem_bytes = T::size_u32();
KernelBuilder::new(kernel_name)
.target(SmVersion::Sm80)
.param("w_ptr", PtxType::U64)
.param("v_i_ptr", PtxType::U64)
.param("h_ij_bits", PtxType::U64)
.param("n", PtxType::U32)
.body(move |b| {
let gid = b.global_thread_id_x();
let n_param = b.load_param_u32("n");
let gid_inner = gid.clone();
b.if_lt_u32(gid, n_param, move |b| {
let tid = gid_inner;
let w_ptr = b.load_param_u64("w_ptr");
let v_i_ptr = b.load_param_u64("v_i_ptr");
let h_bits = b.load_param_u64("h_ij_bits");
let h_ij = reinterpret_bits::<T>(b, h_bits);
let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
let w_val = load_global_float::<T>(b, w_addr.clone());
let vi_addr = b.byte_offset_addr(v_i_ptr, tid, elem_bytes);
let vi_val = load_global_float::<T>(b, vi_addr);
let proj = mul_float::<T>(b, h_ij, vi_val);
let w_new = sub_float::<T>(b, w_val, proj);
store_global_float::<T>(b, w_addr, w_new);
});
b.ret();
})
.build()
.map_err(|e| SparseError::PtxGeneration(e.to_string()))
}
fn emit_dot_product_reduce_f64(_n: usize) -> SparseResult<String> {
emit_dot_product_reduce_typed::<f64>("dot_product_reduce_f64")
}
fn emit_dot_product_reduce_f32(_n: usize) -> SparseResult<String> {
emit_dot_product_reduce_typed::<f32>("dot_product_reduce_f32")
}
fn emit_dot_product_reduce_typed<T: oxicuda_blas::GpuFloat>(
kernel_name: &str,
) -> SparseResult<String> {
let elem_bytes = T::size_u32();
KernelBuilder::new(kernel_name)
.target(SmVersion::Sm80)
.param("a_ptr", PtxType::U64)
.param("b_ptr", PtxType::U64)
.param("result_ptr", PtxType::U64)
.param("n", PtxType::U32)
.body(move |b| {
let gid = b.global_thread_id_x();
let n_param = b.load_param_u32("n");
let gid_for_lane = gid.clone();
let prod = load_float_imm::<T>(b, 0.0);
let gid_inner = gid.clone();
let prod_inner = prod.clone();
b.if_lt_u32(gid, n_param, move |b| {
let tid = gid_inner;
let a_ptr = b.load_param_u64("a_ptr");
let b_ptr_reg = b.load_param_u64("b_ptr");
let a_addr = b.byte_offset_addr(a_ptr, tid.clone(), elem_bytes);
let a_val = load_global_float::<T>(b, a_addr);
let b_addr = b.byte_offset_addr(b_ptr_reg, tid, elem_bytes);
let b_val = load_global_float::<T>(b, b_addr);
let p = mul_float::<T>(b, a_val, b_val);
let suffix = if T::SIZE == 8 { "f64" } else { "f32" };
b.raw_ptx(&format!("mov.{suffix} {prod_inner}, {p};"));
});
let reduced = emit_warp_reduce_sum::<T>(b, prod);
let lane = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("and.b32 {lane}, {gid_for_lane}, 31;"));
let not_lane_0 = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.ne.u32 {not_lane_0}, {lane}, 0;"));
let skip_label = b.fresh_label("dot_skip");
b.branch_if(not_lane_0, &skip_label);
let result_ptr = b.load_param_u64("result_ptr");
crate::ptx_helpers::emit_atomic_add_float::<T>(b, result_ptr, reduced);
b.label(&skip_label);
b.ret();
})
.build()
.map_err(|e| SparseError::PtxGeneration(e.to_string()))
}
fn emit_norm_sq_reduce_f64(_n: usize) -> SparseResult<String> {
emit_norm_sq_reduce_typed::<f64>("norm_sq_reduce_f64")
}
fn emit_norm_sq_reduce_f32(_n: usize) -> SparseResult<String> {
emit_norm_sq_reduce_typed::<f32>("norm_sq_reduce_f32")
}
fn emit_norm_sq_reduce_typed<T: oxicuda_blas::GpuFloat>(kernel_name: &str) -> SparseResult<String> {
let elem_bytes = T::size_u32();
KernelBuilder::new(kernel_name)
.target(SmVersion::Sm80)
.param("v_ptr", PtxType::U64)
.param("result_ptr", PtxType::U64)
.param("n", PtxType::U32)
.body(move |b| {
let gid = b.global_thread_id_x();
let n_param = b.load_param_u32("n");
let gid_for_lane = gid.clone();
let sq = load_float_imm::<T>(b, 0.0);
let gid_inner = gid.clone();
let sq_inner = sq.clone();
b.if_lt_u32(gid, n_param, move |b| {
let tid = gid_inner;
let v_ptr = b.load_param_u64("v_ptr");
let v_addr = b.byte_offset_addr(v_ptr, tid, elem_bytes);
let v_val = load_global_float::<T>(b, v_addr);
let p = mul_float::<T>(b, v_val.clone(), v_val);
let suffix = if T::SIZE == 8 { "f64" } else { "f32" };
b.raw_ptx(&format!("mov.{suffix} {sq_inner}, {p};"));
});
let reduced = emit_warp_reduce_sum::<T>(b, sq);
let lane = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("and.b32 {lane}, {gid_for_lane}, 31;"));
let not_lane_0 = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.ne.u32 {not_lane_0}, {lane}, 0;"));
let skip_label = b.fresh_label("norm_skip");
b.branch_if(not_lane_0, &skip_label);
let result_ptr = b.load_param_u64("result_ptr");
crate::ptx_helpers::emit_atomic_add_float::<T>(b, result_ptr, reduced);
b.label(&skip_label);
b.ret();
})
.build()
.map_err(|e| SparseError::PtxGeneration(e.to_string()))
}
fn reinterpret_bits<T: oxicuda_blas::GpuFloat>(
b: &mut BodyBuilder<'_>,
bits: Register,
) -> Register {
crate::ptx_helpers::reinterpret_bits_to_float::<T>(b, bits)
}
fn sub_float<T: oxicuda_blas::GpuFloat>(
b: &mut BodyBuilder<'_>,
a: Register,
bv: Register,
) -> Register {
if T::PTX_TYPE == PtxType::F32 {
let dst = b.alloc_reg(PtxType::F32);
b.raw_ptx(&format!("sub.rn.f32 {dst}, {a}, {bv};"));
dst
} else {
let dst = b.alloc_reg(PtxType::F64);
b.raw_ptx(&format!("sub.rn.f64 {dst}, {a}, {bv};"));
dst
}
}
fn div_float<T: oxicuda_blas::GpuFloat>(
b: &mut BodyBuilder<'_>,
a: Register,
bv: Register,
) -> Register {
if T::PTX_TYPE == PtxType::F32 {
let dst = b.alloc_reg(PtxType::F32);
b.raw_ptx(&format!("div.rn.f32 {dst}, {a}, {bv};"));
dst
} else {
let dst = b.alloc_reg(PtxType::F64);
b.raw_ptx(&format!("div.rn.f64 {dst}, {a}, {bv};"));
dst
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ptx_helpers::test_support::assert_assembles_and_clean;
#[test]
fn krylov_reductions_f32_f64_assemble_sm86() {
let dot_f32 = emit_dot_product_reduce_f32(1024).expect("dot f32");
assert_assembles_and_clean("krylov_dot_f32", &dot_f32);
let dot_f64 = emit_dot_product_reduce_f64(1024).expect("dot f64");
assert_assembles_and_clean("krylov_dot_f64", &dot_f64);
let norm_f32 = emit_norm_sq_reduce_f32(1024).expect("norm f32");
assert_assembles_and_clean("krylov_norm_f32", &norm_f32);
let norm_f64 = emit_norm_sq_reduce_f64(1024).expect("norm f64");
assert_assembles_and_clean("krylov_norm_f64", &norm_f64);
assert!(
!dot_f64.contains("0F00000000") && !norm_f64.contains("0F00000000"),
"f64 Krylov reduction kernels must not materialize an f32 0.0 immediate"
);
}
#[test]
fn lanczos_new_valid_config() {
let config = LanczosConfig {
max_iterations: 50,
tolerance: 1e-10,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let plan = LanczosPlan::new(config, 100);
assert!(plan.is_ok());
let plan = plan.expect("test: valid config should succeed");
assert_eq!(plan.dimension(), 100);
}
#[test]
fn lanczos_rejects_zero_dimension() {
let config = LanczosConfig {
max_iterations: 10,
tolerance: 1e-6,
num_eigenvalues: 3,
which: EigenTarget::SmallestMagnitude,
};
let result = LanczosPlan::new(config, 0);
assert!(result.is_err());
match result {
Err(SparseError::InvalidArgument(msg)) => {
assert!(msg.contains("dimension"));
}
other => panic!("expected InvalidArgument, got: {other:?}"),
}
}
#[test]
fn lanczos_rejects_zero_eigenvalues() {
let config = LanczosConfig {
max_iterations: 10,
tolerance: 1e-6,
num_eigenvalues: 0,
which: EigenTarget::LargestAlgebraic,
};
let result = LanczosPlan::new(config, 100);
assert!(result.is_err());
}
#[test]
fn lanczos_rejects_iterations_less_than_eigenvalues() {
let config = LanczosConfig {
max_iterations: 3,
tolerance: 1e-6,
num_eigenvalues: 10,
which: EigenTarget::SmallestAlgebraic,
};
let result = LanczosPlan::new(config, 100);
assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
}
#[test]
fn lanczos_rejects_iterations_greater_than_n() {
let config = LanczosConfig {
max_iterations: 200,
tolerance: 1e-6,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let result = LanczosPlan::new(config, 100);
assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
}
#[test]
fn lanczos_rejects_non_positive_tolerance() {
let config = LanczosConfig {
max_iterations: 50,
tolerance: 0.0,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let result = LanczosPlan::new(config, 100);
assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
let config_neg = LanczosConfig {
max_iterations: 50,
tolerance: -1e-6,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let result_neg = LanczosPlan::new(config_neg, 100);
assert!(matches!(result_neg, Err(SparseError::InvalidArgument(_))));
}
#[test]
fn lanczos_step_ptx_f64_generates() {
let config = LanczosConfig {
max_iterations: 30,
tolerance: 1e-10,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
let ptx = plan.generate_lanczos_step_ptx();
assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry lanczos_step_f64"));
assert!(ptx_str.contains(".target sm_80"));
assert!(ptx_str.contains("w_ptr"));
assert!(ptx_str.contains("v_j_ptr"));
}
#[test]
fn lanczos_step_ptx_f32_generates() {
let config = LanczosConfig {
max_iterations: 20,
tolerance: 1e-6,
num_eigenvalues: 3,
which: EigenTarget::SmallestMagnitude,
};
let plan = LanczosPlan::new(config, 500).expect("test: valid config");
let ptx = plan.generate_lanczos_step_ptx_f32();
assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry lanczos_step_f32"));
}
#[test]
fn lanczos_reorthogonalize_ptx_generates() {
let config = LanczosConfig {
max_iterations: 30,
tolerance: 1e-10,
num_eigenvalues: 5,
which: EigenTarget::LargestAlgebraic,
};
let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
let ptx = plan.generate_reorthogonalize_ptx();
assert!(ptx.is_ok(), "Reorthogonalize PTX failed: {ptx:?}");
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry reorthogonalize_f64"));
assert!(ptx_str.contains("w_ptr"));
}
#[test]
fn arnoldi_new_valid_config() {
let config = ArnoldiConfig {
max_iterations: 50,
tolerance: 1e-10,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let plan = ArnoldiPlan::new(config, 200);
assert!(plan.is_ok());
let plan = plan.expect("test: valid config should succeed");
assert_eq!(plan.dimension(), 200);
}
#[test]
fn arnoldi_rejects_invalid_config() {
let config = ArnoldiConfig {
max_iterations: 10,
tolerance: 1e-6,
num_eigenvalues: 3,
which: EigenTarget::LargestMagnitude,
};
assert!(ArnoldiPlan::new(config, 0).is_err());
let config2 = ArnoldiConfig {
max_iterations: 500,
tolerance: 1e-6,
num_eigenvalues: 3,
which: EigenTarget::SmallestMagnitude,
};
assert!(ArnoldiPlan::new(config2, 100).is_err());
let config3 = ArnoldiConfig {
max_iterations: 5,
tolerance: 1e-6,
num_eigenvalues: 20,
which: EigenTarget::LargestAlgebraic,
};
assert!(ArnoldiPlan::new(config3, 100).is_err());
}
#[test]
fn arnoldi_step_ptx_f64_generates() {
let config = ArnoldiConfig {
max_iterations: 30,
tolerance: 1e-10,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
let ptx = plan.generate_arnoldi_step_ptx();
assert!(ptx.is_ok(), "Arnoldi PTX failed: {ptx:?}");
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry arnoldi_step_f64"));
assert!(ptx_str.contains("w_ptr"));
}
#[test]
fn arnoldi_step_ptx_f32_generates() {
let config = ArnoldiConfig {
max_iterations: 20,
tolerance: 1e-6,
num_eigenvalues: 3,
which: EigenTarget::SmallestAlgebraic,
};
let plan = ArnoldiPlan::new(config, 300).expect("test: valid config");
let ptx = plan.generate_arnoldi_step_ptx_f32();
assert!(ptx.is_ok(), "Arnoldi f32 PTX failed: {ptx:?}");
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry arnoldi_step_f32"));
}
#[test]
fn arnoldi_gram_schmidt_ptx_generates() {
let config = ArnoldiConfig {
max_iterations: 30,
tolerance: 1e-10,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
let ptx = plan.generate_gram_schmidt_ptx();
assert!(ptx.is_ok(), "Gram-Schmidt PTX failed: {ptx:?}");
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry gram_schmidt_f64"));
}
#[test]
fn lanczos_workspace_size_f64() {
let config = LanczosConfig {
max_iterations: 50,
tolerance: 1e-10,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
let ws = plan.workspace_bytes_f64();
assert_eq!(ws, 416_800);
}
#[test]
fn lanczos_workspace_size_f32() {
let config = LanczosConfig {
max_iterations: 50,
tolerance: 1e-10,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
let ws = plan.workspace_bytes_f32();
assert_eq!(ws, 208_400);
}
#[test]
fn arnoldi_workspace_size_f64() {
let config = ArnoldiConfig {
max_iterations: 30,
tolerance: 1e-10,
num_eigenvalues: 5,
which: EigenTarget::LargestMagnitude,
};
let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
let ws = plan.workspace_bytes_f64();
assert_eq!(ws, 135_440);
}
#[test]
fn lanczos_result_tridiagonal_structure() {
let result = LanczosResult {
eigenvalues: vec![5.0, 3.0, 1.0],
alpha: vec![4.0, 3.5, 2.0, 1.5, 1.0], beta: vec![1.2, 0.8, 0.5, 0.3], iterations: 5,
converged: true,
};
assert_eq!(result.alpha.len(), 5);
assert_eq!(result.beta.len(), result.alpha.len() - 1);
assert!(result.converged);
assert_eq!(result.iterations, 5);
}
#[test]
#[allow(clippy::needless_range_loop)]
fn arnoldi_result_hessenberg_structure() {
let k = 4;
let mut h = vec![vec![0.0; k]; k + 1]; for j in 0..k {
for i in 0..=j + 1 {
h[i][j] = (i + j + 1) as f64;
}
}
for j in 0..k {
for i in (j + 2)..(k + 1) {
assert!(
(h[i][j]).abs() < 1e-15,
"h[{i}][{j}] should be zero in upper Hessenberg"
);
}
}
let result = ArnoldiResult {
eigenvalues: vec![(3.0, 0.5), (3.0, -0.5), (1.0, 0.0)],
hessenberg: h,
iterations: k,
converged: true,
};
assert_eq!(result.hessenberg.len(), k + 1);
assert_eq!(result.hessenberg[0].len(), k);
assert!(result.converged);
let (r1, i1) = result.eigenvalues[0];
let (r2, i2) = result.eigenvalues[1];
assert!((r1 - r2).abs() < 1e-15, "conjugate pair: same real part");
assert!(
(i1 + i2).abs() < 1e-15,
"conjugate pair: opposite imag part"
);
}
#[test]
fn eigen_target_variants() {
let targets = [
EigenTarget::LargestMagnitude,
EigenTarget::SmallestMagnitude,
EigenTarget::LargestAlgebraic,
EigenTarget::SmallestAlgebraic,
];
for i in 0..targets.len() {
for j in (i + 1)..targets.len() {
assert_ne!(targets[i], targets[j]);
}
}
}
#[test]
fn dot_product_reduce_ptx_f64_generates() {
let ptx = emit_dot_product_reduce_f64(1000);
assert!(ptx.is_ok(), "dot product PTX failed: {ptx:?}");
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry dot_product_reduce_f64"));
}
#[test]
fn dot_product_reduce_ptx_f32_generates() {
let ptx = emit_dot_product_reduce_f32(1000);
assert!(ptx.is_ok());
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry dot_product_reduce_f32"));
}
#[test]
fn norm_sq_reduce_ptx_generates() {
let ptx_f64 = emit_norm_sq_reduce_f64(1000);
assert!(ptx_f64.is_ok());
let ptx_str = ptx_f64.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry norm_sq_reduce_f64"));
let ptx_f32 = emit_norm_sq_reduce_f32(1000);
assert!(ptx_f32.is_ok());
let ptx_str_f32 = ptx_f32.expect("test: PTX gen should succeed");
assert!(ptx_str_f32.contains(".entry norm_sq_reduce_f32"));
}
#[test]
fn plan_config_accessors() {
let lanczos_config = LanczosConfig {
max_iterations: 40,
tolerance: 1e-8,
num_eigenvalues: 10,
which: EigenTarget::SmallestAlgebraic,
};
let plan = LanczosPlan::new(lanczos_config.clone(), 200).expect("test: valid config");
assert_eq!(plan.config().max_iterations, 40);
assert_eq!(plan.config().num_eigenvalues, 10);
assert!((plan.config().tolerance - 1e-8).abs() < 1e-15);
assert_eq!(plan.config().which, EigenTarget::SmallestAlgebraic);
let arnoldi_config = ArnoldiConfig {
max_iterations: 25,
tolerance: 1e-12,
num_eigenvalues: 6,
which: EigenTarget::LargestAlgebraic,
};
let aplan = ArnoldiPlan::new(arnoldi_config, 300).expect("test: valid config");
assert_eq!(aplan.config().max_iterations, 25);
assert_eq!(aplan.config().num_eigenvalues, 6);
assert_eq!(aplan.dimension(), 300);
}
}