use super::{
size_buckets::CudaBufferPoolSizeBuckets, CudaBufferPool, CudaBufferPoolFree,
CudaBufferPoolInner, CudaBufferPoolState,
};
use crate::{
allocation::host_allocation_error, context::CudaContext, error::select_resource_release_error,
memory::CudaDeviceBuffer, CudaError,
};
use std::sync::{Arc, Mutex};
const DEFAULT_MAX_CACHED_BYTES: usize = 512 * 1024 * 1024;
const DEFAULT_MAX_CACHED_BUFFERS: usize = 128;
const DEFAULT_MAX_SIZE_BUCKETS: usize = 64;
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CudaBufferPoolLimits {
pub max_cached_bytes: usize,
pub max_cached_buffers: usize,
pub max_size_buckets: usize,
}
impl Default for CudaBufferPoolLimits {
fn default() -> Self {
Self {
max_cached_bytes: DEFAULT_MAX_CACHED_BYTES,
max_cached_buffers: DEFAULT_MAX_CACHED_BUFFERS,
max_size_buckets: DEFAULT_MAX_SIZE_BUCKETS,
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CudaBufferPoolDiagnostics {
pub limits: CudaBufferPoolLimits,
pub cached_buffers: usize,
pub cached_bytes: usize,
pub cached_size_buckets: usize,
pub deferred_buffers: usize,
pub deferred_bytes: usize,
pub reuse_holds: usize,
pub peak_cached_buffers: usize,
pub peak_cached_bytes: usize,
pub peak_cached_size_buckets: usize,
pub peak_deferred_buffers: usize,
pub peak_deferred_bytes: usize,
pub evicted_buffers: usize,
pub rejected_buffers: usize,
pub metadata_failures: usize,
}
#[derive(Clone, Copy, Debug, Default)]
pub(super) struct CudaBufferPoolMetrics {
pub(super) peak_cached_buffers: usize,
pub(super) peak_cached_bytes: usize,
pub(super) peak_cached_size_buckets: usize,
pub(super) peak_deferred_buffers: usize,
pub(super) peak_deferred_bytes: usize,
pub(super) evicted_buffers: usize,
pub(super) rejected_buffers: usize,
pub(super) metadata_failures: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct CacheInventory {
buffers: usize,
bytes: usize,
size_buckets: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CacheAdmissionDecision {
Admit,
Evict,
Reject,
}
impl CudaBufferPool {
pub fn new(context: CudaContext) -> Self {
Self::with_limits(context, CudaBufferPoolLimits::default())
}
#[doc(hidden)]
pub fn with_limits(context: CudaContext, limits: CudaBufferPoolLimits) -> Self {
Self::with_free(context, limits, CudaBufferPoolFree::FirstFit(Vec::new()))
}
pub(in super::super) fn new_size_buckets(context: CudaContext) -> Self {
Self::best_fit_with_limits(context, CudaBufferPoolLimits::default())
}
#[doc(hidden)]
pub fn best_fit_with_limits(context: CudaContext, limits: CudaBufferPoolLimits) -> Self {
Self::with_free(
context,
limits,
CudaBufferPoolFree::SizeBuckets(CudaBufferPoolSizeBuckets::new()),
)
}
fn with_free(
context: CudaContext,
limits: CudaBufferPoolLimits,
free: CudaBufferPoolFree,
) -> Self {
Self {
inner: Arc::new(CudaBufferPoolInner {
context,
limits,
state: Mutex::new(CudaBufferPoolState {
free,
deferred: Vec::new(),
deferred_bytes: 0,
reuse_holds: 0,
metrics: CudaBufferPoolMetrics::default(),
}),
}),
}
}
#[doc(hidden)]
pub fn diagnostics(&self) -> Result<CudaBufferPoolDiagnostics, CudaError> {
self.inner
.context
.inner
.ensure_resource_lifetime_available()?;
let state = self
.inner
.state
.lock()
.map_err(|error| CudaError::StatePoisoned {
message: error.to_string(),
})?;
let inventory = state.free.inventory();
Ok(CudaBufferPoolDiagnostics {
limits: self.inner.limits,
cached_buffers: inventory.buffers,
cached_bytes: inventory.bytes,
cached_size_buckets: inventory.size_buckets,
deferred_buffers: state.deferred.len(),
deferred_bytes: state.deferred_bytes,
reuse_holds: state.reuse_holds,
peak_cached_buffers: state.metrics.peak_cached_buffers,
peak_cached_bytes: state.metrics.peak_cached_bytes,
peak_cached_size_buckets: state.metrics.peak_cached_size_buckets,
peak_deferred_buffers: state.metrics.peak_deferred_buffers,
peak_deferred_bytes: state.metrics.peak_deferred_bytes,
evicted_buffers: state.metrics.evicted_buffers,
rejected_buffers: state.metrics.rejected_buffers,
metadata_failures: state.metrics.metadata_failures,
})
}
}
impl CudaBufferPoolInner {
pub(super) fn recycle_completed_buffer(
&self,
buffer: CudaDeviceBuffer,
) -> Result<(), CudaError> {
let mut candidate = Some(buffer);
loop {
let Some(candidate_buffer) = candidate.as_ref() else {
return Err(CudaError::InternalInvariant {
what: "completed CUDA cache candidate ownership was lost",
});
};
let candidate_bytes = candidate_buffer.byte_len();
let mut state = match self.state.lock() {
Ok(state) => state,
Err(error) => {
std::mem::forget(candidate.take());
return Err(CudaError::StatePoisoned {
message: error.to_string(),
});
}
};
let inventory = state.free.inventory();
let adds_size_bucket = state.free.candidate_adds_size_bucket(candidate_bytes);
match cache_admission_decision(
self.limits,
inventory,
candidate_bytes,
adds_size_bucket,
) {
CacheAdmissionDecision::Reject => {
state.metrics.rejected_buffers =
state.metrics.rejected_buffers.saturating_add(1);
let rejected = candidate.take();
drop(state);
drop(rejected);
return self.context.inner.ensure_resource_lifetime_available();
}
CacheAdmissionDecision::Evict => {
let Some(evicted) = state.free.evict_deterministic() else {
let unretained = candidate.take();
drop(state);
drop(unretained);
return Err(CudaError::InternalInvariant {
what: "CUDA buffer cache selected eviction without a victim",
});
};
state.metrics.evicted_buffers = state.metrics.evicted_buffers.saturating_add(1);
drop(state);
drop(evicted);
if let Err(error) = self.context.inner.ensure_resource_lifetime_available() {
std::mem::forget(candidate.take());
return Err(error);
}
}
CacheAdmissionDecision::Admit => {
let Some(admitted) = candidate.take() else {
drop(state);
return Err(CudaError::InternalInvariant {
what: "CUDA buffer cache admitted a missing candidate",
});
};
if let Err((error, unretained)) = state.free.try_recycle(admitted) {
state.metrics.metadata_failures =
state.metrics.metadata_failures.saturating_add(1);
drop(state);
drop(unretained);
if let Err(release_error) =
self.context.inner.ensure_resource_lifetime_available()
{
return Err(select_resource_release_error(error, release_error));
}
return Err(error);
}
observe_cache_high_water(&mut state);
return Ok(());
}
}
}
}
}
impl CudaBufferPoolFree {
fn inventory(&self) -> CacheInventory {
match self {
Self::FirstFit(buffers) => CacheInventory {
buffers: buffers.len(),
bytes: buffers.iter().fold(0usize, |total, buffer| {
total.saturating_add(buffer.byte_len())
}),
size_buckets: 0,
},
Self::SizeBuckets(buckets) => CacheInventory {
buffers: buckets.cached_count(),
bytes: buckets.cached_bytes(),
size_buckets: buckets.bucket_count(),
},
}
}
fn candidate_adds_size_bucket(&self, candidate_bytes: usize) -> bool {
match self {
Self::FirstFit(_) => false,
Self::SizeBuckets(buckets) => !buckets.contains_size(candidate_bytes),
}
}
fn evict_deterministic(&mut self) -> Option<CudaDeviceBuffer> {
match self {
Self::FirstFit(buffers) => (!buffers.is_empty()).then(|| buffers.remove(0)),
Self::SizeBuckets(buckets) => buckets.evict_largest_oldest(),
}
}
fn try_recycle(
&mut self,
buffer: CudaDeviceBuffer,
) -> Result<(), (CudaError, CudaDeviceBuffer)> {
match self {
Self::FirstFit(buffers) => {
if buffers.try_reserve(1).is_err() {
let error =
host_allocation_error::<CudaDeviceBuffer>(buffers.len().saturating_add(1));
return Err((error, buffer));
}
buffers.push(buffer);
Ok(())
}
Self::SizeBuckets(buckets) => buckets.try_recycle(buffer),
}
}
}
fn cache_admission_decision(
limits: CudaBufferPoolLimits,
inventory: CacheInventory,
candidate_bytes: usize,
adds_size_bucket: bool,
) -> CacheAdmissionDecision {
if limits.max_cached_buffers == 0
|| candidate_bytes > limits.max_cached_bytes
|| (adds_size_bucket && limits.max_size_buckets == 0)
{
return CacheAdmissionDecision::Reject;
}
let next_buffers = inventory.buffers.checked_add(1);
let next_bytes = inventory.bytes.checked_add(candidate_bytes);
let next_buckets = inventory
.size_buckets
.checked_add(usize::from(adds_size_bucket));
let fits = next_buffers.is_some_and(|count| count <= limits.max_cached_buffers)
&& next_bytes.is_some_and(|bytes| bytes <= limits.max_cached_bytes)
&& next_buckets.is_some_and(|count| count <= limits.max_size_buckets);
if fits {
CacheAdmissionDecision::Admit
} else if inventory.buffers == 0 {
CacheAdmissionDecision::Reject
} else {
CacheAdmissionDecision::Evict
}
}
pub(super) fn checked_deferred_bytes(current: usize, added: usize) -> Result<usize, CudaError> {
current
.checked_add(added)
.ok_or(CudaError::InternalInvariant {
what: "CUDA deferred buffer byte accounting overflow",
})
}
pub(super) fn observe_deferred_high_water(state: &mut CudaBufferPoolState) {
state.metrics.peak_deferred_buffers = state
.metrics
.peak_deferred_buffers
.max(state.deferred.len());
state.metrics.peak_deferred_bytes = state.metrics.peak_deferred_bytes.max(state.deferred_bytes);
}
fn observe_cache_high_water(state: &mut CudaBufferPoolState) {
let inventory = state.free.inventory();
state.metrics.peak_cached_buffers = state.metrics.peak_cached_buffers.max(inventory.buffers);
state.metrics.peak_cached_bytes = state.metrics.peak_cached_bytes.max(inventory.bytes);
state.metrics.peak_cached_size_buckets = state
.metrics
.peak_cached_size_buckets
.max(inventory.size_buckets);
}
#[cfg(test)]
mod tests;