use std::ffi::c_void;
use cudarc::driver::sys::{
cuDeviceGetDefaultMemPool, cuMemAllocFromPoolAsync, cuMemFreeAsync, cuMemPoolGetAttribute,
cuMemPoolSetAttribute, cuMemPoolTrimTo, CUdevice, CUdeviceptr, CUmemPool_attribute_enum,
CUmemoryPool, CUstream,
};
use cudarc::driver::CudaContext;
use vyre_driver::BackendError;
use super::allocations::cuda_check;
const RETAIN_ALL_FREED_BYTES: u64 = u64::MAX;
#[derive(Debug, Clone)]
pub struct CudaStreamOrderedPool {
pool: CUmemoryPool,
}
unsafe impl Send for CudaStreamOrderedPool {}
unsafe impl Sync for CudaStreamOrderedPool {}
impl CudaStreamOrderedPool {
pub fn for_context(ctx: &CudaContext) -> Result<Self, BackendError> {
let device: CUdevice = ctx.cu_device();
let mut pool: CUmemoryPool = std::ptr::null_mut();
unsafe {
cuda_check(
cuDeviceGetDefaultMemPool(&mut pool, device),
"cuDeviceGetDefaultMemPool",
)?;
}
if pool.is_null() {
return Err(BackendError::DispatchFailed {
code: None,
message: "cuDeviceGetDefaultMemPool reported success but returned a null pool handle. Fix: update the CUDA driver; stream-ordered memory pools require a driver that exposes a per-device default pool.".to_string(),
});
}
let this = Self { pool };
this.set_release_threshold(RETAIN_ALL_FREED_BYTES)?;
Ok(this)
}
pub fn set_release_threshold(&self, bytes: u64) -> Result<(), BackendError> {
let value = bytes;
unsafe {
cuda_check(
cuMemPoolSetAttribute(
self.pool,
CUmemPool_attribute_enum::CU_MEMPOOL_ATTR_RELEASE_THRESHOLD,
&value as *const u64 as *mut c_void,
),
"cuMemPoolSetAttribute(RELEASE_THRESHOLD)",
)?;
}
Ok(())
}
pub fn alloc_async(&self, byte_len: usize, stream: CUstream) -> Result<u64, BackendError> {
if byte_len == 0 {
return Err(BackendError::InvalidProgram {
fix: "Fix: CudaStreamOrderedPool::alloc_async cannot allocate zero device bytes. Keep zero-sized buffers as null sentinels or request at least one byte.".to_string(),
});
}
let mut ptr: CUdeviceptr = 0;
unsafe {
cuda_check(
cuMemAllocFromPoolAsync(&mut ptr, byte_len, self.pool, stream),
"cuMemAllocFromPoolAsync",
)?;
}
if ptr == 0 {
return Err(BackendError::DispatchFailed {
code: None,
message: format!(
"cuMemAllocFromPoolAsync returned a null device pointer after reporting success for {byte_len} byte(s). Fix: update the CUDA driver or avoid this allocation shape."
),
});
}
Ok(ptr)
}
pub fn free_async(&self, ptr: u64, stream: CUstream) -> Result<(), BackendError> {
if ptr == 0 {
return Ok(());
}
unsafe {
cuda_check(cuMemFreeAsync(ptr, stream), "cuMemFreeAsync")?;
}
Ok(())
}
pub fn reserved_bytes(&self) -> Result<u64, BackendError> {
self.attr_u64(
CUmemPool_attribute_enum::CU_MEMPOOL_ATTR_RESERVED_MEM_CURRENT,
"CU_MEMPOOL_ATTR_RESERVED_MEM_CURRENT",
)
}
pub fn used_bytes(&self) -> Result<u64, BackendError> {
self.attr_u64(
CUmemPool_attribute_enum::CU_MEMPOOL_ATTR_USED_MEM_CURRENT,
"CU_MEMPOOL_ATTR_USED_MEM_CURRENT",
)
}
fn attr_u64(
&self,
attr: CUmemPool_attribute_enum,
label: &'static str,
) -> Result<u64, BackendError> {
let mut value: u64 = 0;
unsafe {
cuda_check(
cuMemPoolGetAttribute(self.pool, attr, &mut value as *mut u64 as *mut c_void),
label,
)?;
}
Ok(value)
}
pub fn trim(&self, min_keep_bytes: usize) -> Result<(), BackendError> {
unsafe {
cuda_check(
cuMemPoolTrimTo(self.pool, min_keep_bytes),
"cuMemPoolTrimTo",
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::dispatch::CudaBackend;
use crate::stream::CudaStream;
use cudarc::driver::sys::{cuMemcpyDtoHAsync_v2, cuMemsetD32Async};
use std::ffi::c_void;
#[test]
fn stream_ordered_pool_serves_usable_memory_and_reuses_reserved_blocks_on_gpu() {
let backend = match CudaBackend::acquire() {
Ok(backend) => backend,
Err(error) => panic!(
"CUDA device required for the stream-ordered pool evidence test but acquire failed: {error}. Fix: run on a host with a visible CUDA GPU (nvidia-smi -L); do not skip GPU tests on a GPU host."
),
};
let pool = CudaStreamOrderedPool::for_context(&backend.ctx)
.expect("bind default stream-ordered memory pool");
let stream = CudaStream::non_blocking().expect("create non-blocking stream");
const WORDS: usize = 1024;
const BYTES: usize = WORDS * 4;
const FILL: u32 = 0xABCD_1234;
let ptr = pool
.alloc_async(BYTES, stream.raw())
.expect("stream-ordered allocation");
unsafe {
cuda_check(
cuMemsetD32Async(ptr, FILL, WORDS, stream.raw()),
"cuMemsetD32Async",
)
.expect("memset stream-ordered block");
}
let mut host = vec![0u32; WORDS];
unsafe {
cuda_check(
cuMemcpyDtoHAsync_v2(host.as_mut_ptr() as *mut c_void, ptr, BYTES, stream.raw()),
"cuMemcpyDtoHAsync_v2",
)
.expect("copy stream-ordered block to host");
}
stream.synchronize().expect("sync after fill+readback");
assert!(
host.iter().all(|&word| word == FILL),
"stream-ordered pool must return usable device memory: every one of {WORDS} words should read back as {FILL:#010x}, got e.g. {:#010x}",
host[0]
);
let used_live = pool.used_bytes().expect("query used bytes while live");
assert!(
used_live >= BYTES as u64,
"pool used-bytes ({used_live}) must reflect the live {BYTES}-byte allocation"
);
pool.free_async(ptr, stream.raw())
.expect("stream-ordered free");
stream.synchronize().expect("sync after free");
let reserved_after_free = pool
.reserved_bytes()
.expect("query reserved bytes after free");
assert!(
reserved_after_free >= BYTES as u64,
"release threshold must keep the freed {BYTES}-byte block reserved for reuse; reserved={reserved_after_free}"
);
let ptr2 = pool
.alloc_async(BYTES, stream.raw())
.expect("re-allocate after free");
stream.synchronize().expect("sync after realloc");
let reserved_after_realloc = pool
.reserved_bytes()
.expect("query reserved bytes after realloc");
assert_eq!(
reserved_after_realloc, reserved_after_free,
"re-allocating a just-freed same-size block must reuse the reserved memory, not grow the pool's OS reservation (before={reserved_after_free}, after={reserved_after_realloc})"
);
pool.free_async(ptr2, stream.raw())
.expect("free second allocation");
stream.synchronize().expect("final sync");
pool.trim(0).expect("trim pool reservation to zero-keep");
let reserved_after_trim = pool
.reserved_bytes()
.expect("query reserved bytes after trim");
assert!(
reserved_after_trim < reserved_after_realloc,
"trim(0) must release retained reservation back to the OS (before={reserved_after_realloc}, after={reserved_after_trim})"
);
}
#[test]
fn stream_ordered_pool_rejects_zero_bytes_and_accounts_multiple_live_blocks_on_gpu() {
let backend = match CudaBackend::acquire() {
Ok(backend) => backend,
Err(error) => panic!(
"CUDA device required for the stream-ordered pool telemetry test but acquire failed: {error}. Fix: run on a host with a visible CUDA GPU (nvidia-smi -L); do not skip GPU tests on a GPU host."
),
};
let pool = CudaStreamOrderedPool::for_context(&backend.ctx)
.expect("bind default stream-ordered memory pool");
let stream = CudaStream::non_blocking().expect("create non-blocking stream");
assert!(
pool.alloc_async(0, stream.raw()).is_err(),
"a zero-byte stream-ordered request must error as a null sentinel, not allocate"
);
const A: usize = 4096;
const B: usize = 8192;
const C: usize = 16384;
let pa = pool.alloc_async(A, stream.raw()).expect("alloc A");
let pb = pool.alloc_async(B, stream.raw()).expect("alloc B");
let pc = pool.alloc_async(C, stream.raw()).expect("alloc C");
stream.synchronize().expect("sync after three allocs");
let used_all = pool
.used_bytes()
.expect("used bytes with three live blocks");
assert!(
used_all >= (A + B + C) as u64,
"used-bytes ({used_all}) must account all three live blocks (>= {} B)",
A + B + C
);
pool.free_async(pb, stream.raw()).expect("free B");
stream.synchronize().expect("sync after freeing B");
let used_after = pool.used_bytes().expect("used bytes after freeing B");
assert!(
used_after < used_all,
"freeing a live block must reduce used-bytes, not stay flat (before={used_all}, after={used_after})"
);
assert!(
used_after >= (A + C) as u64,
"used-bytes ({used_after}) must still account the two blocks left live (>= {} B)",
A + C
);
pool.free_async(pa, stream.raw()).expect("free A");
pool.free_async(pc, stream.raw()).expect("free C");
stream.synchronize().expect("final cleanup sync");
}
}