Files
rustytorch/integration_tests/src/cusolver_integration.rs
T
2026-03-04 00:08:42 +00:00

745 lines
24 KiB
Rust

//! Integration tests for cuSOLVER functionality
//!
//! These tests verify that cuSOLVER integration works correctly across
//! the entire RTX tensor ecosystem and provides expected performance benefits.
use crate::{IntegrationTestConfig, TestResults, integration_test};
use rtx_tensor::{Tensor, Device};
#[cfg(feature = "cuda")]
use rtx_tensor::cusolver::{
CuSolverContext, DenseSolver, SparseSolver, BatchedSolver,
SvdConfig, QrConfig, BatchedSvdConfig
};
use rtx_science::{
computing::CuSolverScientific,
physics::{CuSolverPINN, PhysicsConfig}
};
use anyhow::Result;
use std::time::Instant;
use tracing::{info, warn};
/// Run comprehensive cuSOLVER integration tests
pub async fn run_cusolver_tests(config: &IntegrationTestConfig) -> Result<TestResults> {
info!("Starting cuSOLVER integration tests");
let mut results = TestResults::new();
if config.skip_gpu_tests {
results.add_skip("GPU tests disabled");
return Ok(results);
}
// Basic functionality tests
integration_test!("cuSOLVER Context Creation", || test_cusolver_context_creation(config), results);
integration_test!("Dense Operations", || test_dense_operations(config), results);
integration_test!("Sparse Operations", || test_sparse_operations(config), results);
integration_test!("Batched Operations", || test_batched_operations(config), results);
// Scientific computing tests
integration_test!("Scientific Computing PCA", || test_scientific_pca(config), results);
integration_test!("Linear System Solving", || test_linear_system_solving(config), results);
integration_test!("Matrix Factorization", || test_matrix_factorization(config), results);
// Physics simulation tests
integration_test!("Physics Heat Equation", || test_physics_heat_equation(config), results);
integration_test!("Physics Eigenvalue Problem", || test_physics_eigenvalue(config), results);
integration_test!("Physics PCA Analysis", || test_physics_pca(config), results);
// Numerical accuracy tests
integration_test!("SVD Reconstruction Accuracy", || test_svd_accuracy(config), results);
integration_test!("Linear Solve Accuracy", || test_solve_accuracy(config), results);
integration_test!("Eigenvalue Accuracy", || test_eigenvalue_accuracy(config), results);
// Performance benchmarks (if performance mode enabled)
if config.performance_mode {
integration_test!("SVD Performance Benchmark", || test_svd_performance(config), results);
integration_test!("Batched Performance Benchmark", || test_batched_performance(config), results);
integration_test!("Memory Usage Benchmark", || test_memory_usage(config), results);
}
// Error handling tests
integration_test!("Error Handling", || test_error_handling(config), results);
info!("cuSOLVER integration tests completed");
results.print_summary();
Ok(results)
}
#[cfg(feature = "cuda")]
async fn test_cusolver_context_creation(_config: &IntegrationTestConfig) -> Result<()> {
let device = Device::cuda(0)?;
let context = CuSolverContext::new(&device)?;
if !context.is_initialized() {
anyhow::bail!("cuSOLVER context not properly initialized");
}
info!("✓ cuSOLVER context created successfully");
Ok(())
}
#[cfg(not(feature = "cuda"))]
async fn test_cusolver_context_creation(_config: &IntegrationTestConfig) -> Result<()> {
warn!("CUDA not available, skipping cuSOLVER context test");
Ok(())
}
#[cfg(feature = "cuda")]
async fn test_dense_operations(_config: &IntegrationTestConfig) -> Result<()> {
let device = Device::cuda(0)?;
let context = CuSolverContext::new(&device)?;
let solver = DenseSolver::new(context);
// Test SVD on a known matrix
let matrix = Tensor::from_data(
vec![4.0f32, 1.0, 2.0, 3.0, 5.0, 6.0],
vec![2, 3],
&device,
)?;
let config = SvdConfig::default();
let svd_result = solver.svd(&matrix, &config);
match svd_result {
Ok(svd) => {
if svd.u.is_none() || svd.vt.is_none() {
anyhow::bail!("SVD did not compute U or VT matrices");
}
if svd.s.shape().dims()[0] != 2 {
anyhow::bail!("SVD produced wrong number of singular values");
}
info!("✓ Dense SVD operation successful");
},
Err(e) => {
warn!("Dense SVD fell back to CPU: {}", e);
}
}
// Test QR decomposition
let square_matrix = Tensor::randn(&[100, 80], &device)?;
let qr_config = QrConfig::default();
let qr_result = solver.qr(&square_matrix, &qr_config);
match qr_result {
Ok(qr) => {
if qr.q.is_none() {
anyhow::bail!("QR did not compute Q matrix");
}
info!("✓ Dense QR operation successful");
},
Err(e) => {
warn!("Dense QR fell back to CPU: {}", e);
}
}
Ok(())
}
#[cfg(not(feature = "cuda"))]
async fn test_dense_operations(_config: &IntegrationTestConfig) -> Result<()> {
warn!("CUDA not available, skipping dense operations test");
Ok(())
}
#[cfg(feature = "cuda")]
async fn test_sparse_operations(_config: &IntegrationTestConfig) -> Result<()> {
let device = Device::cuda(0)?;
let context = CuSolverContext::new(&device)?;
let solver = SparseSolver::new(context);
// Create a test sparse matrix
let sparse_matrix = create_test_sparse_matrix(&device)?;
let rhs = Tensor::ones([3], &device)?;
let config = rtx_tensor::cusolver::sparse::IterativeSolverConfig::default();
let result = solver.solve_iterative(&sparse_matrix, &rhs, &config);
match result {
Ok(solve_result) => {
if solve_result.iterations == 0 {
anyhow::bail!("Iterative solver did not run any iterations");
}
info!("✓ Sparse iterative solver completed in {} iterations", solve_result.iterations);
},
Err(e) => {
warn!("Sparse solver fell back or failed: {}", e);
}
}
Ok(())
}
#[cfg(not(feature = "cuda"))]
async fn test_sparse_operations(_config: &IntegrationTestConfig) -> Result<()> {
warn!("CUDA not available, skipping sparse operations test");
Ok(())
}
#[cfg(feature = "cuda")]
fn create_test_sparse_matrix(device: &Device) -> Result<rtx_tensor::sparse::SparseCSR> {
use rtx_tensor::sparse::SparseCSR;
// Create a simple 3x3 tridiagonal matrix
let values = vec![2.0f32, -1.0, 2.0, -1.0, 2.0];
let col_indices = vec![0, 1, 1, 2, 2];
let row_ptr = vec![0, 2, 4, 5];
let shape = rtx_tensor::Shape::from([3, 3]);
Ok(SparseCSR::from_csr_arrays(row_ptr, col_indices, values, shape, device)?)
}
#[cfg(feature = "cuda")]
async fn test_batched_operations(_config: &IntegrationTestConfig) -> Result<()> {
let device = Device::cuda(0)?;
let context = CuSolverContext::new(&device)?;
let solver = BatchedSolver::new(context)?;
// Create batch of matrices
let matrices: Result<Vec<_>, _> = (0..8)
.map(|_| Tensor::randn(&[16, 12], &device))
.collect();
let matrices = matrices?;
let config = BatchedSvdConfig::default();
let result = solver.svd_batched(&matrices, &config);
match result {
Ok(batch_result) => {
if batch_result.results.len() != 8 {
anyhow::bail!("Batched operation processed wrong number of matrices");
}
if batch_result.total_time < 0.0 {
anyhow::bail!("Invalid timing measurement");
}
info!("✓ Batched SVD processed {} matrices in {:.2}ms",
batch_result.results.len(), batch_result.total_time * 1000.0);
},
Err(e) => {
warn!("Batched operation fell back: {}", e);
}
}
Ok(())
}
#[cfg(not(feature = "cuda"))]
async fn test_batched_operations(_config: &IntegrationTestConfig) -> Result<()> {
warn!("CUDA not available, skipping batched operations test");
Ok(())
}
async fn test_scientific_pca(_config: &IntegrationTestConfig) -> Result<()> {
let device = if cfg!(feature = "cuda") {
Device::cuda(0).unwrap_or_else(|_| Device::cpu())
} else {
Device::cpu()
};
let scientific = CuSolverScientific::new(device.clone())?;
// Create test data matrix
let data = Tensor::randn(&[100, 20], &device)?;
let result = scientific.pca(&data, 5);
match result {
Ok(pca_result) => {
if pca_result.n_components != 5 {
anyhow::bail!("PCA returned wrong number of components");
}
if pca_result.components.shape().dims()[0] != 5 {
anyhow::bail!("PCA components have wrong shape");
}
if pca_result.explained_variance_ratio <= 0.0 {
anyhow::bail!("PCA explained variance ratio is invalid");
}
info!("✓ Scientific PCA: {} components, {:.2}% variance explained",
pca_result.n_components,
pca_result.explained_variance_ratio * 100.0);
},
Err(e) => {
warn!("Scientific PCA fell back to CPU: {}", e);
}
}
Ok(())
}
async fn test_linear_system_solving(_config: &IntegrationTestConfig) -> Result<()> {
let device = if cfg!(feature = "cuda") {
Device::cuda(0).unwrap_or_else(|_| Device::cpu())
} else {
Device::cpu()
};
let scientific = CuSolverScientific::new(device.clone())?;
// Create a well-conditioned system
let a = Tensor::eye(10, &device)?;
let b = Tensor::ones([10, 1], &device)?;
let result = scientific.solve_linear_system(&a, &b);
match result {
Ok(solve_result) => {
if !solve_result.converged && solve_result.residual_norm > 1e-8 {
anyhow::bail!("Linear system did not converge adequately");
}
info!("✓ Linear system solved: residual = {:.2e}, condition = {:.2e}",
solve_result.residual_norm, solve_result.condition_number);
},
Err(e) => {
warn!("Linear system solving fell back: {}", e);
}
}
Ok(())
}
async fn test_matrix_factorization(_config: &IntegrationTestConfig) -> Result<()> {
let device = if cfg!(feature = "cuda") {
Device::cuda(0).unwrap_or_else(|_| Device::cpu())
} else {
Device::cpu()
};
let scientific = CuSolverScientific::new(device.clone())?;
let matrix = Tensor::eye(8, &device)?;
// Test QR factorization
let qr_result = scientific.matrix_factorization(&matrix, rtx_science::computing::FactorizationMethod::QR);
if let Ok(qr_fact) = qr_result {
if let rtx_science::computing::FactorizationResult::QR { r, .. } = qr_fact
&& r.shape().dims() != vec![8, 8] {
anyhow::bail!("QR factorization produced wrong R matrix shape");
}
info!("✓ QR factorization successful");
}
// Test LU factorization
let lu_result = scientific.matrix_factorization(&matrix, rtx_science::computing::FactorizationMethod::LU);
if let Ok(lu_fact) = lu_result {
if let rtx_science::computing::FactorizationResult::LU { l, u, .. } = lu_fact
&& (l.shape().dims() != vec![8, 8] || u.shape().dims() != vec![8, 8]) {
anyhow::bail!("LU factorization produced wrong matrix shapes");
}
info!("✓ LU factorization successful");
}
Ok(())
}
async fn test_physics_heat_equation(_config: &IntegrationTestConfig) -> Result<()> {
let device = if cfg!(feature = "cuda") {
Device::cuda(0).unwrap_or_else(|_| Device::cpu())
} else {
Device::cpu()
};
let config = PhysicsConfig {
n_collocation: 25, // Small for testing
n_boundary: 10,
tolerance: 1e-6,
..Default::default()
};
let pinn = CuSolverPINN::new(device, config)?;
let initial_condition = |x: f64, _y: f64| (std::f64::consts::PI * x).sin();
let boundary_condition = |_x: f64, _y: f64, _t: f64| 0.0; // Homogeneous
let result = pinn.solve_heat_equation(
0.1, // alpha
initial_condition,
boundary_condition,
0.5, // t_final (shorter for testing)
);
match result {
Ok(solution) => {
if solution.x_coords.shape().dims()[0] == 0 {
anyhow::bail!("Heat equation solution has no spatial coordinates");
}
info!("✓ Heat equation solved: condition = {:.2e}, converged = {}",
solution.condition_number, solution.converged);
},
Err(e) => {
warn!("Heat equation solver fell back: {}", e);
}
}
Ok(())
}
async fn test_physics_eigenvalue(_config: &IntegrationTestConfig) -> Result<()> {
let device = if cfg!(feature = "cuda") {
Device::cuda(0).unwrap_or_else(|_| Device::cpu())
} else {
Device::cpu()
};
let config = PhysicsConfig::default();
let pinn = CuSolverPINN::new(device, config)?;
// Harmonic oscillator potential
let potential = |x: f64| 0.5 * x * x;
let result = pinn.solve_quantum_eigenvalue_problem(potential, 3);
match result {
Ok(eigen_result) => {
if eigen_result.n_computed == 0 {
anyhow::bail!("No eigenvalues computed");
}
info!("✓ Quantum eigenvalue problem: {} states computed, condition = {:.2e}",
eigen_result.n_computed, eigen_result.condition_number);
// Check ground state energy for harmonic oscillator (should be ~0.5)
if let Ok(e0) = eigen_result.energies.get(&[0]) {
let energy = e0.abs();
if energy > 10.0 {
warn!("Ground state energy seems high: {:.4}", energy);
} else {
info!("Ground state energy: {:.4}", energy);
}
}
},
Err(e) => {
warn!("Quantum eigenvalue problem fell back: {}", e);
}
}
Ok(())
}
async fn test_physics_pca(_config: &IntegrationTestConfig) -> Result<()> {
let device = if cfg!(feature = "cuda") {
Device::cuda(0).unwrap_or_else(|_| Device::cpu())
} else {
Device::cpu()
};
let config = PhysicsConfig::default();
let pinn = CuSolverPINN::new(device.clone(), config)?;
// Generate mock physics data (could be simulation snapshots)
let data = Tensor::randn(&[50, 10], &device)?;
let result = pinn.physics_pca(&data, 3);
match result {
Ok(pca_result) => {
if pca_result.n_components != 3 {
anyhow::bail!("Physics PCA returned wrong number of components");
}
info!("✓ Physics PCA: {} components, {:.2}% variance",
pca_result.n_components,
pca_result.explained_variance * 100.0);
},
Err(e) => {
warn!("Physics PCA fell back: {}", e);
}
}
Ok(())
}
async fn test_svd_accuracy(_config: &IntegrationTestConfig) -> Result<()> {
let device = if cfg!(feature = "cuda") {
Device::cuda(0).unwrap_or_else(|_| Device::cpu())
} else {
Device::cpu()
};
// Create a known matrix for testing
let matrix = Tensor::from_data(
vec![4.0f32, 7.0, 2.0, 6.0],
vec![2, 2],
&device,
)?;
let svd_result = matrix.svd(true, true)?;
// Reconstruct: A = U * S * VT
let s_diag = Tensor::diag(&svd_result.s)?;
let us = svd_result.u.matmul(&s_diag)?;
let reconstructed = us.matmul(&svd_result.vt)?;
// Check reconstruction error
let error_tensor = matrix.sub(&reconstructed)?;
// Compute Frobenius norm manually since norm_l2 doesn't exist
let squared = error_tensor.mul(&error_tensor)?;
let sum = squared.sum(None)?;
let error = sum.item()?.sqrt();
if error > 1e-4 {
anyhow::bail!("SVD reconstruction error too large: {error:.2e}");
}
info!("✓ SVD reconstruction accuracy: error = {:.2e}", error);
Ok(())
}
async fn test_solve_accuracy(_config: &IntegrationTestConfig) -> Result<()> {
let device = if cfg!(feature = "cuda") {
Device::cuda(0).unwrap_or_else(|_| Device::cpu())
} else {
Device::cpu()
};
// Solve: [2, 1; 1, 2] * x = [3; 3], expected solution: x = [1, 1]
let a = Tensor::from_data(vec![2.0f32, 1.0, 1.0, 2.0], vec![2, 2], &device)?;
let b = Tensor::from_data(vec![3.0f32, 3.0], vec![2, 1], &device)?;
let a_inv = a.inverse()?;
let solution = a_inv.matmul(&b)?;
// Check residual: ||Ax - b||
let ax = a.matmul(&solution)?;
let residual = ax.sub(&b)?;
// Compute norm manually
let squared = residual.mul(&residual)?;
let sum = squared.sum(None)?;
let residual_val = sum.item()?.sqrt();
if residual_val > 1e-4 {
anyhow::bail!("Linear solve residual too large: {residual_val:.2e}");
}
// Check if solution is close to expected [1, 1]
let x1 = solution.get(&[0, 0])?;
if (x1 - 1.0).abs() > 1e-3 {
warn!("Solution component deviation: {:.4} (expected ~1.0)", x1);
}
info!("✓ Linear solve accuracy: residual = {:.2e}, solution[0] = {:.4}",
residual_val, x1);
Ok(())
}
async fn test_eigenvalue_accuracy(_config: &IntegrationTestConfig) -> Result<()> {
let device = if cfg!(feature = "cuda") {
Device::cuda(0).unwrap_or_else(|_| Device::cpu())
} else {
Device::cpu()
};
// Create symmetric matrix with known eigenvalues
let matrix = Tensor::from_data(
vec![3.0f32, 1.0, 1.0, 3.0],
vec![2, 2],
&device,
)?;
let eigen_result = matrix.symeig(true)?;
// For this matrix, eigenvalues should be [2, 4]
let eigenvals = eigen_result.eigenvalues;
let first_eval = eigenvals.get(&[0])?;
// Eigenvalues should be positive and reasonable
if first_eval <= 0.0 || first_eval > 10.0 {
warn!("Eigenvalue seems unreasonable: {:.4}", first_eval);
}
info!("✓ Eigenvalue computation: first eigenvalue = {:.4}", first_eval);
Ok(())
}
#[cfg(feature = "cuda")]
async fn test_svd_performance(_config: &IntegrationTestConfig) -> Result<()> {
let device = Device::cuda(0)?;
let context = CuSolverContext::new(&device)?;
let solver = DenseSolver::new(context);
let sizes = vec![256, 512, 1024];
info!("SVD Performance Benchmark:");
for size in sizes {
let matrix = Tensor::randn(&[size, size], &device)?;
let config = SvdConfig::default();
// Warmup
for _ in 0..3 {
let _ = solver.svd(&matrix, &config);
}
// Benchmark
let iterations = 10;
let start = Instant::now();
for _ in 0..iterations {
let _ = solver.svd(&matrix, &config);
}
let avg_time = start.elapsed().as_millis() as f64 / iterations as f64;
// Estimate GFLOPS
let flops = (size * size * size) as f64 * 14.0;
let gflops = flops / (avg_time / 1000.0) / 1e9;
info!(" {}x{}: {:.1}ms, {:.1} GFLOPS", size, size, avg_time, gflops);
// Performance assertion
if avg_time > 1000.0 && size <= 512 {
anyhow::bail!("SVD performance too slow for size {size}: {avg_time:.1}ms");
}
}
info!("✓ SVD performance benchmark completed");
Ok(())
}
#[cfg(not(feature = "cuda"))]
async fn test_svd_performance(_config: &IntegrationTestConfig) -> Result<()> {
warn!("CUDA not available, skipping SVD performance test");
Ok(())
}
#[cfg(feature = "cuda")]
async fn test_batched_performance(_config: &IntegrationTestConfig) -> Result<()> {
let device = Device::cuda(0)?;
let context = CuSolverContext::new(&device)?;
let solver = BatchedSolver::new(context)?;
let batch_sizes = vec![4, 8, 16];
let matrix_size = 128;
info!("Batched Performance Benchmark:");
for batch_size in batch_sizes {
let matrices: Result<Vec<_>, _> = (0..batch_size)
.map(|_| Tensor::randn(&[matrix_size, matrix_size], &device))
.collect();
let matrices = matrices?;
let config = BatchedSvdConfig::default();
let start = Instant::now();
let result = solver.svd_batched(&matrices, &config);
let total_time = start.elapsed().as_millis() as f64;
match result {
Ok(_) => {
let per_matrix = total_time / batch_size as f64;
info!(" Batch {}: {:.1}ms total, {:.1}ms per matrix",
batch_size, total_time, per_matrix);
// Performance assertion
if per_matrix > 100.0 {
anyhow::bail!("Batched performance too slow: {per_matrix:.1}ms per matrix");
}
},
Err(e) => {
warn!("Batched operation failed for batch size {}: {}", batch_size, e);
}
}
}
info!("✓ Batched performance benchmark completed");
Ok(())
}
#[cfg(not(feature = "cuda"))]
async fn test_batched_performance(_config: &IntegrationTestConfig) -> Result<()> {
warn!("CUDA not available, skipping batched performance test");
Ok(())
}
#[cfg(feature = "cuda")]
async fn test_memory_usage(_config: &IntegrationTestConfig) -> Result<()> {
let device = Device::cuda(0)?;
let context = CuSolverContext::new(&device)?;
// Check memory statistics
let initial_stats = context.memory_stats();
info!("Initial memory usage: {} MB", initial_stats.current_usage / (1024 * 1024));
// Perform operations and check memory usage
let solver = DenseSolver::new(context.clone());
let matrix = Tensor::randn(&[1000, 1000], &device)?;
let config = SvdConfig::default();
let _ = solver.svd(&matrix, &config);
let final_stats = context.memory_stats();
let memory_increase = final_stats.current_usage - initial_stats.current_usage;
info!("Memory increase: {} MB", memory_increase / (1024 * 1024));
info!("Peak usage: {} MB", final_stats.peak_usage / (1024 * 1024));
// Memory assertion
if memory_increase > 1024 * 1024 * 1024 { // 1GB
warn!("High memory usage detected: {} MB", memory_increase / (1024 * 1024));
}
info!("✓ Memory usage test completed");
Ok(())
}
#[cfg(not(feature = "cuda"))]
async fn test_memory_usage(_config: &IntegrationTestConfig) -> Result<()> {
warn!("CUDA not available, skipping memory usage test");
Ok(())
}
async fn test_error_handling(_config: &IntegrationTestConfig) -> Result<()> {
let device = Device::cpu();
// Test with invalid matrix dimensions
let invalid_matrix = Tensor::ones([5], &device)?;
let svd_result = invalid_matrix.svd(true, true);
if svd_result.is_ok() {
anyhow::bail!("Expected SVD to fail with 1D tensor");
}
// Test with mismatched dimensions for matrix multiplication
let a = Tensor::ones([3, 3], &device)?;
let b = Tensor::ones([2, 1], &device)?;
let matmul_result = a.matmul(&b);
if matmul_result.is_ok() {
// Some implementations might succeed with broadcasting, others might fail
// This is implementation-dependent behavior
info!("Matrix multiplication with mismatched dimensions succeeded (broadcasting?)");
} else {
info!("Matrix multiplication properly rejected mismatched dimensions");
}
// Test with empty matrix
let empty_result = Tensor::zeros([0, 0], &device);
if let Ok(empty_matrix) = empty_result {
let empty_svd = empty_matrix.svd(true, true);
if empty_svd.is_ok() {
warn!("SVD succeeded on empty matrix (unexpected)");
}
}
info!("✓ Error handling tests completed");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::IntegrationTestConfig;
#[tokio::test]
async fn test_cusolver_integration_suite() {
let config = IntegrationTestConfig::default();
let results = run_cusolver_tests(&config).await;
match results {
Ok(test_results) => {
assert!(test_results.success_rate() > 0.5, "Success rate too low");
println!("cuSOLVER integration tests: {:.1}% success rate",
test_results.success_rate() * 100.0);
},
Err(e) => {
panic!("Integration test suite failed: {}", e);
}
}
}
}