Skip to main content

trustformers_optim/
cache_friendly.rs

1//! Cache-friendly optimization algorithms for improved memory performance.
2//!
3//! This module implements optimization algorithms with cache-friendly memory access patterns,
4//! reducing cache misses and improving overall performance, especially for large models.
5//!
6//! # Key Optimizations
7//!
8//! - **Blocked/Tiled Operations**: Process data in cache-sized blocks
9//! - **Memory Layout Optimization**: Structure data for optimal cache utilization
10//! - **Data Prefetching**: Improve cache hit rates with strategic prefetching
11//! - **Loop Fusion**: Combine operations to reduce memory bandwidth requirements
12//! - **Vectorization-Friendly**: Design for SIMD instruction utilization
13
14// reason: research-stage module — reserved API/scaffolding fields and methods
15// retained intentionally for in-progress features; not yet on active call paths.
16#![allow(dead_code)]
17
18use crate::common::{BiasCorrection, ParameterUpdate};
19use std::collections::HashMap;
20use trustformers_core::errors::{Result, TrustformersError};
21use trustformers_core::tensor::Tensor;
22use trustformers_core::traits::Optimizer;
23
24/// Cache configuration parameters for memory-aware optimizers.
25#[derive(Debug, Clone)]
26pub struct CacheConfig {
27    /// L1 cache size in bytes (typically 32KB)
28    pub l1_cache_size: usize,
29    /// L2 cache size in bytes (typically 256KB-1MB)
30    pub l2_cache_size: usize,
31    /// L3 cache size in bytes (typically 8MB-32MB)
32    pub l3_cache_size: usize,
33    /// Cache line size in bytes (typically 64 bytes)
34    pub cache_line_size: usize,
35    /// Block size for tiled operations
36    pub block_size: usize,
37    /// Whether to enable prefetching
38    pub enable_prefetching: bool,
39    /// Prefetch distance (number of cache lines ahead)
40    pub prefetch_distance: usize,
41}
42
43impl Default for CacheConfig {
44    fn default() -> Self {
45        Self {
46            l1_cache_size: 32 * 1024,       // 32KB
47            l2_cache_size: 256 * 1024,      // 256KB
48            l3_cache_size: 8 * 1024 * 1024, // 8MB
49            cache_line_size: 64,            // 64 bytes
50            block_size: 1024,               // Process 1024 elements at a time
51            enable_prefetching: true,
52            prefetch_distance: 4,
53        }
54    }
55}
56
57impl CacheConfig {
58    /// Detects cache configuration from the system (simplified version).
59    pub fn detect_system() -> Self {
60        // In a real implementation, this would use cpuid or similar
61        // to detect actual cache sizes
62        Self::default()
63    }
64
65    /// Configures for L1 cache optimization (small blocks, high frequency access).
66    pub fn l1_optimized() -> Self {
67        Self {
68            block_size: 512, // Smaller blocks for L1
69            ..Default::default()
70        }
71    }
72
73    /// Configures for L2 cache optimization (medium blocks).
74    pub fn l2_optimized() -> Self {
75        Self {
76            block_size: 2048, // Medium blocks for L2
77            ..Default::default()
78        }
79    }
80
81    /// Configures for L3 cache optimization (larger blocks).
82    pub fn l3_optimized() -> Self {
83        Self {
84            block_size: 8192, // Larger blocks for L3
85            ..Default::default()
86        }
87    }
88
89    /// Calculates optimal block size based on cache hierarchy.
90    pub fn optimal_block_size_for_arrays(&self, num_arrays: usize) -> usize {
91        // Account for multiple arrays (momentum, variance, parameters)
92        let available_cache = self.l2_cache_size / num_arrays;
93        let elements_per_cache = available_cache / std::mem::size_of::<f32>();
94
95        // Use power of 2 for better memory alignment
96        let mut block_size = 64;
97        while block_size * 2 <= elements_per_cache && block_size < 16384 {
98            block_size *= 2;
99        }
100
101        block_size.min(self.block_size)
102    }
103}
104
105/// Cache-friendly memory layout for optimizer state.
106///
107/// This structure organizes optimizer state data to maximize cache utilization
108/// by grouping frequently accessed data together.
109#[derive(Debug)]
110pub struct CacheFriendlyState {
111    /// Interleaved momentum and variance data for cache efficiency
112    /// Format: \[momentum\[i\], variance\[i\], momentum\[i+1\], variance\[i+1\], ...\]
113    pub interleaved_buffers: HashMap<usize, Vec<f32>>,
114    /// Parameter metadata for efficient access
115    pub param_metadata: HashMap<usize, ParameterMetadata>,
116    /// Current step counter
117    pub step: usize,
118    /// Cache configuration
119    pub cache_config: CacheConfig,
120}
121
122/// Metadata for efficient parameter processing.
123#[derive(Debug, Clone)]
124pub struct ParameterMetadata {
125    /// Starting offset in interleaved buffer
126    pub offset: usize,
127    /// Number of elements
128    pub size: usize,
129    /// Optimal block size for this parameter
130    pub block_size: usize,
131    /// Last access timestamp for cache management
132    pub last_access: usize,
133}
134
135impl CacheFriendlyState {
136    /// Creates a new cache-friendly state with the given configuration.
137    pub fn new(cache_config: CacheConfig) -> Self {
138        Self {
139            interleaved_buffers: HashMap::new(),
140            param_metadata: HashMap::new(),
141            step: 0,
142            cache_config,
143        }
144    }
145
146    /// Allocates buffers for a parameter with optimal memory layout.
147    pub fn allocate_parameter(&mut self, param_id: usize, size: usize) -> Result<()> {
148        // Allocate interleaved momentum and variance
149        // Format: [m0, v0, m1, v1, ..., mn, vn]
150        let buffer_size = size * 2; // momentum + variance
151        let buffer = vec![0.0; buffer_size];
152
153        let metadata = ParameterMetadata {
154            offset: 0,
155            size,
156            block_size: self.cache_config.optimal_block_size_for_arrays(3), // param, momentum, variance
157            last_access: self.step,
158        };
159
160        self.interleaved_buffers.insert(param_id, buffer);
161        self.param_metadata.insert(param_id, metadata);
162
163        Ok(())
164    }
165
166    /// Gets direct access to interleaved buffer for efficient in-place operations.
167    pub fn get_interleaved_buffer_mut(&mut self, param_id: usize) -> Option<(&mut [f32], usize)> {
168        if let (Some(buffer), Some(metadata)) = (
169            self.interleaved_buffers.get_mut(&param_id),
170            self.param_metadata.get_mut(&param_id),
171        ) {
172            metadata.last_access = self.step;
173            Some((buffer.as_mut_slice(), metadata.size))
174        } else {
175            None
176        }
177    }
178
179    /// Gets momentum and variance slices for a parameter (backward compatibility).
180    /// Note: This creates temporary vectors and is less efficient than get_interleaved_buffer_mut.
181    pub fn get_buffers_mut(&mut self, param_id: usize) -> Option<(Vec<f32>, Vec<f32>)> {
182        if let (Some(buffer), Some(metadata)) = (
183            self.interleaved_buffers.get(&param_id),
184            self.param_metadata.get_mut(&param_id),
185        ) {
186            metadata.last_access = self.step;
187
188            // Extract momentum and variance from interleaved buffer
189            let mut momentum = Vec::with_capacity(metadata.size);
190            let mut variance = Vec::with_capacity(metadata.size);
191
192            for i in 0..metadata.size {
193                momentum.push(buffer[i * 2]);
194                variance.push(buffer[i * 2 + 1]);
195            }
196
197            Some((momentum, variance))
198        } else {
199            None
200        }
201    }
202
203    /// Updates interleaved buffers with new momentum and variance values.
204    pub fn update_buffers(
205        &mut self,
206        param_id: usize,
207        momentum: &[f32],
208        variance: &[f32],
209    ) -> Result<()> {
210        if let Some(buffer) = self.interleaved_buffers.get_mut(&param_id) {
211            if momentum.len() != variance.len() || momentum.len() * 2 != buffer.len() {
212                return Err(TrustformersError::tensor_op_error(
213                    "Buffer size mismatch",
214                    "update_buffers",
215                ));
216            }
217
218            // Update interleaved buffer
219            for i in 0..momentum.len() {
220                buffer[i * 2] = momentum[i];
221                buffer[i * 2 + 1] = variance[i];
222            }
223
224            Ok(())
225        } else {
226            Err(TrustformersError::tensor_op_error(
227                "Parameter not found",
228                "update_buffers",
229            ))
230        }
231    }
232
233    /// Clears unused buffers to free memory.
234    pub fn garbage_collect(&mut self, access_threshold: usize) {
235        let current_step = self.step;
236        let stale_params: Vec<usize> = self
237            .param_metadata
238            .iter()
239            .filter(|(_, metadata)| current_step - metadata.last_access > access_threshold)
240            .map(|(id, _)| *id)
241            .collect();
242
243        for param_id in stale_params {
244            self.interleaved_buffers.remove(&param_id);
245            self.param_metadata.remove(&param_id);
246        }
247    }
248}
249
250/// Cache-friendly Adam optimizer implementation.
251///
252/// This optimizer uses blocked processing and optimized memory layouts
253/// to minimize cache misses and improve performance.
254#[derive(Debug)]
255pub struct CacheFriendlyAdam {
256    /// Learning rate
257    lr: f32,
258    /// Beta coefficients for momentum and variance
259    betas: (f32, f32),
260    /// Epsilon for numerical stability
261    eps: f32,
262    /// Weight decay coefficient
263    weight_decay: f32,
264    /// Cache-friendly state
265    state: CacheFriendlyState,
266}
267
268impl CacheFriendlyAdam {
269    /// Creates a new cache-friendly Adam optimizer.
270    pub fn new(lr: f32, betas: (f32, f32), eps: f32, weight_decay: f32) -> Self {
271        Self::with_cache_config(lr, betas, eps, weight_decay, CacheConfig::default())
272    }
273
274    /// Creates a cache-friendly Adam optimizer with custom cache configuration.
275    pub fn with_cache_config(
276        lr: f32,
277        betas: (f32, f32),
278        eps: f32,
279        weight_decay: f32,
280        cache_config: CacheConfig,
281    ) -> Self {
282        Self {
283            lr,
284            betas,
285            eps,
286            weight_decay,
287            state: CacheFriendlyState::new(cache_config),
288        }
289    }
290
291    /// Updates parameter using cache-friendly blocked processing (legacy wrapper).
292    fn update_parameter_blocked(
293        &mut self,
294        param: &mut [f32],
295        grad: &[f32],
296        param_id: String,
297    ) -> Result<()> {
298        // Convert string ID to numeric ID for compatibility
299        let numeric_id = param_id.as_ptr() as usize;
300        self.update_parameter_blocked_fast(param, grad, numeric_id)
301    }
302
303    /// Fast parameter update using numeric IDs to avoid string formatting overhead.
304    fn update_parameter_blocked_fast(
305        &mut self,
306        param: &mut [f32],
307        grad: &[f32],
308        param_id: usize,
309    ) -> Result<()> {
310        let size = param.len();
311        if grad.len() != size {
312            return Err(TrustformersError::tensor_op_error(
313                "Parameter and gradient size mismatch",
314                "update_parameter_blocked_fast",
315            ));
316        }
317
318        // Ensure parameter buffers are allocated with correct size
319        if !self.state.param_metadata.contains_key(&param_id) {
320            self.state.allocate_parameter(param_id, size)?;
321        } else {
322            // Check if size has changed and reallocate if needed
323            let current_size =
324                self.state.param_metadata.get(&param_id).map(|meta| meta.size).unwrap_or(0);
325            if current_size != size {
326                self.state.allocate_parameter(param_id, size)?;
327            }
328        }
329
330        // Extract needed values before borrowing the buffer
331        let step = self.state.step + 1;
332        let block_size = self
333            .state
334            .param_metadata
335            .get(&param_id)
336            .map(|meta| meta.block_size)
337            .unwrap_or(1024);
338        let _enable_prefetching = self.state.cache_config.enable_prefetching;
339
340        let (bias_correction1, bias_correction2) =
341            BiasCorrection::compute_adam_corrections(self.betas.0, self.betas.1, step);
342
343        // Get direct access to interleaved buffer for efficient operations
344        let (interleaved_buffer, _param_size) =
345            self.state.get_interleaved_buffer_mut(param_id).ok_or_else(|| {
346                TrustformersError::tensor_op_error(
347                    "Failed to get parameter buffers",
348                    "update_parameter_blocked_fast",
349                )
350            })?;
351
352        // For smaller tensors (< 4096 elements), use direct processing to avoid block overhead
353        if size < 4096 {
354            // Direct processing with inlined operations for better performance
355            for i in 0..size {
356                let grad_val = grad[i] + self.weight_decay * param[i];
357
358                // Work directly with interleaved buffer: [m0, v0, m1, v1, ...]
359                let momentum_idx = i * 2;
360                let variance_idx = i * 2 + 1;
361
362                // Update momentum and variance with inlined EMA operations
363                interleaved_buffer[momentum_idx] = self.betas.0 * interleaved_buffer[momentum_idx]
364                    + (1.0 - self.betas.0) * grad_val;
365                interleaved_buffer[variance_idx] = self.betas.1 * interleaved_buffer[variance_idx]
366                    + (1.0 - self.betas.1) * grad_val * grad_val;
367
368                // Apply bias-corrected update with inlined operations
369                let m_hat = interleaved_buffer[momentum_idx] / bias_correction1;
370                let v_hat = interleaved_buffer[variance_idx] / bias_correction2;
371
372                param[i] -= self.lr * m_hat / (v_hat.sqrt() + self.eps);
373            }
374        } else {
375            // Use block processing for larger tensors where cache benefits matter
376            let num_blocks = size.div_ceil(block_size);
377
378            for block_idx in 0..num_blocks {
379                let start = block_idx * block_size;
380                let end = (start + block_size).min(size);
381
382                // Note: Prefetching removed to avoid borrowing conflicts - this is a minor optimization
383                // Process current block with inlined operations
384                for i in start..end {
385                    let grad_val = grad[i] + self.weight_decay * param[i];
386
387                    // Work directly with interleaved buffer: [m0, v0, m1, v1, ...]
388                    let momentum_idx = i * 2;
389                    let variance_idx = i * 2 + 1;
390
391                    interleaved_buffer[momentum_idx] = self.betas.0
392                        * interleaved_buffer[momentum_idx]
393                        + (1.0 - self.betas.0) * grad_val;
394                    interleaved_buffer[variance_idx] = self.betas.1
395                        * interleaved_buffer[variance_idx]
396                        + (1.0 - self.betas.1) * grad_val * grad_val;
397
398                    let m_hat = interleaved_buffer[momentum_idx] / bias_correction1;
399                    let v_hat = interleaved_buffer[variance_idx] / bias_correction2;
400
401                    param[i] -= self.lr * m_hat / (v_hat.sqrt() + self.eps);
402                }
403            }
404        }
405
406        // No need to update buffers - we worked directly with the interleaved buffer
407        Ok(())
408    }
409
410    /// Processes a block with fused operations for better cache utilization.
411    #[inline]
412    fn process_block_fused(
413        &self,
414        param_block: &mut [f32],
415        grad_block: &[f32],
416        momentum_block: &mut [f32],
417        variance_block: &mut [f32],
418        bias_correction1: f32,
419        bias_correction2: f32,
420    ) {
421        // Fuse all operations for maximum cache efficiency
422        for i in 0..param_block.len() {
423            let grad_val = grad_block[i] + self.weight_decay * param_block[i];
424
425            // Update momentum and variance in one pass
426            ParameterUpdate::update_ema(&mut momentum_block[i], grad_val, self.betas.0);
427            ParameterUpdate::update_ema(&mut variance_block[i], grad_val * grad_val, self.betas.1);
428
429            // Apply bias-corrected update immediately
430            let m_hat = momentum_block[i] / bias_correction1;
431            let v_hat = variance_block[i] / bias_correction2;
432
433            ParameterUpdate::adam_update(&mut param_block[i], self.lr, m_hat, v_hat, self.eps);
434        }
435    }
436
437    /// Software prefetch hint for better cache performance.
438    #[inline]
439    fn prefetch_block(&self, block: &[f32]) {
440        // Implement cache-friendly prefetching for different architectures
441        if block.is_empty() {
442            return;
443        }
444
445        // Get pointer to first element
446        let ptr = block.as_ptr();
447
448        // Use architecture-specific prefetch instructions when available
449        #[cfg(target_arch = "x86_64")]
450        {
451            // Prefetch data into L1 cache (temporal locality)
452            // This provides a hint to the processor to load data into cache
453            // Use ptr.wrapping_add to avoid bounds checking in hot path
454            unsafe {
455                std::arch::x86_64::_mm_prefetch(ptr as *const i8, std::arch::x86_64::_MM_HINT_T0);
456
457                // For larger blocks, prefetch multiple cache lines
458                if block.len() > 16 {
459                    // More than one cache line (64 bytes / 4 bytes per f32)
460                    let mid_ptr = ptr.wrapping_add(block.len() / 2);
461                    std::arch::x86_64::_mm_prefetch(
462                        mid_ptr as *const i8,
463                        std::arch::x86_64::_MM_HINT_T0,
464                    );
465                }
466            }
467        }
468
469        #[cfg(target_arch = "aarch64")]
470        {
471            // ARM64 prefetch using inline assembly
472            unsafe {
473                std::arch::asm!(
474                    "prfm pldl1keep, [{}]",
475                    in(reg) ptr,
476                    options(nostack, preserves_flags)
477                );
478            }
479        }
480
481        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
482        {
483            // Fallback: access first element to trigger cache load
484            // This is a software hint that usually gets optimized away
485            // but can help with cache warming on some architectures
486            let _ = unsafe { std::ptr::read_volatile(ptr) };
487        }
488    }
489
490    /// Gets cache utilization statistics.
491    pub fn cache_stats(&self) -> CacheStats {
492        let buffer_memory: usize = self
493            .state
494            .interleaved_buffers
495            .values()
496            .map(|buffer| buffer.len() * std::mem::size_of::<f32>())
497            .sum();
498
499        let num_params = self.state.param_metadata.len();
500        let total_elements: usize = self.state.param_metadata.values().map(|meta| meta.size).sum();
501
502        CacheStats {
503            buffer_memory_bytes: buffer_memory,
504            num_parameters: num_params,
505            total_elements,
506            cache_config: self.state.cache_config.clone(),
507            estimated_l1_utilization: self
508                .estimate_cache_utilization(buffer_memory, self.state.cache_config.l1_cache_size),
509            estimated_l2_utilization: self
510                .estimate_cache_utilization(buffer_memory, self.state.cache_config.l2_cache_size),
511        }
512    }
513
514    /// Estimates cache utilization percentage.
515    fn estimate_cache_utilization(&self, working_set_size: usize, cache_size: usize) -> f32 {
516        if cache_size == 0 {
517            return 1.0;
518        }
519        (working_set_size as f32 / cache_size as f32).min(1.0)
520    }
521
522    /// Performs garbage collection on unused parameters.
523    pub fn cleanup_unused_params(&mut self, steps_threshold: usize) {
524        self.state.garbage_collect(steps_threshold);
525    }
526}
527
528impl Optimizer for CacheFriendlyAdam {
529    fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
530        match (parameter, grad) {
531            (Tensor::F32(param), Tensor::F32(grad_arr)) => {
532                // Use pointer address as numeric ID to avoid string formatting overhead
533                let param_id = param.as_ptr() as usize;
534                let param_slice = param.as_slice_mut().ok_or_else(|| {
535                    TrustformersError::tensor_op_error(
536                        "Parameter tensor is not contiguous",
537                        "update",
538                    )
539                })?;
540                let grad_slice = grad_arr.as_slice().ok_or_else(|| {
541                    TrustformersError::tensor_op_error(
542                        "Gradient tensor is not contiguous",
543                        "update",
544                    )
545                })?;
546                self.update_parameter_blocked_fast(param_slice, grad_slice, param_id)
547            },
548            _ => Err(TrustformersError::tensor_op_error(
549                "Unsupported tensor types for CacheFriendlyAdam",
550                "update",
551            )),
552        }
553    }
554
555    fn zero_grad(&mut self) {
556        // No explicit gradient storage
557    }
558
559    fn step(&mut self) {
560        self.state.step += 1;
561    }
562
563    fn get_lr(&self) -> f32 {
564        self.lr
565    }
566
567    fn set_lr(&mut self, lr: f32) {
568        self.lr = lr;
569    }
570}
571
572/// Cache performance statistics.
573#[derive(Debug, Clone)]
574pub struct CacheStats {
575    /// Total memory used by optimizer buffers
576    pub buffer_memory_bytes: usize,
577    /// Number of parameters being optimized
578    pub num_parameters: usize,
579    /// Total number of parameter elements
580    pub total_elements: usize,
581    /// Cache configuration used
582    pub cache_config: CacheConfig,
583    /// Estimated L1 cache utilization (0.0 to 1.0)
584    pub estimated_l1_utilization: f32,
585    /// Estimated L2 cache utilization (0.0 to 1.0)
586    pub estimated_l2_utilization: f32,
587}
588
589impl CacheStats {
590    /// Suggests optimization strategies based on cache utilization.
591    pub fn optimization_suggestions(&self) -> Vec<String> {
592        let mut suggestions = Vec::new();
593
594        if self.estimated_l1_utilization > 0.8 {
595            suggestions.push("Consider reducing block size for better L1 cache fit".to_string());
596        }
597
598        if self.estimated_l2_utilization > 0.9 {
599            suggestions
600                .push("Working set exceeds L2 cache; consider parameter partitioning".to_string());
601        }
602
603        if self.cache_config.block_size > 8192 {
604            suggestions.push("Large block size may cause cache thrashing".to_string());
605        }
606
607        if !self.cache_config.enable_prefetching {
608            suggestions.push("Enable prefetching for potential performance gains".to_string());
609        }
610
611        if suggestions.is_empty() {
612            suggestions.push("Cache utilization appears optimal".to_string());
613        }
614
615        suggestions
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    #[test]
624    fn test_cache_config_creation() {
625        let config = CacheConfig::default();
626        assert_eq!(config.l1_cache_size, 32 * 1024);
627        assert_eq!(config.cache_line_size, 64);
628        assert!(config.enable_prefetching);
629
630        let l1_config = CacheConfig::l1_optimized();
631        assert_eq!(l1_config.block_size, 512);
632    }
633
634    #[test]
635    fn test_optimal_block_size() {
636        let config = CacheConfig::default();
637        let block_size = config.optimal_block_size_for_arrays(3);
638        assert!(block_size > 0);
639        assert!(block_size <= config.block_size);
640        assert_eq!(block_size & (block_size - 1), 0); // Should be power of 2
641    }
642
643    #[test]
644    fn test_cache_friendly_state() {
645        let mut state = CacheFriendlyState::new(CacheConfig::default());
646
647        // Test parameter allocation
648        let param_id = 12345usize;
649        state.allocate_parameter(param_id, 100).expect("Operation failed in test");
650
651        assert!(state.param_metadata.contains_key(&param_id));
652        assert!(state.interleaved_buffers.contains_key(&param_id));
653
654        // Test buffer access
655        let (momentum, variance) =
656            state.get_buffers_mut(param_id).expect("Operation failed in test");
657        assert_eq!(momentum.len(), 100);
658        assert_eq!(variance.len(), 100);
659    }
660
661    #[test]
662    fn test_cache_friendly_adam() {
663        let optimizer = CacheFriendlyAdam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
664        assert_eq!(optimizer.get_lr(), 1e-3);
665        assert_eq!(optimizer.betas, (0.9, 0.999));
666        assert_eq!(optimizer.eps, 1e-8);
667        assert_eq!(optimizer.weight_decay, 0.01);
668    }
669
670    #[test]
671    fn test_cache_stats() {
672        let optimizer = CacheFriendlyAdam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
673        let stats = optimizer.cache_stats();
674
675        assert_eq!(stats.num_parameters, 0);
676        assert_eq!(stats.total_elements, 0);
677        assert_eq!(stats.buffer_memory_bytes, 0);
678
679        let suggestions = stats.optimization_suggestions();
680        assert!(!suggestions.is_empty());
681    }
682
683    #[test]
684    fn test_cache_utilization_estimation() {
685        let optimizer = CacheFriendlyAdam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
686
687        let utilization = optimizer.estimate_cache_utilization(16 * 1024, 32 * 1024);
688        assert_eq!(utilization, 0.5);
689
690        let over_utilization = optimizer.estimate_cache_utilization(64 * 1024, 32 * 1024);
691        assert_eq!(over_utilization, 1.0);
692    }
693
694    #[test]
695    fn test_garbage_collection() {
696        let mut state = CacheFriendlyState::new(CacheConfig::default());
697
698        // Add some parameters
699        let param1_id = 11111usize;
700        let param2_id = 22222usize;
701        state.allocate_parameter(param1_id, 100).expect("Operation failed in test");
702        state.allocate_parameter(param2_id, 200).expect("Operation failed in test");
703
704        // Simulate time passing
705        state.step = 1000;
706
707        // Access only param1
708        state.get_buffers_mut(param1_id);
709
710        // Garbage collect with threshold of 10 steps
711        state.garbage_collect(10);
712
713        // param1 should remain, param2 should be removed
714        assert!(state.param_metadata.contains_key(&param1_id));
715        assert!(!state.param_metadata.contains_key(&param2_id));
716    }
717}