use crate::{Tensor, TensorElement};
use oxicuda_backend::{BackendResult, ComputeBackend};
pub use oxicuda_backend::{BinaryOp, ReduceOp, UnaryOp};
#[inline]
fn f32_as_bytes(data: &[f32]) -> &[u8] {
unsafe { std::slice::from_raw_parts(data.as_ptr().cast::<u8>(), std::mem::size_of_val(data)) }
}
#[inline]
fn bytes_to_f32(bytes: &[u8]) -> Vec<f32> {
bytes
.chunks_exact(4)
.map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
fn with_buffers<R>(
backend: &dyn ComputeBackend,
sizes: &[usize],
f: impl FnOnce(&[u64]) -> BackendResult<R>,
) -> BackendResult<R> {
let mut ptrs: Vec<u64> = Vec::with_capacity(sizes.len());
for &size in sizes {
match backend.alloc(size) {
Ok(ptr) => ptrs.push(ptr),
Err(err) => {
for &ptr in &ptrs {
let _ = backend.free(ptr);
}
return Err(err);
}
}
}
let result = f(&ptrs);
for &ptr in &ptrs {
let _ = backend.free(ptr);
}
result
}
fn run_unary_f32(
backend: &dyn ComputeBackend,
op: UnaryOp,
input: &[f32],
) -> BackendResult<Vec<f32>> {
let n = input.len();
let bytes = std::mem::size_of_val(input);
with_buffers(backend, &[bytes, bytes], |ptrs| {
let (input_ptr, output_ptr) = (ptrs[0], ptrs[1]);
backend.copy_htod(input_ptr, f32_as_bytes(input))?;
backend.unary(op, input_ptr, output_ptr, n)?;
let mut out = vec![0u8; bytes];
backend.copy_dtoh(&mut out, output_ptr)?;
Ok(bytes_to_f32(&out))
})
}
fn run_binary_f32(
backend: &dyn ComputeBackend,
op: BinaryOp,
a: &[f32],
b: &[f32],
) -> BackendResult<Vec<f32>> {
debug_assert_eq!(a.len(), b.len());
let n = a.len();
let bytes = std::mem::size_of_val(a);
with_buffers(backend, &[bytes, bytes, bytes], |ptrs| {
let (a_ptr, b_ptr, out_ptr) = (ptrs[0], ptrs[1], ptrs[2]);
backend.copy_htod(a_ptr, f32_as_bytes(a))?;
backend.copy_htod(b_ptr, f32_as_bytes(b))?;
backend.binary(op, a_ptr, b_ptr, out_ptr, n)?;
let mut out = vec![0u8; bytes];
backend.copy_dtoh(&mut out, out_ptr)?;
Ok(bytes_to_f32(&out))
})
}
fn active_backend() -> Option<&'static dyn ComputeBackend> {
#[cfg(feature = "cuda")]
{
use crate::cuda_backend::CudaBackend;
use std::sync::OnceLock;
static BACKEND: OnceLock<Option<CudaBackend>> = OnceLock::new();
let backend = BACKEND.get_or_init(|| {
let mut backend = CudaBackend::new();
backend.init().ok()?;
if backend.has_gpu_context() {
Some(backend)
} else {
None
}
});
return backend.as_ref().map(|b| b as &dyn ComputeBackend);
}
#[cfg(not(feature = "cuda"))]
None
}
pub fn try_unary_f32<T: TensorElement>(input: &Tensor<T>, op: UnaryOp) -> Option<Tensor<T>> {
use std::any::TypeId;
if TypeId::of::<T>() != TypeId::of::<f32>() {
return None;
}
if !matches!(input.device, crate::DeviceType::Cuda(_)) {
return None;
}
let backend = active_backend()?;
let data = input.data().ok()?;
let f32_slice: &[f32] =
unsafe { std::slice::from_raw_parts(data.as_ptr().cast::<f32>(), data.len()) };
let result_f32 = run_unary_f32(backend, op, f32_slice).ok()?;
let result_t: Vec<T> = unsafe {
let mut v = std::mem::ManuallyDrop::new(result_f32);
Vec::from_raw_parts(v.as_mut_ptr().cast::<T>(), v.len(), v.capacity())
};
Tensor::<T>::from_data(result_t, input.shape().dims().to_vec(), input.device).ok()
}
pub fn try_binary_f32<T: TensorElement>(
lhs: &Tensor<T>,
rhs: &Tensor<T>,
op: BinaryOp,
) -> Option<Tensor<T>> {
use std::any::TypeId;
if TypeId::of::<T>() != TypeId::of::<f32>() {
return None;
}
if !matches!(lhs.device, crate::DeviceType::Cuda(_)) {
return None;
}
let backend = active_backend()?;
let lhs_data = lhs.data().ok()?;
let rhs_data = rhs.data().ok()?;
if lhs_data.len() != rhs_data.len() {
return None;
}
let lhs_f32: &[f32] =
unsafe { std::slice::from_raw_parts(lhs_data.as_ptr().cast::<f32>(), lhs_data.len()) };
let rhs_f32: &[f32] =
unsafe { std::slice::from_raw_parts(rhs_data.as_ptr().cast::<f32>(), rhs_data.len()) };
let result_f32 = run_binary_f32(backend, op, lhs_f32, rhs_f32).ok()?;
let result_t: Vec<T> = unsafe {
let mut v = std::mem::ManuallyDrop::new(result_f32);
Vec::from_raw_parts(v.as_mut_ptr().cast::<T>(), v.len(), v.capacity())
};
Tensor::<T>::from_data(result_t, lhs.shape().dims().to_vec(), lhs.device).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use oxicuda_backend::CpuBackend;
#[test]
fn unary_relu_through_compute_backend() {
let mut backend = CpuBackend::new();
backend.init().expect("backend init");
let out = run_unary_f32(&backend, UnaryOp::Relu, &[-2.0, -0.5, 0.0, 1.5, 3.0])
.expect("relu dispatch");
assert_eq!(out, vec![0.0, 0.0, 0.0, 1.5, 3.0]);
assert_eq!(backend.live_allocations(), 0);
}
#[test]
fn unary_sigmoid_through_compute_backend() {
let mut backend = CpuBackend::new();
backend.init().expect("backend init");
let out = run_unary_f32(&backend, UnaryOp::Sigmoid, &[0.0]).expect("sigmoid dispatch");
assert!((out[0] - 0.5).abs() < 1e-6);
assert_eq!(backend.live_allocations(), 0);
}
#[test]
fn binary_add_and_mul_through_compute_backend() {
let mut backend = CpuBackend::new();
backend.init().expect("backend init");
let a = [1.0f32, 2.0, 3.0];
let b = [10.0f32, 20.0, 30.0];
assert_eq!(
run_binary_f32(&backend, BinaryOp::Add, &a, &b).expect("add dispatch"),
vec![11.0, 22.0, 33.0]
);
assert_eq!(
run_binary_f32(&backend, BinaryOp::Mul, &a, &b).expect("mul dispatch"),
vec![10.0, 40.0, 90.0]
);
assert_eq!(backend.live_allocations(), 0);
}
#[test]
fn cpu_tensor_declines_gpu_dispatch() {
let tensor = Tensor::from_data(vec![1.0f32, -1.0], vec![2], crate::DeviceType::Cpu)
.expect("tensor creation");
assert!(try_unary_f32(&tensor, UnaryOp::Relu).is_none());
}
#[cfg(feature = "cuda")]
#[test]
fn unary_relu_runs_on_real_gpu() {
let Some(backend) = active_backend() else {
eprintln!("no CUDA device available; skipping real-GPU relu test");
return;
};
let input = vec![-2.0f32, -0.5, 0.0, 1.5, 3.0, -7.0, 4.0, 0.25];
let got = run_unary_f32(backend, UnaryOp::Relu, &input).expect("GPU relu dispatch failed");
let expect: Vec<f32> = input.iter().map(|&x| x.max(0.0)).collect();
assert_eq!(got, expect, "GPU relu result must match CPU reference");
}
#[cfg(feature = "cuda")]
#[test]
fn tensor_path_unary_dispatches_to_gpu() {
if active_backend().is_none() {
return;
}
let t = Tensor::from_data(
vec![-1.0f32, 2.0, -3.0, 4.0],
vec![4],
crate::DeviceType::Cuda(0),
)
.expect("cuda tensor");
let out = try_unary_f32(&t, UnaryOp::Relu).expect("Tensor GPU path returned None");
assert_eq!(out.to_vec().expect("vec"), vec![0.0, 2.0, 0.0, 4.0]);
}
#[cfg(feature = "cuda")]
#[test]
fn binary_add_runs_on_real_gpu() {
let Some(backend) = active_backend() else {
return;
};
let a = [1.0f32, 2.0, 3.0, 4.0];
let b = [10.0f32, 20.0, 30.0, 40.0];
let got = run_binary_f32(backend, BinaryOp::Add, &a, &b).expect("GPU add dispatch failed");
assert_eq!(got, vec![11.0, 22.0, 33.0, 44.0]);
}
}