use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use crate::runtime::{CopyCompleted, CudaRuntime, PinnedStaging};
static GLOBAL_PINNED_ALLOC_CALLS: AtomicU64 = AtomicU64::new(0);
static GLOBAL_PINNED_REUSES: AtomicU64 = AtomicU64::new(0);
pub fn global_pinned_alloc_calls() -> u64 {
GLOBAL_PINNED_ALLOC_CALLS.load(Ordering::Relaxed)
}
pub fn global_pinned_reuses() -> u64 {
GLOBAL_PINNED_REUSES.load(Ordering::Relaxed)
}
pub(crate) fn reset_pinned_pool_counters() {
GLOBAL_PINNED_ALLOC_CALLS.store(0, Ordering::Relaxed);
GLOBAL_PINNED_REUSES.store(0, Ordering::Relaxed);
}
const DEFAULT_MAX_BUFFERS: usize = 8;
const DEFAULT_MAX_BYTES: usize = 512 * 1024 * 1024;
pub struct PinnedStagingPool {
runtime: Arc<CudaRuntime>,
free: Mutex<Vec<PinnedStaging>>,
max_buffers: usize,
max_bytes: usize,
alloc_calls: AtomicU64,
reuses: AtomicU64,
}
impl PinnedStagingPool {
pub fn new(runtime: Arc<CudaRuntime>) -> Arc<Self> {
Self::with_bounds(runtime, DEFAULT_MAX_BUFFERS, DEFAULT_MAX_BYTES)
}
pub fn with_bounds(
runtime: Arc<CudaRuntime>,
max_buffers: usize,
max_bytes: usize,
) -> Arc<Self> {
Arc::new(Self {
runtime,
free: Mutex::new(Vec::new()),
max_buffers: max_buffers.max(1),
max_bytes,
alloc_calls: AtomicU64::new(0),
reuses: AtomicU64::new(0),
})
}
pub fn acquire(
self: &Arc<Self>,
len: usize,
) -> Result<PooledStaging, onnx_runtime_ep_api::EpError> {
let reused = {
let mut free = self.free.lock().expect("pinned staging pool poisoned");
let mut best: Option<usize> = None;
for (idx, buffer) in free.iter().enumerate() {
if buffer.len() >= len {
match best {
Some(b) if free[b].len() <= buffer.len() => {}
_ => best = Some(idx),
}
}
}
best.map(|idx| free.swap_remove(idx))
};
let staging = match reused {
Some(staging) => {
self.reuses.fetch_add(1, Ordering::Relaxed);
GLOBAL_PINNED_REUSES.fetch_add(1, Ordering::Relaxed);
staging
}
None => {
let staging = self.runtime.alloc_pinned(len)?;
self.alloc_calls.fetch_add(1, Ordering::Relaxed);
GLOBAL_PINNED_ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
staging
}
};
Ok(PooledStaging {
pool: Arc::clone(self),
staging: Some(staging),
})
}
pub fn release(&self, staging: PinnedStaging, _completed: CopyCompleted) {
let mut free = self.free.lock().expect("pinned staging pool poisoned");
let retained: usize = free.iter().map(PinnedStaging::len).sum();
if free.len() < self.max_buffers && retained.saturating_add(staging.len()) <= self.max_bytes
{
free.push(staging);
}
}
#[cfg(test)]
pub fn free_len(&self) -> usize {
self.free
.lock()
.expect("pinned staging pool poisoned")
.len()
}
pub fn alloc_calls(&self) -> u64 {
self.alloc_calls.load(Ordering::Relaxed)
}
pub fn reuses(&self) -> u64 {
self.reuses.load(Ordering::Relaxed)
}
pub fn can_retain_concurrent(&self, len: usize, count: usize) -> bool {
count <= self.max_buffers && len.saturating_mul(count) <= self.max_bytes
}
}
pub struct PooledStaging {
pool: Arc<PinnedStagingPool>,
staging: Option<PinnedStaging>,
}
impl PooledStaging {
pub fn staging_mut(&mut self) -> &mut PinnedStaging {
self.staging
.as_mut()
.expect("pooled staging present until into_inner/retire/Drop")
}
pub fn as_slice(&self) -> &[u8] {
self.staging
.as_ref()
.expect("pooled staging present until into_inner/retire/Drop")
.as_slice()
}
pub fn into_inner(mut self) -> PinnedStaging {
self.staging
.take()
.expect("pooled staging present until into_inner/retire/Drop")
}
pub fn retire(mut self, completed: CopyCompleted) {
if let Some(staging) = self.staging.take() {
self.pool.release(staging, completed);
}
}
}
impl Drop for PooledStaging {
fn drop(&mut self) {
drop(self.staging.take());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn runtime() -> Option<Arc<CudaRuntime>> {
CudaRuntime::new(0).ok().map(Arc::new)
}
#[test]
fn reused_buffers_do_not_reallocate_per_page_in() {
let Some(runtime) = runtime() else {
eprintln!("SKIPPED (no CUDA runtime): pinned-pool reuse guard did NOT run.");
return;
};
let pool = PinnedStagingPool::new(runtime);
let len = 4 * 1024 * 1024;
let page_ins = 64u64;
for _ in 0..page_ins {
let mut staging = pool.acquire(len).unwrap();
staging.staging_mut().as_mut_slice()[..len].fill(0xAB);
staging.retire(CopyCompleted::new_for_test()); }
assert_eq!(
pool.alloc_calls(),
1,
"a per-page-in pinned allocation silently returned: {page_ins} page-ins should \
reuse one pooled buffer, not re-alloc"
);
assert_eq!(pool.reuses(), page_ins - 1);
assert!(
pool.alloc_calls() < page_ins,
"pinned allocations ({}) must stay far below page-ins ({page_ins})",
pool.alloc_calls()
);
}
#[test]
fn reuse_if_large_enough_then_grow() {
let Some(runtime) = runtime() else {
eprintln!("SKIPPED (no CUDA runtime): pinned-pool sizing guard did NOT run.");
return;
};
let pool = PinnedStagingPool::new(runtime);
let big = pool.acquire(8 * 1024 * 1024).unwrap();
big.retire(CopyCompleted::new_for_test());
assert_eq!(pool.alloc_calls(), 1);
let small = pool.acquire(1024 * 1024).unwrap();
assert_eq!(pool.alloc_calls(), 1);
assert_eq!(pool.reuses(), 1);
small.retire(CopyCompleted::new_for_test());
let bigger = pool.acquire(16 * 1024 * 1024).unwrap();
assert_eq!(pool.alloc_calls(), 2);
bigger.retire(CopyCompleted::new_for_test());
}
#[test]
fn retention_is_bounded_by_buffer_count() {
let Some(runtime) = runtime() else {
eprintln!("SKIPPED (no CUDA runtime): pinned-pool bound guard did NOT run.");
return;
};
let max_buffers = 2;
let pool = PinnedStagingPool::with_bounds(runtime, max_buffers, usize::MAX);
let a = pool.acquire(1024).unwrap();
let b = pool.acquire(1024).unwrap();
let c = pool.acquire(1024).unwrap();
a.retire(CopyCompleted::new_for_test());
b.retire(CopyCompleted::new_for_test());
c.retire(CopyCompleted::new_for_test());
assert!(
pool.free_len() <= max_buffers,
"free-list grew past its bound: {} > {max_buffers}",
pool.free_len()
);
}
#[test]
fn retention_is_bounded_by_bytes() {
let Some(runtime) = runtime() else {
eprintln!("SKIPPED (no CUDA runtime): pinned-pool byte-bound guard did NOT run.");
return;
};
let pool = PinnedStagingPool::with_bounds(runtime, 8, 1024 * 1024 + 1);
let a = pool.acquire(1024 * 1024).unwrap();
let b = pool.acquire(1024 * 1024).unwrap();
a.retire(CopyCompleted::new_for_test());
b.retire(CopyCompleted::new_for_test());
assert_eq!(
pool.free_len(),
1,
"byte ceiling must cap retained pinned host memory"
);
}
}