Skip to main content

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;