Files
rustytorch/crates/training/rtx-distributed/src/memory_pool.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

1021 lines
31 KiB
Rust

//! GPU Memory Pool
//!
//! This module provides efficient GPU memory management through pooling:
//! - Reduces allocation overhead by reusing memory blocks
//! - Supports multiple allocation strategies (best-fit, first-fit)
//! - Provides memory statistics and fragmentation tracking
//! - Thread-safe for concurrent allocations
//! - Automatic defragmentation and compaction
//!
//! Memory pooling can reduce allocation overhead by 10-100x for
//! frequently allocated tensor sizes during training.
use crate::error::{DistributedError, Result};
use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
// =============================================================================
// Configuration
// =============================================================================
/// Memory allocation strategy
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AllocationStrategy {
/// Best-fit: Find smallest block that fits (less fragmentation)
BestFit,
/// First-fit: Use first block that fits (faster)
FirstFit,
/// Buddy allocator: Power-of-2 block sizes
Buddy,
/// Slab allocator: Fixed-size blocks for common sizes
Slab,
}
/// Configuration for memory pool
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryPoolConfig {
/// Initial pool size in bytes
pub initial_size_bytes: usize,
/// Maximum pool size in bytes (0 = unlimited)
pub max_size_bytes: usize,
/// Allocation strategy
pub strategy: AllocationStrategy,
/// Minimum block size in bytes
pub min_block_size: usize,
/// Alignment requirement in bytes
pub alignment: usize,
/// Enable automatic defragmentation
pub auto_defrag: bool,
/// Fragmentation threshold to trigger defrag (0.0-1.0)
pub defrag_threshold: f32,
/// Enable memory usage statistics
pub enable_stats: bool,
/// Cache freed blocks for reuse
pub cache_freed_blocks: bool,
/// Maximum cached blocks per size class
pub max_cached_per_size: usize,
}
impl Default for MemoryPoolConfig {
fn default() -> Self {
Self {
initial_size_bytes: 256 * 1024 * 1024, // 256MB
max_size_bytes: 0, // Unlimited
strategy: AllocationStrategy::BestFit,
min_block_size: 512,
alignment: 256, // GPU typically needs 256-byte alignment
auto_defrag: true,
defrag_threshold: 0.3, // Defrag when 30% fragmented
enable_stats: true,
cache_freed_blocks: true,
max_cached_per_size: 32,
}
}
}
// =============================================================================
// Memory Block
// =============================================================================
/// Unique identifier for a memory block
pub type BlockId = u64;
/// State of a memory block
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockState {
/// Block is free and available
Free,
/// Block is allocated and in use
Allocated,
/// Block is cached (freed but held for reuse)
Cached,
}
/// A memory block in the pool
#[derive(Debug, Clone)]
pub struct MemoryBlock {
/// Unique block ID
pub id: BlockId,
/// Offset from pool base address
pub offset: usize,
/// Size in bytes
pub size: usize,
/// Current state
pub state: BlockState,
/// Allocation timestamp (if allocated)
pub allocated_at: Option<Instant>,
/// Tag for debugging (e.g., "gradient", "activation")
pub tag: Option<String>,
/// Device ID
pub device_id: i32,
}
impl MemoryBlock {
/// Create a new free block
pub fn new(id: BlockId, offset: usize, size: usize, device_id: i32) -> Self {
Self {
id,
offset,
size,
state: BlockState::Free,
allocated_at: None,
tag: None,
device_id,
}
}
/// Check if block can satisfy allocation request
pub fn can_satisfy(&self, size: usize, alignment: usize) -> bool {
if self.state != BlockState::Free && self.state != BlockState::Cached {
return false;
}
let aligned_offset = (self.offset + alignment - 1) & !(alignment - 1);
let padding = aligned_offset - self.offset;
self.size >= size + padding
}
/// Get aligned offset within this block
pub fn aligned_offset(&self, alignment: usize) -> usize {
(self.offset + alignment - 1) & !(alignment - 1)
}
}
// =============================================================================
// Size Class Cache
// =============================================================================
/// Cache for commonly-used allocation sizes
pub struct SizeClassCache {
/// Size class -> list of cached block IDs
cache: HashMap<usize, VecDeque<BlockId>>,
/// Maximum blocks per size class
max_per_size: usize,
/// Size class granularity
granularity: usize,
}
impl SizeClassCache {
/// Create a new size class cache
pub fn new(max_per_size: usize, granularity: usize) -> Self {
Self {
cache: HashMap::new(),
max_per_size,
granularity,
}
}
/// Get size class for a given size
fn size_class(&self, size: usize) -> usize {
// Round up to nearest granularity
((size + self.granularity - 1) / self.granularity) * self.granularity
}
/// Try to get a cached block for the given size
pub fn get(&mut self, size: usize) -> Option<BlockId> {
let size_class = self.size_class(size);
self.cache.get_mut(&size_class)?.pop_front()
}
/// Cache a freed block
pub fn put(&mut self, size: usize, block_id: BlockId) -> bool {
let size_class = self.size_class(size);
let queue = self.cache.entry(size_class).or_default();
if queue.len() < self.max_per_size {
queue.push_back(block_id);
true
} else {
false
}
}
/// Clear all cached blocks
pub fn clear(&mut self) {
self.cache.clear();
}
/// Get total cached block count
pub fn cached_count(&self) -> usize {
self.cache
.values()
.map(std::collections::VecDeque::len)
.sum()
}
}
// =============================================================================
// Memory Pool Statistics
// =============================================================================
/// Statistics for memory pool usage
#[derive(Debug, Default, Clone)]
pub struct MemoryPoolStats {
/// Total pool size in bytes
pub total_size_bytes: usize,
/// Currently allocated bytes
pub allocated_bytes: usize,
/// Peak allocated bytes
pub peak_allocated_bytes: usize,
/// Free bytes (including fragmented)
pub free_bytes: usize,
/// Number of allocations
pub allocation_count: usize,
/// Number of deallocations
pub deallocation_count: usize,
/// Number of cache hits
pub cache_hits: usize,
/// Number of cache misses
pub cache_misses: usize,
/// Fragmentation ratio (0.0 = none, 1.0 = fully fragmented)
pub fragmentation_ratio: f32,
/// Number of free blocks
pub free_block_count: usize,
/// Average allocation size
pub avg_allocation_size: usize,
/// Number of defragmentation runs
pub defrag_count: usize,
}
impl MemoryPoolStats {
/// Calculate cache hit ratio
pub fn cache_hit_ratio(&self) -> f32 {
let total = self.cache_hits + self.cache_misses;
if total == 0 {
0.0
} else {
self.cache_hits as f32 / total as f32
}
}
/// Calculate utilization ratio
pub fn utilization(&self) -> f32 {
if self.total_size_bytes == 0 {
0.0
} else {
self.allocated_bytes as f32 / self.total_size_bytes as f32
}
}
}
// =============================================================================
// Memory Pool
// =============================================================================
/// GPU memory pool for efficient allocation
pub struct MemoryPool {
/// Configuration
config: MemoryPoolConfig,
/// Device ID
device_id: i32,
/// All memory blocks
blocks: RwLock<HashMap<BlockId, MemoryBlock>>,
/// Free blocks sorted by offset (for coalescing)
free_blocks: RwLock<BTreeMap<usize, BlockId>>,
/// Size class cache
size_cache: Mutex<SizeClassCache>,
/// Next block ID
next_block_id: AtomicUsize,
/// Statistics
stats: RwLock<MemoryPoolStats>,
/// Base address (simulated - real impl would use CUDA)
base_address: usize,
/// Current pool size
current_size: AtomicUsize,
}
impl MemoryPool {
/// Create a new memory pool
pub fn new(config: MemoryPoolConfig, device_id: i32) -> Result<Self> {
let initial_size = config.initial_size_bytes;
let size_cache = SizeClassCache::new(config.max_cached_per_size, config.min_block_size);
let pool = Self {
config,
device_id,
blocks: RwLock::new(HashMap::new()),
free_blocks: RwLock::new(BTreeMap::new()),
size_cache: Mutex::new(size_cache),
next_block_id: AtomicUsize::new(1),
stats: RwLock::new(MemoryPoolStats::default()),
base_address: 0x1000_0000, // Simulated base address
current_size: AtomicUsize::new(initial_size),
};
// Initialize with a single free block
let initial_block = MemoryBlock::new(0, 0, initial_size, device_id);
{
let mut blocks = pool.blocks.write();
let mut free_blocks = pool.free_blocks.write();
blocks.insert(0, initial_block);
free_blocks.insert(0, 0);
}
// Initialize stats
{
let mut stats = pool.stats.write();
stats.total_size_bytes = initial_size;
stats.free_bytes = initial_size;
stats.free_block_count = 1;
}
Ok(pool)
}
/// Allocate memory from the pool
pub fn allocate(&self, size: usize) -> Result<MemoryAllocation> {
self.allocate_with_tag(size, None)
}
/// Allocate memory with a debug tag
pub fn allocate_with_tag(&self, size: usize, tag: Option<String>) -> Result<MemoryAllocation> {
let aligned_size = self.align_size(size);
// Try cache first
if self.config.cache_freed_blocks {
let mut cache = self.size_cache.lock();
if let Some(block_id) = cache.get(aligned_size) {
let mut blocks = self.blocks.write();
if let Some(block) = blocks.get_mut(&block_id) {
if block.state == BlockState::Cached && block.size >= aligned_size {
block.state = BlockState::Allocated;
block.allocated_at = Some(Instant::now());
block.tag = tag;
// Update stats
let mut stats = self.stats.write();
stats.cache_hits += 1;
stats.allocation_count += 1;
stats.allocated_bytes += block.size;
stats.free_bytes -= block.size;
stats.peak_allocated_bytes =
stats.peak_allocated_bytes.max(stats.allocated_bytes);
return Ok(MemoryAllocation {
block_id,
ptr: self.base_address + block.offset,
size: block.size,
device_id: self.device_id,
});
}
}
}
// Update cache miss stat
let mut stats = self.stats.write();
stats.cache_misses += 1;
}
// Find a suitable block using the configured strategy
let block_id = self.find_block(aligned_size)?;
// Allocate from the block
self.allocate_from_block(block_id, aligned_size, tag)
}
/// Find a suitable block for allocation
fn find_block(&self, size: usize) -> Result<BlockId> {
match self.config.strategy {
AllocationStrategy::BestFit => self.find_best_fit(size),
AllocationStrategy::FirstFit => self.find_first_fit(size),
AllocationStrategy::Buddy => self.find_buddy_block(size),
AllocationStrategy::Slab => self.find_slab_block(size),
}
}
/// Best-fit allocation strategy
fn find_best_fit(&self, size: usize) -> Result<BlockId> {
let blocks = self.blocks.read();
let free_blocks = self.free_blocks.read();
let mut best_id = None;
let mut best_size = usize::MAX;
for &block_id in free_blocks.values() {
if let Some(block) = blocks.get(&block_id) {
if block.can_satisfy(size, self.config.alignment) && block.size < best_size {
best_id = Some(block_id);
best_size = block.size;
}
}
}
best_id.ok_or_else(|| {
DistributedError::runtime(format!(
"No suitable block found for {} bytes allocation",
size
))
})
}
/// First-fit allocation strategy
fn find_first_fit(&self, size: usize) -> Result<BlockId> {
let blocks = self.blocks.read();
let free_blocks = self.free_blocks.read();
for &block_id in free_blocks.values() {
if let Some(block) = blocks.get(&block_id) {
if block.can_satisfy(size, self.config.alignment) {
return Ok(block_id);
}
}
}
Err(DistributedError::runtime(format!(
"No suitable block found for {} bytes allocation",
size
)))
}
/// Buddy allocator strategy
fn find_buddy_block(&self, size: usize) -> Result<BlockId> {
// Round up to power of 2
let buddy_size = size.next_power_of_two();
self.find_best_fit(buddy_size)
}
/// Slab allocator strategy
fn find_slab_block(&self, size: usize) -> Result<BlockId> {
// Round up to slab size class
let slab_size = self.align_size(size);
self.find_best_fit(slab_size)
}
/// Allocate from a specific block
fn allocate_from_block(
&self,
block_id: BlockId,
size: usize,
tag: Option<String>,
) -> Result<MemoryAllocation> {
let mut blocks = self.blocks.write();
let mut free_blocks = self.free_blocks.write();
// First, read the block info we need
let (aligned_offset, block_offset, block_orig_size) = {
let block = blocks
.get(&block_id)
.ok_or_else(|| DistributedError::runtime("Block not found"))?;
if block.state != BlockState::Free {
return Err(DistributedError::runtime("Block is not free"));
}
(
block.aligned_offset(self.config.alignment),
block.offset,
block.size,
)
};
let remaining = block_orig_size - size;
// Split block if there's enough remaining space
if remaining >= self.config.min_block_size {
let new_block_id = self.next_block_id.fetch_add(1, Ordering::SeqCst) as BlockId;
let new_block =
MemoryBlock::new(new_block_id, block_offset + size, remaining, self.device_id);
free_blocks.insert(new_block.offset, new_block_id);
blocks.insert(new_block_id, new_block);
}
// Now update the original block
let alloc_size = if remaining >= self.config.min_block_size {
size
} else {
block_orig_size
};
if let Some(block) = blocks.get_mut(&block_id) {
if remaining >= self.config.min_block_size {
block.size = size;
}
block.state = BlockState::Allocated;
block.allocated_at = Some(Instant::now());
block.tag = tag;
}
// Remove from free list
free_blocks.remove(&block_offset);
let ptr = self.base_address + aligned_offset;
// Update stats
{
let mut stats = self.stats.write();
stats.allocation_count += 1;
stats.allocated_bytes += alloc_size;
stats.free_bytes -= alloc_size;
stats.free_block_count = free_blocks.len();
stats.peak_allocated_bytes = stats.peak_allocated_bytes.max(stats.allocated_bytes);
if stats.allocation_count > 0 {
stats.avg_allocation_size = stats.allocated_bytes / stats.allocation_count;
}
}
Ok(MemoryAllocation {
block_id,
ptr,
size: alloc_size,
device_id: self.device_id,
})
}
/// Free an allocation
pub fn free(&self, allocation: &MemoryAllocation) -> Result<()> {
let mut blocks = self.blocks.write();
let mut free_blocks = self.free_blocks.write();
let block = blocks
.get_mut(&allocation.block_id)
.ok_or_else(|| DistributedError::runtime("Block not found"))?;
if block.state != BlockState::Allocated {
return Err(DistributedError::runtime("Block is not allocated"));
}
let block_size = block.size;
let block_offset = block.offset;
// Try to cache the block
if self.config.cache_freed_blocks {
let mut cache = self.size_cache.lock();
if cache.put(block_size, allocation.block_id) {
block.state = BlockState::Cached;
block.allocated_at = None;
block.tag = None;
// Update stats
let mut stats = self.stats.write();
stats.deallocation_count += 1;
stats.allocated_bytes -= block_size;
stats.free_bytes += block_size;
return Ok(());
}
}
// Mark as free
block.state = BlockState::Free;
block.allocated_at = None;
block.tag = None;
// Add to free list
free_blocks.insert(block_offset, allocation.block_id);
// Update stats
{
let mut stats = self.stats.write();
stats.deallocation_count += 1;
stats.allocated_bytes -= block_size;
stats.free_bytes += block_size;
stats.free_block_count = free_blocks.len();
}
// Try to coalesce with neighbors
drop(blocks);
drop(free_blocks);
self.coalesce_neighbors(allocation.block_id)?;
// Check if defrag is needed
if self.config.auto_defrag {
let stats = self.stats.read();
if stats.fragmentation_ratio > self.config.defrag_threshold {
drop(stats);
self.defragment()?;
}
}
Ok(())
}
/// Coalesce free blocks with neighbors
fn coalesce_neighbors(&self, block_id: BlockId) -> Result<()> {
let mut blocks = self.blocks.write();
let mut free_blocks = self.free_blocks.write();
let block = match blocks.get(&block_id) {
Some(b) if b.state == BlockState::Free => b.clone(),
_ => return Ok(()),
};
// Find and merge with next block
let next_offset = block.offset + block.size;
if let Some(&next_id) = free_blocks.get(&next_offset) {
if let Some(next_block) = blocks.get(&next_id) {
if next_block.state == BlockState::Free {
let merged_size = block.size + next_block.size;
// Remove next block
free_blocks.remove(&next_offset);
blocks.remove(&next_id);
// Update current block
if let Some(current) = blocks.get_mut(&block_id) {
current.size = merged_size;
}
}
}
}
// Update fragmentation
self.update_fragmentation(&blocks, &free_blocks);
Ok(())
}
/// Update fragmentation ratio
fn update_fragmentation(
&self,
blocks: &HashMap<BlockId, MemoryBlock>,
free_blocks: &BTreeMap<usize, BlockId>,
) {
if free_blocks.is_empty() {
let mut stats = self.stats.write();
stats.fragmentation_ratio = 0.0;
return;
}
// Calculate fragmentation as ratio of free blocks to total free space
let mut total_free = 0;
let mut largest_free = 0;
for &block_id in free_blocks.values() {
if let Some(block) = blocks.get(&block_id) {
total_free += block.size;
largest_free = largest_free.max(block.size);
}
}
let mut stats = self.stats.write();
if total_free > 0 {
// Fragmentation = 1 - (largest_free / total_free)
stats.fragmentation_ratio = 1.0 - (largest_free as f32 / total_free as f32);
} else {
stats.fragmentation_ratio = 0.0;
}
}
/// Defragment the pool
pub fn defragment(&self) -> Result<()> {
// In a real implementation, this would:
// 1. Wait for all allocations to complete
// 2. Copy data to compact free space
// 3. Update block offsets
// For simulation, we just clear the cache and update stats
let mut cache = self.size_cache.lock();
cache.clear();
let mut stats = self.stats.write();
stats.defrag_count += 1;
Ok(())
}
/// Align size to minimum block size
fn align_size(&self, size: usize) -> usize {
let min = self.config.min_block_size;
((size + min - 1) / min) * min
}
/// Get pool statistics
pub fn stats(&self) -> MemoryPoolStats {
self.stats.read().clone()
}
/// Get device ID
pub fn device_id(&self) -> i32 {
self.device_id
}
/// Get current pool size
pub fn current_size(&self) -> usize {
self.current_size.load(Ordering::SeqCst)
}
/// Clear the pool
pub fn clear(&self) -> Result<()> {
let mut blocks = self.blocks.write();
let mut free_blocks = self.free_blocks.write();
let mut cache = self.size_cache.lock();
blocks.clear();
free_blocks.clear();
cache.clear();
// Reinitialize with single free block
let size = self.current_size.load(Ordering::SeqCst);
let block = MemoryBlock::new(0, 0, size, self.device_id);
blocks.insert(0, block);
free_blocks.insert(0, 0);
// Reset stats
let mut stats = self.stats.write();
*stats = MemoryPoolStats {
total_size_bytes: size,
free_bytes: size,
free_block_count: 1,
..Default::default()
};
Ok(())
}
}
// =============================================================================
// Memory Allocation Handle
// =============================================================================
/// Handle to an allocated memory region
#[derive(Debug, Clone)]
pub struct MemoryAllocation {
/// Block ID in the pool
pub block_id: BlockId,
/// Pointer to allocated memory
pub ptr: usize,
/// Size in bytes
pub size: usize,
/// Device ID
pub device_id: i32,
}
impl MemoryAllocation {
/// Get the memory address
pub fn address(&self) -> usize {
self.ptr
}
/// Get size in bytes
pub fn size_bytes(&self) -> usize {
self.size
}
}
// =============================================================================
// Thread-Safe Wrapper
// =============================================================================
/// Thread-safe shared memory pool
pub type SharedMemoryPool = Arc<MemoryPool>;
/// Create a shared memory pool
pub fn shared_memory_pool(config: MemoryPoolConfig, device_id: i32) -> Result<SharedMemoryPool> {
Ok(Arc::new(MemoryPool::new(config, device_id)?))
}
// =============================================================================
// Multi-Device Pool Manager
// =============================================================================
/// Manages memory pools across multiple devices
pub struct MultiDevicePoolManager {
/// Pools per device
pools: HashMap<i32, SharedMemoryPool>,
/// Configuration
config: MemoryPoolConfig,
}
impl MultiDevicePoolManager {
/// Create a new multi-device pool manager
pub fn new(config: MemoryPoolConfig) -> Self {
Self {
pools: HashMap::new(),
config,
}
}
/// Get or create pool for a device
pub fn get_pool(&mut self, device_id: i32) -> Result<SharedMemoryPool> {
if let Some(pool) = self.pools.get(&device_id) {
return Ok(pool.clone());
}
let pool = shared_memory_pool(self.config.clone(), device_id)?;
self.pools.insert(device_id, pool.clone());
Ok(pool)
}
/// Get statistics for all devices
pub fn all_stats(&self) -> HashMap<i32, MemoryPoolStats> {
self.pools
.iter()
.map(|(&id, pool)| (id, pool.stats()))
.collect()
}
/// Clear all pools
pub fn clear_all(&self) -> Result<()> {
for pool in self.pools.values() {
pool.clear()?;
}
Ok(())
}
}
// =============================================================================
// Tests
// =============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_pool_config_default() {
let config = MemoryPoolConfig::default();
assert_eq!(config.initial_size_bytes, 256 * 1024 * 1024);
assert_eq!(config.strategy, AllocationStrategy::BestFit);
assert_eq!(config.alignment, 256);
}
#[test]
fn test_memory_block_creation() {
let block = MemoryBlock::new(1, 1024, 4096, 0);
assert_eq!(block.id, 1);
assert_eq!(block.offset, 1024);
assert_eq!(block.size, 4096);
assert_eq!(block.state, BlockState::Free);
}
#[test]
fn test_block_can_satisfy() {
let block = MemoryBlock::new(1, 0, 4096, 0);
assert!(block.can_satisfy(1024, 256));
assert!(block.can_satisfy(4096, 256));
assert!(!block.can_satisfy(8192, 256));
}
#[test]
fn test_memory_pool_creation() {
let config = MemoryPoolConfig::default();
let pool = MemoryPool::new(config, 0).unwrap();
assert_eq!(pool.device_id(), 0);
assert_eq!(pool.current_size(), 256 * 1024 * 1024);
}
#[test]
fn test_simple_allocation() {
let config = MemoryPoolConfig {
initial_size_bytes: 1024 * 1024, // 1MB
..Default::default()
};
let pool = MemoryPool::new(config, 0).unwrap();
let alloc = pool.allocate(4096).unwrap();
assert!(alloc.ptr > 0);
assert!(alloc.size >= 4096);
let stats = pool.stats();
assert_eq!(stats.allocation_count, 1);
assert!(stats.allocated_bytes >= 4096);
}
#[test]
fn test_allocation_and_free() {
let config = MemoryPoolConfig {
initial_size_bytes: 1024 * 1024,
..Default::default()
};
let pool = MemoryPool::new(config, 0).unwrap();
let alloc = pool.allocate(4096).unwrap();
let stats_before = pool.stats();
pool.free(&alloc).unwrap();
let stats_after = pool.stats();
assert_eq!(stats_after.deallocation_count, 1);
assert!(stats_after.allocated_bytes < stats_before.allocated_bytes);
}
#[test]
fn test_multiple_allocations() {
let config = MemoryPoolConfig {
initial_size_bytes: 1024 * 1024,
..Default::default()
};
let pool = MemoryPool::new(config, 0).unwrap();
let alloc1 = pool.allocate(1024).unwrap();
let alloc2 = pool.allocate(2048).unwrap();
let alloc3 = pool.allocate(4096).unwrap();
assert_ne!(alloc1.ptr, alloc2.ptr);
assert_ne!(alloc2.ptr, alloc3.ptr);
let stats = pool.stats();
assert_eq!(stats.allocation_count, 3);
}
#[test]
fn test_size_class_cache() {
let mut cache = SizeClassCache::new(10, 512);
cache.put(1024, 1);
cache.put(1024, 2);
cache.put(2048, 3);
assert_eq!(cache.get(1024), Some(1));
assert_eq!(cache.get(1024), Some(2));
assert_eq!(cache.get(1024), None);
assert_eq!(cache.get(2048), Some(3));
}
#[test]
fn test_cache_hit() {
let config = MemoryPoolConfig {
initial_size_bytes: 1024 * 1024,
cache_freed_blocks: true,
..Default::default()
};
let pool = MemoryPool::new(config, 0).unwrap();
// Allocate and free
let alloc1 = pool.allocate(4096).unwrap();
pool.free(&alloc1).unwrap();
// Allocate same size - should hit cache
let _alloc2 = pool.allocate(4096).unwrap();
let stats = pool.stats();
assert!(stats.cache_hits >= 1);
}
#[test]
fn test_allocation_with_tag() {
let config = MemoryPoolConfig {
initial_size_bytes: 1024 * 1024,
..Default::default()
};
let pool = MemoryPool::new(config, 0).unwrap();
let alloc = pool
.allocate_with_tag(4096, Some("gradient".to_string()))
.unwrap();
assert!(alloc.size >= 4096);
}
#[test]
fn test_pool_clear() {
let config = MemoryPoolConfig {
initial_size_bytes: 1024 * 1024,
..Default::default()
};
let pool = MemoryPool::new(config, 0).unwrap();
let _ = pool.allocate(4096).unwrap();
let _ = pool.allocate(4096).unwrap();
pool.clear().unwrap();
let stats = pool.stats();
assert_eq!(stats.allocation_count, 0);
assert_eq!(stats.free_block_count, 1);
}
#[test]
fn test_multi_device_manager() {
let config = MemoryPoolConfig {
initial_size_bytes: 1024 * 1024,
..Default::default()
};
let mut manager = MultiDevicePoolManager::new(config);
let pool0 = manager.get_pool(0).unwrap();
let pool1 = manager.get_pool(1).unwrap();
assert_eq!(pool0.device_id(), 0);
assert_eq!(pool1.device_id(), 1);
let all_stats = manager.all_stats();
assert_eq!(all_stats.len(), 2);
}
#[test]
fn test_memory_pool_stats() {
let stats = MemoryPoolStats {
total_size_bytes: 1000,
allocated_bytes: 500,
cache_hits: 10,
cache_misses: 5,
..Default::default()
};
assert!((stats.utilization() - 0.5).abs() < 0.01);
assert!((stats.cache_hit_ratio() - 0.666).abs() < 0.01);
}
#[test]
fn test_allocation_strategy() {
assert_eq!(AllocationStrategy::BestFit, AllocationStrategy::BestFit);
assert_ne!(AllocationStrategy::BestFit, AllocationStrategy::FirstFit);
}
}