use crate::compute::uninit_vec;
use cubecl_core::server::IoError;
use cubecl_environment::backtrace::BackTrace;
use cubecl_server::config::memory::CudaAllocator;
use cubecl_server::storage::{ComputeStorage, StorageHandle, StorageId, StorageUtilization};
use cudarc::driver::DriverError;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
enum AllocationKind {
Async,
Sync,
Empty,
}
pub struct GpuStorage {
memory: HashMap<StorageId, (cudarc::driver::sys::CUdeviceptr, AllocationKind, u64)>,
deallocations: Vec<StorageId>,
allocated: u64,
ptr_bindings: PtrBindings,
stream: cudarc::driver::sys::CUstream,
mem_alignment: usize,
allocator: CudaAllocator,
}
#[derive(Debug)]
pub struct GpuResource {
pub ptr: u64,
pub binding: *mut std::ffi::c_void,
pub size: u64,
}
impl GpuResource {
pub fn new(ptr: u64, binding: *mut std::ffi::c_void, size: u64) -> Self {
Self { ptr, binding, size }
}
}
impl GpuStorage {
pub fn new(
mem_alignment: usize,
stream: cudarc::driver::sys::CUstream,
allocator: CudaAllocator,
) -> Self {
Self {
memory: HashMap::new(),
deallocations: Vec::new(),
allocated: 0,
ptr_bindings: PtrBindings::new(),
mem_alignment,
stream,
allocator,
}
}
fn perform_deallocations(&mut self) {
self.deallocations
.drain(..)
.filter_map(|id| self.memory.remove(&id))
.for_each(|(ptr, kind, size)| unsafe {
self.allocated -= size;
let result = match kind {
AllocationKind::Empty => Ok(()),
AllocationKind::Sync => cudarc::driver::result::free_sync(ptr),
AllocationKind::Async => cudarc::driver::result::free_async(ptr, self.stream),
};
if let Err(e) = result {
eprintln!("CUDA free error: {}", e);
}
});
}
}
unsafe impl Send for GpuResource {}
unsafe impl Send for GpuStorage {}
impl core::fmt::Debug for GpuStorage {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("GpuStorage").finish()
}
}
struct PtrBindings {
slots: Vec<cudarc::driver::sys::CUdeviceptr>,
cursor: usize,
}
impl PtrBindings {
fn new() -> Self {
Self {
slots: unsafe { uninit_vec(crate::device::CUDA_MAX_BINDINGS as usize) },
cursor: 0,
}
}
fn register(&mut self, ptr: u64) -> &u64 {
self.slots[self.cursor] = ptr;
let ptr_ref = self.slots.get(self.cursor).unwrap();
self.cursor += 1;
if self.cursor >= self.slots.len() {
self.cursor = 0;
}
ptr_ref
}
}
impl ComputeStorage for GpuStorage {
type Resource = GpuResource;
fn alignment(&self) -> usize {
self.mem_alignment
}
fn get(&mut self, handle: &StorageHandle) -> Result<Self::Resource, IoError> {
let (ptr, _, _) =
self.memory
.get(&handle.id)
.ok_or_else(|| IoError::StorageHandleNotFound {
reason: format!("{} in the CUDA gpu storage", handle.id).into(),
backtrace: BackTrace::capture(),
})?;
let offset = handle.offset();
let size = handle.size();
let ptr = self.ptr_bindings.register(ptr + offset);
Ok(GpuResource::new(
*ptr,
ptr as *const cudarc::driver::sys::CUdeviceptr as *mut std::ffi::c_void,
size,
))
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip(self, size))
)]
fn alloc(&mut self, size: u64) -> Result<StorageHandle, IoError> {
let id = StorageId::new();
let allocation = if size == 0 {
Ok((0, AllocationKind::Empty))
} else {
unsafe {
match self.allocator {
CudaAllocator::Sync => cudarc::driver::result::malloc_sync(size as usize)
.map(|ptr| (ptr, AllocationKind::Sync)),
CudaAllocator::Async => {
cudarc::driver::result::malloc_async(self.stream, size as usize)
.map(|ptr| (ptr, AllocationKind::Async))
.or_else(|_| {
cudarc::driver::result::malloc_sync(size as usize)
.map(|ptr| (ptr, AllocationKind::Sync))
})
}
}
}
};
let (ptr, kind) = match allocation {
Ok(allocation) => allocation,
Err(DriverError(cudarc::driver::sys::CUresult::CUDA_ERROR_OUT_OF_MEMORY)) => {
return Err(IoError::OutOfMemory {
size,
backtrace: BackTrace::capture(),
});
}
Err(other) => {
return Err(IoError::Unknown {
description: format!("CUDA allocation error: {other}"),
backtrace: BackTrace::capture(),
});
}
};
self.memory.insert(id, (ptr, kind, size));
self.allocated += size;
Ok(StorageHandle::new(
id,
StorageUtilization { offset: 0, size },
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
fn dealloc(&mut self, id: StorageId) {
self.deallocations.push(id);
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
fn flush(&mut self) {
self.perform_deallocations();
}
fn bytes_allocated(&self) -> u64 {
self.allocated
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_size_allocations() {
let context = cudarc::driver::CudaContext::new(0).unwrap();
let stream = context.new_stream().unwrap();
for allocator in [CudaAllocator::Sync, CudaAllocator::Async] {
let mut storage = GpuStorage::new(512, stream.cu_stream(), allocator);
for size in [0, 16] {
let handle = storage.alloc(size).unwrap();
let resource = storage.get(&handle).unwrap();
assert_eq!(handle.size(), size);
assert_eq!(resource.size, size);
assert_eq!(resource.ptr == 0, size == 0);
assert_eq!(storage.bytes_allocated(), size);
storage.dealloc(handle.id);
storage.flush();
stream.synchronize().unwrap();
assert_eq!(storage.bytes_allocated(), 0);
}
}
}
}