#[cfg(feature = "parallel")]
use rayon::prelude::*;
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_linalg::{eigh as linalg_eigh, svd as linalg_svd};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::alloc::{alloc_zeroed, dealloc, handle_alloc_error, Layout};
#[cfg(target_arch = "aarch64")]
use std::arch::aarch64::*;
use std::ops::{Deref, DerefMut};
use std::ptr::NonNull;
use std::slice;
#[cfg(feature = "gpu")]
use oxicuda_memory::DeviceBuffer;
#[cfg(feature = "gpu")]
use oxicuda_solver::{EigJob, SolverHandle, SvdJob};
#[cfg(feature = "gpu")]
use scirs2_core::ndarray::ShapeBuilder;
#[cfg(feature = "gpu")]
use sklears_core::gpu::{GpuArray, GpuContext, GpuMatrixOps};
#[derive(Debug, Clone)]
pub struct AccelerationConfig {
pub enable_simd: bool,
pub enable_parallel: bool,
pub enable_mixed_precision: bool,
pub enable_gpu: bool,
pub gpu_device_id: i32,
pub gpu_memory_limit: Option<usize>,
pub num_threads: Option<usize>,
pub memory_alignment: usize,
}
impl Default for AccelerationConfig {
fn default() -> Self {
Self {
enable_simd: true,
enable_parallel: true,
enable_mixed_precision: false,
enable_gpu: false, gpu_device_id: 0, gpu_memory_limit: None, num_threads: None, memory_alignment: 32, }
}
}
pub struct SimdMatrixOps {
config: AccelerationConfig,
}
impl SimdMatrixOps {
pub fn new() -> Self {
Self {
config: AccelerationConfig::default(),
}
}
pub fn with_config(mut self, config: AccelerationConfig) -> Self {
self.config = config;
self
}
pub fn dot_product_simd(&self, a: &Array1<Float>, b: &Array1<Float>) -> Result<Float> {
if a.len() != b.len() {
return Err(SklearsError::InvalidInput(
"Vector dimensions must match for dot product".to_string(),
));
}
if !self.config.enable_simd {
return Ok(self.dot_product_fallback(a, b));
}
#[cfg(target_arch = "aarch64")]
{
self.dot_product_neon(a, b)
}
#[cfg(not(target_arch = "aarch64"))]
{
Ok(self.dot_product_fallback(a, b))
}
}
#[cfg(target_arch = "aarch64")]
fn dot_product_neon(&self, a: &Array1<Float>, b: &Array1<Float>) -> Result<Float> {
let n = a.len();
let mut sum;
if std::mem::size_of::<Float>() == 8 {
let chunks = n / 2;
let _remainder = n % 2;
unsafe {
let mut acc = vdupq_n_f64(0.0);
for i in 0..chunks {
let idx = i * 2;
let va = vld1q_f64(a.as_ptr().add(idx));
let vb = vld1q_f64(b.as_ptr().add(idx));
acc = vfmaq_f64(acc, va, vb);
}
sum = vgetq_lane_f64(acc, 0) + vgetq_lane_f64(acc, 1);
for i in (chunks * 2)..n {
sum += a[i] * b[i];
}
}
} else {
let chunks = n / 4;
let _remainder = n % 4;
unsafe {
let mut acc = vdupq_n_f32(0.0);
let a_ptr = a.as_ptr() as *const f32;
let b_ptr = b.as_ptr() as *const f32;
for i in 0..chunks {
let idx = i * 4;
let va = vld1q_f32(a_ptr.add(idx));
let vb = vld1q_f32(b_ptr.add(idx));
acc = vfmaq_f32(acc, va, vb);
}
let sum_vec = vpaddq_f32(acc, acc);
let sum_vec2 = vpaddq_f32(sum_vec, sum_vec);
sum = vgetq_lane_f32(sum_vec2, 0) as Float;
for i in (chunks * 4)..n {
sum += a[i] * b[i];
}
}
}
Ok(sum)
}
fn dot_product_fallback(&self, a: &Array1<Float>, b: &Array1<Float>) -> Float {
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
}
pub fn matrix_vector_mul_simd(
&self,
matrix: &Array2<Float>,
vector: &Array1<Float>,
) -> Result<Array1<Float>> {
let (m, n) = matrix.dim();
if n != vector.len() {
return Err(SklearsError::InvalidInput(
"Matrix columns must match vector length".to_string(),
));
}
if !self.config.enable_simd || !self.config.enable_parallel {
return Ok(self.matrix_vector_mul_fallback(matrix, vector));
}
#[cfg(feature = "parallel")]
let result: Vec<Float> = (0..m)
.into_par_iter()
.map(|i| {
let row = matrix.row(i);
self.dot_product_simd(&row.to_owned(), vector)
.unwrap_or(0.0)
})
.collect();
#[cfg(not(feature = "parallel"))]
let result: Vec<Float> = (0..m)
.map(|i| {
let row = matrix.row(i);
self.dot_product_simd(&row.to_owned(), vector)
.unwrap_or(0.0)
})
.collect();
Ok(Array1::from_vec(result))
}
fn matrix_vector_mul_fallback(
&self,
matrix: &Array2<Float>,
vector: &Array1<Float>,
) -> Array1<Float> {
matrix.dot(vector)
}
pub fn elementwise_add_simd(
&self,
a: &Array1<Float>,
b: &Array1<Float>,
) -> Result<Array1<Float>> {
if a.len() != b.len() {
return Err(SklearsError::InvalidInput(
"Array dimensions must match".to_string(),
));
}
if !self.config.enable_simd {
return Ok(a + b);
}
#[cfg(target_arch = "aarch64")]
{
self.elementwise_add_neon(a, b)
}
#[cfg(not(target_arch = "aarch64"))]
{
Ok(a + b)
}
}
#[cfg(target_arch = "aarch64")]
fn elementwise_add_neon(&self, a: &Array1<Float>, b: &Array1<Float>) -> Result<Array1<Float>> {
let n = a.len();
let mut result = Array1::<Float>::zeros(n);
if std::mem::size_of::<Float>() == 8 {
let chunks = n / 2;
unsafe {
for i in 0..chunks {
let idx = i * 2;
let va = vld1q_f64(a.as_ptr().add(idx));
let vb = vld1q_f64(b.as_ptr().add(idx));
let vr = vaddq_f64(va, vb);
vst1q_f64(result.as_mut_ptr().add(idx), vr);
}
for i in (chunks * 2)..n {
result[i] = a[i] + b[i];
}
}
} else {
let chunks = n / 4;
let a_ptr = a.as_ptr() as *const f32;
let b_ptr = b.as_ptr() as *const f32;
let result_ptr = result.as_mut_ptr() as *mut f32;
unsafe {
for i in 0..chunks {
let idx = i * 4;
let va = vld1q_f32(a_ptr.add(idx));
let vb = vld1q_f32(b_ptr.add(idx));
let vr = vaddq_f32(va, vb);
vst1q_f32(result_ptr.add(idx), vr);
}
for i in (chunks * 4)..n {
result[i] = a[i] + b[i];
}
}
}
Ok(result)
}
pub fn elementwise_mul_simd(
&self,
a: &Array1<Float>,
b: &Array1<Float>,
) -> Result<Array1<Float>> {
if a.len() != b.len() {
return Err(SklearsError::InvalidInput(
"Array dimensions must match".to_string(),
));
}
if !self.config.enable_simd {
return Ok(a * b);
}
#[cfg(target_arch = "aarch64")]
{
self.elementwise_mul_neon(a, b)
}
#[cfg(not(target_arch = "aarch64"))]
{
Ok(a * b)
}
}
#[cfg(target_arch = "aarch64")]
fn elementwise_mul_neon(&self, a: &Array1<Float>, b: &Array1<Float>) -> Result<Array1<Float>> {
let n = a.len();
let mut result = Array1::<Float>::zeros(n);
if std::mem::size_of::<Float>() == 8 {
let chunks = n / 2;
unsafe {
for i in 0..chunks {
let idx = i * 2;
let va = vld1q_f64(a.as_ptr().add(idx));
let vb = vld1q_f64(b.as_ptr().add(idx));
let vr = vmulq_f64(va, vb);
vst1q_f64(result.as_mut_ptr().add(idx), vr);
}
for i in (chunks * 2)..n {
result[i] = a[i] * b[i];
}
}
} else {
let chunks = n / 4;
let a_ptr = a.as_ptr() as *const f32;
let b_ptr = b.as_ptr() as *const f32;
let result_ptr = result.as_mut_ptr() as *mut f32;
unsafe {
for i in 0..chunks {
let idx = i * 4;
let va = vld1q_f32(a_ptr.add(idx));
let vb = vld1q_f32(b_ptr.add(idx));
let vr = vmulq_f32(va, vb);
vst1q_f32(result_ptr.add(idx), vr);
}
for i in (chunks * 4)..n {
result[i] = a[i] * b[i];
}
}
}
Ok(result)
}
pub fn vector_exp_simd(&self, input: &Array1<Float>) -> Array1<Float> {
if !self.config.enable_simd || !self.config.enable_parallel {
return input.mapv(|x| x.exp());
}
#[cfg(feature = "parallel")]
let result: Vec<Float> = input.par_iter().map(|&x| x.exp()).collect();
#[cfg(not(feature = "parallel"))]
let result: Vec<Float> = input.iter().map(|&x| x.exp()).collect();
Array1::from_vec(result)
}
pub fn vector_sqrt_simd(&self, input: &Array1<Float>) -> Array1<Float> {
if !self.config.enable_simd || !self.config.enable_parallel {
return input.mapv(|x| x.sqrt());
}
#[cfg(feature = "parallel")]
let result: Vec<Float> = input.par_iter().map(|&x| x.sqrt()).collect();
#[cfg(not(feature = "parallel"))]
let result: Vec<Float> = input.iter().map(|&x| x.sqrt()).collect();
Array1::from_vec(result)
}
pub fn vector_sin_simd(&self, input: &Array1<Float>) -> Array1<Float> {
if !self.config.enable_simd || !self.config.enable_parallel {
return input.mapv(|x| x.sin());
}
#[cfg(feature = "parallel")]
let result: Vec<Float> = input.par_iter().map(|&x| x.sin()).collect();
#[cfg(not(feature = "parallel"))]
let result: Vec<Float> = input.iter().map(|&x| x.sin()).collect();
Array1::from_vec(result)
}
}
impl Default for SimdMatrixOps {
fn default() -> Self {
Self::new()
}
}
pub struct ParallelDecomposition {
config: AccelerationConfig,
}
impl ParallelDecomposition {
pub fn new() -> Self {
Self {
config: AccelerationConfig::default(),
}
}
pub fn with_config(mut self, config: AccelerationConfig) -> Self {
self.config = config;
self
}
pub fn parallel_svd(
&self,
matrix: &Array2<Float>,
) -> Result<(Array2<Float>, Array1<Float>, Array2<Float>)> {
let (m, n) = matrix.dim();
if !self.config.enable_parallel {
return self.sequential_svd(matrix);
}
if m > 1000 && n > 1000 {
self.block_parallel_svd(matrix)
} else {
self.sequential_svd(matrix)
}
}
fn block_parallel_svd(
&self,
matrix: &Array2<Float>,
) -> Result<(Array2<Float>, Array1<Float>, Array2<Float>)> {
self.sequential_svd(matrix)
}
fn sequential_svd(
&self,
matrix: &Array2<Float>,
) -> Result<(Array2<Float>, Array1<Float>, Array2<Float>)> {
let (u, s, vt) = linalg_svd(&matrix.view(), false, self.config.num_threads)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
Ok((u, s, vt))
}
pub fn parallel_eigendecomposition(
&self,
matrix: &Array2<Float>,
) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square for eigendecomposition".to_string(),
));
}
if !self.config.enable_parallel || n < 500 {
return self.sequential_eigendecomposition(matrix);
}
self.block_parallel_eigendecomposition(matrix)
}
fn block_parallel_eigendecomposition(
&self,
matrix: &Array2<Float>,
) -> Result<(Array1<Float>, Array2<Float>)> {
self.sequential_eigendecomposition(matrix)
}
fn sequential_eigendecomposition(
&self,
matrix: &Array2<Float>,
) -> Result<(Array1<Float>, Array2<Float>)> {
let (eigenvalues, eigenvectors) = linalg_eigh(&matrix.view(), self.config.num_threads)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
Ok((eigenvalues, eigenvectors))
}
pub fn parallel_matrix_multiply(
&self,
a: &Array2<Float>,
b: &Array2<Float>,
) -> Result<Array2<Float>> {
let (m, k1) = a.dim();
let (k2, n) = b.dim();
if k1 != k2 {
return Err(SklearsError::InvalidInput(
"Matrix dimensions incompatible for multiplication".to_string(),
));
}
if !self.config.enable_parallel {
return Ok(a.dot(b));
}
if m > 100 && n > 100 && k1 > 100 {
self.tiled_parallel_multiply(a, b)
} else {
Ok(a.dot(b))
}
}
fn tiled_parallel_multiply(
&self,
a: &Array2<Float>,
b: &Array2<Float>,
) -> Result<Array2<Float>> {
let (m, _k) = a.dim();
let (_, n) = b.dim();
let tile_size = 64;
let mut result = Array2::<Float>::zeros((m, n));
#[cfg(feature = "parallel")]
{
let row_chunks: Vec<_> = result
.axis_chunks_iter_mut(scirs2_core::ndarray::Axis(0), tile_size)
.collect();
row_chunks
.into_par_iter()
.enumerate()
.for_each(|(tile_idx, mut out_chunk)| {
let row_start = tile_idx * tile_size;
let row_end = row_start + out_chunk.nrows();
let a_rows = a.slice(scirs2_core::ndarray::s![row_start..row_end, ..]);
out_chunk.assign(&a_rows.dot(b));
});
}
#[cfg(not(feature = "parallel"))]
{
result
.axis_chunks_iter_mut(scirs2_core::ndarray::Axis(0), tile_size)
.enumerate()
.for_each(|(tile_idx, mut out_chunk)| {
let row_start = tile_idx * tile_size;
let row_end = row_start + out_chunk.nrows();
let a_rows = a.slice(scirs2_core::ndarray::s![row_start..row_end, ..]);
out_chunk.assign(&a_rows.dot(b));
});
}
Ok(result)
}
}
impl Default for ParallelDecomposition {
fn default() -> Self {
Self::new()
}
}
pub struct MixedPrecisionOps {
config: AccelerationConfig,
}
impl MixedPrecisionOps {
pub fn new() -> Self {
Self {
config: AccelerationConfig::default(),
}
}
pub fn with_config(mut self, config: AccelerationConfig) -> Self {
self.config = config;
self
}
pub fn to_single_precision(&self, input: &Array1<f64>) -> Array1<f32> {
if self.config.enable_parallel {
#[cfg(feature = "parallel")]
let result: Vec<f32> = input.par_iter().map(|&x| x as f32).collect();
#[cfg(not(feature = "parallel"))]
let result: Vec<f32> = input.iter().map(|&x| x as f32).collect();
Array1::from_vec(result)
} else {
input.mapv(|x| x as f32)
}
}
pub fn to_double_precision(&self, input: &Array1<f32>) -> Array1<f64> {
if self.config.enable_parallel {
#[cfg(feature = "parallel")]
let result: Vec<f64> = input.par_iter().map(|&x| x as f64).collect();
#[cfg(not(feature = "parallel"))]
let result: Vec<f64> = input.iter().map(|&x| x as f64).collect();
Array1::from_vec(result)
} else {
input.mapv(|x| x as f64)
}
}
pub fn mixed_precision_multiply(
&self,
a: &Array2<f64>,
b: &Array2<f64>,
) -> Result<Array2<f64>> {
if !self.config.enable_mixed_precision {
return Ok(a.dot(b));
}
let (_m, k1) = a.dim();
let (k2, _n) = b.dim();
if k1 != k2 {
return Err(SklearsError::InvalidInput(
"Matrix dimensions incompatible for multiplication".to_string(),
));
}
let a_f32 = a.mapv(|x| x as f32);
let b_f32 = b.mapv(|x| x as f32);
let result_f32 = a_f32.dot(&b_f32);
let result = result_f32.mapv(|x| x as f64);
Ok(result)
}
}
impl Default for MixedPrecisionOps {
fn default() -> Self {
Self::new()
}
}
pub struct AlignedMemoryOps {
alignment: usize,
}
pub struct AlignedBuffer {
ptr: NonNull<Float>,
len: usize,
alignment: usize,
}
impl AlignedMemoryOps {
pub fn new(alignment: usize) -> Self {
Self {
alignment: Self::sanitize_alignment(alignment),
}
}
fn sanitize_alignment(requested: usize) -> usize {
let base = requested.max(std::mem::align_of::<Float>()).max(1);
if base.is_power_of_two() {
base
} else {
base.next_power_of_two()
}
}
pub fn create_aligned_array(&self, size: usize) -> AlignedBuffer {
AlignedBuffer::new(size, self.alignment)
}
pub fn is_aligned(&self, data: &[Float]) -> bool {
let ptr = data.as_ptr() as usize;
ptr.is_multiple_of(self.alignment)
}
pub fn ensure_aligned(&self, data: &Array1<Float>) -> AlignedBuffer {
let mut buffer = self.create_aligned_array(data.len());
for (dst, src) in buffer.as_mut_slice().iter_mut().zip(data.iter()) {
*dst = *src;
}
buffer
}
}
impl Default for AlignedMemoryOps {
fn default() -> Self {
Self::new(32) }
}
impl AlignedBuffer {
pub fn new(size: usize, alignment: usize) -> Self {
let alignment = alignment
.max(std::mem::align_of::<Float>())
.next_power_of_two();
if size == 0 {
return Self {
ptr: NonNull::dangling(),
len: 0,
alignment,
};
}
let elem_size = std::mem::size_of::<Float>();
let total_size = elem_size
.checked_mul(size)
.expect("Requested buffer size exceeds addressable memory");
let layout = Layout::from_size_align(total_size, alignment)
.expect("Invalid layout for aligned allocation");
unsafe {
let ptr = alloc_zeroed(layout);
if ptr.is_null() {
handle_alloc_error(layout);
}
Self {
ptr: NonNull::new_unchecked(ptr as *mut Float),
len: size,
alignment,
}
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn as_slice(&self) -> &[Float] {
unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}
pub fn as_mut_slice(&mut self) -> &mut [Float] {
unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
}
}
impl Deref for AlignedBuffer {
type Target = [Float];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl DerefMut for AlignedBuffer {
fn deref_mut(&mut self) -> &mut Self::Target {
self.as_mut_slice()
}
}
impl Drop for AlignedBuffer {
fn drop(&mut self) {
if self.len == 0 {
return;
}
let elem_size = std::mem::size_of::<Float>();
let total_size = elem_size * self.len;
if let Ok(layout) = Layout::from_size_align(total_size, self.alignment) {
unsafe {
dealloc(self.ptr.as_ptr() as *mut u8, layout);
}
}
}
}
#[cfg(feature = "gpu")]
pub struct GpuAcceleration {
config: AccelerationConfig,
context: Option<GpuContext>,
}
#[cfg(feature = "gpu")]
impl GpuAcceleration {
pub fn new() -> Result<Self> {
Self::with_config(AccelerationConfig::default())
}
fn cpu_only() -> Self {
Self {
config: AccelerationConfig {
enable_gpu: false,
..AccelerationConfig::default()
},
context: None,
}
}
pub fn with_config(config: AccelerationConfig) -> Result<Self> {
if !config.enable_gpu {
eprintln!(
"[GpuAcceleration] GPU disabled by config (enable_gpu=false). \
Running on CPU. num_threads={:?}, alignment={} bytes.",
config.num_threads, config.memory_alignment
);
return Ok(Self {
config,
context: None,
});
}
eprintln!(
"[GpuAcceleration] Requesting GPU device {} with memory_limit={:?}.",
config.gpu_device_id, config.gpu_memory_limit
);
let context = match GpuContext::with_device_id(config.gpu_device_id as usize) {
Ok(Some(ctx)) => Some(ctx),
Ok(None) => {
eprintln!(
"[GpuAcceleration] No GPU detected at device {} (or CUDA driver unavailable); \
falling back to CPU.",
config.gpu_device_id
);
None
}
Err(e) => {
return Err(SklearsError::InvalidInput(format!(
"Failed to initialize GPU device {}: {e}",
config.gpu_device_id
)));
}
};
Ok(Self { config, context })
}
pub fn config(&self) -> &AccelerationConfig {
&self.config
}
pub fn is_gpu_available(&self) -> bool {
self.config.enable_gpu && self.context.is_some()
}
pub fn gpu_memory_info(&self) -> Result<(usize, usize)> {
if let Some(ref ctx) = self.context {
let info = ctx.memory_info()?;
let effective_total = self.config.gpu_memory_limit.unwrap_or(info.total);
Ok((info.free.min(effective_total), effective_total))
} else {
Err(SklearsError::InvalidInput("GPU not available".to_string()))
}
}
pub fn gpu_matrix_multiply(
&self,
a: &Array2<Float>,
b: &Array2<Float>,
) -> Result<Array2<Float>> {
if !self.is_gpu_available() {
return Err(SklearsError::InvalidInput("GPU not available".to_string()));
}
let ctx = self
.context
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("GPU not available".to_string()))?;
let (_m, k1) = a.dim();
let (k2, _n) = b.dim();
if k1 != k2 {
return Err(SklearsError::InvalidInput(
"Matrix dimensions incompatible for multiplication".to_string(),
));
}
let a_gpu = GpuArray::<Float>::from_array2(ctx, a)?;
let b_gpu = GpuArray::<Float>::from_array2(ctx, b)?;
let c_gpu = a_gpu.matmul(&b_gpu)?;
c_gpu.to_array2()
}
pub fn gpu_svd(
&self,
matrix: &Array2<Float>,
) -> Result<(Array2<Float>, Array1<Float>, Array2<Float>)> {
if !self.is_gpu_available() {
return Err(SklearsError::InvalidInput("GPU not available".to_string()));
}
let ctx = self
.context
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("GPU not available".to_string()))?;
match Self::gpu_svd_on_device(ctx, matrix) {
Ok(result) => Ok(result),
Err(gpu_err) => {
eprintln!(
"[GpuAcceleration] on-device SVD failed ({gpu_err}); \
falling back to CPU scirs2_linalg::svd."
);
let (u, s, vt) = linalg_svd(&matrix.view(), false, self.config.num_threads)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
Ok((u, s, vt))
}
}
}
fn gpu_svd_on_device(
ctx: &GpuContext,
matrix: &Array2<Float>,
) -> Result<(Array2<Float>, Array1<Float>, Array2<Float>)> {
let (m, n) = matrix.dim();
if m == 0 || n == 0 {
return Err(SklearsError::InvalidInput(
"Matrix must be non-empty for SVD".to_string(),
));
}
let k = m.min(n);
ctx.context()
.set_current()
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let col_major: Vec<Float> = matrix.t().iter().copied().collect();
let mut a_buf = DeviceBuffer::from_host(&col_major)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let mut handle = SolverHandle::new(ctx.context())
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let result = oxicuda_solver::dense::svd(
&mut handle,
&mut a_buf,
m as u32,
n as u32,
m as u32,
SvdJob::Thin,
)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let u_data = result.u.ok_or_else(|| {
SklearsError::NumericalError("oxicuda_solver::svd: missing U".to_string())
})?;
let vt_data = result.vt.ok_or_else(|| {
SklearsError::NumericalError("oxicuda_solver::svd: missing Vt".to_string())
})?;
let u = Array2::from_shape_vec((m, k).f(), u_data)
.map_err(|e| SklearsError::NumericalError(format!("reshape U: {e}")))?;
let vt = Array2::from_shape_vec((k, n).f(), vt_data)
.map_err(|e| SklearsError::NumericalError(format!("reshape Vt: {e}")))?;
let s = Array1::from_vec(result.singular_values);
Ok((u, s, vt))
}
pub fn gpu_eigendecomposition(
&self,
matrix: &Array2<Float>,
) -> Result<(Array1<Float>, Array2<Float>)> {
if !self.is_gpu_available() {
return Err(SklearsError::InvalidInput("GPU not available".to_string()));
}
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square for eigendecomposition".to_string(),
));
}
let ctx = self
.context
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("GPU not available".to_string()))?;
match Self::gpu_eigh_on_device(ctx, matrix) {
Ok(result) => Ok(result),
Err(gpu_err) => {
eprintln!(
"[GpuAcceleration] on-device eigendecomposition failed ({gpu_err}); \
falling back to CPU scirs2_linalg::eigh."
);
let (eigenvals, eigenvecs) =
linalg_eigh(&matrix.view(), self.config.num_threads)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
Ok((eigenvals, eigenvecs))
}
}
}
fn gpu_eigh_on_device(
ctx: &GpuContext,
matrix: &Array2<Float>,
) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
if n == 0 {
return Err(SklearsError::InvalidInput(
"Matrix must be non-empty for eigendecomposition".to_string(),
));
}
ctx.context()
.set_current()
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let col_major: Vec<Float> = matrix.t().iter().copied().collect();
let mut a_buf = DeviceBuffer::from_host(&col_major)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let mut eigenvalues_buf = DeviceBuffer::<Float>::zeroed(n)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let mut handle = SolverHandle::new(ctx.context())
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
oxicuda_solver::dense::syevd(
&mut handle,
&mut a_buf,
n as u32,
n as u32,
&mut eigenvalues_buf,
EigJob::ValuesAndVectors,
)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let mut eigenvalues_host = vec![0.0 as Float; n];
eigenvalues_buf
.copy_to_host(&mut eigenvalues_host)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let mut eigenvectors_host = vec![0.0 as Float; n * n];
a_buf
.copy_to_host(&mut eigenvectors_host)
.map_err(|e| SklearsError::NumericalError(e.to_string()))?;
let eigenvalues = Array1::from_vec(eigenvalues_host);
let eigenvectors = Array2::from_shape_vec((n, n).f(), eigenvectors_host)
.map_err(|e| SklearsError::NumericalError(format!("reshape eigenvectors: {e}")))?;
Ok((eigenvalues, eigenvectors))
}
pub fn batch_gpu_multiply(&self, matrices: &[Array2<Float>]) -> Result<Vec<Array2<Float>>> {
if !self.is_gpu_available() {
return Err(SklearsError::InvalidInput("GPU not available".to_string()));
}
let mut results = Vec::with_capacity(matrices.len() / 2);
for chunk in matrices.chunks_exact(2) {
let result = self.gpu_matrix_multiply(&chunk[0], &chunk[1])?;
results.push(result);
}
Ok(results)
}
pub fn free_gpu_memory(&self) -> Result<()> {
if let Some(ref ctx) = self.context {
ctx.synchronize()?;
}
Ok(())
}
pub fn profile_gpu_operation<F, T>(&self, operation: F) -> Result<(T, std::time::Duration)>
where
F: FnOnce() -> Result<T>,
{
let start = std::time::Instant::now();
let result = operation()?;
self.free_gpu_memory()?;
let duration = start.elapsed();
Ok((result, duration))
}
}
#[cfg(feature = "gpu")]
impl Default for GpuAcceleration {
fn default() -> Self {
Self::new().unwrap_or_else(|_| {
let config = AccelerationConfig {
enable_gpu: false,
..AccelerationConfig::default()
};
Self {
config,
context: None,
}
})
}
}
#[cfg(feature = "gpu")]
pub struct GpuDecomposition {
gpu_acceleration: GpuAcceleration,
}
#[cfg(feature = "gpu")]
impl GpuDecomposition {
pub fn new() -> Result<Self> {
Ok(Self {
gpu_acceleration: GpuAcceleration::new()?,
})
}
pub fn with_config(config: AccelerationConfig) -> Result<Self> {
Ok(Self {
gpu_acceleration: GpuAcceleration::with_config(config)?,
})
}
pub fn gpu_pca(
&self,
data: &Array2<Float>,
n_components: usize,
) -> Result<(Array2<Float>, Array1<Float>, Array2<Float>)> {
let (m, n) = data.dim();
if n_components > m.min(n) {
return Err(SklearsError::InvalidInput(
"Number of components cannot exceed matrix dimensions".to_string(),
));
}
let col_means = data
.mean_axis(scirs2_core::ndarray::Axis(0))
.ok_or_else(|| {
SklearsError::InvalidInput("data must have at least one row for PCA".to_string())
})?;
let centered_data = data - &col_means.insert_axis(scirs2_core::ndarray::Axis(0));
let (u, s, vt) = self.gpu_acceleration.gpu_svd(¢ered_data)?;
let u_truncated = u
.slice(scirs2_core::ndarray::s![.., ..n_components])
.to_owned();
let s_truncated = s.slice(scirs2_core::ndarray::s![..n_components]).to_owned();
let vt_truncated = vt
.slice(scirs2_core::ndarray::s![..n_components, ..])
.to_owned();
Ok((u_truncated, s_truncated, vt_truncated))
}
pub fn gpu_factorize(&self, matrix: &Array2<Float>) -> Result<(Array2<Float>, Array2<Float>)> {
let (m, n) = matrix.dim();
let k = m.min(n);
let (u, s, vt) = self.gpu_acceleration.gpu_svd(matrix)?;
let s_sqrt = s.mapv(|x| x.sqrt());
let factor_a = &u.slice(scirs2_core::ndarray::s![.., ..k])
* &s_sqrt.view().insert_axis(scirs2_core::ndarray::Axis(0));
let factor_b = &s_sqrt.view().insert_axis(scirs2_core::ndarray::Axis(1))
* &vt.slice(scirs2_core::ndarray::s![..k, ..]);
Ok((factor_a, factor_b))
}
}
#[cfg(feature = "gpu")]
impl Default for GpuDecomposition {
fn default() -> Self {
Self::new().unwrap_or_else(|_| Self {
gpu_acceleration: GpuAcceleration::cpu_only(),
})
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
#[test]
fn test_simd_dot_product() {
let simd_ops = SimdMatrixOps::new();
let a = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let b = Array1::from_vec(vec![5.0, 6.0, 7.0, 8.0]);
let result = simd_ops
.dot_product_simd(&a, &b)
.expect("operation should succeed");
let expected = 1.0 * 5.0 + 2.0 * 6.0 + 3.0 * 7.0 + 4.0 * 8.0;
assert!((result - expected).abs() < 1e-10);
}
#[test]
fn test_simd_elementwise_add() {
let simd_ops = SimdMatrixOps::new();
let a = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let b = Array1::from_vec(vec![5.0, 6.0, 7.0, 8.0]);
let result = simd_ops
.elementwise_add_simd(&a, &b)
.expect("operation should succeed");
let expected = Array1::from_vec(vec![6.0, 8.0, 10.0, 12.0]);
for (r, e) in result.iter().zip(expected.iter()) {
assert!((r - e).abs() < 1e-10);
}
}
#[test]
fn test_simd_elementwise_mul() {
let simd_ops = SimdMatrixOps::new();
let a = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let b = Array1::from_vec(vec![5.0, 6.0, 7.0, 8.0]);
let result = simd_ops
.elementwise_mul_simd(&a, &b)
.expect("operation should succeed");
let expected = Array1::from_vec(vec![5.0, 12.0, 21.0, 32.0]);
for (r, e) in result.iter().zip(expected.iter()) {
assert!((r - e).abs() < 1e-10);
}
}
#[test]
fn test_matrix_vector_mul_simd() {
let simd_ops = SimdMatrixOps::new();
let matrix = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
.expect("shape and data length should match");
let vector = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let result = simd_ops
.matrix_vector_mul_simd(&matrix, &vector)
.expect("operation should succeed");
let expected = Array1::from_vec(vec![14.0, 32.0]);
for (r, e) in result.iter().zip(expected.iter()) {
assert!((r - e).abs() < 1e-10);
}
}
#[test]
fn test_parallel_operations() {
let parallel_ops = ParallelDecomposition::new();
let matrix = Array2::eye(3);
let result = parallel_ops.parallel_eigendecomposition(&matrix);
assert!(result.is_ok());
let (eigenvals, eigenvecs) = result.expect("operation should succeed");
assert_eq!(eigenvals.len(), 3);
assert_eq!(eigenvecs.dim(), (3, 3));
}
#[test]
fn test_sequential_svd_reconstructs_matrix() {
let config = AccelerationConfig {
enable_parallel: false, ..AccelerationConfig::default()
};
let parallel_ops = ParallelDecomposition::new().with_config(config);
let matrix =
Array2::from_shape_vec((3, 3), vec![4.0, 1.0, 2.0, 0.0, 3.0, 1.0, 5.0, 2.0, 6.0])
.expect("shape and data length should match");
let (u, s, vt) = parallel_ops
.parallel_svd(&matrix)
.expect("SVD should succeed");
assert_eq!(u.dim(), (3, 3));
assert_eq!(s.len(), 3);
assert_eq!(vt.dim(), (3, 3));
assert!(s.iter().any(|&x| (x - 1.0).abs() > 1e-6));
let us = &u * &s.view().insert_axis(scirs2_core::ndarray::Axis(0));
let reconstructed = us.dot(&vt);
for (r, e) in reconstructed.iter().zip(matrix.iter()) {
assert!((r - e).abs() < 1e-8, "reconstructed={r}, expected={e}");
}
}
#[test]
fn test_block_parallel_svd_reconstructs_matrix() {
let parallel_ops = ParallelDecomposition::new();
let matrix =
Array2::from_shape_vec((3, 3), vec![4.0, 1.0, 2.0, 0.0, 3.0, 1.0, 5.0, 2.0, 6.0])
.expect("shape and data length should match");
let (u, s, vt) = parallel_ops
.block_parallel_svd(&matrix)
.expect("block SVD should succeed");
assert!(s.iter().any(|&x| (x - 1.0).abs() > 1e-6));
let us = &u * &s.view().insert_axis(scirs2_core::ndarray::Axis(0));
let reconstructed = us.dot(&vt);
for (r, e) in reconstructed.iter().zip(matrix.iter()) {
assert!((r - e).abs() < 1e-8, "reconstructed={r}, expected={e}");
}
}
#[test]
fn test_sequential_eigendecomposition_reconstructs_symmetric_matrix() {
let config = AccelerationConfig {
enable_parallel: false,
..AccelerationConfig::default()
};
let parallel_ops = ParallelDecomposition::new().with_config(config);
let matrix =
Array2::from_shape_vec((3, 3), vec![2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 1.0, 2.0])
.expect("shape and data length should match");
let (eigenvalues, eigenvectors) = parallel_ops
.parallel_eigendecomposition(&matrix)
.expect("eigendecomposition should succeed");
assert_eq!(eigenvalues.len(), 3);
assert_eq!(eigenvectors.dim(), (3, 3));
assert!(eigenvalues.iter().any(|&x| (x - 1.0).abs() > 1e-6));
for j in 0..3 {
let v = eigenvectors.column(j);
let av = matrix.dot(&v);
let lambda_v = v.mapv(|x| x * eigenvalues[j]);
for (a, b) in av.iter().zip(lambda_v.iter()) {
assert!((a - b).abs() < 1e-8, "A*v={a}, lambda*v={b}");
}
}
}
#[test]
fn test_tiled_parallel_multiply_matches_reference() {
let parallel_ops = ParallelDecomposition::new();
let m = 130;
let k = 150;
let n = 110;
let a = Array2::from_shape_fn((m, k), |(i, j)| ((i * 7 + j * 3) % 13) as Float);
let b = Array2::from_shape_fn((k, n), |(i, j)| ((i * 5 + j * 11) % 17) as Float);
let result = parallel_ops
.parallel_matrix_multiply(&a, &b)
.expect("multiply should succeed");
let expected = a.dot(&b);
assert_eq!(result.dim(), expected.dim());
for (r, e) in result.iter().zip(expected.iter()) {
assert!((r - e).abs() < 1e-6, "result={r}, expected={e}");
}
}
#[test]
fn test_mixed_precision() {
let mixed_ops = MixedPrecisionOps::new();
let input_f64 = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let converted_f32 = mixed_ops.to_single_precision(&input_f64);
let converted_back = mixed_ops.to_double_precision(&converted_f32);
for (orig, back) in input_f64.iter().zip(converted_back.iter()) {
assert!((orig - back).abs() < 1e-6); }
}
#[test]
fn test_aligned_memory() {
let aligned_ops = AlignedMemoryOps::new(32);
let aligned_vec = aligned_ops.create_aligned_array(10);
assert_eq!(aligned_vec.len(), 10);
assert!(aligned_ops.is_aligned(&aligned_vec));
let array = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let aligned_array = aligned_ops.ensure_aligned(&array);
assert_eq!(aligned_array.len(), array.len());
assert!(aligned_ops.is_aligned(&aligned_array));
assert_eq!(
aligned_array.as_slice(),
array.as_slice().expect("slice operation should succeed")
);
}
#[test]
fn test_vectorized_functions() {
let simd_ops = SimdMatrixOps::new();
let input = Array1::from_vec(vec![0.0, 1.0, 2.0]);
let exp_result = simd_ops.vector_exp_simd(&input);
let expected_exp = input.mapv(|x| x.exp());
for (r, e) in exp_result.iter().zip(expected_exp.iter()) {
assert!((r - e).abs() < 1e-10);
}
let sqrt_result = simd_ops.vector_sqrt_simd(&Array1::from_vec(vec![1.0, 4.0, 9.0]));
let expected_sqrt = Array1::from_vec(vec![1.0, 2.0, 3.0]);
for (r, e) in sqrt_result.iter().zip(expected_sqrt.iter()) {
assert!((r - e).abs() < 1e-10);
}
}
#[test]
fn test_acceleration_config() {
let config = AccelerationConfig {
enable_simd: false,
enable_parallel: false,
enable_mixed_precision: true,
enable_gpu: false,
gpu_device_id: 0,
gpu_memory_limit: None,
num_threads: Some(4),
memory_alignment: 64,
};
let simd_ops = SimdMatrixOps::new().with_config(config.clone());
let a = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let b = Array1::from_vec(vec![4.0, 5.0, 6.0]);
let result = simd_ops
.dot_product_simd(&a, &b)
.expect("operation should succeed");
let expected = 32.0; assert!((result - expected).abs() < 1e-10);
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_acceleration_creation() {
let _gpu_acc = GpuAcceleration::default();
let _ = _gpu_acc;
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_config() {
let config = AccelerationConfig {
enable_gpu: true,
gpu_device_id: 0,
gpu_memory_limit: Some(1024 * 1024 * 1024), ..AccelerationConfig::default()
};
assert!(config.enable_gpu);
assert_eq!(config.gpu_device_id, 0);
assert_eq!(config.gpu_memory_limit, Some(1024 * 1024 * 1024));
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_acceleration_with_config_falls_back_when_no_gpu() {
let config = AccelerationConfig {
enable_gpu: true,
..AccelerationConfig::default()
};
let gpu_acc = GpuAcceleration::with_config(config).expect(
"GPU init must fall back to CPU gracefully (Ok), not hard-error, when no GPU is present",
);
let _ = gpu_acc.is_gpu_available();
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_svd_reconstructs_matrix_when_available() {
let config = AccelerationConfig {
enable_gpu: true,
..AccelerationConfig::default()
};
let Ok(gpu_acc) = GpuAcceleration::with_config(config) else {
return;
};
if !gpu_acc.is_gpu_available() {
eprintln!("skipping test_gpu_svd_reconstructs_matrix_when_available: no GPU detected");
return;
}
let matrix =
Array2::from_shape_vec((3, 3), vec![4.0, 1.0, 2.0, 0.0, 3.0, 1.0, 5.0, 2.0, 6.0])
.expect("shape and data length should match");
let (u, s, vt) = gpu_acc
.gpu_svd(&matrix)
.expect("GPU SVD should succeed when a GPU is available");
let us = &u * &s.view().insert_axis(scirs2_core::ndarray::Axis(0));
let reconstructed = us.dot(&vt);
for (r, e) in reconstructed.iter().zip(matrix.iter()) {
assert!((r - e).abs() < 1e-6, "reconstructed={r}, expected={e}");
}
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_matrix_multiply_identity() {
let config = AccelerationConfig {
enable_gpu: true,
..AccelerationConfig::default()
};
if let Ok(gpu_acc) = GpuAcceleration::with_config(config) {
if gpu_acc.is_gpu_available() {
let identity = Array2::from_shape_vec((2, 2), vec![1.0, 0.0, 0.0, 1.0])
.expect("shape and data length should match");
let b = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0])
.expect("shape and data length should match");
if let Ok(result) = gpu_acc.gpu_matrix_multiply(&identity, &b) {
assert_eq!(result.shape(), b.shape());
for (r, e) in result.iter().zip(b.iter()) {
assert!((r - e).abs() < 1e-10);
}
}
}
}
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_decomposition_fallback() {
let gpu_decomp = GpuDecomposition::default();
let matrix =
Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
.expect("operation should succeed");
if let Ok((factor_a, factor_b)) = gpu_decomp.gpu_factorize(&matrix) {
assert_eq!(factor_a.nrows(), 3);
assert_eq!(factor_b.ncols(), 3);
}
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_pca_basic() {
let gpu_decomp = GpuDecomposition::default();
let data = Array2::from_shape_vec(
(4, 3),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
)
.expect("operation should succeed");
if let Ok((u, s, vt)) = gpu_decomp.gpu_pca(&data, 2) {
assert_eq!(u.ncols(), 2);
assert_eq!(s.len(), 2);
assert_eq!(vt.nrows(), 2);
}
}
}