use std::collections::HashMap;
use std::sync::{Mutex, PoisonError};
use cuda_core::sys;
use super::device::Device;
use super::error::{CudaError, Result, check};
const PAGE: usize = 16 << 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct MemoryStats {
pub active: usize,
pub peak: usize,
pub cache: usize,
pub memory_limit: usize,
pub cache_limit: usize,
pub system_allocations: u64,
}
#[derive(Default)]
struct State {
free: HashMap<usize, Vec<sys::CUdeviceptr>>,
stats: MemoryStats,
}
pub(crate) struct Pool {
state: Mutex<State>,
ctx: sys::CUcontext,
}
unsafe impl Send for Pool {}
unsafe impl Sync for Pool {}
fn rounded(n: usize) -> usize {
match n {
0..=8 => 8,
n if n < PAGE => n.next_power_of_two(),
n => n.div_ceil(PAGE) * PAGE,
}
}
impl Pool {
pub(crate) fn new(total: usize, ctx: sys::CUcontext) -> Self {
let limit = total / 20 * 19;
Self {
ctx,
state: Mutex::new(State {
stats: MemoryStats {
memory_limit: limit,
cache_limit: limit,
..Default::default()
},
..Default::default()
}),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
unsafe { sys::cuCtxSetCurrent(self.ctx) };
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
fn alloc(&self, len: usize) -> Result<(sys::CUdeviceptr, usize)> {
let size = rounded(len);
let mut s = self.lock();
if s.stats.active + size > s.stats.memory_limit {
return Err(CudaError::OutOfMemory(len));
}
let ptr = match s.free.get_mut(&size).and_then(Vec::pop) {
Some(p) => {
s.stats.cache -= size;
p
}
None => {
let mut p: sys::CUdeviceptr = 0;
let r = unsafe {
sys::cuMemAllocManaged(
&mut p,
size,
sys::CUmemAttach_flags_enum_CU_MEM_ATTACH_GLOBAL,
)
};
if r != sys::cudaError_enum_CUDA_SUCCESS {
Self::release(&mut s, usize::MAX);
let r = unsafe {
sys::cuMemAllocManaged(
&mut p,
size,
sys::CUmemAttach_flags_enum_CU_MEM_ATTACH_GLOBAL,
)
};
if r == sys::cudaError_enum_CUDA_ERROR_OUT_OF_MEMORY {
return Err(CudaError::OutOfMemory(len));
}
check("cuMemAllocManaged", r)?;
}
s.stats.system_allocations += 1;
p
}
};
s.stats.active += size;
s.stats.peak = s.stats.peak.max(s.stats.active);
Ok((ptr, size))
}
fn free(&self, ptr: sys::CUdeviceptr, size: usize) {
let mut s = self.lock();
s.stats.active -= size;
s.stats.cache += size;
s.free.entry(size).or_default().push(ptr);
let excess = s.stats.cache.saturating_sub(s.stats.cache_limit);
if excess > 0 {
Self::release(&mut s, excess);
}
}
fn release(s: &mut State, bytes: usize) {
let mut freed = 0;
let mut sizes: Vec<usize> = s.free.keys().copied().collect();
sizes.sort_unstable_by(|a, b| b.cmp(a));
for size in sizes {
while freed < bytes {
let Some(p) = s.free.get_mut(&size).and_then(Vec::pop) else {
break;
};
unsafe { sys::cuMemFree_v2(p) };
s.stats.cache -= size;
freed += size;
}
}
}
pub(crate) fn stats(&self) -> MemoryStats {
self.lock().stats
}
pub(crate) fn set_memory_limit(&self, bytes: usize) -> usize {
std::mem::replace(&mut self.lock().stats.memory_limit, bytes)
}
pub(crate) fn set_cache_limit(&self, bytes: usize) -> usize {
let mut s = self.lock();
let old = std::mem::replace(&mut s.stats.cache_limit, bytes);
if s.stats.cache > bytes {
let excess = s.stats.cache - bytes;
Self::release(&mut s, excess);
}
old
}
pub(crate) fn clear_cache(&self) {
Self::release(&mut self.lock(), usize::MAX);
}
pub(crate) fn reset_peak(&self) {
let mut s = self.lock();
s.stats.peak = s.stats.active;
}
}
pub struct Buffer {
ptr: sys::CUdeviceptr,
len: usize,
size: usize,
device: &'static Device,
}
unsafe impl Send for Buffer {}
unsafe impl Sync for Buffer {}
impl std::fmt::Debug for Buffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Buffer({:#x}, {} bytes)", self.ptr, self.len)
}
}
impl Buffer {
pub fn new(device: &'static Device, len: usize) -> Result<Self> {
let (ptr, size) = device.pool.alloc(len)?;
Ok(Self {
ptr,
len,
size,
device,
})
}
pub fn device_ptr(&self) -> sys::CUdeviceptr {
self.ptr
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr as *const u8, self.len) }
}
pub fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut u8, self.len) }
}
pub fn prefetch(&self) -> Result<()> {
let mut location: sys::CUmemLocation = unsafe { std::mem::zeroed() };
location.type_ = sys::CUmemLocationType_enum_CU_MEM_LOCATION_TYPE_DEVICE;
location.__bindgen_anon_1.id = self.device.device.cu_device();
self.device
.device
.bind_to_thread()
.map_err(super::error::driver("cuCtxSetCurrent"))?;
check("cuMemPrefetchAsync", unsafe {
sys::cuMemPrefetchAsync_v2(
self.ptr,
self.len,
location,
0,
self.device.stream.cu_stream(),
)
})
}
}
impl Drop for Buffer {
fn drop(&mut self) {
self.device.pool.free(self.ptr, self.size);
}
}
impl Device {
pub fn memory(&self) -> MemoryStats {
self.pool.stats()
}
pub fn set_memory_limit(&self, bytes: usize) -> usize {
self.pool.set_memory_limit(bytes)
}
pub fn set_cache_limit(&self, bytes: usize) -> usize {
self.pool.set_cache_limit(bytes)
}
pub fn clear_cache(&self) {
self.pool.clear_cache();
}
pub fn reset_peak(&self) {
self.pool.reset_peak();
}
}