tenferro_cpu_basic/buffer_pool.rs
1//! Typed host buffer pooling for reusable tensor allocations.
2//!
3//! # Examples
4//!
5//! ```rust
6//! use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
7//!
8//! let mut pool = BufferPool::new();
9//! let mut buf = pool.acquire_zeroed::<f64>(4);
10//! buf.fill(1.0);
11//! <f64 as PoolScalar>::pool_release(&mut pool, buf);
12//! assert_eq!(pool.len(), 1);
13//! ```
14
15use std::collections::BTreeMap;
16use std::env;
17use std::ffi::OsString;
18use std::fmt;
19use std::mem::{size_of, ManuallyDrop, MaybeUninit};
20use std::sync::{Arc, Mutex, MutexGuard, OnceLock, Weak};
21use tenferro_tensor::HostBufferRecycler;
22
23use num_complex::{Complex32, Complex64};
24
25use crate::CacheStats;
26
27/// Non-Copy proof of a tracked pool checkout.
28#[derive(Debug)]
29pub(crate) enum UninitCheckoutToken {
30 /// The allocation was freshly allocated.
31 Fresh { actual_capacity: usize },
32 /// Retained storage was removed, with its actual capacity.
33 Reused { actual_capacity: usize },
34}
35
36/// Environment variable overriding the CPU buffer-pool retention cap in bytes.
37///
38/// The value is parsed as an unsigned integer. Invalid values fall back to
39/// [`DEFAULT_MAX_RETAINED_CAPACITY_BYTES`].
40pub const BUFFER_POOL_MAX_RETAINED_BYTES_ENV: &str = "TENFERRO_BUFFER_POOL_MAX_RETAINED_BYTES";
41
42/// Default retained CPU buffer capacity per backend.
43///
44/// The cap keeps long-running workloads from accumulating obsolete buffer
45/// sizes as tensor shapes grow while still preserving reuse for hot working
46/// sets.
47pub const DEFAULT_MAX_RETAINED_CAPACITY_BYTES: usize = 100 * 1024 * 1024;
48
49static DEFAULT_MAX_RETAINED_CAPACITY_FROM_ENV: OnceLock<usize> = OnceLock::new();
50
51/// Snapshot of typed host buffers retained by a [`BufferPool`].
52///
53/// `buffers` counts retained `Vec` allocations, while `capacity_bytes` counts
54/// their total element capacity in bytes. Allocators may keep freed memory in
55/// process-local arenas after a pool is cleared, so this reports memory that is
56/// still live in the pool rather than operating-system RSS.
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
58pub struct BufferPoolStats {
59 /// Number of retained vector allocations.
60 pub buffers: usize,
61 /// Total retained vector capacity in bytes.
62 pub capacity_bytes: usize,
63}
64
65/// Typed buffer pool keyed by element capacity and separated by scalar type.
66///
67/// Each supported dtype has an independent best-fit pool. Acquired buffers are
68/// returned without zero-initialization so kernels can avoid redundant writes
69/// when they fully overwrite the output. Use [`PoolScalar::pool_acquire_zeroed`]
70/// when the caller may read the buffer before writing every element.
71///
72/// # Examples
73///
74/// ```rust
75/// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
76///
77/// let mut pool = BufferPool::new();
78/// let buf = pool.acquire_zeroed::<f32>(8);
79/// <f32 as PoolScalar>::pool_release(&mut pool, buf);
80/// assert_eq!(pool.len(), 1);
81/// ```
82pub struct BufferPool {
83 state: Arc<SharedPool>,
84}
85
86#[derive(Debug)]
87struct SharedPool(Mutex<PoolState>);
88
89#[derive(Debug)]
90struct PoolState {
91 f64_pool: BTreeMap<usize, Vec<Vec<f64>>>,
92 f32_pool: BTreeMap<usize, Vec<Vec<f32>>>,
93 i32_pool: BTreeMap<usize, Vec<Vec<i32>>>,
94 i64_pool: BTreeMap<usize, Vec<Vec<i64>>>,
95 bool_pool: BTreeMap<usize, Vec<Vec<bool>>>,
96 c64_pool: BTreeMap<usize, Vec<Vec<Complex64>>>,
97 c32_pool: BTreeMap<usize, Vec<Vec<Complex32>>>,
98 f64_in_flight: BTreeMap<usize, usize>,
99 f32_in_flight: BTreeMap<usize, usize>,
100 i32_in_flight: BTreeMap<usize, usize>,
101 i64_in_flight: BTreeMap<usize, usize>,
102 bool_in_flight: BTreeMap<usize, usize>,
103 c64_in_flight: BTreeMap<usize, usize>,
104 c32_in_flight: BTreeMap<usize, usize>,
105 retained_capacity_bytes: usize,
106 max_retained_capacity_bytes: usize,
107}
108
109impl fmt::Debug for BufferPool {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.debug_struct("BufferPool")
112 .field("stats", &self.stats())
113 .field(
114 "max_retained_capacity_bytes",
115 &self.max_retained_capacity_bytes(),
116 )
117 .finish_non_exhaustive()
118 }
119}
120
121/// Scalar types supported by [`BufferPool`].
122///
123/// The trait is sealed to the scalar dtypes that tenferro currently pools for
124/// CPU execution.
125///
126/// # Examples
127///
128/// ```rust
129/// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
130///
131/// let mut pool = BufferPool::new();
132/// let mut buf = pool.acquire_zeroed::<f64>(2);
133/// buf.copy_from_slice(&[3.0, 4.0]);
134/// <f64 as PoolScalar>::pool_release(&mut pool, buf);
135/// ```
136pub trait PoolScalar:
137 Copy + Sized + Send + Sync + tenferro_tensor::TensorScalar + private::Sealed
138{
139 /// Zero value used to initialize acquired buffers.
140 fn pool_zero() -> Self;
141
142 /// Acquire a buffer with length `len` and every element set to zero.
143 ///
144 /// This is the safe path for callers that may read the buffer before every
145 /// element is overwritten. Full-overwrite kernels should use
146 /// [`crate::PooledUninitOutput`] or an operation-specific uninitialized
147 /// destination guard.
148 ///
149 /// # Examples
150 ///
151 /// ```rust
152 /// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
153 ///
154 /// let mut pool = BufferPool::new();
155 /// let buf = <f64 as PoolScalar>::pool_acquire_zeroed(&mut pool, 2);
156 /// assert_eq!(buf, vec![0.0, 0.0]);
157 /// ```
158 fn pool_acquire_zeroed(pool: &mut BufferPool, len: usize) -> Vec<Self>;
159
160 /// Return a buffer to the typed pool for later reuse.
161 ///
162 /// Zero-capacity buffers are ignored.
163 ///
164 /// # Examples
165 ///
166 /// ```rust
167 /// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
168 ///
169 /// let mut pool = BufferPool::new();
170 /// let buf = vec![1.0_f32; 4];
171 /// <f32 as PoolScalar>::pool_release(&mut pool, buf);
172 /// assert_eq!(pool.len(), 1);
173 /// ```
174 fn pool_release(pool: &mut BufferPool, buf: Vec<Self>);
175}
176
177pub(crate) mod private {
178 use std::mem::MaybeUninit;
179
180 // INVARIANT: this sealed crate-private trait is the only implementation
181 // boundary for tracked guard tokens; it is not part of the public API.
182 #[allow(private_interfaces)]
183 pub trait Sealed {
184 /// Checks out uninitialized storage with an exact cleanup token.
185 ///
186 /// # Errors
187 /// Returns `Error::Validation` if retained-capacity byte accounting
188 /// overflows, or `Error::BackendSource` if fresh allocation fails.
189 fn pool_acquire_uninit_tracked(
190 pool: &mut super::BufferPool,
191 len: usize,
192 ) -> crate::Result<(Vec<MaybeUninit<Self>>, super::UninitCheckoutToken)>
193 where
194 Self: Sized;
195 fn pool_finish_recycled(
196 pool: &mut super::BufferPool,
197 checkout: super::UninitCheckoutToken,
198 ) -> std::sync::Weak<dyn tenferro_tensor::HostBufferRecycler<Self>>
199 where
200 Self: Sized;
201
202 fn pool_discard_uninit(
203 pool: &mut super::BufferPool,
204 data: Vec<MaybeUninit<Self>>,
205 checkout: super::UninitCheckoutToken,
206 ) where
207 Self: Sized;
208 }
209}
210
211fn take_best_fit<T>(pool: &mut BTreeMap<usize, Vec<Vec<T>>>, len: usize) -> Option<Vec<T>> {
212 // Keep the capacity bin after it empties. A kernel that acquires and
213 // releases the same scratch size every call would otherwise remove and
214 // re-insert this map node on every call, and each re-insert is itself an
215 // allocation — which is what the pool exists to avoid. Empty bins hold no
216 // buffers, so `pool_len` and the retained-bytes accounting are unaffected,
217 // and `clear` still drops them.
218 let key = *pool.range(len..).find(|(_, vecs)| !vecs.is_empty())?.0;
219 pool.get_mut(&key)?.pop()
220}
221
222fn pool_len<T>(pool: &BTreeMap<usize, Vec<Vec<T>>>) -> usize {
223 pool.values().map(Vec::len).sum()
224}
225
226fn evict_one_from_pool<T>(pool: &mut BTreeMap<usize, Vec<Vec<T>>>) -> Option<usize> {
227 // Retained bins may be empty (see `take_best_fit`), and an empty bin has
228 // nothing to evict.
229 let key = *pool.iter().find(|(_, vecs)| !vecs.is_empty())?.0;
230 let vecs = pool.get_mut(&key)?;
231 let _ = vecs.pop()?;
232 if vecs.is_empty() {
233 // Eviction runs because the pool is over its retention limit, so here
234 // the bin is dropped rather than kept for reuse.
235 pool.remove(&key);
236 }
237 Some(key.saturating_mul(size_of::<T>()))
238}
239
240#[derive(Clone, Copy)]
241enum TypedPoolKind {
242 F64,
243 F32,
244 I32,
245 I64,
246 Bool,
247 C64,
248 C32,
249}
250
251fn smallest_pool_candidate<T>(
252 pool: &BTreeMap<usize, Vec<Vec<T>>>,
253 kind: TypedPoolKind,
254) -> Option<(usize, TypedPoolKind)> {
255 // Only a bin that still holds a buffer is an eviction candidate.
256 pool.iter()
257 .find(|(_, vecs)| !vecs.is_empty())
258 .map(|(&capacity, _)| (capacity.saturating_mul(size_of::<T>()), kind))
259}
260
261fn increment_in_flight(in_flight: &mut BTreeMap<usize, usize>, cap: usize) {
262 if cap > 0 {
263 *in_flight.entry(cap).or_default() += 1;
264 }
265}
266
267fn decrement_in_flight(in_flight: &mut BTreeMap<usize, usize>, cap: usize) {
268 if cap == 0 {
269 return;
270 }
271 let Some(count) = in_flight.get_mut(&cap) else {
272 return;
273 };
274 // Keep the zero entry for the same reason `take_best_fit` keeps an empty
275 // bin: a steady acquire/release cycle must not re-insert this node every
276 // call. `replenish_in_flight_for` skips zero counts, and
277 // `clear_in_flight_retained` still drops them.
278 //
279 // The decrement saturates because a caller may release a buffer this pool
280 // never handed out. That used to be a no-op only because the entry was
281 // removed at zero; with the entry retained it has to stay a no-op here.
282 *count = count.saturating_sub(1);
283}
284
285fn replenish_in_flight_for<T>(
286 pool: &mut BTreeMap<usize, Vec<Vec<T>>>,
287 in_flight: &mut BTreeMap<usize, usize>,
288 retained_capacity_bytes: &mut usize,
289) {
290 for (&cap, &count) in in_flight.iter() {
291 for _ in 0..count {
292 let mut replacement = Vec::new();
293 if replacement.try_reserve_exact(cap).is_err() {
294 continue;
295 }
296 let actual_cap = replacement.capacity();
297 *retained_capacity_bytes =
298 retained_capacity_bytes.saturating_add(actual_cap.saturating_mul(size_of::<T>()));
299 pool.entry(actual_cap).or_default().push(replacement);
300 }
301 }
302 in_flight.clear();
303}
304
305macro_rules! impl_pool_scalar {
306 ($ty:ty, $field:ident, $in_flight:ident, $zero:expr) => {
307 // INVARIANT: this implementation is reachable only through the sealed
308 // crate-private helper and cannot be named by sibling crates.
309 #[allow(private_interfaces)]
310 impl private::Sealed for $ty {
311 fn pool_acquire_uninit_tracked(
312 pool: &mut BufferPool,
313 len: usize,
314 ) -> crate::Result<(Vec<MaybeUninit<Self>>, UninitCheckoutToken)> {
315 let mut state = lock_pool(&pool.state);
316 let pool = &mut *state;
317 match take_best_fit(&mut pool.$field, len) {
318 Some(buf) => {
319 let cap = buf.capacity();
320 let bytes = cap.checked_mul(size_of::<Self>()).ok_or_else(|| {
321 crate::Error::invalid_argument(
322 "pooled_uninit_output",
323 "length",
324 "pool capacity byte length overflow",
325 )
326 })?;
327 pool.retained_capacity_bytes -= bytes;
328 increment_in_flight(&mut pool.$in_flight, cap);
329 let mut buf = ManuallyDrop::new(buf);
330 // SAFETY: MaybeUninit<Self> has the same layout as Self and len <= cap.
331 let buf = unsafe { Vec::from_raw_parts(buf.as_mut_ptr().cast(), len, cap) };
332 Ok((
333 buf,
334 UninitCheckoutToken::Reused {
335 actual_capacity: cap,
336 },
337 ))
338 }
339 None => {
340 let mut buf = Vec::new();
341 buf.try_reserve_exact(len).map_err(|err| {
342 crate::Error::backend_source("pooled_uninit_output", err)
343 })?;
344 // SAFETY: every bit pattern is valid for MaybeUninit.
345 unsafe { buf.set_len(len) };
346 let actual_capacity = buf.capacity();
347 Ok((buf, UninitCheckoutToken::Fresh { actual_capacity }))
348 }
349 }
350 }
351
352 fn pool_finish_recycled(
353 pool: &mut BufferPool,
354 checkout: UninitCheckoutToken,
355 ) -> Weak<dyn HostBufferRecycler<Self>> {
356 if let UninitCheckoutToken::Reused { actual_capacity } = checkout {
357 decrement_in_flight(&mut lock_pool(&pool.state).$in_flight, actual_capacity);
358 }
359 let owner: Arc<dyn HostBufferRecycler<Self>> = pool.state.clone();
360 Arc::downgrade(&owner)
361 }
362
363 fn pool_discard_uninit(
364 pool: &mut BufferPool,
365 data: Vec<MaybeUninit<Self>>,
366 checkout: UninitCheckoutToken,
367 ) {
368 drop(data);
369 let mut state = lock_pool(&pool.state);
370 let pool = &mut *state;
371 if let UninitCheckoutToken::Reused { actual_capacity } = checkout {
372 decrement_in_flight(&mut pool.$in_flight, actual_capacity);
373 }
374 }
375 }
376
377 impl PoolScalar for $ty {
378 fn pool_zero() -> Self {
379 $zero
380 }
381
382 fn pool_acquire_zeroed(pool: &mut BufferPool, len: usize) -> Vec<Self> {
383 let mut state = lock_pool(&pool.state);
384 let pool = &mut *state;
385 match take_best_fit(&mut pool.$field, len) {
386 Some(mut buf) => {
387 pool.retained_capacity_bytes = pool
388 .retained_capacity_bytes
389 .saturating_sub(buf.capacity().saturating_mul(size_of::<Self>()));
390 increment_in_flight(&mut pool.$in_flight, buf.capacity());
391 buf.resize(len, Self::pool_zero());
392 buf.fill(Self::pool_zero());
393 buf
394 }
395 None => vec![Self::pool_zero(); len],
396 }
397 }
398
399 fn pool_release(pool: &mut BufferPool, buf: Vec<Self>) {
400 decrement_in_flight(&mut lock_pool(&pool.state).$in_flight, buf.capacity());
401 pool.state.recycle(buf);
402 }
403 }
404
405 impl HostBufferRecycler<$ty> for SharedPool {
406 fn recycle(&self, buf: Vec<$ty>) {
407 let mut state = lock_pool(self);
408 let pool = &mut *state;
409 let cap = buf.capacity();
410 if cap > 0 {
411 pool.retained_capacity_bytes = pool
412 .retained_capacity_bytes
413 .saturating_add(cap.saturating_mul(size_of::<$ty>()));
414 pool.$field.entry(cap).or_default().push(buf);
415 pool.enforce_retention_limit();
416 }
417 }
418 }
419 };
420}
421
422impl_pool_scalar!(f64, f64_pool, f64_in_flight, 0.0);
423impl_pool_scalar!(f32, f32_pool, f32_in_flight, 0.0);
424impl_pool_scalar!(i32, i32_pool, i32_in_flight, 0);
425impl_pool_scalar!(i64, i64_pool, i64_in_flight, 0);
426impl_pool_scalar!(bool, bool_pool, bool_in_flight, false);
427impl_pool_scalar!(Complex64, c64_pool, c64_in_flight, Complex64::new(0.0, 0.0));
428impl_pool_scalar!(Complex32, c32_pool, c32_in_flight, Complex32::new(0.0, 0.0));
429
430impl BufferPool {
431 pub(crate) fn checkout_handle(&self) -> Self {
432 Self {
433 state: Arc::clone(&self.state),
434 }
435 }
436
437 #[cfg(test)]
438 pub(crate) fn in_flight_is_empty(&self) -> bool {
439 // A capacity keeps its bookkeeping entry after its last checkout is
440 // returned (see `decrement_in_flight`), so "nothing is checked out"
441 // means every count is zero, not that the maps are empty.
442 fn none_in_flight(in_flight: &BTreeMap<usize, usize>) -> bool {
443 in_flight.values().all(|&count| count == 0)
444 }
445 let state = lock_pool(&self.state);
446 none_in_flight(&state.f64_in_flight)
447 && none_in_flight(&state.f32_in_flight)
448 && none_in_flight(&state.i32_in_flight)
449 && none_in_flight(&state.i64_in_flight)
450 && none_in_flight(&state.bool_in_flight)
451 && none_in_flight(&state.c64_in_flight)
452 && none_in_flight(&state.c32_in_flight)
453 }
454 /// Create an empty typed buffer pool.
455 ///
456 /// # Examples
457 ///
458 /// ```rust
459 /// use tenferro_cpu_basic::buffer_pool::BufferPool;
460 ///
461 /// let pool = BufferPool::new();
462 /// assert!(pool.is_empty());
463 /// ```
464 pub fn new() -> Self {
465 Self::with_max_retained_capacity_bytes(default_max_retained_capacity_bytes())
466 }
467
468 /// Create an empty typed buffer pool with a specific retention cap.
469 ///
470 /// A cap of zero disables retention. Use [`BufferPool::unbounded`] only for
471 /// diagnostics or workloads that are externally memory-limited.
472 ///
473 /// # Examples
474 ///
475 /// ```rust
476 /// use tenferro_cpu_basic::buffer_pool::BufferPool;
477 ///
478 /// let pool = BufferPool::with_max_retained_capacity_bytes(1024);
479 /// assert_eq!(pool.max_retained_capacity_bytes(), 1024);
480 /// ```
481 pub fn with_max_retained_capacity_bytes(max_retained_capacity_bytes: usize) -> Self {
482 Self {
483 state: Arc::new(SharedPool(Mutex::new(PoolState {
484 f64_pool: BTreeMap::new(),
485 f32_pool: BTreeMap::new(),
486 i32_pool: BTreeMap::new(),
487 i64_pool: BTreeMap::new(),
488 bool_pool: BTreeMap::new(),
489 c64_pool: BTreeMap::new(),
490 c32_pool: BTreeMap::new(),
491 f64_in_flight: BTreeMap::new(),
492 f32_in_flight: BTreeMap::new(),
493 i32_in_flight: BTreeMap::new(),
494 i64_in_flight: BTreeMap::new(),
495 bool_in_flight: BTreeMap::new(),
496 c64_in_flight: BTreeMap::new(),
497 c32_in_flight: BTreeMap::new(),
498 retained_capacity_bytes: 0,
499 max_retained_capacity_bytes,
500 }))),
501 }
502 }
503
504 /// Create an empty typed buffer pool without a retention cap.
505 ///
506 /// This preserves the historical behavior and is mainly useful for
507 /// diagnostics or controlled benchmarks.
508 ///
509 /// # Examples
510 ///
511 /// ```rust
512 /// use tenferro_cpu_basic::buffer_pool::BufferPool;
513 ///
514 /// let pool = BufferPool::unbounded();
515 /// assert_eq!(pool.max_retained_capacity_bytes(), usize::MAX);
516 /// ```
517 pub fn unbounded() -> Self {
518 Self::with_max_retained_capacity_bytes(usize::MAX)
519 }
520
521 /// Maximum retained typed host-buffer capacity in bytes.
522 ///
523 /// # Examples
524 ///
525 /// ```rust
526 /// use tenferro_cpu_basic::buffer_pool::BufferPool;
527 ///
528 /// let pool = BufferPool::with_max_retained_capacity_bytes(4096);
529 /// assert_eq!(pool.max_retained_capacity_bytes(), 4096);
530 /// ```
531 pub fn max_retained_capacity_bytes(&self) -> usize {
532 lock_pool(&self.state).max_retained_capacity_bytes
533 }
534
535 /// Update the maximum retained typed host-buffer capacity in bytes.
536 ///
537 /// Shrinking below the currently retained capacity immediately evicts
538 /// retained buffers until the new cap is satisfied. A cap of zero disables
539 /// retention.
540 ///
541 /// # Examples
542 ///
543 /// ```
544 /// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
545 ///
546 /// let mut pool = BufferPool::with_max_retained_capacity_bytes(1024);
547 /// <f64 as PoolScalar>::pool_release(&mut pool, Vec::with_capacity(128));
548 /// pool.set_max_retained_capacity_bytes(0);
549 /// assert_eq!(pool.max_retained_capacity_bytes(), 0);
550 /// assert!(pool.is_empty());
551 /// ```
552 pub fn set_max_retained_capacity_bytes(&mut self, max_retained_capacity_bytes: usize) {
553 let mut state = lock_pool(&self.state);
554 state.max_retained_capacity_bytes = max_retained_capacity_bytes;
555 state.enforce_retention_limit();
556 }
557
558 /// Number of retained buffers across all typed pools.
559 ///
560 /// # Examples
561 ///
562 /// ```rust
563 /// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
564 ///
565 /// let mut pool = BufferPool::new();
566 /// <f64 as PoolScalar>::pool_release(&mut pool, vec![0.0; 2]);
567 /// assert_eq!(pool.len(), 1);
568 /// ```
569 pub fn len(&self) -> usize {
570 self.stats().buffers
571 }
572
573 /// Total retained typed host-buffer capacity in bytes.
574 ///
575 /// This counts capacity that is still live in the pool. The operating
576 /// system RSS may remain high after clearing the pool because the process
577 /// allocator can keep freed pages for future allocations.
578 ///
579 /// # Examples
580 ///
581 /// ```rust
582 /// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
583 ///
584 /// let mut pool = BufferPool::new();
585 /// <f64 as PoolScalar>::pool_release(&mut pool, Vec::with_capacity(2));
586 /// assert_eq!(pool.retained_capacity_bytes(), 16);
587 /// ```
588 pub fn retained_capacity_bytes(&self) -> usize {
589 self.stats().capacity_bytes
590 }
591
592 /// Snapshot retained-buffer count and capacity.
593 ///
594 /// # Examples
595 ///
596 /// ```rust
597 /// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
598 ///
599 /// let mut pool = BufferPool::new();
600 /// <f32 as PoolScalar>::pool_release(&mut pool, Vec::with_capacity(4));
601 /// let stats = pool.stats();
602 /// assert_eq!(stats.buffers, 1);
603 /// assert_eq!(stats.capacity_bytes, 16);
604 /// ```
605 pub fn stats(&self) -> BufferPoolStats {
606 let state = lock_pool(&self.state);
607 BufferPoolStats {
608 buffers: pool_len(&state.f64_pool)
609 + pool_len(&state.f32_pool)
610 + pool_len(&state.i32_pool)
611 + pool_len(&state.i64_pool)
612 + pool_len(&state.bool_pool)
613 + pool_len(&state.c64_pool)
614 + pool_len(&state.c32_pool),
615 capacity_bytes: state.retained_capacity_bytes,
616 }
617 }
618
619 /// Return cache-style stats for the buffers retained by this pool.
620 ///
621 /// `entries` is the number of retained buffers, and `retained_bytes` is the
622 /// total retained vector capacity in bytes.
623 ///
624 /// # Examples
625 ///
626 /// ```
627 /// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
628 ///
629 /// let mut pool = BufferPool::new();
630 /// <f32 as PoolScalar>::pool_release(&mut pool, Vec::with_capacity(4));
631 /// let stats = pool.cache_stats();
632 /// assert_eq!(stats.entries, 1);
633 /// assert_eq!(stats.retained_bytes, 16);
634 /// ```
635 pub fn cache_stats(&self) -> CacheStats {
636 let stats = self.stats();
637 CacheStats {
638 entries: stats.buffers,
639 retained_bytes: stats.capacity_bytes,
640 hits: 0,
641 misses: 0,
642 evictions: 0,
643 clears: 0,
644 }
645 }
646
647 /// Acquire a typed vector with length 0 and at least `cap` capacity.
648 ///
649 /// Returned buffers come from the typed pool when possible and are ready
650 /// for push-based population.
651 ///
652 /// # Examples
653 ///
654 /// ```rust
655 /// use tenferro_cpu_basic::buffer_pool::BufferPool;
656 ///
657 /// let mut pool = BufferPool::new();
658 /// let mut buf = pool.acquire_with_capacity::<f64>(4);
659 /// buf.extend_from_slice(&[1.0, 2.0]);
660 /// assert_eq!(buf.len(), 2);
661 /// assert!(buf.capacity() >= 4);
662 /// ```
663 pub fn acquire_with_capacity<T: PoolScalar>(&mut self, cap: usize) -> Vec<T> {
664 if cap == 0 {
665 return Vec::new();
666 }
667
668 let (data, _checkout) = <T as private::Sealed>::pool_acquire_uninit_tracked(self, cap)
669 .expect("validated typed pool capacity must be acquirable");
670 let mut data = ManuallyDrop::new(data);
671 let ptr = data.as_mut_ptr().cast::<T>();
672 let capacity = data.capacity();
673 // SAFETY: `MaybeUninit<T>` and `T` have identical layouts, and the
674 // returned vector has length zero, so no element is read before push.
675 unsafe { Vec::from_raw_parts(ptr, 0, capacity) }
676 }
677
678 /// Acquire a typed vector with length `len` initialized to zero.
679 ///
680 /// Use this only when the caller may read elements before overwriting the
681 /// entire buffer. Full-overwrite kernels should use
682 /// [`crate::PooledUninitOutput`] to avoid the initialization cost.
683 ///
684 /// # Examples
685 ///
686 /// ```rust
687 /// use tenferro_cpu_basic::buffer_pool::BufferPool;
688 ///
689 /// let mut pool = BufferPool::new();
690 /// let buf = pool.acquire_zeroed::<f32>(3);
691 /// assert_eq!(buf, vec![0.0, 0.0, 0.0]);
692 /// ```
693 pub fn acquire_zeroed<T: PoolScalar>(&mut self, len: usize) -> Vec<T> {
694 T::pool_acquire_zeroed(self, len)
695 }
696
697 /// Whether all typed pools are empty.
698 ///
699 /// # Examples
700 ///
701 /// ```rust
702 /// use tenferro_cpu_basic::buffer_pool::BufferPool;
703 ///
704 /// let pool = BufferPool::new();
705 /// assert!(pool.is_empty());
706 /// ```
707 pub fn is_empty(&self) -> bool {
708 self.len() == 0
709 }
710
711 /// Drop all retained buffers from the pool.
712 ///
713 /// This releases the vectors owned by the pool. The process allocator may
714 /// still keep freed pages mapped for reuse, so operating-system RSS is not
715 /// guaranteed to fall immediately.
716 ///
717 /// # Examples
718 ///
719 /// ```rust
720 /// use tenferro_cpu_basic::buffer_pool::{BufferPool, PoolScalar};
721 ///
722 /// let mut pool = BufferPool::new();
723 /// <f64 as PoolScalar>::pool_release(&mut pool, Vec::with_capacity(8));
724 /// pool.clear();
725 /// assert!(pool.is_empty());
726 /// ```
727 pub fn clear(&mut self) {
728 let mut state = lock_pool(&self.state);
729 state.clear();
730 }
731
732 #[doc(hidden)]
733 pub fn clear_in_flight_retained(&mut self) {
734 lock_pool(&self.state).clear_in_flight_retained();
735 }
736
737 #[doc(hidden)]
738 pub fn replenish_in_flight_retained(&mut self) {
739 lock_pool(&self.state).replenish_in_flight_retained();
740 }
741}
742
743// INVARIANT: no user callback or tensor destructor runs under this lock; it
744// protects only scalar Vec bins and their accounting, never execution admission.
745fn lock_pool(state: &SharedPool) -> MutexGuard<'_, PoolState> {
746 state
747 .0
748 .lock()
749 .unwrap_or_else(std::sync::PoisonError::into_inner)
750}
751
752impl PoolState {
753 fn clear(&mut self) {
754 self.f64_pool.clear();
755 self.f32_pool.clear();
756 self.i32_pool.clear();
757 self.i64_pool.clear();
758 self.bool_pool.clear();
759 self.c64_pool.clear();
760 self.c32_pool.clear();
761 self.clear_in_flight_retained();
762 self.retained_capacity_bytes = 0;
763 }
764
765 #[doc(hidden)]
766 pub fn clear_in_flight_retained(&mut self) {
767 self.f64_in_flight.clear();
768 self.f32_in_flight.clear();
769 self.i32_in_flight.clear();
770 self.i64_in_flight.clear();
771 self.bool_in_flight.clear();
772 self.c64_in_flight.clear();
773 self.c32_in_flight.clear();
774 }
775
776 #[doc(hidden)]
777 pub fn replenish_in_flight_retained(&mut self) {
778 replenish_in_flight_for(
779 &mut self.f64_pool,
780 &mut self.f64_in_flight,
781 &mut self.retained_capacity_bytes,
782 );
783 replenish_in_flight_for(
784 &mut self.f32_pool,
785 &mut self.f32_in_flight,
786 &mut self.retained_capacity_bytes,
787 );
788 replenish_in_flight_for(
789 &mut self.i32_pool,
790 &mut self.i32_in_flight,
791 &mut self.retained_capacity_bytes,
792 );
793 replenish_in_flight_for(
794 &mut self.i64_pool,
795 &mut self.i64_in_flight,
796 &mut self.retained_capacity_bytes,
797 );
798 replenish_in_flight_for(
799 &mut self.bool_pool,
800 &mut self.bool_in_flight,
801 &mut self.retained_capacity_bytes,
802 );
803 replenish_in_flight_for(
804 &mut self.c64_pool,
805 &mut self.c64_in_flight,
806 &mut self.retained_capacity_bytes,
807 );
808 replenish_in_flight_for(
809 &mut self.c32_pool,
810 &mut self.c32_in_flight,
811 &mut self.retained_capacity_bytes,
812 );
813 self.enforce_retention_limit();
814 }
815
816 fn enforce_retention_limit(&mut self) {
817 while self.retained_capacity_bytes > self.max_retained_capacity_bytes {
818 let Some(evicted_bytes) = self.evict_smallest_retained_buffer() else {
819 self.retained_capacity_bytes = 0;
820 return;
821 };
822 if evicted_bytes == 0 {
823 if self.retained_capacity_bytes == 0 {
824 self.retained_capacity_bytes = 0;
825 return;
826 }
827 continue;
828 }
829 self.retained_capacity_bytes =
830 self.retained_capacity_bytes.saturating_sub(evicted_bytes);
831 }
832 }
833
834 fn evict_smallest_retained_buffer(&mut self) -> Option<usize> {
835 let candidates = [
836 smallest_pool_candidate(&self.f64_pool, TypedPoolKind::F64),
837 smallest_pool_candidate(&self.f32_pool, TypedPoolKind::F32),
838 smallest_pool_candidate(&self.i32_pool, TypedPoolKind::I32),
839 smallest_pool_candidate(&self.i64_pool, TypedPoolKind::I64),
840 smallest_pool_candidate(&self.bool_pool, TypedPoolKind::Bool),
841 smallest_pool_candidate(&self.c64_pool, TypedPoolKind::C64),
842 smallest_pool_candidate(&self.c32_pool, TypedPoolKind::C32),
843 ];
844 let (_, kind) = candidates
845 .into_iter()
846 .flatten()
847 .min_by_key(|(bytes, _)| *bytes)?;
848 match kind {
849 TypedPoolKind::F64 => evict_one_from_pool(&mut self.f64_pool),
850 TypedPoolKind::F32 => evict_one_from_pool(&mut self.f32_pool),
851 TypedPoolKind::I32 => evict_one_from_pool(&mut self.i32_pool),
852 TypedPoolKind::I64 => evict_one_from_pool(&mut self.i64_pool),
853 TypedPoolKind::Bool => evict_one_from_pool(&mut self.bool_pool),
854 TypedPoolKind::C64 => evict_one_from_pool(&mut self.c64_pool),
855 TypedPoolKind::C32 => evict_one_from_pool(&mut self.c32_pool),
856 }
857 }
858}
859
860fn default_max_retained_capacity_bytes() -> usize {
861 *DEFAULT_MAX_RETAINED_CAPACITY_FROM_ENV.get_or_init(|| {
862 parse_default_max_retained_capacity_bytes(env::var_os(BUFFER_POOL_MAX_RETAINED_BYTES_ENV))
863 })
864}
865
866fn parse_default_max_retained_capacity_bytes(value: Option<OsString>) -> usize {
867 value
868 .and_then(|value| value.into_string().ok())
869 .and_then(|value| value.parse().ok())
870 .unwrap_or(DEFAULT_MAX_RETAINED_CAPACITY_BYTES)
871}
872
873impl Default for BufferPool {
874 fn default() -> Self {
875 Self::new()
876 }
877}
878
879#[cfg(test)]
880mod tests;