Skip to main content

ipfrs_tensorlogic/
memory_pool.rs

1//! Tensor Memory Pool
2//!
3//! This module provides two complementary memory pool implementations:
4//!
5//! 1. **[`TensorMemoryPool`]** — Slab-based pool with size-class bucketing for
6//!    efficient allocation of variable-sized tensor buffers.
7//!
8//! 2. **[`TensorBlockPool`]** — Pre-allocated fixed-size block pool that reduces
9//!    allocation overhead by maintaining a reservoir of identically-sized memory
10//!    blocks with owner tracking, reservation support, and defragmentation.
11//!
12//! # Slab Pool (TensorMemoryPool)
13//!
14//! Uses four size classes (Small/Medium/Large/Huge) with power-of-two bucket
15//! sizes.  Best suited when tensor sizes vary widely.
16//!
17//! ```
18//! use ipfrs_tensorlogic::memory_pool::{TensorMemoryPool, SizeClass};
19//!
20//! let mut pool = TensorMemoryPool::new(64);
21//! let slot_id = pool.allocate(2048, 1).expect("example: should succeed in docs");
22//! assert!(pool.deallocate(slot_id, 2));
23//! let slot_id2 = pool.allocate(1024, 3).expect("example: should succeed in docs");
24//! assert_eq!(slot_id, slot_id2);
25//! ```
26//!
27//! # Block Pool (TensorBlockPool)
28//!
29//! All blocks share a single configured size.  Supports reservation, owner
30//! tracking, generation counting, defragmentation, and shrink-to-fit.
31//!
32//! ```
33//! use ipfrs_tensorlogic::memory_pool::{TensorBlockPool, PoolConfig};
34//!
35//! let config = PoolConfig::default();
36//! let mut pool = TensorBlockPool::new(config);
37//! let id = pool.allocate("matmul").expect("example: should succeed in docs");
38//! assert!(pool.deallocate(id).is_ok());
39//! ```
40
41use std::collections::HashMap;
42
43// ===========================================================================
44// Part 1: Slab-based TensorMemoryPool (original implementation)
45// ===========================================================================
46
47// ---------------------------------------------------------------------------
48// SizeClass
49// ---------------------------------------------------------------------------
50
51/// Categorises a byte count into one of four allocation buckets.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
53pub enum SizeClass {
54    /// <= 4 096 bytes
55    Small,
56    /// <= 65 536 bytes
57    Medium,
58    /// <= 1 048 576 bytes
59    Large,
60    /// > 1 048 576 bytes
61    Huge,
62}
63
64impl SizeClass {
65    /// Classify a byte count into the appropriate size class.
66    pub fn classify(bytes: u64) -> SizeClass {
67        if bytes <= 4_096 {
68            SizeClass::Small
69        } else if bytes <= 65_536 {
70            SizeClass::Medium
71        } else if bytes <= 1_048_576 {
72            SizeClass::Large
73        } else {
74            SizeClass::Huge
75        }
76    }
77
78    /// Return the canonical bucket allocation size for this class.
79    pub fn bucket_size(&self) -> u64 {
80        match self {
81            SizeClass::Small => 4_096,
82            SizeClass::Medium => 65_536,
83            SizeClass::Large => 1_048_576,
84            SizeClass::Huge => 16_777_216,
85        }
86    }
87}
88
89// ---------------------------------------------------------------------------
90// PoolSlot
91// ---------------------------------------------------------------------------
92
93/// A single slot managed by the [`TensorMemoryPool`].
94#[derive(Clone, Debug)]
95pub struct PoolSlot {
96    /// Unique identifier for this slot.
97    pub slot_id: u64,
98    /// Size class this slot belongs to.
99    pub size_class: SizeClass,
100    /// Bytes requested when this slot was allocated (<= `size_class.bucket_size()`).
101    pub allocated_bytes: u64,
102    /// Whether this slot is currently checked out.
103    pub in_use: bool,
104    /// The monotonic tick at which this slot was last accessed.
105    pub last_used_tick: u64,
106}
107
108// ---------------------------------------------------------------------------
109// MemoryPoolStats (slab pool)
110// ---------------------------------------------------------------------------
111
112/// Aggregate statistics for a [`TensorMemoryPool`].
113#[derive(Clone, Debug)]
114pub struct MemoryPoolStats {
115    /// Total number of slots currently tracked by the pool.
116    pub total_slots: usize,
117    /// Number of slots that are currently checked out.
118    pub in_use_slots: usize,
119    /// Number of slots available for reuse.
120    pub free_slots: usize,
121    /// Sum of `bucket_size` for every slot in the pool.
122    pub total_allocated_bytes: u64,
123    /// Sum of `(bucket_size - allocated_bytes)` for every in-use slot.
124    pub wasted_bytes: u64,
125}
126
127impl MemoryPoolStats {
128    /// Fraction of slots currently in use (`0.0` when the pool is empty).
129    pub fn utilization(&self) -> f64 {
130        if self.total_slots == 0 {
131            0.0
132        } else {
133            self.in_use_slots as f64 / self.total_slots as f64
134        }
135    }
136}
137
138// ---------------------------------------------------------------------------
139// TensorMemoryPool (slab-based)
140// ---------------------------------------------------------------------------
141
142/// A slab-based memory pool that pre-allocates tensor buffers organised into
143/// size-class buckets to minimise allocation overhead on hot inference paths.
144pub struct TensorMemoryPool {
145    /// All slots, keyed by `slot_id`.
146    pub slots: HashMap<u64, PoolSlot>,
147    /// Counter used to generate unique slot IDs.
148    pub next_slot_id: u64,
149    /// Hard upper bound on the number of slots the pool may hold.
150    pub max_slots: usize,
151    /// Total successful allocations since creation.
152    pub total_allocations: u64,
153    /// Total successful deallocations since creation.
154    pub total_deallocations: u64,
155}
156
157impl TensorMemoryPool {
158    /// Create a new pool with the given slot capacity.
159    pub fn new(max_slots: usize) -> Self {
160        TensorMemoryPool {
161            slots: HashMap::new(),
162            next_slot_id: 0,
163            max_slots,
164            total_allocations: 0,
165            total_deallocations: 0,
166        }
167    }
168
169    /// Attempt to check out a slot suitable for `requested_bytes` at the given
170    /// logical `tick`.
171    pub fn allocate(&mut self, requested_bytes: u64, tick: u64) -> Option<u64> {
172        let class = SizeClass::classify(requested_bytes);
173
174        let reuse_id = self
175            .slots
176            .values()
177            .filter(|s| !s.in_use && s.size_class == class)
178            .map(|s| s.slot_id)
179            .next();
180
181        if let Some(id) = reuse_id {
182            let slot = self.slots.get_mut(&id)?;
183            slot.in_use = true;
184            slot.allocated_bytes = requested_bytes;
185            slot.last_used_tick = tick;
186            self.total_allocations += 1;
187            return Some(id);
188        }
189
190        if self.slots.len() >= self.max_slots {
191            return None;
192        }
193
194        let id = self.next_slot_id;
195        self.next_slot_id += 1;
196
197        let slot = PoolSlot {
198            slot_id: id,
199            size_class: class,
200            allocated_bytes: requested_bytes,
201            in_use: true,
202            last_used_tick: tick,
203        };
204        self.slots.insert(id, slot);
205        self.total_allocations += 1;
206        Some(id)
207    }
208
209    /// Return slot `slot_id` to the pool at the given `tick`.
210    ///
211    /// Returns `false` if the slot does not exist or was already free.
212    pub fn deallocate(&mut self, slot_id: u64, tick: u64) -> bool {
213        match self.slots.get_mut(&slot_id) {
214            Some(slot) if slot.in_use => {
215                slot.in_use = false;
216                slot.last_used_tick = tick;
217                self.total_deallocations += 1;
218                true
219            }
220            _ => false,
221        }
222    }
223
224    /// Evict every **free** slot whose last-used tick is at least `idle_ticks`
225    /// ticks in the past.
226    pub fn evict_idle(&mut self, tick: u64, idle_ticks: u64) {
227        self.slots.retain(|_, slot| {
228            if slot.in_use {
229                return true;
230            }
231            let age = tick.saturating_sub(slot.last_used_tick);
232            age < idle_ticks
233        });
234    }
235
236    /// Return all slots belonging to `class`, sorted by `slot_id` ascending.
237    pub fn slots_for_class(&self, class: SizeClass) -> Vec<&PoolSlot> {
238        let mut result: Vec<&PoolSlot> = self
239            .slots
240            .values()
241            .filter(|s| s.size_class == class)
242            .collect();
243        result.sort_by_key(|s| s.slot_id);
244        result
245    }
246
247    /// Compute aggregate statistics for the current pool state.
248    pub fn stats(&self) -> MemoryPoolStats {
249        let total_slots = self.slots.len();
250        let in_use_slots = self.slots.values().filter(|s| s.in_use).count();
251        let free_slots = total_slots - in_use_slots;
252
253        let total_allocated_bytes: u64 = self
254            .slots
255            .values()
256            .map(|s| s.size_class.bucket_size())
257            .sum();
258
259        let wasted_bytes: u64 = self
260            .slots
261            .values()
262            .filter(|s| s.in_use)
263            .map(|s| s.size_class.bucket_size().saturating_sub(s.allocated_bytes))
264            .sum();
265
266        MemoryPoolStats {
267            total_slots,
268            in_use_slots,
269            free_slots,
270            total_allocated_bytes,
271            wasted_bytes,
272        }
273    }
274}
275
276// ===========================================================================
277// Part 2: TensorBlockPool — pre-allocated fixed-size block pool
278// ===========================================================================
279
280// ---------------------------------------------------------------------------
281// BlockStatus
282// ---------------------------------------------------------------------------
283
284/// Status of a single block within a [`TensorBlockPool`].
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub enum BlockStatus {
287    /// Available for allocation.
288    Free,
289    /// Currently owned by an operation.
290    Allocated,
291    /// Temporarily held for future use.
292    Reserved,
293}
294
295// ---------------------------------------------------------------------------
296// MemoryBlock
297// ---------------------------------------------------------------------------
298
299/// A single block managed by the [`TensorBlockPool`].
300#[derive(Debug, Clone)]
301pub struct MemoryBlock {
302    /// Unique identifier for this block.
303    pub id: u64,
304    /// Size of the block in bytes.
305    pub size: usize,
306    /// Current status of the block.
307    pub status: BlockStatus,
308    /// Name of the operation that currently owns this block.
309    pub owner: Option<String>,
310    /// Incremented on each reuse (deallocation cycle).
311    pub generation: u64,
312}
313
314// ---------------------------------------------------------------------------
315// PoolConfig
316// ---------------------------------------------------------------------------
317
318/// Configuration for a [`TensorBlockPool`].
319#[derive(Debug, Clone)]
320pub struct PoolConfig {
321    /// Number of blocks to pre-allocate on construction.
322    pub initial_blocks: usize,
323    /// Size of each block in bytes.
324    pub block_size: usize,
325    /// Maximum number of blocks the pool may hold.
326    pub max_blocks: usize,
327    /// Whether the pool may grow beyond `initial_blocks` (up to `max_blocks`).
328    pub allow_growth: bool,
329}
330
331impl Default for PoolConfig {
332    fn default() -> Self {
333        PoolConfig {
334            initial_blocks: 64,
335            block_size: 4096,
336            max_blocks: 1024,
337            allow_growth: true,
338        }
339    }
340}
341
342// ---------------------------------------------------------------------------
343// BlockPoolStats
344// ---------------------------------------------------------------------------
345
346/// Aggregate statistics for a [`TensorBlockPool`].
347#[derive(Debug, Clone)]
348pub struct BlockPoolStats {
349    /// Total number of blocks in the pool.
350    pub total_blocks: usize,
351    /// Number of blocks with status [`BlockStatus::Free`].
352    pub free_blocks: usize,
353    /// Number of blocks with status [`BlockStatus::Allocated`].
354    pub allocated_blocks: usize,
355    /// Number of blocks with status [`BlockStatus::Reserved`].
356    pub reserved_blocks: usize,
357    /// Peak number of simultaneously allocated blocks.
358    pub peak_allocated: usize,
359    /// Total successful allocations since creation.
360    pub total_allocations: u64,
361    /// Total successful deallocations since creation.
362    pub total_deallocations: u64,
363    /// Fraction of blocks currently allocated (allocated / total).
364    pub utilization: f64,
365}
366
367// ---------------------------------------------------------------------------
368// TensorBlockPool
369// ---------------------------------------------------------------------------
370
371/// Pre-allocated memory pool for tensor operations to reduce allocation
372/// overhead.
373///
374/// All blocks share a single configured size.  The pool supports allocation,
375/// deallocation, reservation, defragmentation, and shrink-to-fit.
376pub struct TensorBlockPool {
377    config: PoolConfig,
378    blocks: Vec<MemoryBlock>,
379    next_id: u64,
380    allocations: u64,
381    deallocations: u64,
382    peak_allocated: usize,
383    current_allocated: usize,
384}
385
386impl TensorBlockPool {
387    /// Create a new block pool, pre-allocating `config.initial_blocks` blocks.
388    pub fn new(config: PoolConfig) -> Self {
389        let mut blocks = Vec::with_capacity(config.initial_blocks);
390        for i in 0..config.initial_blocks {
391            blocks.push(MemoryBlock {
392                id: i as u64,
393                size: config.block_size,
394                status: BlockStatus::Free,
395                owner: None,
396                generation: 0,
397            });
398        }
399        TensorBlockPool {
400            next_id: config.initial_blocks as u64,
401            config,
402            blocks,
403            allocations: 0,
404            deallocations: 0,
405            peak_allocated: 0,
406            current_allocated: 0,
407        }
408    }
409
410    /// Allocate a free block, marking it as [`BlockStatus::Allocated`] and
411    /// assigning it to `owner`.
412    ///
413    /// If no free block is available and `allow_growth` is true, a new block is
414    /// created (up to `max_blocks`).  Returns an error if the pool is exhausted.
415    pub fn allocate(&mut self, owner: &str) -> Result<u64, String> {
416        // Search for a free block.
417        if let Some(block) = self
418            .blocks
419            .iter_mut()
420            .find(|b| b.status == BlockStatus::Free)
421        {
422            block.status = BlockStatus::Allocated;
423            block.owner = Some(owner.to_string());
424            self.allocations += 1;
425            self.current_allocated += 1;
426            if self.current_allocated > self.peak_allocated {
427                self.peak_allocated = self.current_allocated;
428            }
429            return Ok(block.id);
430        }
431
432        // No free block — try to grow.
433        if !self.config.allow_growth {
434            return Err("pool exhausted and growth is disabled".to_string());
435        }
436        if self.blocks.len() >= self.config.max_blocks {
437            return Err(format!(
438                "pool exhausted: max_blocks ({}) reached",
439                self.config.max_blocks
440            ));
441        }
442
443        let id = self.next_id;
444        self.next_id += 1;
445        self.blocks.push(MemoryBlock {
446            id,
447            size: self.config.block_size,
448            status: BlockStatus::Allocated,
449            owner: Some(owner.to_string()),
450            generation: 0,
451        });
452        self.allocations += 1;
453        self.current_allocated += 1;
454        if self.current_allocated > self.peak_allocated {
455            self.peak_allocated = self.current_allocated;
456        }
457        Ok(id)
458    }
459
460    /// Deallocate the block with the given `block_id`, marking it
461    /// [`BlockStatus::Free`], clearing its owner, and incrementing its
462    /// generation.
463    pub fn deallocate(&mut self, block_id: u64) -> Result<(), String> {
464        let block = self
465            .blocks
466            .iter_mut()
467            .find(|b| b.id == block_id)
468            .ok_or_else(|| format!("block {} not found", block_id))?;
469
470        if block.status != BlockStatus::Allocated {
471            return Err(format!(
472                "block {} is not allocated (status: {:?})",
473                block_id, block.status
474            ));
475        }
476
477        block.status = BlockStatus::Free;
478        block.owner = None;
479        block.generation += 1;
480        self.deallocations += 1;
481        self.current_allocated = self.current_allocated.saturating_sub(1);
482        Ok(())
483    }
484
485    /// Reserve a free block for future use.
486    ///
487    /// Transitions a block from [`BlockStatus::Free`] to
488    /// [`BlockStatus::Reserved`].
489    pub fn reserve(&mut self, block_id: u64, owner: &str) -> Result<(), String> {
490        let block = self
491            .blocks
492            .iter_mut()
493            .find(|b| b.id == block_id)
494            .ok_or_else(|| format!("block {} not found", block_id))?;
495
496        if block.status != BlockStatus::Free {
497            return Err(format!(
498                "block {} is not free (status: {:?})",
499                block_id, block.status
500            ));
501        }
502
503        block.status = BlockStatus::Reserved;
504        block.owner = Some(owner.to_string());
505        Ok(())
506    }
507
508    /// Release a reservation, returning the block to [`BlockStatus::Free`].
509    pub fn release_reservation(&mut self, block_id: u64) -> Result<(), String> {
510        let block = self
511            .blocks
512            .iter_mut()
513            .find(|b| b.id == block_id)
514            .ok_or_else(|| format!("block {} not found", block_id))?;
515
516        if block.status != BlockStatus::Reserved {
517            return Err(format!(
518                "block {} is not reserved (status: {:?})",
519                block_id, block.status
520            ));
521        }
522
523        block.status = BlockStatus::Free;
524        block.owner = None;
525        Ok(())
526    }
527
528    /// Look up a block by ID.
529    pub fn get_block(&self, block_id: u64) -> Option<&MemoryBlock> {
530        self.blocks.iter().find(|b| b.id == block_id)
531    }
532
533    /// Number of free blocks.
534    pub fn free_count(&self) -> usize {
535        self.blocks
536            .iter()
537            .filter(|b| b.status == BlockStatus::Free)
538            .count()
539    }
540
541    /// Number of allocated blocks.
542    pub fn allocated_count(&self) -> usize {
543        self.blocks
544            .iter()
545            .filter(|b| b.status == BlockStatus::Allocated)
546            .count()
547    }
548
549    /// Fraction of blocks currently allocated (allocated / total).
550    /// Returns `0.0` if the pool is empty.
551    pub fn utilization(&self) -> f64 {
552        if self.blocks.is_empty() {
553            0.0
554        } else {
555            self.allocated_count() as f64 / self.blocks.len() as f64
556        }
557    }
558
559    /// Compact the block list: move all [`BlockStatus::Free`] blocks to the
560    /// end.  Resets the generation of relocated free blocks to zero.
561    ///
562    /// Returns the number of free blocks that were relocated.
563    pub fn defragment(&mut self) -> usize {
564        // Partition: non-free first, free last.  Count how many free blocks
565        // existed *before* the first non-free block (i.e. how many were moved).
566        let before_free_positions: Vec<usize> = self
567            .blocks
568            .iter()
569            .enumerate()
570            .filter(|(_, b)| b.status == BlockStatus::Free)
571            .map(|(i, _)| i)
572            .collect();
573
574        // Stable partition so allocated/reserved order is preserved.
575        self.blocks.sort_by_key(|b| {
576            if b.status == BlockStatus::Free {
577                1u8
578            } else {
579                0u8
580            }
581        });
582
583        // Reset generation on free blocks that were compacted.
584        let mut relocated = 0usize;
585        for block in &mut self.blocks {
586            if block.status == BlockStatus::Free {
587                block.generation = 0;
588                relocated += 1;
589            }
590        }
591
592        // Only count blocks that actually changed position.
593        // A simpler metric: return the number of free blocks that existed
594        // before (all of them were potentially moved during sort).
595        let _ = before_free_positions; // consumed above for counting
596        relocated
597    }
598
599    /// Remove trailing [`BlockStatus::Free`] blocks from the pool, shrinking
600    /// it.  Returns the number of blocks removed.
601    pub fn shrink_to_fit(&mut self) -> usize {
602        let mut removed = 0usize;
603        while self
604            .blocks
605            .last()
606            .is_some_and(|b| b.status == BlockStatus::Free)
607        {
608            self.blocks.pop();
609            removed += 1;
610        }
611        removed
612    }
613
614    /// Return a snapshot of pool statistics.
615    pub fn block_stats(&self) -> BlockPoolStats {
616        let total_blocks = self.blocks.len();
617        let free_blocks = self.free_count();
618        let allocated_blocks = self.allocated_count();
619        let reserved_blocks = self
620            .blocks
621            .iter()
622            .filter(|b| b.status == BlockStatus::Reserved)
623            .count();
624
625        BlockPoolStats {
626            total_blocks,
627            free_blocks,
628            allocated_blocks,
629            reserved_blocks,
630            peak_allocated: self.peak_allocated,
631            total_allocations: self.allocations,
632            total_deallocations: self.deallocations,
633            utilization: self.utilization(),
634        }
635    }
636}
637
638// ===========================================================================
639// Tests
640// ===========================================================================
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    // =======================================================================
647    // Part 1 — TensorMemoryPool (slab-based) tests
648    // =======================================================================
649
650    #[test]
651    fn slab_new_starts_empty() {
652        let pool = TensorMemoryPool::new(1024);
653        assert_eq!(pool.slots.len(), 0);
654        assert_eq!(pool.total_allocations, 0);
655        assert_eq!(pool.total_deallocations, 0);
656    }
657
658    #[test]
659    fn slab_allocate_creates_slot_when_empty() {
660        let mut pool = TensorMemoryPool::new(1024);
661        let id = pool.allocate(100, 1);
662        assert!(id.is_some());
663        assert_eq!(pool.slots.len(), 1);
664    }
665
666    #[test]
667    fn slab_allocate_reuses_free_slot_of_same_class() {
668        let mut pool = TensorMemoryPool::new(1024);
669        let id1 = pool.allocate(100, 1);
670        if let Some(id) = id1 {
671            pool.deallocate(id, 2);
672            let id2 = pool.allocate(200, 3);
673            assert_eq!(id2, Some(id));
674            assert_eq!(pool.slots.len(), 1);
675        }
676    }
677
678    #[test]
679    fn slab_allocate_creates_new_slot_when_no_free_of_class() {
680        let mut pool = TensorMemoryPool::new(1024);
681        let _id1 = pool.allocate(100, 1);
682        let id2 = pool.allocate(10_000, 2);
683        assert!(id2.is_some());
684        assert_eq!(pool.slots.len(), 2);
685    }
686
687    #[test]
688    fn slab_allocate_returns_none_at_max_slots() {
689        let mut pool = TensorMemoryPool::new(2);
690        let _a = pool.allocate(100, 1);
691        let _b = pool.allocate(200, 2);
692        let c = pool.allocate(300, 3);
693        assert!(c.is_none());
694    }
695
696    #[test]
697    fn slab_allocate_increments_total_allocations() {
698        let mut pool = TensorMemoryPool::new(1024);
699        pool.allocate(100, 1);
700        pool.allocate(200, 2);
701        assert_eq!(pool.total_allocations, 2);
702    }
703
704    #[test]
705    fn classify_boundary_4096() {
706        assert_eq!(SizeClass::classify(4096), SizeClass::Small);
707    }
708
709    #[test]
710    fn classify_boundary_4097() {
711        assert_eq!(SizeClass::classify(4097), SizeClass::Medium);
712    }
713
714    #[test]
715    fn classify_boundary_65536() {
716        assert_eq!(SizeClass::classify(65_536), SizeClass::Medium);
717    }
718
719    #[test]
720    fn classify_boundary_65537() {
721        assert_eq!(SizeClass::classify(65_537), SizeClass::Large);
722    }
723
724    #[test]
725    fn classify_boundary_1048576() {
726        assert_eq!(SizeClass::classify(1_048_576), SizeClass::Large);
727    }
728
729    #[test]
730    fn classify_boundary_1048577() {
731        assert_eq!(SizeClass::classify(1_048_577), SizeClass::Huge);
732    }
733
734    #[test]
735    fn bucket_size_small() {
736        assert_eq!(SizeClass::Small.bucket_size(), 4_096);
737    }
738
739    #[test]
740    fn bucket_size_medium() {
741        assert_eq!(SizeClass::Medium.bucket_size(), 65_536);
742    }
743
744    #[test]
745    fn bucket_size_large() {
746        assert_eq!(SizeClass::Large.bucket_size(), 1_048_576);
747    }
748
749    #[test]
750    fn bucket_size_huge() {
751        assert_eq!(SizeClass::Huge.bucket_size(), 16_777_216);
752    }
753
754    #[test]
755    fn size_class_ordering() {
756        assert!(SizeClass::Small < SizeClass::Medium);
757        assert!(SizeClass::Medium < SizeClass::Large);
758        assert!(SizeClass::Large < SizeClass::Huge);
759    }
760
761    #[test]
762    fn slab_deallocate_marks_slot_free() {
763        let mut pool = TensorMemoryPool::new(1024);
764        if let Some(id) = pool.allocate(100, 1) {
765            assert!(pool.slots[&id].in_use);
766            pool.deallocate(id, 2);
767            assert!(!pool.slots[&id].in_use);
768        }
769    }
770
771    #[test]
772    fn slab_deallocate_returns_false_for_unknown_id() {
773        let mut pool = TensorMemoryPool::new(1024);
774        assert!(!pool.deallocate(9999, 1));
775    }
776
777    #[test]
778    fn slab_deallocate_returns_false_if_already_free() {
779        let mut pool = TensorMemoryPool::new(1024);
780        if let Some(id) = pool.allocate(100, 1) {
781            pool.deallocate(id, 2);
782            assert!(!pool.deallocate(id, 3));
783        }
784    }
785
786    #[test]
787    fn slab_deallocate_increments_total_deallocations() {
788        let mut pool = TensorMemoryPool::new(1024);
789        if let Some(id) = pool.allocate(100, 1) {
790            pool.deallocate(id, 2);
791            assert_eq!(pool.total_deallocations, 1);
792        }
793    }
794
795    #[test]
796    fn slab_evict_idle_removes_old_free_slots() {
797        let mut pool = TensorMemoryPool::new(1024);
798        if let Some(id) = pool.allocate(100, 0) {
799            pool.deallocate(id, 1);
800            pool.evict_idle(100, 10);
801            assert!(pool.slots.is_empty());
802        }
803    }
804
805    #[test]
806    fn slab_evict_idle_keeps_recent_free_slots() {
807        let mut pool = TensorMemoryPool::new(1024);
808        if let Some(id) = pool.allocate(100, 0) {
809            pool.deallocate(id, 95);
810            pool.evict_idle(100, 10);
811            assert_eq!(pool.slots.len(), 1);
812        }
813    }
814
815    #[test]
816    fn slab_evict_idle_does_not_evict_in_use_slots() {
817        let mut pool = TensorMemoryPool::new(1024);
818        let _id = pool.allocate(100, 0);
819        pool.evict_idle(9999, 1);
820        assert_eq!(pool.slots.len(), 1);
821    }
822
823    #[test]
824    fn slab_slots_for_class_filters_and_sorts() {
825        let mut pool = TensorMemoryPool::new(1024);
826        let _s1 = pool.allocate(100, 1);
827        let _m1 = pool.allocate(10_000, 2);
828        let _s2 = pool.allocate(200, 3);
829        let small_slots = pool.slots_for_class(SizeClass::Small);
830        assert_eq!(small_slots.len(), 2);
831        assert!(small_slots[0].slot_id < small_slots[1].slot_id);
832        let medium_slots = pool.slots_for_class(SizeClass::Medium);
833        assert_eq!(medium_slots.len(), 1);
834    }
835
836    #[test]
837    fn slab_stats_total_slots_in_use_free() {
838        let mut pool = TensorMemoryPool::new(1024);
839        if let Some(id1) = pool.allocate(100, 1) {
840            let _id2 = pool.allocate(200, 2);
841            pool.deallocate(id1, 3);
842            let s = pool.stats();
843            assert_eq!(s.total_slots, 2);
844            assert_eq!(s.in_use_slots, 1);
845            assert_eq!(s.free_slots, 1);
846        }
847    }
848
849    #[test]
850    fn slab_stats_total_allocated_bytes() {
851        let mut pool = TensorMemoryPool::new(1024);
852        let _id = pool.allocate(100, 1);
853        let s = pool.stats();
854        assert_eq!(s.total_allocated_bytes, 4_096);
855    }
856
857    #[test]
858    fn slab_stats_wasted_bytes_calculation() {
859        let mut pool = TensorMemoryPool::new(1024);
860        let _id = pool.allocate(100, 1);
861        let s = pool.stats();
862        assert_eq!(s.wasted_bytes, 4_096 - 100);
863    }
864
865    #[test]
866    fn slab_stats_utilization_computed() {
867        let mut pool = TensorMemoryPool::new(1024);
868        if let Some(id1) = pool.allocate(100, 1) {
869            let _id2 = pool.allocate(200, 2);
870            pool.deallocate(id1, 3);
871            let s = pool.stats();
872            let util = s.utilization();
873            assert!((util - 0.5).abs() < f64::EPSILON);
874        }
875    }
876
877    #[test]
878    fn slab_stats_utilization_empty_pool() {
879        let pool = TensorMemoryPool::new(1024);
880        assert_eq!(pool.stats().utilization(), 0.0);
881    }
882
883    // =======================================================================
884    // Part 2 — TensorBlockPool tests (25+)
885    // =======================================================================
886
887    // -- Construction / Pre-allocation --
888
889    #[test]
890    fn block_pool_initial_preallocation() {
891        let config = PoolConfig {
892            initial_blocks: 16,
893            block_size: 1024,
894            max_blocks: 64,
895            allow_growth: true,
896        };
897        let pool = TensorBlockPool::new(config);
898        assert_eq!(pool.blocks.len(), 16);
899        assert_eq!(pool.free_count(), 16);
900        assert_eq!(pool.allocated_count(), 0);
901    }
902
903    #[test]
904    fn block_pool_default_config() {
905        let config = PoolConfig::default();
906        assert_eq!(config.initial_blocks, 64);
907        assert_eq!(config.block_size, 4096);
908        assert_eq!(config.max_blocks, 1024);
909        assert!(config.allow_growth);
910    }
911
912    #[test]
913    fn block_pool_zero_initial_blocks() {
914        let config = PoolConfig {
915            initial_blocks: 0,
916            block_size: 256,
917            max_blocks: 10,
918            allow_growth: true,
919        };
920        let pool = TensorBlockPool::new(config);
921        assert_eq!(pool.blocks.len(), 0);
922        assert_eq!(pool.free_count(), 0);
923    }
924
925    // -- Allocate / Deallocate lifecycle --
926
927    #[test]
928    fn block_pool_allocate_returns_valid_id() {
929        let mut pool = TensorBlockPool::new(PoolConfig::default());
930        let id = pool.allocate("matmul");
931        assert!(id.is_ok());
932    }
933
934    #[test]
935    fn block_pool_allocate_marks_block_allocated() {
936        let mut pool = TensorBlockPool::new(PoolConfig::default());
937        let id = pool.allocate("conv2d").expect("should allocate");
938        let block = pool.get_block(id).expect("block should exist");
939        assert_eq!(block.status, BlockStatus::Allocated);
940        assert_eq!(block.owner.as_deref(), Some("conv2d"));
941    }
942
943    #[test]
944    fn block_pool_deallocate_frees_block() {
945        let mut pool = TensorBlockPool::new(PoolConfig::default());
946        let id = pool.allocate("relu").expect("should allocate");
947        pool.deallocate(id).expect("should deallocate");
948        let block = pool.get_block(id).expect("block should exist");
949        assert_eq!(block.status, BlockStatus::Free);
950        assert!(block.owner.is_none());
951    }
952
953    #[test]
954    fn block_pool_deallocate_nonexistent_errors() {
955        let mut pool = TensorBlockPool::new(PoolConfig::default());
956        let result = pool.deallocate(99999);
957        assert!(result.is_err());
958    }
959
960    #[test]
961    fn block_pool_deallocate_free_block_errors() {
962        let config = PoolConfig {
963            initial_blocks: 4,
964            block_size: 256,
965            max_blocks: 8,
966            allow_growth: false,
967        };
968        let mut pool = TensorBlockPool::new(config);
969        // Block 0 is free (pre-allocated but not allocated).
970        let result = pool.deallocate(0);
971        assert!(result.is_err());
972    }
973
974    #[test]
975    fn block_pool_allocate_deallocate_cycle() {
976        let config = PoolConfig {
977            initial_blocks: 2,
978            block_size: 512,
979            max_blocks: 2,
980            allow_growth: false,
981        };
982        let mut pool = TensorBlockPool::new(config);
983
984        let id1 = pool.allocate("op_a").expect("first alloc");
985        let id2 = pool.allocate("op_b").expect("second alloc");
986        assert!(pool.allocate("op_c").is_err()); // pool full
987
988        pool.deallocate(id1).expect("dealloc id1");
989        let id3 = pool.allocate("op_c").expect("reuse freed block");
990        assert_eq!(id3, id1); // reuses the freed block
991        pool.deallocate(id2).expect("dealloc id2");
992        pool.deallocate(id3).expect("dealloc id3");
993
994        assert_eq!(pool.free_count(), 2);
995        assert_eq!(pool.allocated_count(), 0);
996    }
997
998    // -- Generation tracking --
999
1000    #[test]
1001    fn block_pool_generation_increments_on_reuse() {
1002        let mut pool = TensorBlockPool::new(PoolConfig::default());
1003        let id = pool.allocate("gen_test").expect("alloc");
1004        assert_eq!(pool.get_block(id).expect("exists").generation, 0);
1005
1006        pool.deallocate(id).expect("dealloc");
1007        assert_eq!(pool.get_block(id).expect("exists").generation, 1);
1008
1009        // Allocate again (same block reused), then deallocate.
1010        let id2 = pool.allocate("gen_test_2").expect("realloc");
1011        assert_eq!(id2, id);
1012        pool.deallocate(id2).expect("dealloc again");
1013        assert_eq!(pool.get_block(id).expect("exists").generation, 2);
1014    }
1015
1016    // -- Reserve / Release --
1017
1018    #[test]
1019    fn block_pool_reserve_free_block() {
1020        let config = PoolConfig {
1021            initial_blocks: 4,
1022            block_size: 256,
1023            max_blocks: 4,
1024            allow_growth: false,
1025        };
1026        let mut pool = TensorBlockPool::new(config);
1027
1028        pool.reserve(0, "future_op").expect("should reserve");
1029        let block = pool.get_block(0).expect("exists");
1030        assert_eq!(block.status, BlockStatus::Reserved);
1031        assert_eq!(block.owner.as_deref(), Some("future_op"));
1032    }
1033
1034    #[test]
1035    fn block_pool_reserve_non_free_errors() {
1036        let mut pool = TensorBlockPool::new(PoolConfig::default());
1037        let id = pool.allocate("busy").expect("alloc");
1038        let result = pool.reserve(id, "should_fail");
1039        assert!(result.is_err());
1040    }
1041
1042    #[test]
1043    fn block_pool_release_reservation() {
1044        let config = PoolConfig {
1045            initial_blocks: 4,
1046            block_size: 256,
1047            max_blocks: 4,
1048            allow_growth: false,
1049        };
1050        let mut pool = TensorBlockPool::new(config);
1051
1052        pool.reserve(1, "temp").expect("reserve");
1053        pool.release_reservation(1).expect("release");
1054        let block = pool.get_block(1).expect("exists");
1055        assert_eq!(block.status, BlockStatus::Free);
1056        assert!(block.owner.is_none());
1057    }
1058
1059    #[test]
1060    fn block_pool_release_non_reserved_errors() {
1061        let mut pool = TensorBlockPool::new(PoolConfig::default());
1062        let id = pool.allocate("op").expect("alloc");
1063        let result = pool.release_reservation(id);
1064        assert!(result.is_err());
1065    }
1066
1067    #[test]
1068    fn block_pool_reserved_blocks_not_allocatable() {
1069        let config = PoolConfig {
1070            initial_blocks: 1,
1071            block_size: 256,
1072            max_blocks: 1,
1073            allow_growth: false,
1074        };
1075        let mut pool = TensorBlockPool::new(config);
1076        pool.reserve(0, "held").expect("reserve");
1077        // The only block is reserved, so allocation should fail.
1078        let result = pool.allocate("want_block");
1079        assert!(result.is_err());
1080    }
1081
1082    // -- Pool growth --
1083
1084    #[test]
1085    fn block_pool_growth_when_allowed() {
1086        let config = PoolConfig {
1087            initial_blocks: 2,
1088            block_size: 128,
1089            max_blocks: 4,
1090            allow_growth: true,
1091        };
1092        let mut pool = TensorBlockPool::new(config);
1093
1094        let _a = pool.allocate("a").expect("alloc a");
1095        let _b = pool.allocate("b").expect("alloc b");
1096        // Both initial blocks used; pool should grow.
1097        let c = pool.allocate("c");
1098        assert!(c.is_ok());
1099        assert_eq!(pool.blocks.len(), 3);
1100    }
1101
1102    #[test]
1103    fn block_pool_growth_stops_at_max_blocks() {
1104        let config = PoolConfig {
1105            initial_blocks: 1,
1106            block_size: 128,
1107            max_blocks: 2,
1108            allow_growth: true,
1109        };
1110        let mut pool = TensorBlockPool::new(config);
1111
1112        let _a = pool.allocate("a").expect("alloc a");
1113        let _b = pool.allocate("b").expect("alloc b (growth)");
1114        let c = pool.allocate("c");
1115        assert!(c.is_err());
1116    }
1117
1118    #[test]
1119    fn block_pool_no_growth_when_disabled() {
1120        let config = PoolConfig {
1121            initial_blocks: 1,
1122            block_size: 128,
1123            max_blocks: 100,
1124            allow_growth: false,
1125        };
1126        let mut pool = TensorBlockPool::new(config);
1127
1128        let _a = pool.allocate("a").expect("alloc a");
1129        let b = pool.allocate("b");
1130        assert!(b.is_err());
1131    }
1132
1133    // -- Utilization --
1134
1135    #[test]
1136    fn block_pool_utilization_calculation() {
1137        let config = PoolConfig {
1138            initial_blocks: 4,
1139            block_size: 256,
1140            max_blocks: 4,
1141            allow_growth: false,
1142        };
1143        let mut pool = TensorBlockPool::new(config);
1144
1145        let _a = pool.allocate("a").expect("alloc");
1146        let _b = pool.allocate("b").expect("alloc");
1147        // 2 allocated out of 4.
1148        let u = pool.utilization();
1149        assert!((u - 0.5).abs() < f64::EPSILON);
1150    }
1151
1152    #[test]
1153    fn block_pool_utilization_empty() {
1154        let config = PoolConfig {
1155            initial_blocks: 0,
1156            block_size: 256,
1157            max_blocks: 10,
1158            allow_growth: true,
1159        };
1160        let pool = TensorBlockPool::new(config);
1161        assert!((pool.utilization()).abs() < f64::EPSILON);
1162    }
1163
1164    // -- Peak tracking --
1165
1166    #[test]
1167    fn block_pool_peak_allocated_tracking() {
1168        let mut pool = TensorBlockPool::new(PoolConfig::default());
1169
1170        let a = pool.allocate("a").expect("alloc");
1171        let b = pool.allocate("b").expect("alloc");
1172        let _c = pool.allocate("c").expect("alloc");
1173        // peak = 3
1174        pool.deallocate(a).expect("dealloc");
1175        pool.deallocate(b).expect("dealloc");
1176        // current = 1, but peak should still be 3
1177        assert_eq!(pool.peak_allocated, 3);
1178    }
1179
1180    // -- Free / Allocated counts --
1181
1182    #[test]
1183    fn block_pool_free_allocated_counts() {
1184        let config = PoolConfig {
1185            initial_blocks: 5,
1186            block_size: 256,
1187            max_blocks: 5,
1188            allow_growth: false,
1189        };
1190        let mut pool = TensorBlockPool::new(config);
1191
1192        let a = pool.allocate("a").expect("alloc");
1193        let _b = pool.allocate("b").expect("alloc");
1194        pool.reserve(2, "r").expect("reserve");
1195
1196        assert_eq!(pool.free_count(), 2); // blocks 3, 4
1197        assert_eq!(pool.allocated_count(), 2); // a, b
1198        pool.deallocate(a).expect("dealloc");
1199        assert_eq!(pool.free_count(), 3);
1200        assert_eq!(pool.allocated_count(), 1);
1201    }
1202
1203    // -- Stats --
1204
1205    #[test]
1206    fn block_pool_stats_accuracy() {
1207        let config = PoolConfig {
1208            initial_blocks: 8,
1209            block_size: 512,
1210            max_blocks: 8,
1211            allow_growth: false,
1212        };
1213        let mut pool = TensorBlockPool::new(config);
1214
1215        let a = pool.allocate("a").expect("alloc");
1216        let _b = pool.allocate("b").expect("alloc");
1217        let _c = pool.allocate("c").expect("alloc");
1218        pool.reserve(3, "reserved_op").expect("reserve");
1219        pool.deallocate(a).expect("dealloc");
1220
1221        let stats = pool.block_stats();
1222        assert_eq!(stats.total_blocks, 8);
1223        assert_eq!(stats.free_blocks, 5); // 4 untouched + 1 deallocated
1224        assert_eq!(stats.allocated_blocks, 2);
1225        assert_eq!(stats.reserved_blocks, 1);
1226        assert_eq!(stats.peak_allocated, 3);
1227        assert_eq!(stats.total_allocations, 3);
1228        assert_eq!(stats.total_deallocations, 1);
1229        // utilization = 2/8 = 0.25
1230        assert!((stats.utilization - 0.25).abs() < f64::EPSILON);
1231    }
1232
1233    // -- Defragment --
1234
1235    #[test]
1236    fn block_pool_defragment_moves_free_to_end() {
1237        let config = PoolConfig {
1238            initial_blocks: 4,
1239            block_size: 256,
1240            max_blocks: 4,
1241            allow_growth: false,
1242        };
1243        let mut pool = TensorBlockPool::new(config);
1244
1245        // Allocate all, then free blocks 0 and 2 to create gaps.
1246        let id0 = pool.allocate("a").expect("alloc");
1247        let _id1 = pool.allocate("b").expect("alloc");
1248        let id2 = pool.allocate("c").expect("alloc");
1249        let _id3 = pool.allocate("d").expect("alloc");
1250        pool.deallocate(id0).expect("dealloc");
1251        pool.deallocate(id2).expect("dealloc");
1252
1253        let relocated = pool.defragment();
1254        assert!(relocated > 0);
1255
1256        // After defragment, allocated blocks come first.
1257        let statuses: Vec<BlockStatus> = pool.blocks.iter().map(|b| b.status).collect();
1258        let first_free = statuses.iter().position(|s| *s == BlockStatus::Free);
1259        let last_alloc = statuses.iter().rposition(|s| *s == BlockStatus::Allocated);
1260        if let (Some(ff), Some(la)) = (first_free, last_alloc) {
1261            assert!(la < ff, "allocated blocks should precede free blocks");
1262        }
1263    }
1264
1265    #[test]
1266    fn block_pool_defragment_resets_generation() {
1267        let config = PoolConfig {
1268            initial_blocks: 2,
1269            block_size: 256,
1270            max_blocks: 2,
1271            allow_growth: false,
1272        };
1273        let mut pool = TensorBlockPool::new(config);
1274
1275        let id = pool.allocate("x").expect("alloc");
1276        pool.deallocate(id).expect("dealloc");
1277        // generation is now 1
1278        assert_eq!(pool.get_block(id).expect("exists").generation, 1);
1279
1280        pool.defragment();
1281        // After defragment, free block generations are reset to 0.
1282        for block in &pool.blocks {
1283            if block.status == BlockStatus::Free {
1284                assert_eq!(block.generation, 0);
1285            }
1286        }
1287    }
1288
1289    // -- Shrink to fit --
1290
1291    #[test]
1292    fn block_pool_shrink_to_fit_removes_trailing_free() {
1293        let config = PoolConfig {
1294            initial_blocks: 6,
1295            block_size: 256,
1296            max_blocks: 6,
1297            allow_growth: false,
1298        };
1299        let mut pool = TensorBlockPool::new(config);
1300
1301        // Allocate first 3, leave last 3 free.
1302        let _a = pool.allocate("a").expect("alloc");
1303        let _b = pool.allocate("b").expect("alloc");
1304        let _c = pool.allocate("c").expect("alloc");
1305
1306        // Defragment so free blocks are at the end.
1307        pool.defragment();
1308
1309        let removed = pool.shrink_to_fit();
1310        assert_eq!(removed, 3);
1311        assert_eq!(pool.blocks.len(), 3);
1312    }
1313
1314    #[test]
1315    fn block_pool_shrink_to_fit_no_trailing_free() {
1316        let config = PoolConfig {
1317            initial_blocks: 2,
1318            block_size: 256,
1319            max_blocks: 2,
1320            allow_growth: false,
1321        };
1322        let mut pool = TensorBlockPool::new(config);
1323
1324        let _a = pool.allocate("a").expect("alloc");
1325        let _b = pool.allocate("b").expect("alloc");
1326        let removed = pool.shrink_to_fit();
1327        assert_eq!(removed, 0);
1328    }
1329
1330    // -- get_block --
1331
1332    #[test]
1333    fn block_pool_get_block_existing() {
1334        let pool = TensorBlockPool::new(PoolConfig::default());
1335        let block = pool.get_block(0);
1336        assert!(block.is_some());
1337        assert_eq!(block.expect("exists").id, 0);
1338    }
1339
1340    #[test]
1341    fn block_pool_get_block_nonexistent() {
1342        let pool = TensorBlockPool::new(PoolConfig::default());
1343        assert!(pool.get_block(99999).is_none());
1344    }
1345
1346    // -- Block size in pre-allocated blocks --
1347
1348    #[test]
1349    fn block_pool_blocks_have_correct_size() {
1350        let config = PoolConfig {
1351            initial_blocks: 3,
1352            block_size: 2048,
1353            max_blocks: 10,
1354            allow_growth: true,
1355        };
1356        let pool = TensorBlockPool::new(config);
1357        for block in &pool.blocks {
1358            assert_eq!(block.size, 2048);
1359        }
1360    }
1361
1362    // -- Grown blocks also get correct size --
1363
1364    #[test]
1365    fn block_pool_grown_block_has_correct_size() {
1366        let config = PoolConfig {
1367            initial_blocks: 0,
1368            block_size: 8192,
1369            max_blocks: 5,
1370            allow_growth: true,
1371        };
1372        let mut pool = TensorBlockPool::new(config);
1373        let id = pool.allocate("grown").expect("alloc via growth");
1374        let block = pool.get_block(id).expect("exists");
1375        assert_eq!(block.size, 8192);
1376    }
1377}