#![cfg(feature = "pool")]
use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use oxicuda_driver::error::{CudaError, CudaResult, check};
use oxicuda_driver::ffi::{
CU_EVENT_DISABLE_TIMING, CUDA_ERROR_NOT_READY, CUcontext, CUdeviceptr, CUevent,
CUmemAllocationHandleType, CUmemAllocationType, CUmemLocation, CUmemLocationType,
CUmemPoolProps, CUmemoryPool, CUstream,
};
use oxicuda_driver::loader::{DriverApi, try_driver};
use oxicuda_driver::stream::Stream;
use tracing::warn;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PoolStats {
pub allocated_bytes: usize,
pub peak_bytes: usize,
pub allocation_count: u64,
pub free_count: u64,
}
#[derive(Debug)]
struct MemoryPoolInner {
handle: u64,
device_ordinal: i32,
ctx: CUcontext,
threshold_bytes: AtomicUsize,
cached_bytes: AtomicUsize,
stats: Mutex<PoolStats>,
free_bins: Mutex<HashMap<usize, Vec<(CUdeviceptr, CUevent)>>>,
}
impl MemoryPoolInner {
fn with_device_context<R>(
&self,
f: impl FnOnce(&'static DriverApi) -> CudaResult<R>,
) -> CudaResult<R> {
let api = try_driver()?;
let mut prev = CUcontext::default();
check(unsafe { (api.cu_ctx_get_current)(&mut prev) })?;
check(unsafe { (api.cu_ctx_set_current)(self.ctx) })?;
let result = f(api);
let restore_rc = check(unsafe { (api.cu_ctx_set_current)(prev) });
match result {
Ok(value) => restore_rc.map(|()| value),
Err(e) => {
if let Err(restore_err) = restore_rc {
warn!(
"failed to restore previous CUDA context after pool operation \
(original error: {e}): {restore_err}"
);
}
Err(e)
}
}
}
fn allocate_fresh(&self, bytes: usize) -> CudaResult<CUdeviceptr> {
self.with_device_context(|api| {
let mut ptr: CUdeviceptr = 0;
let rc = unsafe { (api.cu_mem_alloc_v2)(&mut ptr, bytes) };
oxicuda_driver::check(rc)?;
Ok(ptr)
})
}
fn free_ptr(&self, ptr: CUdeviceptr) -> CudaResult<()> {
self.with_device_context(|api| {
let rc = unsafe { (api.cu_mem_free_v2)(ptr) };
oxicuda_driver::check(rc)
})
}
fn record_recycle_event(&self, stream: CUstream) -> CudaResult<CUevent> {
self.with_device_context(|api| {
let mut event = CUevent::default();
check(unsafe { (api.cu_event_create)(&mut event, CU_EVENT_DISABLE_TIMING) })?;
if let Err(e) = check(unsafe { (api.cu_event_record)(event, stream) }) {
let _ = unsafe { (api.cu_event_destroy_v2)(event) };
return Err(e);
}
Ok(event)
})
}
fn event_ready(&self, event: CUevent) -> bool {
if event.is_null() {
return true;
}
self.with_device_context(|api| {
let rc = unsafe { (api.cu_event_query)(event) };
if rc == 0 {
Ok(true)
} else if rc == CUDA_ERROR_NOT_READY {
Ok(false)
} else {
Err(CudaError::from_raw(rc))
}
})
.unwrap_or(false)
}
fn synchronize_event(&self, event: CUevent) -> CudaResult<()> {
if event.is_null() {
return Ok(());
}
self.with_device_context(|api| check(unsafe { (api.cu_event_synchronize)(event) }))
}
fn destroy_event(&self, event: CUevent) {
if event.is_null() {
return;
}
let result =
self.with_device_context(|api| check(unsafe { (api.cu_event_destroy_v2)(event) }));
if let Err(e) = result {
warn!("cuEventDestroy_v2 failed for pooled-buffer recycle event: {e}");
}
}
fn try_pop_reuse(&self, bytes: usize) -> CudaResult<Option<CUdeviceptr>> {
let popped = {
let mut bins = self.free_bins.lock().map_err(|_| CudaError::Unknown(0))?;
let Some(vec) = bins.get_mut(&bytes) else {
return Ok(None);
};
let ready_idx = vec.iter().position(|(_, event)| self.event_ready(*event));
ready_idx.map(|idx| vec.swap_remove(idx))
};
let Some((ptr, event)) = popped else {
return Ok(None);
};
self.destroy_event(event);
self.cached_bytes.fetch_sub(bytes, Ordering::Relaxed);
Ok(Some(ptr))
}
fn stash_freed(&self, ptr: CUdeviceptr, bytes: usize, event: CUevent) -> CudaResult<()> {
let mut bins = self.free_bins.lock().map_err(|_| CudaError::Unknown(0))?;
bins.entry(bytes).or_default().push((ptr, event));
self.cached_bytes.fetch_add(bytes, Ordering::Relaxed);
Ok(())
}
fn release_cached_until(&self, keep_bytes: usize) -> CudaResult<()> {
loop {
let cached = self.cached_bytes.load(Ordering::Relaxed);
if cached <= keep_bytes {
return Ok(());
}
let popped = {
let mut bins = self.free_bins.lock().map_err(|_| CudaError::Unknown(0))?;
let mut candidate: Option<(usize, CUdeviceptr, CUevent)> = None;
for (size, vec) in bins.iter_mut() {
if let Some((ptr, event)) = vec.pop() {
candidate = Some((*size, ptr, event));
break;
}
}
candidate
};
let Some((size, ptr, event)) = popped else {
return Ok(());
};
self.synchronize_event(event)?;
self.destroy_event(event);
self.free_ptr(ptr)?;
self.cached_bytes.fetch_sub(size, Ordering::Relaxed);
}
}
fn update_alloc_stats(&self, bytes: usize) {
if let Ok(mut stats) = self.stats.lock() {
stats.allocated_bytes = stats.allocated_bytes.saturating_add(bytes);
stats.allocation_count = stats.allocation_count.saturating_add(1);
if stats.allocated_bytes > stats.peak_bytes {
stats.peak_bytes = stats.allocated_bytes;
}
}
}
fn update_free_stats(&self, bytes: usize) {
if let Ok(mut stats) = self.stats.lock() {
stats.allocated_bytes = stats.allocated_bytes.saturating_sub(bytes);
stats.free_count = stats.free_count.saturating_add(1);
}
}
}
impl Drop for MemoryPoolInner {
fn drop(&mut self) {
let Ok(mut bins) = self.free_bins.lock() else {
return;
};
let mut to_free: Vec<(CUdeviceptr, CUevent)> = Vec::new();
for vec in bins.values_mut() {
to_free.append(vec);
}
drop(bins);
for (ptr, event) in to_free {
if let Err(e) = self.synchronize_event(event) {
warn!("cuEventSynchronize failed while draining pool on drop: {e}");
}
self.destroy_event(event);
if let Err(e) = self.free_ptr(ptr) {
warn!("failed to free pooled pointer {ptr:#x} during drop: {e}");
}
}
if !self.ctx.is_null() {
if let Ok(api) = try_driver() {
let rc = unsafe { (api.cu_device_primary_ctx_release_v2)(self.device_ordinal) };
if rc != 0 {
warn!(
cuda_error = rc,
device_ordinal = self.device_ordinal,
"cuDevicePrimaryCtxRelease_v2 failed while dropping MemoryPool"
);
}
}
}
}
}
pub struct MemoryPool {
inner: Arc<MemoryPoolInner>,
}
impl MemoryPool {
pub fn new(device_ordinal: i32) -> CudaResult<Self> {
if device_ordinal < 0 {
return Err(CudaError::InvalidDevice);
}
let api = try_driver()?;
let mut ctx = CUcontext::default();
check(unsafe { (api.cu_device_primary_ctx_retain)(&mut ctx, device_ordinal) })?;
Ok(Self {
inner: Arc::new(MemoryPoolInner {
handle: 0,
device_ordinal,
ctx,
threshold_bytes: AtomicUsize::new(0),
cached_bytes: AtomicUsize::new(0),
stats: Mutex::new(PoolStats::default()),
free_bins: Mutex::new(HashMap::new()),
}),
})
}
#[inline]
pub fn raw_handle(&self) -> u64 {
self.inner.handle
}
#[inline]
pub fn device_ordinal(&self) -> i32 {
self.inner.device_ordinal
}
#[inline]
pub fn stats(&self) -> PoolStats {
self.inner.stats.lock().map(|s| *s).unwrap_or_default()
}
pub fn trim(&mut self, min_bytes: usize) -> CudaResult<()> {
self.inner.release_cached_until(min_bytes)
}
pub fn set_threshold(&mut self, bytes: usize) -> CudaResult<()> {
self.inner.threshold_bytes.store(bytes, Ordering::Relaxed);
self.inner.release_cached_until(bytes)
}
}
pub struct PooledBuffer<T: Copy> {
ptr: CUdeviceptr,
len: usize,
bytes: usize,
pool: Arc<MemoryPoolInner>,
stream: CUstream,
_phantom: PhantomData<T>,
}
impl<T: Copy> PooledBuffer<T> {
pub fn alloc_async(pool: &MemoryPool, n: usize, stream: &Stream) -> CudaResult<Self> {
if n == 0 {
return Err(CudaError::InvalidValue);
}
let bytes = n
.checked_mul(std::mem::size_of::<T>())
.ok_or(CudaError::InvalidValue)?;
let ptr = if let Some(reused) = pool.inner.try_pop_reuse(bytes)? {
reused
} else {
pool.inner.allocate_fresh(bytes)?
};
pool.inner.update_alloc_stats(bytes);
Ok(Self {
ptr,
len: n,
bytes,
pool: Arc::clone(&pool.inner),
stream: stream.raw(),
_phantom: PhantomData,
})
}
#[inline]
pub fn len(&self) -> usize {
self.len
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn byte_size(&self) -> usize {
self.bytes
}
#[inline]
pub fn as_device_ptr(&self) -> CUdeviceptr {
self.ptr
}
}
impl<T: Copy> Drop for PooledBuffer<T> {
fn drop(&mut self) {
if self.ptr == 0 {
return;
}
let event = match self.pool.record_recycle_event(self.stream) {
Ok(event) => event,
Err(e) => {
warn!(
"failed to record pooled-buffer recycle event ({e}); falling back to a \
blocking stream synchronize before freeing directly"
);
if let Ok(api) = try_driver() {
let rc = unsafe { (api.cu_stream_synchronize)(self.stream) };
if rc != 0 {
warn!(cuda_error = rc, "fallback cuStreamSynchronize failed");
}
}
if let Err(free_err) = self.pool.free_ptr(self.ptr) {
warn!("direct free of pooled pointer failed: {free_err}");
}
self.pool.update_free_stats(self.bytes);
self.ptr = 0;
return;
}
};
if let Err(e) = self.pool.stash_freed(self.ptr, self.bytes, event) {
warn!("failed to return pooled pointer to free list: {e}; freeing directly");
self.pool.destroy_event(event);
if let Err(free_err) = self.pool.free_ptr(self.ptr) {
warn!("direct free of pooled pointer failed: {free_err}");
}
self.pool.update_free_stats(self.bytes);
self.ptr = 0;
return;
}
self.pool.update_free_stats(self.bytes);
let threshold = self.pool.threshold_bytes.load(Ordering::Relaxed);
if let Err(e) = self.pool.release_cached_until(threshold) {
warn!("pool threshold trim failed: {e}");
}
self.ptr = 0;
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct NativeMemoryPoolProps {
pub device_ordinal: i32,
pub max_size_bytes: usize,
}
pub struct NativeMemoryPool {
raw: CUmemoryPool,
device_ordinal: i32,
}
unsafe impl Send for NativeMemoryPool {}
unsafe impl Sync for NativeMemoryPool {}
impl NativeMemoryPool {
pub fn new(props: NativeMemoryPoolProps) -> CudaResult<Self> {
if props.device_ordinal < 0 {
return Err(CudaError::InvalidDevice);
}
let api = try_driver()?;
let f = api.cu_mem_pool_create.ok_or(CudaError::NotSupported)?;
let pool_props = CUmemPoolProps {
alloc_type: CUmemAllocationType::Pinned as u32,
handle_types: CUmemAllocationHandleType::None as u32,
location: CUmemLocation {
loc_type: CUmemLocationType::Device as u32,
id: props.device_ordinal,
},
max_size: props.max_size_bytes,
..CUmemPoolProps::default()
};
let mut raw = CUmemoryPool::default();
check(unsafe { f(&mut raw, &pool_props) })?;
Ok(Self {
raw,
device_ordinal: props.device_ordinal,
})
}
#[inline]
pub fn raw(&self) -> CUmemoryPool {
self.raw
}
#[inline]
pub fn device_ordinal(&self) -> i32 {
self.device_ordinal
}
pub fn alloc_async(&self, bytes: usize, stream: &Stream) -> CudaResult<CUdeviceptr> {
if bytes == 0 {
return Err(CudaError::InvalidValue);
}
let api = try_driver()?;
let f = api
.cu_mem_alloc_from_pool_async
.ok_or(CudaError::NotSupported)?;
let mut ptr: CUdeviceptr = 0;
check(unsafe { f(&mut ptr, bytes, self.raw, stream.raw()) })?;
Ok(ptr)
}
pub fn free_async(&self, ptr: CUdeviceptr, stream: &Stream) -> CudaResult<()> {
let api = try_driver()?;
let f = api.cu_mem_free_async.ok_or(CudaError::NotSupported)?;
check(unsafe { f(ptr, stream.raw()) })
}
pub fn destroy(mut self) -> CudaResult<()> {
self.destroy_inner()
}
fn destroy_inner(&mut self) -> CudaResult<()> {
if self.raw.is_null() {
return Ok(());
}
let api = try_driver()?;
let f = api.cu_mem_pool_destroy.ok_or(CudaError::NotSupported)?;
let result = check(unsafe { f(self.raw) });
self.raw = CUmemoryPool::default();
result
}
}
impl Drop for NativeMemoryPool {
fn drop(&mut self) {
if let Err(e) = self.destroy_inner() {
warn!("failed to destroy native memory pool during drop: {e}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_driver_unavailable(err: &CudaError) -> bool {
matches!(err, CudaError::NotInitialized | CudaError::NotSupported)
}
#[test]
fn native_memory_pool_props_default() {
let props = NativeMemoryPoolProps::default();
assert_eq!(props.device_ordinal, 0);
assert_eq!(props.max_size_bytes, 0);
}
#[test]
fn native_memory_pool_new_negative_device_fails() {
let props = NativeMemoryPoolProps {
device_ordinal: -1,
max_size_bytes: 0,
};
let result = NativeMemoryPool::new(props);
assert_eq!(result.err(), Some(CudaError::InvalidDevice));
}
#[test]
fn native_memory_pool_new_no_driver_returns_driver_unavailable() {
let result = NativeMemoryPool::new(NativeMemoryPoolProps::default());
match result {
Ok(pool) => {
let destroy = pool.destroy();
assert!(destroy.is_ok(), "destroy failed: {destroy:?}");
}
Err(e) => assert!(
is_driver_unavailable(&e),
"expected driver-unavailable error, got {e:?}"
),
}
}
#[cfg(target_os = "macos")]
#[test]
fn macos_native_pool_returns_not_initialized() {
let result = NativeMemoryPool::new(NativeMemoryPoolProps::default());
let err = match result {
Err(e) => e,
Ok(_) => panic!("expected NotInitialized on macOS, got Ok"),
};
assert!(
matches!(err, CudaError::NotInitialized),
"expected NotInitialized, got {err:?}"
);
}
#[test]
fn memory_pool_new_negative_device_fails() {
let result = MemoryPool::new(-1);
assert_eq!(result.err(), Some(CudaError::InvalidDevice));
}
#[cfg(feature = "gpu-tests")]
mod gpu_tests {
use super::*;
use std::ffi::c_void;
use std::sync::Arc;
fn real_context() -> Option<Arc<oxicuda_driver::context::Context>> {
if oxicuda_driver::init().is_err()
|| oxicuda_driver::device::Device::count().unwrap_or(0) == 0
{
return None;
}
let dev = oxicuda_driver::device::Device::get(0).ok()?;
oxicuda_driver::context::Context::new(&dev)
.ok()
.map(Arc::new)
}
#[test]
fn memory_pool_binds_device_and_round_trips_data() {
let Some(ctx) = real_context() else {
return;
};
let Ok(pool) = MemoryPool::new(0) else {
return;
};
assert_eq!(pool.device_ordinal(), 0);
let Ok(stream) = Stream::new(&ctx) else {
return;
};
let host_in: Vec<f32> = (0..256).map(|i| i as f32).collect();
let api = try_driver().expect("driver must be present under gpu-tests");
let buf =
PooledBuffer::<f32>::alloc_async(&pool, 256, &stream).expect("alloc_async failed");
assert_eq!(buf.len(), 256);
assert_eq!(buf.byte_size(), 256 * std::mem::size_of::<f32>());
let rc = unsafe {
(api.cu_memcpy_htod_v2)(
buf.as_device_ptr(),
host_in.as_ptr().cast::<c_void>(),
buf.byte_size(),
)
};
check(rc).expect("HtoD copy failed");
let mut host_out = vec![0.0f32; 256];
let rc = unsafe {
(api.cu_memcpy_dtoh_v2)(
host_out.as_mut_ptr().cast::<c_void>(),
buf.as_device_ptr(),
buf.byte_size(),
)
};
check(rc).expect("DtoH copy failed");
assert_eq!(host_out, host_in);
drop(buf);
let stats = pool.stats();
assert_eq!(stats.allocation_count, 1);
assert_eq!(stats.free_count, 1);
}
#[test]
fn memory_pool_reuse_after_drop_preserves_data_integrity() {
let Some(ctx) = real_context() else {
return;
};
let Ok(pool) = MemoryPool::new(0) else {
return;
};
let Ok(stream) = Stream::new(&ctx) else {
return;
};
let api = try_driver().expect("driver must be present under gpu-tests");
let first_pattern: Vec<u32> = vec![0xAAAA_AAAA; 128];
{
let buf = PooledBuffer::<u32>::alloc_async(&pool, 128, &stream)
.expect("first alloc_async failed");
let rc = unsafe {
(api.cu_memcpy_htod_v2)(
buf.as_device_ptr(),
first_pattern.as_ptr().cast::<c_void>(),
buf.byte_size(),
)
};
check(rc).expect("first HtoD copy failed");
}
let second_pattern: Vec<u32> = (0..128u32).collect();
let buf2 = PooledBuffer::<u32>::alloc_async(&pool, 128, &stream)
.expect("second alloc_async failed");
let rc = unsafe {
(api.cu_memcpy_htod_v2)(
buf2.as_device_ptr(),
second_pattern.as_ptr().cast::<c_void>(),
buf2.byte_size(),
)
};
check(rc).expect("second HtoD copy failed");
let mut readback = vec![0u32; 128];
let rc = unsafe {
(api.cu_memcpy_dtoh_v2)(
readback.as_mut_ptr().cast::<c_void>(),
buf2.as_device_ptr(),
buf2.byte_size(),
)
};
check(rc).expect("DtoH copy failed");
assert_eq!(readback, second_pattern);
let stats = pool.stats();
assert_eq!(stats.allocation_count, 2);
assert_eq!(stats.free_count, 1);
}
}
}