use crate::{Tensor, TensorElement};
#[cfg(test)]
use oxicuda_backend::BackendResult;
use oxicuda_backend::ComputeBackend;
use std::sync::{Arc, Once, RwLock};
use torsh_core::sync::RwLockExt;
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)) }
}
#[cfg(test)]
#[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()
}
#[cfg(test)]
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
}
#[cfg(test)]
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))
})
}
#[cfg(test)]
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))
})
}
static BACKEND: RwLock<Option<Arc<dyn ComputeBackend>>> = RwLock::new(None);
static AUTO_INIT: Once = Once::new();
pub fn install_backend(backend: Arc<dyn ComputeBackend>) -> Option<Arc<dyn ComputeBackend>> {
BACKEND.write_or_recover().replace(backend)
}
pub fn clear_backend() -> Option<Arc<dyn ComputeBackend>> {
BACKEND.write_or_recover().take()
}
pub(crate) fn active_backend() -> Option<Arc<dyn ComputeBackend>> {
if let Some(backend) = BACKEND.read_or_recover().as_ref() {
return Some(Arc::clone(backend));
}
AUTO_INIT.call_once(|| {
#[cfg(feature = "cuda")]
{
use crate::cuda_backend::CudaBackend;
let mut backend = CudaBackend::new();
if backend.init().is_ok() && backend.has_gpu_context() {
*BACKEND.write_or_recover() = Some(Arc::new(backend));
}
}
});
BACKEND.read_or_recover().as_ref().map(Arc::clone)
}
const F32_BYTES: usize = std::mem::size_of::<f32>();
#[inline]
fn f32_byte_size(elements: usize) -> Option<usize> {
elements.checked_mul(F32_BYTES)
}
fn residency_of<T: TensorElement>(
tensor: &Tensor<T>,
backend: &Arc<dyn ComputeBackend>,
) -> Option<Arc<crate::storage::DeviceBuffer>> {
if tensor.is_view() {
return None;
}
let buffer = tensor.storage.device_buffer()?;
if !Arc::ptr_eq(buffer.backend(), backend) {
return None;
}
if buffer.dtype() != torsh_core::dtype::DType::F32 {
return None;
}
if buffer.bytes() != f32_byte_size(tensor.numel())? {
return None;
}
Some(Arc::clone(buffer))
}
fn upload_f32<T: TensorElement>(backend: &dyn ComputeBackend, tensor: &Tensor<T>) -> Option<u64> {
let ptr = backend.alloc(f32_byte_size(tensor.numel())?).ok()?;
let uploaded = tensor.with_contiguous_data(|data| {
let data_f32: &[f32] =
unsafe { std::slice::from_raw_parts(data.as_ptr().cast::<f32>(), data.len()) };
backend
.copy_htod(ptr, f32_as_bytes(data_f32))
.map_err(|e| torsh_core::error::TorshError::InvalidOperation(format!("{e}")))
});
if uploaded.is_err() {
let _ = backend.free(ptr);
return None;
}
Some(ptr)
}
fn device_tensor<T: TensorElement>(
ptr: u64,
bytes: usize,
shape: Vec<usize>,
device: crate::DeviceType,
backend: &Arc<dyn ComputeBackend>,
) -> Tensor<T> {
let buffer = crate::storage::DeviceBuffer::adopt(
ptr,
bytes,
torsh_core::dtype::DType::F32,
Arc::clone(backend),
);
let storage = crate::storage::TensorStorage::device(Arc::new(buffer));
Tensor::<T>::from_device_storage(storage, shape, device)
}
fn is_dispatchable<T: TensorElement>(tensor: &Tensor<T>) -> bool {
std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>()
&& matches!(tensor.device, crate::DeviceType::Cuda(_))
&& tensor.numel() > 0
}
pub fn try_unary_f32<T: TensorElement>(input: &Tensor<T>, op: UnaryOp) -> Option<Tensor<T>> {
if !is_dispatchable(input) {
return None;
}
let backend = active_backend()?;
let numel = input.numel();
let output_bytes = f32_byte_size(numel)?;
let resident = residency_of(input, &backend);
let (input_ptr, scratch) = match &resident {
Some(buffer) => (buffer.ptr(), None),
None => {
let ptr = upload_f32(&*backend, input)?;
(ptr, Some(ptr))
}
};
let output_ptr = match backend.alloc(output_bytes) {
Ok(ptr) => ptr,
Err(_) => {
free_scratch(&*backend, scratch);
return None;
}
};
if backend.unary(op, input_ptr, output_ptr, numel).is_err() {
let _ = backend.free(output_ptr);
free_scratch(&*backend, scratch);
return None;
}
free_scratch(&*backend, scratch);
Some(device_tensor(
output_ptr,
output_bytes,
input.shape().dims().to_vec(),
input.device,
&backend,
))
}
fn free_scratch(backend: &dyn ComputeBackend, scratch: Option<u64>) {
if let Some(ptr) = scratch {
let _ = backend.free(ptr);
}
}
pub fn try_binary_f32<T: TensorElement>(
lhs: &Tensor<T>,
rhs: &Tensor<T>,
op: BinaryOp,
) -> Option<Tensor<T>> {
if !is_dispatchable(lhs) || !is_dispatchable(rhs) {
return None;
}
if lhs.device != rhs.device || lhs.numel() != rhs.numel() {
return None;
}
let backend = active_backend()?;
let numel = lhs.numel();
let output_bytes = f32_byte_size(numel)?;
let lhs_resident = residency_of(lhs, &backend);
let (lhs_ptr, lhs_scratch) = match &lhs_resident {
Some(buffer) => (buffer.ptr(), None),
None => {
let ptr = upload_f32(&*backend, lhs)?;
(ptr, Some(ptr))
}
};
let rhs_resident = residency_of(rhs, &backend);
let (rhs_ptr, rhs_scratch) = match &rhs_resident {
Some(buffer) => (buffer.ptr(), None),
None => match upload_f32(&*backend, rhs) {
Some(ptr) => (ptr, Some(ptr)),
None => {
free_scratch(&*backend, lhs_scratch);
return None;
}
},
};
let output_ptr = match backend.alloc(output_bytes) {
Ok(ptr) => ptr,
Err(_) => {
free_scratch(&*backend, lhs_scratch);
free_scratch(&*backend, rhs_scratch);
return None;
}
};
if backend
.binary(op, lhs_ptr, rhs_ptr, output_ptr, numel)
.is_err()
{
let _ = backend.free(output_ptr);
free_scratch(&*backend, lhs_scratch);
free_scratch(&*backend, rhs_scratch);
return None;
}
free_scratch(&*backend, lhs_scratch);
free_scratch(&*backend, rhs_scratch);
Some(device_tensor(
output_ptr,
output_bytes,
lhs.shape().dims().to_vec(),
lhs.device,
&backend,
))
}
pub(crate) fn try_upload_f32<T: TensorElement>(
tensor: &Tensor<T>,
device: crate::DeviceType,
) -> Option<Tensor<T>> {
if std::any::TypeId::of::<T>() != std::any::TypeId::of::<f32>() {
return None;
}
let numel = tensor.numel();
if numel == 0 {
return None;
}
let bytes = f32_byte_size(numel)?;
let backend = active_backend()?;
let ptr = upload_f32(&*backend, tensor)?;
Some(device_tensor(
ptr,
bytes,
tensor.shape().dims().to_vec(),
device,
&backend,
))
}
pub fn try_reduce_axis_f32<T: TensorElement>(
input: &Tensor<T>,
op: ReduceOp,
axis: usize,
output_shape: &[usize],
) -> Option<Tensor<T>> {
if !is_dispatchable(input) {
return None;
}
let shape_binding = input.shape();
let dims = shape_binding.dims();
if axis >= dims.len() {
return None;
}
let outer: usize = dims[..axis].iter().product();
let inner: usize = dims[axis + 1..].iter().product();
let output_elements = outer * inner;
if output_elements == 0 || output_elements != output_shape.iter().product::<usize>() {
return None;
}
let output_bytes = f32_byte_size(output_elements)?;
let backend = active_backend()?;
let resident = residency_of(input, &backend);
let (input_ptr, scratch) = match &resident {
Some(buffer) => (buffer.ptr(), None),
None => {
let ptr = upload_f32(&*backend, input)?;
(ptr, Some(ptr))
}
};
let output_ptr = match backend.alloc(output_bytes) {
Ok(ptr) => ptr,
Err(_) => {
free_scratch(&*backend, scratch);
return None;
}
};
if backend
.reduce(op, input_ptr, output_ptr, dims, axis)
.is_err()
{
let _ = backend.free(output_ptr);
free_scratch(&*backend, scratch);
return None;
}
free_scratch(&*backend, scratch);
Some(device_tensor(
output_ptr,
output_bytes,
output_shape.to_vec(),
input.device,
&backend,
))
}
#[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(not(feature = "cuda"))]
#[test]
fn backend_handle_installs_and_clears() {
assert!(clear_backend().is_none());
assert!(active_backend().is_none());
let mut first = CpuBackend::new();
first.init().expect("backend init");
let first: Arc<dyn ComputeBackend> = Arc::new(first);
assert!(install_backend(Arc::clone(&first)).is_none());
let active = active_backend().expect("installed backend must be active");
assert!(Arc::ptr_eq(&active, &first));
let mut second = CpuBackend::new();
second.init().expect("backend init");
let second: Arc<dyn ComputeBackend> = Arc::new(second);
let previous = install_backend(second).expect("install must return the previous handle");
assert!(Arc::ptr_eq(&previous, &first));
assert!(clear_backend().is_some());
assert!(active_backend().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]);
}
}