use std::{collections::VecDeque, sync::Mutex};
use cudarc::driver::{CudaSlice, CudaStream};
use crate::CudaError;
const MAX_RETAINED_BYTES: usize = 256 * 1024 * 1024;
const MAX_BUFFER_BYTES: usize = 128 * 1024 * 1024;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CudaBufferPoolDiagnostics {
pub retained_buffers: usize,
pub retained_bytes: usize,
pub high_water_bytes: usize,
pub hits: u64,
pub misses: u64,
pub evictions: u64,
}
#[derive(Debug)]
struct PoolState {
buffers: VecDeque<CudaSlice<i32>>,
diagnostics: CudaBufferPoolDiagnostics,
}
#[derive(Debug)]
pub(crate) struct CudaBufferPool {
state: Mutex<PoolState>,
}
impl CudaBufferPool {
pub(crate) fn new() -> Self {
Self {
state: Mutex::new(PoolState {
buffers: VecDeque::new(),
diagnostics: CudaBufferPoolDiagnostics::default(),
}),
}
}
pub(crate) fn take(
&self,
stream: &std::sync::Arc<CudaStream>,
elements: usize,
) -> Result<CudaSlice<i32>, CudaError> {
let mut state = self.state.lock().map_err(|_| CudaError::StatePoisoned {
state: "CUDA scratch pool",
})?;
if let Some(index) = state
.buffers
.iter()
.position(|buffer| buffer.len() == elements && buffer.context() == stream.context())
{
let buffer = state
.buffers
.remove(index)
.ok_or(CudaError::StateInvariant {
state: "CUDA scratch pool",
reason: "located buffer disappeared",
})?;
state.diagnostics.retained_buffers =
state.diagnostics.retained_buffers.saturating_sub(1);
state.diagnostics.retained_bytes = state
.diagnostics
.retained_bytes
.checked_sub(buffer.num_bytes())
.ok_or(CudaError::StateInvariant {
state: "CUDA scratch pool",
reason: "retained byte count underflowed",
})?;
state.diagnostics.hits = state.diagnostics.hits.saturating_add(1);
return Ok(buffer);
}
state.diagnostics.misses = state.diagnostics.misses.saturating_add(1);
drop(state);
Ok(stream.alloc_zeros(elements)?)
}
pub(crate) fn recycle(&self, buffer: CudaSlice<i32>) -> Result<(), CudaError> {
let bytes = buffer.num_bytes();
let mut state = self.state.lock().map_err(|_| CudaError::StatePoisoned {
state: "CUDA scratch pool",
})?;
if bytes > MAX_BUFFER_BYTES || bytes > MAX_RETAINED_BYTES {
state.diagnostics.evictions = state.diagnostics.evictions.saturating_add(1);
return Ok(());
}
while state
.diagnostics
.retained_bytes
.checked_add(bytes)
.is_none_or(|sum| sum > MAX_RETAINED_BYTES)
{
let Some(evicted) = state.buffers.pop_front() else {
return Err(CudaError::StateInvariant {
state: "CUDA scratch pool",
reason: "retention bound exceeded without an eviction candidate",
});
};
state.diagnostics.retained_buffers =
state.diagnostics.retained_buffers.saturating_sub(1);
state.diagnostics.retained_bytes = state
.diagnostics
.retained_bytes
.checked_sub(evicted.num_bytes())
.ok_or(CudaError::StateInvariant {
state: "CUDA scratch pool",
reason: "retained byte count underflowed during eviction",
})?;
state.diagnostics.evictions = state.diagnostics.evictions.saturating_add(1);
}
state.diagnostics.retained_bytes += bytes;
state.diagnostics.retained_buffers += 1;
state.diagnostics.high_water_bytes = state
.diagnostics
.high_water_bytes
.max(state.diagnostics.retained_bytes);
state.buffers.push_back(buffer);
Ok(())
}
pub(crate) fn diagnostics(&self) -> Result<CudaBufferPoolDiagnostics, CudaError> {
self.state
.lock()
.map(|state| state.diagnostics)
.map_err(|_| CudaError::StatePoisoned {
state: "CUDA scratch pool",
})
}
}