mod attention;
mod batched;
pub use batched::{dispatch_oxicuda_matmul, dispatch_oxicuda_matmul_resident, BatchedMatmulPlan};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use oxicuda_blas::level3::gemm_api::gemm;
use oxicuda_blas::{BlasHandle, Layout, MatrixDesc, MatrixDescMut, Transpose};
use oxicuda_dnn::norm::layer_norm;
use oxicuda_dnn::types::{TensorDesc, TensorDescMut};
use oxicuda_dnn::DnnHandle;
use oxicuda_memory::DeviceBuffer;
use crate::errors::TrustformersError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OxiCudaBufferId(u64);
impl OxiCudaBufferId {
pub fn new() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
OxiCudaBufferId(COUNTER.fetch_add(1, Ordering::SeqCst))
}
#[inline]
pub fn raw(&self) -> u64 {
self.0
}
}
impl Default for OxiCudaBufferId {
fn default() -> Self {
Self::new()
}
}
type ReleaseFn = Box<dyn Fn(usize, OxiCudaBufferId) + Send + Sync + 'static>;
struct BufferHandleInner {
buffer_id: OxiCudaBufferId,
device_id: usize,
release: ReleaseFn,
}
impl Drop for BufferHandleInner {
fn drop(&mut self) {
(self.release)(self.device_id, self.buffer_id);
}
}
pub struct OxiCudaBufferHandle {
inner: Arc<BufferHandleInner>,
}
impl OxiCudaBufferHandle {
pub fn new(buffer_id: OxiCudaBufferId, device_id: usize) -> Self {
Self::with_release(buffer_id, device_id, Box::new(release_persistent_buffer))
}
pub(crate) fn with_release(
buffer_id: OxiCudaBufferId,
device_id: usize,
release: ReleaseFn,
) -> Self {
Self {
inner: Arc::new(BufferHandleInner {
buffer_id,
device_id,
release,
}),
}
}
#[inline]
pub fn id(&self) -> OxiCudaBufferId {
self.inner.buffer_id
}
#[inline]
pub fn device_id(&self) -> usize {
self.inner.device_id
}
#[inline]
pub fn ref_count(&self) -> usize {
Arc::strong_count(&self.inner)
}
}
impl Clone for OxiCudaBufferHandle {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl std::fmt::Debug for OxiCudaBufferHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OxiCudaBufferHandle")
.field("buffer_id", &self.inner.buffer_id)
.field("device_id", &self.inner.device_id)
.field("ref_count", &Arc::strong_count(&self.inner))
.finish()
}
}
fn release_persistent_buffer(device_id: usize, buffer_id: OxiCudaBufferId) {
let backend = match OXICUDA_BACKENDS.lock() {
Ok(cache) => cache.get(&device_id).cloned(),
Err(_) => None,
};
if let Some(backend) = backend {
let _ = backend.remove_persistent_buffer(&buffer_id);
}
}
pub fn default_cuda_device_id() -> usize {
static DEFAULT: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*DEFAULT.get_or_init(|| {
std::env::var("TRUSTFORMERS_CUDA_DEVICE")
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.unwrap_or(0)
})
}
pub struct OxicudaCudaBackend {
ctx: Arc<oxicuda_driver::Context>,
handle: BlasHandle,
buffer_cache: Mutex<HashMap<OxiCudaBufferId, DeviceBuffer<f32>>>,
}
impl OxicudaCudaBackend {
pub fn new(device_id: usize) -> crate::errors::Result<Self> {
oxicuda_driver::init().map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to initialize CUDA driver: {}", e),
"OxicudaCudaBackend::new",
)
})?;
let device_count = oxicuda_driver::Device::count().map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to enumerate CUDA devices: {}", e),
"OxicudaCudaBackend::new",
)
})?;
let device_count = usize::try_from(device_count).unwrap_or(0);
if device_id >= device_count {
return Err(TrustformersError::hardware_error(
&format!(
"CUDA device index {} out of range: {} device(s) available",
device_id, device_count
),
"OxicudaCudaBackend::new",
));
}
let device = oxicuda_driver::Device::get(device_id as i32).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to get CUDA device: {}", e),
"OxicudaCudaBackend::new",
)
})?;
let ctx = Arc::new(oxicuda_driver::Context::new(&device).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to create CUDA context: {}", e),
"OxicudaCudaBackend::new",
)
})?);
let handle = BlasHandle::new(&ctx).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to create cuBLAS handle: {}", e),
"OxicudaCudaBackend::new",
)
})?;
Ok(Self {
ctx,
handle,
buffer_cache: Mutex::new(HashMap::new()),
})
}
pub fn context(&self) -> &Arc<oxicuda_driver::Context> {
&self.ctx
}
pub fn create_persistent_buffer(&self, data: &[f32]) -> crate::errors::Result<OxiCudaBufferId> {
let buffer = DeviceBuffer::<f32>::from_host(data).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to copy data to device: {}", e),
"create_persistent_buffer",
)
})?;
let buffer_id = OxiCudaBufferId::new();
let mut cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error(
"Failed to lock buffer cache",
"create_persistent_buffer",
)
})?;
cache.insert(buffer_id, buffer);
Ok(buffer_id)
}
pub fn create_persistent_buffer_zeroed(
&self,
len: usize,
) -> crate::errors::Result<OxiCudaBufferId> {
let buffer = DeviceBuffer::<f32>::zeroed(len).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate device buffer of {} f32s: {}", len, e),
"create_persistent_buffer_zeroed",
)
})?;
let buffer_id = OxiCudaBufferId::new();
let mut cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error(
"Failed to lock buffer cache",
"create_persistent_buffer_zeroed",
)
})?;
cache.insert(buffer_id, buffer);
Ok(buffer_id)
}
pub fn get_persistent_buffer(&self, id: &OxiCudaBufferId) -> crate::errors::Result<usize> {
let cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error(
"Failed to lock buffer cache",
"get_persistent_buffer",
)
})?;
cache.get(id).map(|buf| buf.len()).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Buffer {:?} not found in cache", id),
"get_persistent_buffer",
)
})
}
pub fn remove_persistent_buffer(&self, id: &OxiCudaBufferId) -> crate::errors::Result<()> {
let mut cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error(
"Failed to lock buffer cache",
"remove_persistent_buffer",
)
})?;
cache.remove(id);
Ok(())
}
pub fn clear_buffer_cache(&self) -> crate::errors::Result<()> {
let mut cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error("Failed to lock buffer cache", "clear_buffer_cache")
})?;
cache.clear();
Ok(())
}
pub fn buffer_cache_size(&self) -> crate::errors::Result<usize> {
let cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error("Failed to lock buffer cache", "buffer_cache_size")
})?;
Ok(cache.len())
}
fn synchronize_stream(&self, op: &'static str) -> crate::errors::Result<()> {
self.handle.stream().synchronize().map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to synchronize CUDA stream: {}", e),
op,
)
})
}
pub fn download_buffer(&self, buffer_id: &OxiCudaBufferId) -> crate::errors::Result<Vec<f32>> {
let cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error("Failed to lock buffer cache", "download_buffer")
})?;
let buffer = cache.get(buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Buffer {:?} not found in cache", buffer_id),
"download_buffer",
)
})?;
let mut result = vec![0.0f32; buffer.len()];
self.synchronize_stream("download_buffer")?;
buffer.copy_to_host(&mut result).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to copy data from device: {}", e),
"download_buffer",
)
})?;
Ok(result)
}
pub fn buffer_to_cpu(
&self,
buffer_id: &OxiCudaBufferId,
_size: usize,
) -> crate::errors::Result<Vec<f32>> {
self.download_buffer(buffer_id)
}
pub fn matmul_gpu_to_gpu(
&self,
input_buffer_id: &OxiCudaBufferId,
weight_buffer_id: &OxiCudaBufferId,
m: usize,
k: usize,
n: usize,
) -> crate::errors::Result<OxiCudaBufferId> {
let mut cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error("Failed to lock buffer cache", "matmul_gpu_to_gpu")
})?;
let a_buf = cache.get(input_buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Input buffer {:?} not found in cache", input_buffer_id),
"matmul_gpu_to_gpu",
)
})?;
let b_buf = cache.get(weight_buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Weight buffer {:?} not found in cache", weight_buffer_id),
"matmul_gpu_to_gpu",
)
})?;
if a_buf.len() != m * k {
return Err(TrustformersError::shape_error(format!(
"Input buffer length {} doesn't match m {} * k {}",
a_buf.len(),
m,
k
)));
}
if b_buf.len() != k * n {
return Err(TrustformersError::shape_error(format!(
"Weight buffer length {} doesn't match k {} * n {}",
b_buf.len(),
k,
n
)));
}
let mut c_buf = DeviceBuffer::<f32>::alloc(m * n).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate result buffer on device: {}", e),
"matmul_gpu_to_gpu",
)
})?;
let a_desc =
MatrixDesc::from_buffer(a_buf, m as u32, k as u32, Layout::RowMajor).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe input matrix: {}", e),
"matmul_gpu_to_gpu",
)
})?;
let b_desc =
MatrixDesc::from_buffer(b_buf, k as u32, n as u32, Layout::RowMajor).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe weight matrix: {}", e),
"matmul_gpu_to_gpu",
)
})?;
let mut c_desc =
MatrixDescMut::from_buffer(&mut c_buf, m as u32, n as u32, Layout::RowMajor).map_err(
|e| {
TrustformersError::hardware_error(
&format!("Failed to describe result matrix: {}", e),
"matmul_gpu_to_gpu",
)
},
)?;
gemm::<f32>(
&self.handle,
Transpose::NoTrans,
Transpose::NoTrans,
1.0f32,
&a_desc,
&b_desc,
0.0f32,
&mut c_desc,
)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("GEMM execution failed: {}", e),
"matmul_gpu_to_gpu",
)
})?;
let output_id = OxiCudaBufferId::new();
cache.insert(output_id, c_buf);
Ok(output_id)
}
pub fn gelu_gpu_to_gpu(
&self,
input_buffer_id: &OxiCudaBufferId,
size: usize,
) -> crate::errors::Result<OxiCudaBufferId> {
let mut cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error("Failed to lock buffer cache", "gelu_gpu_to_gpu")
})?;
let in_buf = cache.get(input_buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Input buffer {:?} not found in cache", input_buffer_id),
"gelu_gpu_to_gpu",
)
})?;
if in_buf.len() != size {
return Err(TrustformersError::shape_error(format!(
"Input buffer length {} doesn't match size {}",
in_buf.len(),
size
)));
}
let mut out_buf = DeviceBuffer::<f32>::alloc(size).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate output buffer on device: {}", e),
"gelu_gpu_to_gpu",
)
})?;
oxicuda_blas::elementwise::gelu(&self.handle, size as u32, in_buf, &mut out_buf).map_err(
|e| {
TrustformersError::hardware_error(
&format!("GELU execution failed: {}", e),
"gelu_gpu_to_gpu",
)
},
)?;
let output_id = OxiCudaBufferId::new();
cache.insert(output_id, out_buf);
Ok(output_id)
}
pub fn add_bias_gpu_to_gpu(
&self,
input_buffer_id: &OxiCudaBufferId,
bias_buffer_id: &OxiCudaBufferId,
m: usize,
n: usize,
) -> crate::errors::Result<OxiCudaBufferId> {
let total_size = m * n;
let mut cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error("Failed to lock buffer cache", "add_bias_gpu_to_gpu")
})?;
let in_buf = cache.get(input_buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Input buffer {:?} not found in cache", input_buffer_id),
"add_bias_gpu_to_gpu",
)
})?;
let bias_buf = cache.get(bias_buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Bias buffer {:?} not found in cache", bias_buffer_id),
"add_bias_gpu_to_gpu",
)
})?;
if in_buf.len() != total_size {
return Err(TrustformersError::shape_error(format!(
"Input buffer length {} doesn't match m {} * n {}",
in_buf.len(),
m,
n
)));
}
if bias_buf.len() != n {
return Err(TrustformersError::shape_error(format!(
"Bias buffer length {} doesn't match n {}",
bias_buf.len(),
n
)));
}
let mut out_buf = DeviceBuffer::<f32>::alloc(total_size).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate output buffer on device: {}", e),
"add_bias_gpu_to_gpu",
)
})?;
oxicuda_blas::elementwise::bias_add(
&self.handle,
m as u32,
n as u32,
in_buf,
bias_buf,
&mut out_buf,
)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("Bias-add execution failed: {}", e),
"add_bias_gpu_to_gpu",
)
})?;
let output_id = OxiCudaBufferId::new();
cache.insert(output_id, out_buf);
Ok(output_id)
}
pub fn layernorm_gpu_to_gpu(
&self,
input_buffer_id: &OxiCudaBufferId,
weight_buffer_id: &OxiCudaBufferId,
bias_buffer_id: &OxiCudaBufferId,
seq_len: usize,
hidden_size: usize,
eps: f32,
) -> crate::errors::Result<OxiCudaBufferId> {
let total_size = seq_len * hidden_size;
let mut cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error("Failed to lock buffer cache", "layernorm_gpu_to_gpu")
})?;
let in_buf = cache.get(input_buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Input buffer {:?} not found in cache", input_buffer_id),
"layernorm_gpu_to_gpu",
)
})?;
let weight_buf = cache.get(weight_buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Weight buffer {:?} not found in cache", weight_buffer_id),
"layernorm_gpu_to_gpu",
)
})?;
let bias_buf = cache.get(bias_buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Bias buffer {:?} not found in cache", bias_buffer_id),
"layernorm_gpu_to_gpu",
)
})?;
if in_buf.len() != total_size {
return Err(TrustformersError::shape_error(format!(
"Input buffer length {} doesn't match seq_len {} * hidden_size {}",
in_buf.len(),
seq_len,
hidden_size
)));
}
if weight_buf.len() != hidden_size || bias_buf.len() != hidden_size {
return Err(TrustformersError::shape_error(format!(
"Weight/bias buffer lengths ({}, {}) must match hidden_size {}",
weight_buf.len(),
bias_buf.len(),
hidden_size
)));
}
let dnn = DnnHandle::new(self.context()).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to create cuDNN handle: {}", e),
"layernorm_gpu_to_gpu",
)
})?;
let mut out_buf = DeviceBuffer::<f32>::alloc(total_size).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate output buffer on device: {}", e),
"layernorm_gpu_to_gpu",
)
})?;
let in_desc = TensorDesc::<f32>::matrix(in_buf, seq_len as u32, hidden_size as u32)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe input tensor: {}", e),
"layernorm_gpu_to_gpu",
)
})?;
{
let mut out_desc =
TensorDescMut::<f32>::matrix(&mut out_buf, seq_len as u32, hidden_size as u32)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe output tensor: {}", e),
"layernorm_gpu_to_gpu",
)
})?;
layer_norm(&dnn, &in_desc, weight_buf, bias_buf, &mut out_desc, eps).map_err(|e| {
TrustformersError::hardware_error(
&format!("LayerNorm execution failed: {}", e),
"layernorm_gpu_to_gpu",
)
})?;
}
let output_id = OxiCudaBufferId::new();
cache.insert(output_id, out_buf);
Ok(output_id)
}
pub fn matmul_with_cached_weight(
&self,
a: &[f32],
weight_buffer_id: &OxiCudaBufferId,
m: usize,
k: usize,
n: usize,
) -> crate::errors::Result<Vec<f32>> {
if a.len() != m * k {
return Err(TrustformersError::shape_error(format!(
"Activation length {} doesn't match m {} * k {}",
a.len(),
m,
k
)));
}
let a_buf = DeviceBuffer::<f32>::from_host(a).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to upload activations to device: {}", e),
"matmul_with_cached_weight",
)
})?;
let cache = self.buffer_cache.lock().map_err(|_| {
TrustformersError::hardware_error(
"Failed to lock buffer cache",
"matmul_with_cached_weight",
)
})?;
let b_buf = cache.get(weight_buffer_id).ok_or_else(|| {
TrustformersError::hardware_error(
&format!("Weight buffer {:?} not found in cache", weight_buffer_id),
"matmul_with_cached_weight",
)
})?;
if b_buf.len() != k * n {
return Err(TrustformersError::shape_error(format!(
"Weight buffer length {} doesn't match k {} * n {}",
b_buf.len(),
k,
n
)));
}
let mut c_buf = DeviceBuffer::<f32>::alloc(m * n).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate result buffer on device: {}", e),
"matmul_with_cached_weight",
)
})?;
let a_desc = MatrixDesc::from_buffer(&a_buf, m as u32, k as u32, Layout::RowMajor)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe activation matrix: {}", e),
"matmul_with_cached_weight",
)
})?;
let b_desc =
MatrixDesc::from_buffer(b_buf, k as u32, n as u32, Layout::RowMajor).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe weight matrix: {}", e),
"matmul_with_cached_weight",
)
})?;
let mut c_desc =
MatrixDescMut::from_buffer(&mut c_buf, m as u32, n as u32, Layout::RowMajor).map_err(
|e| {
TrustformersError::hardware_error(
&format!("Failed to describe result matrix: {}", e),
"matmul_with_cached_weight",
)
},
)?;
gemm::<f32>(
&self.handle,
Transpose::NoTrans,
Transpose::NoTrans,
1.0f32,
&a_desc,
&b_desc,
0.0f32,
&mut c_desc,
)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("GEMM execution failed: {}", e),
"matmul_with_cached_weight",
)
})?;
let mut result = vec![0.0f32; m * n];
self.synchronize_stream("matmul_with_cached_weight")?;
c_buf.copy_to_host(&mut result).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to copy result back to host: {}", e),
"matmul_with_cached_weight",
)
})?;
Ok(result)
}
pub fn device_info(&self) -> String {
format!(
"CUDA Device (ordinal: {})",
self.context().device().ordinal()
)
}
pub fn matmul_f32(
&self,
a: &[f32],
b: &[f32],
m: usize,
k: usize,
n: usize,
) -> crate::errors::Result<Vec<f32>> {
if a.len() != m * k {
return Err(TrustformersError::shape_error(format!(
"Matrix A length {} doesn't match m {} * k {}",
a.len(),
m,
k
)));
}
if b.len() != k * n {
return Err(TrustformersError::shape_error(format!(
"Matrix B length {} doesn't match k {} * n {}",
b.len(),
k,
n
)));
}
let a_buf = DeviceBuffer::<f32>::from_host(a).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to upload matrix A to device: {}", e),
"matmul_f32",
)
})?;
let b_buf = DeviceBuffer::<f32>::from_host(b).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to upload matrix B to device: {}", e),
"matmul_f32",
)
})?;
let mut c_buf = DeviceBuffer::<f32>::alloc(m * n).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate result buffer on device: {}", e),
"matmul_f32",
)
})?;
let a_desc = MatrixDesc::from_buffer(&a_buf, m as u32, k as u32, Layout::RowMajor)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe matrix A: {}", e),
"matmul_f32",
)
})?;
let b_desc = MatrixDesc::from_buffer(&b_buf, k as u32, n as u32, Layout::RowMajor)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe matrix B: {}", e),
"matmul_f32",
)
})?;
let mut c_desc =
MatrixDescMut::from_buffer(&mut c_buf, m as u32, n as u32, Layout::RowMajor).map_err(
|e| {
TrustformersError::hardware_error(
&format!("Failed to describe result matrix: {}", e),
"matmul_f32",
)
},
)?;
gemm::<f32>(
&self.handle,
Transpose::NoTrans,
Transpose::NoTrans,
1.0f32,
&a_desc,
&b_desc,
0.0f32,
&mut c_desc,
)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("GEMM execution failed: {}", e),
"matmul_f32",
)
})?;
let mut result = vec![0.0f32; m * n];
self.synchronize_stream("matmul_f32")?;
c_buf.copy_to_host(&mut result).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to copy result back to host: {}", e),
"matmul_f32",
)
})?;
Ok(result)
}
pub fn gelu_f32(&self, input: &[f32]) -> crate::errors::Result<Vec<f32>> {
let size = input.len();
if size == 0 {
return Ok(Vec::new());
}
let in_buf = DeviceBuffer::<f32>::from_host(input).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to upload input to device: {}", e),
"gelu_f32",
)
})?;
let mut out_buf = DeviceBuffer::<f32>::alloc(size).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate output buffer on device: {}", e),
"gelu_f32",
)
})?;
oxicuda_blas::elementwise::gelu(&self.handle, size as u32, &in_buf, &mut out_buf).map_err(
|e| {
TrustformersError::hardware_error(
&format!("GELU execution failed: {}", e),
"gelu_f32",
)
},
)?;
let mut result = vec![0.0f32; size];
self.synchronize_stream("gelu_f32")?;
out_buf.copy_to_host(&mut result).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to copy result back to host: {}", e),
"gelu_f32",
)
})?;
Ok(result)
}
pub fn layernorm_f32(
&self,
input: &[f32],
weight: &[f32],
bias: &[f32],
seq_len: usize,
hidden_size: usize,
eps: f32,
) -> crate::errors::Result<Vec<f32>> {
let total_size = seq_len * hidden_size;
if input.len() != total_size {
return Err(TrustformersError::shape_error(format!(
"Input size {} doesn't match seq_len {} * hidden_size {}",
input.len(),
seq_len,
hidden_size
)));
}
if weight.len() != hidden_size || bias.len() != hidden_size {
return Err(TrustformersError::shape_error(
"Weight/bias size must match hidden_size".to_string(),
));
}
if total_size == 0 {
return Ok(Vec::new());
}
let dnn = DnnHandle::new(self.context()).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to create cuDNN handle: {}", e),
"layernorm_f32",
)
})?;
let in_buf = DeviceBuffer::<f32>::from_host(input).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to upload input to device: {}", e),
"layernorm_f32",
)
})?;
let weight_buf = DeviceBuffer::<f32>::from_host(weight).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to upload weight to device: {}", e),
"layernorm_f32",
)
})?;
let bias_buf = DeviceBuffer::<f32>::from_host(bias).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to upload bias to device: {}", e),
"layernorm_f32",
)
})?;
let mut out_buf = DeviceBuffer::<f32>::alloc(total_size).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate output buffer on device: {}", e),
"layernorm_f32",
)
})?;
let in_desc = TensorDesc::<f32>::matrix(&in_buf, seq_len as u32, hidden_size as u32)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe input tensor: {}", e),
"layernorm_f32",
)
})?;
{
let mut out_desc =
TensorDescMut::<f32>::matrix(&mut out_buf, seq_len as u32, hidden_size as u32)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to describe output tensor: {}", e),
"layernorm_f32",
)
})?;
layer_norm(&dnn, &in_desc, &weight_buf, &bias_buf, &mut out_desc, eps).map_err(
|e| {
TrustformersError::hardware_error(
&format!("LayerNorm execution failed: {}", e),
"layernorm_f32",
)
},
)?;
}
let mut result = vec![0.0f32; total_size];
self.synchronize_stream("layernorm_f32")?;
out_buf.copy_to_host(&mut result).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to copy result back to host: {}", e),
"layernorm_f32",
)
})?;
Ok(result)
}
pub fn softmax_causal_f32(
&self,
input: &[f32],
seq_len: usize,
) -> crate::errors::Result<Vec<f32>> {
let total_size = seq_len * seq_len;
if input.len() != total_size {
return Err(TrustformersError::shape_error(format!(
"Input size {} doesn't match seq_len^2 {}",
input.len(),
total_size
)));
}
if total_size == 0 {
return Ok(Vec::new());
}
let in_buf = DeviceBuffer::<f32>::from_host(input).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to upload input to device: {}", e),
"softmax_causal_f32",
)
})?;
let mut out_buf = DeviceBuffer::<f32>::alloc(total_size).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate output buffer on device: {}", e),
"softmax_causal_f32",
)
})?;
oxicuda_blas::reduction::causal_softmax::<f32>(
&self.handle,
seq_len as u32,
seq_len as u32,
seq_len as u32,
&in_buf,
&mut out_buf,
)
.map_err(|e| {
TrustformersError::hardware_error(
&format!("Causal softmax execution failed: {}", e),
"softmax_causal_f32",
)
})?;
let mut result = vec![0.0f32; total_size];
self.synchronize_stream("softmax_causal_f32")?;
out_buf.copy_to_host(&mut result).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to copy result back to host: {}", e),
"softmax_causal_f32",
)
})?;
Ok(result)
}
#[allow(clippy::too_many_arguments)]
pub fn rope_f32(
&self,
input: &[f32],
seq_len: usize,
num_heads: usize,
head_dim: usize,
rotary_ndims: usize,
base: f32,
) -> crate::errors::Result<Vec<f32>> {
let total_size = seq_len * num_heads * head_dim;
if input.len() != total_size {
return Err(TrustformersError::shape_error(format!(
"Input size {} doesn't match seq_len {} * num_heads {} * head_dim {}",
input.len(),
seq_len,
num_heads,
head_dim
)));
}
if total_size == 0 {
return Ok(Vec::new());
}
let dnn = DnnHandle::new(self.context()).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to create cuDNN handle: {}", e),
"rope_f32",
)
})?;
let in_buf = DeviceBuffer::<f32>::from_host(input).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to upload input to device: {}", e),
"rope_f32",
)
})?;
let mut out_buf = DeviceBuffer::<f32>::alloc(total_size).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to allocate output buffer on device: {}", e),
"rope_f32",
)
})?;
oxicuda_dnn::attn::rope_neox_half_split_f32(
&dnn,
&in_buf,
&mut out_buf,
seq_len as u32,
num_heads as u32,
head_dim as u32,
rotary_ndims as u32,
base,
)
.map_err(|e| {
TrustformersError::hardware_error(&format!("RoPE execution failed: {}", e), "rope_f32")
})?;
let mut result = vec![0.0f32; total_size];
self.synchronize_stream("rope_f32")?;
out_buf.copy_to_host(&mut result).map_err(|e| {
TrustformersError::hardware_error(
&format!("Failed to copy result back to host: {}", e),
"rope_f32",
)
})?;
Ok(result)
}
}
pub fn oxicuda_cuda_available() -> bool {
static AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*AVAILABLE.get_or_init(|| {
oxicuda_driver::init().is_ok()
&& matches!(oxicuda_driver::Device::count(), Ok(count) if count > 0)
})
}
static OXICUDA_BACKENDS: once_cell::sync::Lazy<Mutex<HashMap<usize, Arc<OxicudaCudaBackend>>>> =
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
pub fn oxicuda_backend(device_id: usize) -> crate::errors::Result<Arc<OxicudaCudaBackend>> {
let mut cache = OXICUDA_BACKENDS.lock().map_err(|_| {
TrustformersError::hardware_error("Failed to lock oxicuda backend cache", "oxicuda_backend")
})?;
if let std::collections::hash_map::Entry::Vacant(e) = cache.entry(device_id) {
let backend = OxicudaCudaBackend::new(device_id)?;
e.insert(Arc::new(backend));
}
cache.get(&device_id).cloned().ok_or_else(|| {
TrustformersError::hardware_error("oxicuda backend not found", "oxicuda_backend")
})
}
#[cfg(all(
test,
feature = "cuda",
any(target_os = "linux", target_os = "windows")
))]
mod tests {
use super::*;
#[test]
fn oxicuda_cuda_matmul_parity() -> crate::errors::Result<()> {
let a = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
let b = vec![7.0f32, 8.0, 9.0, 10.0, 11.0, 12.0];
let m = 2usize;
let k = 3usize;
let n = 2usize;
let mut expected = vec![0.0f32; m * n];
for i in 0..m {
for j in 0..n {
for p in 0..k {
expected[i * n + j] += a[i * k + p] * b[p * n + j];
}
}
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping oxicuda CUDA test: no CUDA device available");
return Ok(());
},
};
let result = backend.matmul_f32(&a, &b, m, k, n)?;
for idx in 0..(m * n) {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
Ok(())
}
#[test]
fn oxicuda_cuda_gelu_parity() -> crate::errors::Result<()> {
let input = vec![-1.0f32, -0.5, 0.0, 0.5, 1.0, 2.0];
let mut expected = vec![0.0f32; input.len()];
for (idx, &x) in input.iter().enumerate() {
let inner = 0.7978845608f32 * (x + 0.044715f32 * x * x * x);
expected[idx] = 0.5f32 * x * (1.0f32 + inner.tanh());
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping oxicuda CUDA GELU test: no CUDA device available");
return Ok(());
},
};
let result = backend.gelu_f32(&input)?;
for idx in 0..input.len() {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
Ok(())
}
#[test]
fn oxicuda_cuda_layernorm_parity() -> crate::errors::Result<()> {
let seq_len = 2usize;
let hidden_size = 4usize;
let input = vec![1.0f32, 2.0, 3.0, 4.0, 4.0, 3.0, 2.0, 1.0];
let weight = vec![1.0f32; hidden_size];
let bias = vec![0.0f32; hidden_size];
let eps = 1e-5f32;
let mut expected = vec![0.0f32; seq_len * hidden_size];
for row in 0..seq_len {
let offset = row * hidden_size;
let mut sum = 0.0f32;
for i in 0..hidden_size {
sum += input[offset + i];
}
let mean = sum / hidden_size as f32;
let mut var_sum = 0.0f32;
for i in 0..hidden_size {
let diff = input[offset + i] - mean;
var_sum += diff * diff;
}
let variance = var_sum / hidden_size as f32;
let std_dev = (variance + eps).sqrt();
for i in 0..hidden_size {
let normalized = (input[offset + i] - mean) / std_dev;
expected[offset + i] = normalized * weight[i] + bias[i];
}
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping oxicuda CUDA LayerNorm test: no CUDA device available");
return Ok(());
},
};
let result = backend.layernorm_f32(&input, &weight, &bias, seq_len, hidden_size, eps)?;
for idx in 0..(seq_len * hidden_size) {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
Ok(())
}
#[test]
fn oxicuda_cuda_softmax_causal_parity() -> crate::errors::Result<()> {
let seq_len = 4usize;
let total = seq_len * seq_len;
let mut input = vec![0.0f32; total];
for (i, slot) in input.iter_mut().enumerate() {
*slot = (i as f32 * 0.37 - 2.0).sin();
}
let mut expected = vec![0.0f32; total];
for r in 0..seq_len {
let offset = r * seq_len;
let mut max_val = f32::NEG_INFINITY;
for j in 0..=r {
let v = input[offset + j];
if v > max_val {
max_val = v;
}
}
let mut sum = 0.0f32;
for j in 0..=r {
sum += (input[offset + j] - max_val).exp();
}
for j in 0..seq_len {
if j <= r {
expected[offset + j] = (input[offset + j] - max_val).exp() / sum;
} else {
expected[offset + j] = 0.0f32;
}
}
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping oxicuda CUDA causal-softmax test: no CUDA device available");
return Ok(());
},
};
let result = backend.softmax_causal_f32(&input, seq_len)?;
for idx in 0..total {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
Ok(())
}
#[test]
fn oxicuda_cuda_rope_parity() -> crate::errors::Result<()> {
let seq_len = 2usize;
let num_heads = 1usize;
let head_dim = 6usize;
let rotary_ndims = 4usize;
let base = 10000.0f32;
let total = seq_len * num_heads * head_dim;
let mut input = vec![0.0f32; total];
for (idx, slot) in input.iter_mut().enumerate() {
*slot = (idx as f32) * 0.5 + 0.1;
}
let mut expected = vec![0.0f32; total];
let half = rotary_ndims / 2;
for pos in 0..seq_len {
for h in 0..num_heads {
let base_off = (pos * num_heads + h) * head_dim;
for i in 0..half {
let freq = base.powf(-2.0 * (i as f32) / (rotary_ndims as f32));
let angle = (pos as f32) * freq;
let c = angle.cos();
let s = angle.sin();
let x_i = input[base_off + i];
let x_j = input[base_off + i + half];
expected[base_off + i] = x_i * c - x_j * s;
expected[base_off + i + half] = x_i * s + x_j * c;
}
expected[(base_off + rotary_ndims)..(base_off + head_dim)]
.copy_from_slice(&input[(base_off + rotary_ndims)..(base_off + head_dim)]);
}
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping oxicuda CUDA RoPE test: no CUDA device available");
return Ok(());
},
};
let result = backend.rope_f32(&input, seq_len, num_heads, head_dim, rotary_ndims, base)?;
for idx in 0..total {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
Ok(())
}
#[test]
fn oxicuda_cuda_resident_matmul_parity() -> crate::errors::Result<()> {
let a = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
let b = vec![7.0f32, 8.0, 9.0, 10.0, 11.0, 12.0];
let m = 2usize;
let k = 3usize;
let n = 2usize;
let mut expected = vec![0.0f32; m * n];
for i in 0..m {
for j in 0..n {
for p in 0..k {
expected[i * n + j] += a[i * k + p] * b[p * n + j];
}
}
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping oxicuda CUDA resident-matmul test: no CUDA device available");
return Ok(());
},
};
assert_eq!(backend.buffer_cache_size()?, 0);
let a_id = backend.create_persistent_buffer(&a)?;
let b_id = backend.create_persistent_buffer(&b)?;
assert_eq!(backend.buffer_cache_size()?, 2);
assert_eq!(backend.get_persistent_buffer(&a_id)?, m * k);
assert_eq!(backend.get_persistent_buffer(&b_id)?, k * n);
let c_id = backend.matmul_gpu_to_gpu(&a_id, &b_id, m, k, n)?;
assert_eq!(backend.buffer_cache_size()?, 3);
let result = backend.download_buffer(&c_id)?;
assert_eq!(result.len(), m * n);
for idx in 0..(m * n) {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
let result_alias = backend.buffer_to_cpu(&c_id, m * n)?;
assert_eq!(result_alias, result);
backend.remove_persistent_buffer(&a_id)?;
assert_eq!(backend.buffer_cache_size()?, 2);
backend.remove_persistent_buffer(&a_id)?;
assert_eq!(backend.buffer_cache_size()?, 2);
backend.clear_buffer_cache()?;
assert_eq!(backend.buffer_cache_size()?, 0);
let z_id = backend.create_persistent_buffer_zeroed(4)?;
let zeros = backend.download_buffer(&z_id)?;
assert_eq!(zeros, vec![0.0f32; 4]);
Ok(())
}
#[test]
fn oxicuda_cuda_resident_gelu_parity() -> crate::errors::Result<()> {
let input = vec![-1.0f32, -0.5, 0.0, 0.5, 1.0, 2.0];
let size = input.len();
let mut expected = vec![0.0f32; size];
for (idx, &x) in input.iter().enumerate() {
let inner = 0.7978845608f32 * (x + 0.044715f32 * x * x * x);
expected[idx] = 0.5f32 * x * (1.0f32 + inner.tanh());
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping oxicuda CUDA resident-GELU test: no CUDA device available");
return Ok(());
},
};
let in_id = backend.create_persistent_buffer(&input)?;
let out_id = backend.gelu_gpu_to_gpu(&in_id, size)?;
let result = backend.download_buffer(&out_id)?;
assert_eq!(result.len(), size);
for idx in 0..size {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
Ok(())
}
#[test]
fn oxicuda_cuda_resident_add_bias_parity() -> crate::errors::Result<()> {
let m = 3usize;
let n = 4usize;
let input: Vec<f32> = (0..(m * n)).map(|i| (i as f32) * 0.5 - 2.0).collect();
let bias = vec![10.0f32, 20.0, 30.0, 40.0];
let mut expected = vec![0.0f32; m * n];
for i in 0..m {
for j in 0..n {
expected[i * n + j] = input[i * n + j] + bias[j];
}
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping oxicuda CUDA resident-add-bias test: no CUDA device available");
return Ok(());
},
};
let in_id = backend.create_persistent_buffer(&input)?;
let bias_id = backend.create_persistent_buffer(&bias)?;
let out_id = backend.add_bias_gpu_to_gpu(&in_id, &bias_id, m, n)?;
let result = backend.download_buffer(&out_id)?;
assert_eq!(result.len(), m * n);
for idx in 0..(m * n) {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
Ok(())
}
#[test]
fn oxicuda_cuda_resident_layernorm_parity() -> crate::errors::Result<()> {
let seq_len = 2usize;
let hidden_size = 4usize;
let input = vec![1.0f32, 2.0, 3.0, 4.0, 4.0, 3.0, 2.0, 1.0];
let weight = vec![1.0f32; hidden_size];
let bias = vec![0.0f32; hidden_size];
let eps = 1e-5f32;
let mut expected = vec![0.0f32; seq_len * hidden_size];
for row in 0..seq_len {
let offset = row * hidden_size;
let mut sum = 0.0f32;
for i in 0..hidden_size {
sum += input[offset + i];
}
let mean = sum / hidden_size as f32;
let mut var_sum = 0.0f32;
for i in 0..hidden_size {
let diff = input[offset + i] - mean;
var_sum += diff * diff;
}
let variance = var_sum / hidden_size as f32;
let std_dev = (variance + eps).sqrt();
for i in 0..hidden_size {
let normalized = (input[offset + i] - mean) / std_dev;
expected[offset + i] = normalized * weight[i] + bias[i];
}
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!(
"Skipping oxicuda CUDA resident-LayerNorm test: no CUDA device available"
);
return Ok(());
},
};
let in_id = backend.create_persistent_buffer(&input)?;
let weight_id = backend.create_persistent_buffer(&weight)?;
let bias_id = backend.create_persistent_buffer(&bias)?;
let out_id = backend.layernorm_gpu_to_gpu(
&in_id,
&weight_id,
&bias_id,
seq_len,
hidden_size,
eps,
)?;
let result = backend.download_buffer(&out_id)?;
assert_eq!(result.len(), seq_len * hidden_size);
for idx in 0..(seq_len * hidden_size) {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
Ok(())
}
#[test]
fn oxicuda_cuda_matmul_with_cached_weight_parity() -> crate::errors::Result<()> {
let a = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
let b = vec![7.0f32, 8.0, 9.0, 10.0, 11.0, 12.0];
let m = 2usize;
let k = 3usize;
let n = 2usize;
let mut expected = vec![0.0f32; m * n];
for i in 0..m {
for j in 0..n {
for p in 0..k {
expected[i * n + j] += a[i * k + p] * b[p * n + j];
}
}
}
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!(
"Skipping oxicuda CUDA cached-weight matmul test: no CUDA device available"
);
return Ok(());
},
};
let weight_id = backend.create_persistent_buffer(&b)?;
for _ in 0..2 {
let result = backend.matmul_with_cached_weight(&a, &weight_id, m, k, n)?;
assert_eq!(result.len(), m * n);
for idx in 0..(m * n) {
assert!(
(result[idx] - expected[idx]).abs() < 1e-3,
"mismatch at {}: got {} expected {}",
idx,
result[idx],
expected[idx]
);
}
}
Ok(())
}
#[test]
fn oxicuda_cuda_device_info_reports_ordinal() -> crate::errors::Result<()> {
let backend = match OxicudaCudaBackend::new(0) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping oxicuda CUDA device-info test: no CUDA device available");
return Ok(());
},
};
let info = backend.device_info();
assert!(
info.contains("ordinal: 0"),
"device_info should report device ordinal 0, got {:?}",
info
);
Ok(())
}
#[test]
fn oxicuda_backend_singleton_is_shared_and_resident() -> crate::errors::Result<()> {
if OxicudaCudaBackend::new(0).is_err() {
eprintln!("Skipping oxicuda backend singleton test: no CUDA device available");
return Ok(());
}
let b1 = oxicuda_backend(0)?;
let b2 = oxicuda_backend(0)?;
assert!(
Arc::ptr_eq(&b1, &b2),
"oxicuda_backend(0) must return the same Arc on repeated calls"
);
let data = vec![1.0f32, 2.0, 3.0, 4.0];
let id = b1.create_persistent_buffer(&data)?;
let b3 = oxicuda_backend(0)?;
assert_eq!(
b3.get_persistent_buffer(&id)?,
data.len(),
"resident buffer minted via the singleton must be visible to a later handle"
);
let round_trip = b3.download_buffer(&id)?;
assert_eq!(round_trip, data);
b3.remove_persistent_buffer(&id)?;
Ok(())
}
}
#[cfg(test)]
mod resident_handle_tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
#[allow(clippy::type_complexity)]
fn counting_handle(
device_id: usize,
) -> (
OxiCudaBufferHandle,
Arc<AtomicUsize>,
Arc<Mutex<Option<(usize, OxiCudaBufferId)>>>,
) {
let releases = Arc::new(AtomicUsize::new(0));
let observed = Arc::new(Mutex::new(None));
let releases_hook = Arc::clone(&releases);
let observed_hook = Arc::clone(&observed);
let handle = OxiCudaBufferHandle::with_release(
OxiCudaBufferId::new(),
device_id,
Box::new(move |dev, id| {
releases_hook.fetch_add(1, AtomicOrdering::SeqCst);
if let Ok(mut slot) = observed_hook.lock() {
*slot = Some((dev, id));
}
}),
);
(handle, releases, observed)
}
#[test]
fn buffer_handle_releases_exactly_once_after_last_clone() {
let (handle, releases, _) = counting_handle(0);
let clone_a = handle.clone();
let clone_b = clone_a.clone();
assert_eq!(handle.ref_count(), 3);
drop(clone_a);
assert_eq!(releases.load(AtomicOrdering::SeqCst), 0);
drop(handle);
assert_eq!(releases.load(AtomicOrdering::SeqCst), 0);
assert_eq!(clone_b.ref_count(), 1);
drop(clone_b);
assert_eq!(
releases.load(AtomicOrdering::SeqCst),
1,
"release must fire exactly once, on the last drop"
);
}
#[test]
fn buffer_handle_clones_share_identity() {
let (handle, _, _) = counting_handle(2);
let clone = handle.clone();
assert_eq!(handle.id(), clone.id());
assert_eq!(handle.device_id(), clone.device_id());
assert_eq!(clone.device_id(), 2);
}
#[test]
fn buffer_handle_release_receives_device_and_buffer_id() {
let (handle, releases, observed) = counting_handle(7);
let expected_id = handle.id();
drop(handle);
assert_eq!(releases.load(AtomicOrdering::SeqCst), 1);
let slot = observed.lock().unwrap_or_else(|poison| poison.into_inner());
assert_eq!(
*slot,
Some((7, expected_id)),
"release must be invoked with the handle's device ordinal and buffer id"
);
}
#[test]
fn cuda_tensor_data_clone_and_drop_frees_exactly_once() {
let (handle, releases, _) = counting_handle(0);
let data = crate::tensor::CudaTensorData::from_handle(
handle,
vec![2, 2],
crate::tensor::DType::F32,
);
let t1 = crate::tensor::Tensor::CUDA(data);
let t2 = t1.clone();
let t3 = t2.clone();
drop(t1);
drop(t3);
assert_eq!(
releases.load(AtomicOrdering::SeqCst),
0,
"buffer must stay alive while any tensor clone remains"
);
drop(t2);
assert_eq!(
releases.load(AtomicOrdering::SeqCst),
1,
"buffer must be freed exactly once when the last tensor clone drops"
);
}
#[test]
fn release_without_backend_is_a_noop_and_never_constructs_one() {
let device_id = usize::MAX;
release_persistent_buffer(device_id, OxiCudaBufferId::new());
let registry = OXICUDA_BACKENDS.lock().unwrap_or_else(|poison| poison.into_inner());
assert!(
!registry.contains_key(&device_id),
"release path must not create backend registry entries"
);
}
#[test]
fn default_cuda_device_id_is_zero_without_override() {
if std::env::var("TRUSTFORMERS_CUDA_DEVICE").is_err() {
assert_eq!(default_cuda_device_id(), 0);
}
}
}