Skip to main content

optirs_learned/transformer_based_optimizer/
memory_manager.rs

1// Memory management for transformer-based optimizer
2
3use super::config::{CacheEvictionStrategy, MemoryConfig, TransformerBasedOptimizerConfig};
4use crate::error::Result;
5use scirs2_core::ndarray::Array2;
6use scirs2_core::numeric::Float;
7use std::collections::{HashMap, VecDeque};
8use std::fmt::Debug;
9use std::time::{Duration, Instant};
10
11/// Memory management strategy types
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum MemoryManagementStrategy {
14    /// Simple FIFO eviction
15    FIFO,
16    /// Least Recently Used
17    LRU,
18    /// Least Frequently Used
19    LFU,
20    /// Adaptive replacement cache
21    ARC,
22    /// Compressed memory storage
23    Compressed,
24    /// Hierarchical memory organization
25    Hierarchical,
26}
27
28/// Transformer memory manager
29pub struct TransformerMemoryManager<
30    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
31> {
32    /// Memory management strategy
33    strategy: MemoryManagementStrategy,
34
35    /// Configuration
36    config: MemoryConfig,
37
38    /// Primary memory cache
39    primary_cache: MemoryCache<T>,
40
41    /// Secondary cache for overflow
42    secondary_cache: Option<MemoryCache<T>>,
43
44    /// Memory compression manager
45    compression_manager: Option<CompressionManager<T>>,
46
47    /// Memory statistics
48    statistics: MemoryStatistics,
49
50    /// Access patterns tracker
51    access_tracker: AccessTracker,
52
53    /// Memory pressure monitor
54    pressure_monitor: MemoryPressureMonitor,
55
56    /// Model dimension every stored tensor must be `model_dimension` wide.
57    model_dimension: usize,
58}
59
60impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
61    TransformerMemoryManager<T>
62{
63    /// Create new memory manager
64    pub fn new(config: &TransformerBasedOptimizerConfig<T>) -> Result<Self> {
65        let memory_config = config.memory_config.clone();
66        let strategy = match memory_config.eviction_strategy {
67            CacheEvictionStrategy::LRU => MemoryManagementStrategy::LRU,
68            CacheEvictionStrategy::LFU => MemoryManagementStrategy::LFU,
69            CacheEvictionStrategy::FIFO => MemoryManagementStrategy::FIFO,
70            CacheEvictionStrategy::Random => MemoryManagementStrategy::LRU, // Fallback
71        };
72
73        let primary_cache = MemoryCache::new(
74            memory_config.max_cache_size / 2,
75            memory_config.eviction_strategy,
76        )?;
77
78        let secondary_cache = if memory_config.max_cache_size > 1024 * 1024 * 100 {
79            // 100MB
80            Some(MemoryCache::new(
81                memory_config.max_cache_size / 2,
82                CacheEvictionStrategy::FIFO,
83            )?)
84        } else {
85            None
86        };
87
88        let compression_manager = if memory_config.enable_compression {
89            Some(CompressionManager::new()?)
90        } else {
91            None
92        };
93
94        let statistics = MemoryStatistics::new();
95        let access_tracker = AccessTracker::new(1000);
96        let pressure_monitor = MemoryPressureMonitor::new();
97
98        Ok(Self {
99            strategy,
100            config: memory_config,
101            primary_cache,
102            secondary_cache,
103            compression_manager,
104            statistics,
105            access_tracker,
106            pressure_monitor,
107            model_dimension: config.model_dimension,
108        })
109    }
110
111    /// Store tensor in memory with key
112    /// Store `tensor` under `key`.
113    ///
114    /// # Errors
115    /// Returns `Err` when the tensor's feature width differs from the model
116    /// dimension this manager was built for. The manager records the dimension
117    /// at construction but never checked it, so a caller could fill the cache
118    /// with tensors the transformer cannot consume and only find out at the
119    /// point of use.
120    pub fn store(&mut self, key: String, tensor: Array2<T>) -> Result<()> {
121        if tensor.ncols() != self.model_dimension {
122            return Err(crate::error::OptimError::InvalidConfig(format!(
123                "TransformerMemoryManager holds {}-wide tensors but was given {} columns",
124                self.model_dimension,
125                tensor.ncols()
126            )));
127        }
128        let start_time = Instant::now();
129
130        // Check memory pressure and evict if necessary
131        self.pressure_monitor.update(self.get_memory_usage());
132        if self.pressure_monitor.is_high_pressure() {
133            self.evict_memory()?;
134        }
135
136        // Try to store in primary cache first
137        let storage_result = match self.strategy {
138            MemoryManagementStrategy::LRU => self.store_lru(key.clone(), tensor.clone()),
139            MemoryManagementStrategy::LFU => self.store_lfu(key.clone(), tensor.clone()),
140            MemoryManagementStrategy::FIFO => self.store_fifo(key.clone(), tensor.clone()),
141            MemoryManagementStrategy::ARC => self.store_arc(key.clone(), tensor.clone()),
142            MemoryManagementStrategy::Compressed => {
143                self.store_compressed(key.clone(), tensor.clone())
144            }
145            MemoryManagementStrategy::Hierarchical => {
146                self.store_hierarchical(key.clone(), tensor.clone())
147            }
148        };
149
150        // Get tensor length before potential move
151        let tensor_len = tensor.len();
152
153        // If primary cache is full, try secondary cache
154        if storage_result.is_err() && self.secondary_cache.is_some() {
155            if let Some(ref mut secondary) = self.secondary_cache {
156                secondary.store(key.clone(), tensor)?;
157            }
158        }
159
160        // Update statistics
161        let storage_time = start_time.elapsed();
162        self.statistics.record_storage(tensor_len, storage_time);
163        self.access_tracker.record_write(key);
164
165        storage_result
166    }
167
168    /// Retrieve tensor from memory
169    pub fn retrieve(&mut self, key: &str) -> Result<Option<Array2<T>>> {
170        let start_time = Instant::now();
171
172        // Try primary cache first
173        let result = self.primary_cache.retrieve(key)?;
174
175        if result.is_some() {
176            self.access_tracker.record_read(key.to_string());
177            let retrieval_time = start_time.elapsed();
178            self.statistics.record_retrieval(retrieval_time, true);
179            return Ok(result);
180        }
181
182        // Try secondary cache
183        if let Some(ref mut secondary) = self.secondary_cache {
184            let result = secondary.retrieve(key)?;
185            if result.is_some() {
186                self.access_tracker.record_read(key.to_string());
187                let retrieval_time = start_time.elapsed();
188                self.statistics.record_retrieval(retrieval_time, true);
189                return Ok(result);
190            }
191        }
192
193        // Check compressed storage
194        if let Some(ref mut compression) = self.compression_manager {
195            if let Some(compressed_data) = compression.retrieve(key)? {
196                let decompressed = compression.decompress(&compressed_data)?;
197                self.access_tracker.record_read(key.to_string());
198                let retrieval_time = start_time.elapsed();
199                self.statistics.record_retrieval(retrieval_time, true);
200                return Ok(Some(decompressed));
201            }
202        }
203
204        let retrieval_time = start_time.elapsed();
205        self.statistics.record_retrieval(retrieval_time, false);
206        Ok(None)
207    }
208
209    /// Remove tensor from memory
210    pub fn remove(&mut self, key: &str) -> Result<bool> {
211        let mut removed = false;
212
213        if self.primary_cache.remove(key)? {
214            removed = true;
215        }
216
217        if let Some(ref mut secondary) = self.secondary_cache {
218            if secondary.remove(key)? {
219                removed = true;
220            }
221        }
222
223        if let Some(ref mut compression) = self.compression_manager {
224            if compression.remove(key)? {
225                removed = true;
226            }
227        }
228
229        self.access_tracker.record_removal(key.to_string());
230        Ok(removed)
231    }
232
233    /// Clear all memory
234    pub fn clear(&mut self) -> Result<()> {
235        self.primary_cache.clear()?;
236
237        if let Some(ref mut secondary) = self.secondary_cache {
238            secondary.clear()?;
239        }
240
241        if let Some(ref mut compression) = self.compression_manager {
242            compression.clear()?;
243        }
244
245        self.statistics.reset();
246        self.access_tracker.clear();
247        self.pressure_monitor.reset();
248
249        Ok(())
250    }
251
252    /// Get memory usage statistics
253    pub fn get_memory_usage(&self) -> usize {
254        let primary_usage = self.primary_cache.get_memory_usage();
255        let secondary_usage = self
256            .secondary_cache
257            .as_ref()
258            .map(|cache| cache.get_memory_usage())
259            .unwrap_or(0);
260        let compression_usage = self
261            .compression_manager
262            .as_ref()
263            .map(|comp| comp.get_memory_usage())
264            .unwrap_or(0);
265
266        primary_usage + secondary_usage + compression_usage
267    }
268
269    /// Optimize memory layout
270    pub fn optimize_memory(&mut self) -> Result<OptimizationReport> {
271        let start_time = Instant::now();
272        let initial_usage = self.get_memory_usage();
273
274        // Analyze access patterns
275        let access_patterns = self.access_tracker.analyze_patterns();
276
277        // Reorganize based on access frequency
278        self.reorganize_by_frequency(&access_patterns)?;
279
280        // Compress frequently accessed but large items
281        if let Some(ref mut compression) = self.compression_manager {
282            compression.optimize_compression_ratios(&access_patterns)?;
283        }
284
285        // Defragment memory
286        self.defragment_memory()?;
287
288        let final_usage = self.get_memory_usage();
289        let optimization_time = start_time.elapsed();
290
291        Ok(OptimizationReport {
292            initial_memory_usage: initial_usage,
293            final_memory_usage: final_usage,
294            memory_saved: initial_usage.saturating_sub(final_usage),
295            optimization_time,
296            operations_performed: access_patterns.total_accesses,
297        })
298    }
299
300    /// Prefetch data based on access patterns
301    pub fn prefetch(&mut self, keys: Vec<String>) -> Result<usize> {
302        let mut prefetched_count = 0;
303
304        for key in keys {
305            if !self.primary_cache.contains(&key) {
306                // Try to move from secondary to primary cache
307                if let Some(ref mut secondary) = self.secondary_cache {
308                    if let Some(tensor) = secondary.retrieve(&key)? {
309                        if self.primary_cache.store(key.clone(), tensor).is_ok() {
310                            secondary.remove(&key)?;
311                            prefetched_count += 1;
312                        }
313                    }
314                }
315
316                // Try to decompress and move to primary cache
317                if let Some(ref mut compression) = self.compression_manager {
318                    if let Some(compressed_data) = compression.retrieve(&key)? {
319                        let decompressed = compression.decompress(&compressed_data)?;
320                        if self.primary_cache.store(key.clone(), decompressed).is_ok() {
321                            compression.remove(&key)?;
322                            prefetched_count += 1;
323                        }
324                    }
325                }
326            }
327        }
328
329        Ok(prefetched_count)
330    }
331
332    /// Storage strategy implementations
333    fn store_lru(&mut self, key: String, tensor: Array2<T>) -> Result<()> {
334        self.primary_cache.store(key, tensor)
335    }
336
337    fn store_lfu(&mut self, key: String, tensor: Array2<T>) -> Result<()> {
338        // For LFU, we need to track access frequency
339        self.primary_cache.store(key, tensor)
340    }
341
342    fn store_fifo(&mut self, key: String, tensor: Array2<T>) -> Result<()> {
343        self.primary_cache.store(key, tensor)
344    }
345
346    fn store_arc(&mut self, key: String, tensor: Array2<T>) -> Result<()> {
347        // Adaptive Replacement Cache - simplified implementation
348        self.primary_cache.store(key, tensor)
349    }
350
351    fn store_compressed(&mut self, key: String, tensor: Array2<T>) -> Result<()> {
352        if let Some(ref mut compression) = self.compression_manager {
353            let compressed_data = compression.compress(&tensor)?;
354            compression.store(key, compressed_data)?;
355            Ok(())
356        } else {
357            self.primary_cache.store(key, tensor)
358        }
359    }
360
361    fn store_hierarchical(&mut self, key: String, tensor: Array2<T>) -> Result<()> {
362        let tensor_size = tensor.len() * std::mem::size_of::<T>();
363
364        if tensor_size < self.config.allocation_block_size {
365            // Small tensors go to primary cache
366            self.primary_cache.store(key, tensor)
367        } else if let Some(ref mut secondary) = self.secondary_cache {
368            // Large tensors go to secondary cache
369            secondary.store(key, tensor)
370        } else {
371            // Fallback to primary cache
372            self.primary_cache.store(key, tensor)
373        }
374    }
375
376    fn evict_memory(&mut self) -> Result<()> {
377        // Evict from primary cache first
378        self.primary_cache.evict_lru()?;
379
380        // If still under pressure, evict from secondary cache
381        if self.pressure_monitor.is_high_pressure() {
382            if let Some(ref mut secondary) = self.secondary_cache {
383                secondary.evict_lru()?;
384            }
385        }
386
387        Ok(())
388    }
389
390    fn reorganize_by_frequency(&mut self, patterns: &AccessPatterns) -> Result<()> {
391        // Move frequently accessed items to primary cache
392        let frequent_keys: Vec<String> = patterns
393            .frequency_map
394            .iter()
395            .filter(|(_, &count)| count as f64 > patterns.average_frequency)
396            .map(|(key, _)| key.clone())
397            .collect();
398
399        self.prefetch(frequent_keys)?;
400        Ok(())
401    }
402
403    fn defragment_memory(&mut self) -> Result<()> {
404        // Simplified defragmentation - rebuild caches
405        let primary_items = self.primary_cache.get_all_items()?;
406        self.primary_cache.clear()?;
407
408        for (key, tensor) in primary_items {
409            self.primary_cache.store(key, tensor)?;
410        }
411
412        Ok(())
413    }
414
415    /// Get memory statistics
416    pub fn get_statistics(&self) -> &MemoryStatistics {
417        &self.statistics
418    }
419
420    /// Get access patterns
421    pub fn get_access_patterns(&self) -> AccessPatterns {
422        self.access_tracker.analyze_patterns()
423    }
424
425    /// Set memory management strategy
426    pub fn set_strategy(&mut self, strategy: MemoryManagementStrategy) {
427        self.strategy = strategy;
428    }
429
430    /// Get current memory pressure
431    pub fn get_memory_pressure(&self) -> f64 {
432        self.pressure_monitor.get_pressure_ratio()
433    }
434}
435
436/// Memory cache implementation
437pub struct MemoryCache<
438    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
439> {
440    /// Stored tensors
441    storage: HashMap<String, CacheEntry<T>>,
442
443    /// Access order for LRU
444    access_order: VecDeque<String>,
445
446    /// Access frequency for LFU
447    access_frequency: HashMap<String, usize>,
448
449    /// Maximum cache size in bytes
450    max_size: usize,
451
452    /// Current cache size in bytes
453    current_size: usize,
454
455    /// Eviction strategy
456    eviction_strategy: CacheEvictionStrategy,
457
458    /// Insertion order, never re-ordered by an access. Backs FIFO eviction,
459    /// which was previously indistinguishable from LRU.
460    insertion_order: VecDeque<String>,
461}
462
463impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
464    MemoryCache<T>
465{
466    pub fn new(max_size: usize, eviction_strategy: CacheEvictionStrategy) -> Result<Self> {
467        Ok(Self {
468            storage: HashMap::new(),
469            access_order: VecDeque::new(),
470            access_frequency: HashMap::new(),
471            max_size,
472            current_size: 0,
473            eviction_strategy,
474            insertion_order: VecDeque::new(),
475        })
476    }
477
478    /// Insert a tensor, evicting as needed.
479    ///
480    /// The oversized-tensor check happens **before** any eviction. Previously the
481    /// eviction loop ran first, so storing a tensor larger than the whole cache
482    /// drained every existing entry and *then* returned an error — the caller got
483    /// a failure and an empty cache.
484    ///
485    /// # Errors
486    /// Returns `Err` when the tensor alone exceeds `max_size`. The cache is left
487    /// untouched in that case.
488    pub fn store(&mut self, key: String, tensor: Array2<T>) -> Result<()> {
489        let tensor_size = tensor.len() * std::mem::size_of::<T>();
490
491        if tensor_size > self.max_size {
492            return Err(crate::error::OptimError::Other(format!(
493                "Tensor of {tensor_size} bytes is too large for a cache of {} bytes",
494                self.max_size
495            )));
496        }
497
498        // Remove any existing entry first: it frees space and prevents
499        // double-counting when the same key is overwritten.
500        if let Some(old_entry) = self.storage.remove(&key) {
501            self.current_size -= old_entry.size;
502            self.remove_from_access_order(&key);
503            self.insertion_order.retain(|k| k != &key);
504            self.access_frequency.remove(&key);
505        }
506
507        // Now evict until the new entry fits.
508        while self.current_size + tensor_size > self.max_size && !self.storage.is_empty() {
509            self.evict_one()?;
510        }
511
512        // Add new entry
513        let entry = CacheEntry {
514            tensor,
515            size: tensor_size,
516            access_time: Instant::now(),
517            access_count: 1,
518        };
519
520        self.storage.insert(key.clone(), entry);
521        self.current_size += tensor_size;
522        self.update_access_tracking(&key);
523
524        Ok(())
525    }
526
527    pub fn retrieve(&mut self, key: &str) -> Result<Option<Array2<T>>> {
528        let tensor_result = if let Some(entry) = self.storage.get_mut(key) {
529            entry.access_time = Instant::now();
530            entry.access_count += 1;
531            Some(entry.tensor.clone())
532        } else {
533            None
534        };
535
536        // Update access tracking after releasing the mutable borrow
537        if tensor_result.is_some() {
538            self.update_access_tracking(key);
539        }
540
541        Ok(tensor_result)
542    }
543
544    pub fn remove(&mut self, key: &str) -> Result<bool> {
545        if let Some(entry) = self.storage.remove(key) {
546            self.current_size -= entry.size;
547            self.remove_from_access_order(key);
548            self.insertion_order.retain(|k| k != key);
549            self.access_frequency.remove(key);
550            Ok(true)
551        } else {
552            Ok(false)
553        }
554    }
555
556    pub fn contains(&self, key: &str) -> bool {
557        self.storage.contains_key(key)
558    }
559
560    pub fn clear(&mut self) -> Result<()> {
561        self.storage.clear();
562        self.access_order.clear();
563        self.insertion_order.clear();
564        self.access_frequency.clear();
565        self.current_size = 0;
566        Ok(())
567    }
568
569    pub fn get_memory_usage(&self) -> usize {
570        self.current_size
571    }
572
573    pub fn evict_lru(&mut self) -> Result<()> {
574        if let Some(oldest_key) = self.access_order.front().cloned() {
575            self.remove(&oldest_key)?;
576        }
577        Ok(())
578    }
579
580    fn evict_one(&mut self) -> Result<()> {
581        match self.eviction_strategy {
582            CacheEvictionStrategy::LRU => self.evict_lru(),
583            CacheEvictionStrategy::LFU => self.evict_lfu(),
584            CacheEvictionStrategy::FIFO => self.evict_fifo(),
585            CacheEvictionStrategy::Random => self.evict_random(),
586        }
587    }
588
589    fn evict_lfu(&mut self) -> Result<()> {
590        if let Some(lfu_key) = self
591            .access_frequency
592            .iter()
593            .min_by_key(|(_, &freq)| freq)
594            .map(|(key, _)| key.clone())
595        {
596            self.remove(&lfu_key)?;
597        }
598        Ok(())
599    }
600
601    /// Evict the oldest **inserted** entry.
602    ///
603    /// FIFO is insertion order and must ignore accesses. This used to read
604    /// `access_order.front()`, which `update_access_tracking` re-orders on every
605    /// access — making FIFO an exact duplicate of LRU. It now uses a dedicated
606    /// `insertion_order` queue that accesses never touch.
607    fn evict_fifo(&mut self) -> Result<()> {
608        if let Some(first_key) = self.insertion_order.front().cloned() {
609            self.remove(&first_key)?;
610        }
611        Ok(())
612    }
613
614    /// Evict a uniformly random entry.
615    ///
616    /// This used to take `storage.keys().next()`, i.e. whatever the hash order
617    /// put first — deterministic within a process and strongly biased, not
618    /// random. It now draws a uniform index over the live keys.
619    fn evict_random(&mut self) -> Result<()> {
620        if self.storage.is_empty() {
621            return Ok(());
622        }
623        let keys: Vec<String> = self.storage.keys().cloned().collect();
624        let index = scirs2_core::random::thread_rng().gen_range(0..keys.len());
625        let victim = keys[index].clone();
626        self.remove(&victim)?;
627        Ok(())
628    }
629
630    fn update_access_tracking(&mut self, key: &str) {
631        // Update LRU order
632        self.remove_from_access_order(key);
633        self.access_order.push_back(key.to_string());
634
635        // Insertion order is recorded once and never re-ordered by an access,
636        // which is what separates FIFO eviction from LRU eviction.
637        if !self.insertion_order.iter().any(|k| k == key) {
638            self.insertion_order.push_back(key.to_string());
639        }
640
641        // Update LFU frequency
642        *self.access_frequency.entry(key.to_string()).or_insert(0) += 1;
643    }
644
645    fn remove_from_access_order(&mut self, key: &str) {
646        self.access_order.retain(|k| k != key);
647    }
648
649    pub fn get_all_items(&self) -> Result<Vec<(String, Array2<T>)>> {
650        let items = self
651            .storage
652            .iter()
653            .map(|(key, entry)| (key.clone(), entry.tensor.clone()))
654            .collect();
655        Ok(items)
656    }
657}
658
659/// Cache entry
660#[derive(Debug, Clone)]
661pub struct CacheEntry<
662    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
663> {
664    pub tensor: Array2<T>,
665    pub size: usize,
666    pub access_time: Instant,
667    pub access_count: usize,
668}
669
670/// Compression manager
671pub struct CompressionManager<
672    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
673> {
674    /// Compressed storage
675    compressed_storage: HashMap<String, CompressedData<T>>,
676
677    /// Memory usage
678    memory_usage: usize,
679
680    /// Phantom data for type parameter
681    _phantom: std::marker::PhantomData<T>,
682}
683
684impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
685    CompressionManager<T>
686{
687    /// A quantizing compression manager.
688    ///
689    /// There is no configurable target ratio: [`Self::compress`] always
690    /// quantizes to 8-bit codes, so the achieved ratio is fixed by
691    /// `size_of::<T>()` and reported per payload by
692    /// [`CompressedData::compression_ratio`]. The `compression_ratio` this used
693    /// to accept was stored and never consulted by the quantizer, which made it
694    /// a knob that silently did nothing.
695    pub fn new() -> Result<Self> {
696        Ok(Self {
697            compressed_storage: HashMap::new(),
698            memory_usage: 0,
699            _phantom: std::marker::PhantomData,
700        })
701    }
702
703    /// Compress a tensor by affine 8-bit quantization.
704    ///
705    /// Every element is mapped to a `u8` code by
706    /// `code = round((x - min) / step)` with `step = (max - min) / 255`, and the
707    /// payload is those codes plus the two `f64` parameters `min` and `step`.
708    /// `compressed_size` is therefore the **real** byte count of what is stored:
709    /// `codes.len() + 2·size_of::<f64>() + shape metadata`.
710    ///
711    /// This replaces a version that stored the input verbatim as `Vec<T>` and
712    /// reported `compressed_size = bytes / 2` — a fabricated 50% ratio for a
713    /// payload that had not shrunk at all.
714    ///
715    /// Quantization is lossy; the reconstruction error is bounded by `step / 2`
716    /// per element, which [`CompressedData::max_reconstruction_error`] reports. A constant
717    /// tensor (`max == min`) has `step = 0` and round-trips exactly.
718    ///
719    /// # Errors
720    /// Returns `Err` when the tensor is empty or contains a non-finite value
721    /// (there is no finite quantization range for `NaN`/`inf`).
722    pub fn compress(&self, tensor: &Array2<T>) -> Result<CompressedData<T>> {
723        if tensor.is_empty() {
724            return Err(crate::error::OptimError::InsufficientData(
725                "cannot compress an empty tensor".to_string(),
726            ));
727        }
728        let mut min = f64::INFINITY;
729        let mut max = f64::NEG_INFINITY;
730        for value in tensor.iter() {
731            let v = value.to_f64().unwrap_or(f64::NAN);
732            if !v.is_finite() {
733                return Err(crate::error::OptimError::ComputationError(
734                    "cannot quantize a tensor containing a non-finite value".to_string(),
735                ));
736            }
737            if v < min {
738                min = v;
739            }
740            if v > max {
741                max = v;
742            }
743        }
744
745        let levels = 255.0_f64;
746        let step = if max > min { (max - min) / levels } else { 0.0 };
747        let codes: Vec<u8> = tensor
748            .iter()
749            .map(|value| {
750                let v = value.to_f64().unwrap_or(0.0);
751                if step <= 0.0 {
752                    0u8
753                } else {
754                    (((v - min) / step).round()).clamp(0.0, levels) as u8
755                }
756            })
757            .collect();
758
759        let shape = tensor.shape().to_vec();
760        let metadata_bytes =
761            2 * std::mem::size_of::<f64>() + shape.len() * std::mem::size_of::<usize>();
762        Ok(CompressedData::<T> {
763            shape,
764            codes,
765            quantization_min: min,
766            quantization_step: step,
767            original_size: tensor.len() * std::mem::size_of::<T>(),
768            compressed_size: codes_len_bytes(tensor.len()) + metadata_bytes,
769            _phantom: std::marker::PhantomData,
770        })
771    }
772
773    /// Reconstruct a quantized tensor: `x̂ = min + code · step`.
774    ///
775    /// # Errors
776    /// Returns `Err` when the stored shape is not two-dimensional or does not
777    /// match the number of stored codes.
778    pub fn decompress(&self, compressed: &CompressedData<T>) -> Result<Array2<T>> {
779        if compressed.shape.len() != 2 {
780            return Err(crate::error::OptimError::InvalidConfig(format!(
781                "compressed payload has a {}-dimensional shape, expected 2",
782                compressed.shape.len()
783            )));
784        }
785        let expected = compressed.shape[0] * compressed.shape[1];
786        if compressed.codes.len() != expected {
787            return Err(crate::error::OptimError::ComputationError(format!(
788                "compressed payload holds {} codes but its shape implies {expected}",
789                compressed.codes.len()
790            )));
791        }
792        let values: Vec<T> = compressed
793            .codes
794            .iter()
795            .map(|&code| {
796                let v = compressed.quantization_min + code as f64 * compressed.quantization_step;
797                scirs2_core::numeric::NumCast::from(v).unwrap_or_else(|| T::zero())
798            })
799            .collect();
800        Array2::from_shape_vec((compressed.shape[0], compressed.shape[1]), values)
801            .map_err(|_| crate::error::OptimError::Other("Decompression failed".to_string()))
802    }
803
804    pub fn store(&mut self, key: String, compressed: CompressedData<T>) -> Result<()> {
805        self.memory_usage += compressed.compressed_size;
806        self.compressed_storage.insert(key, compressed);
807        Ok(())
808    }
809
810    pub fn retrieve(&self, key: &str) -> Result<Option<CompressedData<T>>> {
811        Ok(self.compressed_storage.get(key).cloned())
812    }
813
814    pub fn remove(&mut self, key: &str) -> Result<bool> {
815        if let Some(compressed) = self.compressed_storage.remove(key) {
816            self.memory_usage -= compressed.compressed_size;
817            Ok(true)
818        } else {
819            Ok(false)
820        }
821    }
822
823    pub fn clear(&mut self) -> Result<()> {
824        self.compressed_storage.clear();
825        self.memory_usage = 0;
826        Ok(())
827    }
828
829    pub fn get_memory_usage(&self) -> usize {
830        self.memory_usage
831    }
832
833    pub fn optimize_compression_ratios(&mut self, _patterns: &AccessPatterns) -> Result<()> {
834        // Optimize compression based on access patterns
835        // This is a placeholder for more sophisticated compression optimization
836        Ok(())
837    }
838}
839
840/// Number of payload bytes `count` 8-bit quantization codes occupy.
841fn codes_len_bytes(count: usize) -> usize {
842    count // one u8 per element
843}
844
845/// A quantized tensor.
846///
847/// The payload is `codes` (one `u8` per element) plus the affine dequantization
848/// parameters. The old version of this struct stored `data: Vec<T>` — the
849/// uncompressed input — while advertising a halved `compressed_size`.
850#[derive(Debug, Clone)]
851pub struct CompressedData<
852    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
853> {
854    /// Original tensor shape (2-D).
855    pub shape: Vec<usize>,
856    /// Quantization codes, row-major.
857    pub codes: Vec<u8>,
858    /// Value the zero code maps to.
859    pub quantization_min: f64,
860    /// Value increment per code step (`0` for a constant tensor).
861    pub quantization_step: f64,
862    /// Bytes the uncompressed tensor occupied.
863    pub original_size: usize,
864    /// Bytes this payload occupies, counted for real.
865    pub compressed_size: usize,
866    _phantom: std::marker::PhantomData<T>,
867}
868
869impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
870    CompressedData<T>
871{
872    /// Achieved compression ratio `compressed_size / original_size`.
873    ///
874    /// A *measurement*, not the configured target: for `f64` input this is
875    /// roughly `1/8` plus metadata, and for `f32` roughly `1/4`.
876    pub fn compression_ratio(&self) -> f64 {
877        if self.original_size == 0 {
878            return 1.0;
879        }
880        self.compressed_size as f64 / self.original_size as f64
881    }
882
883    /// Worst-case absolute reconstruction error, `quantization_step / 2`.
884    pub fn max_reconstruction_error(&self) -> f64 {
885        self.quantization_step / 2.0
886    }
887
888    /// Number of quantized elements.
889    pub fn element_count(&self) -> usize {
890        self.codes.len()
891    }
892}
893
894/// Memory statistics
895#[derive(Debug, Clone)]
896pub struct MemoryStatistics {
897    /// Total storage operations
898    pub total_stores: usize,
899
900    /// Total retrieval operations
901    pub total_retrievals: usize,
902
903    /// Cache hits
904    pub cache_hits: usize,
905
906    /// Cache misses
907    pub cache_misses: usize,
908
909    /// Total bytes stored
910    pub total_bytes_stored: usize,
911
912    /// Average storage time
913    pub average_storage_time: Duration,
914
915    /// Average retrieval time
916    pub average_retrieval_time: Duration,
917
918    /// Memory pressure events
919    pub pressure_events: usize,
920}
921
922impl Default for MemoryStatistics {
923    fn default() -> Self {
924        Self::new()
925    }
926}
927
928impl MemoryStatistics {
929    pub fn new() -> Self {
930        Self {
931            total_stores: 0,
932            total_retrievals: 0,
933            cache_hits: 0,
934            cache_misses: 0,
935            total_bytes_stored: 0,
936            average_storage_time: Duration::new(0, 0),
937            average_retrieval_time: Duration::new(0, 0),
938            pressure_events: 0,
939        }
940    }
941
942    pub fn record_storage(&mut self, bytes: usize, time: Duration) {
943        self.total_stores += 1;
944        self.total_bytes_stored += bytes;
945        self.average_storage_time = (self.average_storage_time * (self.total_stores - 1) as u32
946            + time)
947            / self.total_stores as u32;
948    }
949
950    pub fn record_retrieval(&mut self, time: Duration, hit: bool) {
951        self.total_retrievals += 1;
952        if hit {
953            self.cache_hits += 1;
954        } else {
955            self.cache_misses += 1;
956        }
957        self.average_retrieval_time =
958            (self.average_retrieval_time * (self.total_retrievals - 1) as u32 + time)
959                / self.total_retrievals as u32;
960    }
961
962    pub fn record_pressure_event(&mut self) {
963        self.pressure_events += 1;
964    }
965
966    pub fn get_hit_ratio(&self) -> f64 {
967        if self.total_retrievals > 0 {
968            self.cache_hits as f64 / self.total_retrievals as f64
969        } else {
970            0.0
971        }
972    }
973
974    pub fn reset(&mut self) {
975        *self = Self::new();
976    }
977}
978
979/// Access tracker
980pub struct AccessTracker {
981    /// Read access log
982    read_log: VecDeque<AccessEvent>,
983
984    /// Write access log
985    write_log: VecDeque<AccessEvent>,
986
987    /// Maximum log size
988    max_log_size: usize,
989}
990
991impl AccessTracker {
992    pub fn new(max_log_size: usize) -> Self {
993        Self {
994            read_log: VecDeque::new(),
995            write_log: VecDeque::new(),
996            max_log_size,
997        }
998    }
999
1000    pub fn record_read(&mut self, key: String) {
1001        self.read_log.push_back(AccessEvent {
1002            key,
1003            timestamp: Instant::now(),
1004        });
1005
1006        if self.read_log.len() > self.max_log_size {
1007            self.read_log.pop_front();
1008        }
1009    }
1010
1011    pub fn record_write(&mut self, key: String) {
1012        self.write_log.push_back(AccessEvent {
1013            key,
1014            timestamp: Instant::now(),
1015        });
1016
1017        if self.write_log.len() > self.max_log_size {
1018            self.write_log.pop_front();
1019        }
1020    }
1021
1022    pub fn record_removal(&mut self, _key: String) {
1023        // Record removal operation
1024    }
1025
1026    pub fn analyze_patterns(&self) -> AccessPatterns {
1027        let mut frequency_map = HashMap::new();
1028
1029        // Count access frequencies
1030        for event in self.read_log.iter().chain(self.write_log.iter()) {
1031            *frequency_map.entry(event.key.clone()).or_insert(0) += 1;
1032        }
1033
1034        let total_accesses: usize = frequency_map.values().sum();
1035        let average_frequency = if frequency_map.is_empty() {
1036            0.0
1037        } else {
1038            total_accesses as f64 / frequency_map.len() as f64
1039        };
1040
1041        AccessPatterns {
1042            frequency_map,
1043            average_frequency,
1044            total_accesses,
1045        }
1046    }
1047
1048    pub fn clear(&mut self) {
1049        self.read_log.clear();
1050        self.write_log.clear();
1051    }
1052}
1053
1054/// Access event
1055#[derive(Debug, Clone)]
1056pub struct AccessEvent {
1057    pub key: String,
1058    pub timestamp: Instant,
1059}
1060
1061/// Access patterns analysis
1062#[derive(Debug, Clone)]
1063pub struct AccessPatterns {
1064    pub frequency_map: HashMap<String, usize>,
1065    pub average_frequency: f64,
1066    pub total_accesses: usize,
1067}
1068
1069/// Memory pressure monitor
1070pub struct MemoryPressureMonitor {
1071    /// Current memory usage
1072    current_usage: usize,
1073
1074    /// Maximum allowed memory
1075    max_memory: usize,
1076
1077    /// Pressure thresholds
1078    warning_threshold: f64,
1079    critical_threshold: f64,
1080
1081    /// Pressure history
1082    pressure_history: VecDeque<f64>,
1083}
1084
1085impl Default for MemoryPressureMonitor {
1086    fn default() -> Self {
1087        Self::new()
1088    }
1089}
1090
1091impl MemoryPressureMonitor {
1092    pub fn new() -> Self {
1093        Self {
1094            current_usage: 0,
1095            max_memory: 1024 * 1024 * 1024, // 1GB default
1096            warning_threshold: 0.7,
1097            critical_threshold: 0.9,
1098            pressure_history: VecDeque::new(),
1099        }
1100    }
1101
1102    pub fn update(&mut self, current_usage: usize) {
1103        self.current_usage = current_usage;
1104        let pressure_ratio = self.get_pressure_ratio();
1105
1106        self.pressure_history.push_back(pressure_ratio);
1107        if self.pressure_history.len() > 100 {
1108            self.pressure_history.pop_front();
1109        }
1110    }
1111
1112    pub fn get_pressure_ratio(&self) -> f64 {
1113        if self.max_memory > 0 {
1114            self.current_usage as f64 / self.max_memory as f64
1115        } else {
1116            0.0
1117        }
1118    }
1119
1120    pub fn is_high_pressure(&self) -> bool {
1121        self.get_pressure_ratio() > self.critical_threshold
1122    }
1123
1124    pub fn is_warning_pressure(&self) -> bool {
1125        self.get_pressure_ratio() > self.warning_threshold
1126    }
1127
1128    pub fn reset(&mut self) {
1129        self.current_usage = 0;
1130        self.pressure_history.clear();
1131    }
1132}
1133
1134/// Optimization report
1135#[derive(Debug, Clone)]
1136pub struct OptimizationReport {
1137    pub initial_memory_usage: usize,
1138    pub final_memory_usage: usize,
1139    pub memory_saved: usize,
1140    pub optimization_time: Duration,
1141    pub operations_performed: usize,
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146    use super::*;
1147
1148    #[test]
1149    fn test_memory_manager_creation() {
1150        let config = super::super::config::TransformerBasedOptimizerConfig::<f32>::default();
1151        let manager = TransformerMemoryManager::new(&config);
1152        assert!(manager.is_ok());
1153    }
1154
1155    #[test]
1156    fn test_memory_cache() {
1157        let cache = MemoryCache::<f32>::new(1024 * 1024, CacheEvictionStrategy::LRU);
1158        assert!(cache.is_ok());
1159
1160        let mut c = cache.expect("MemoryCache::new should succeed");
1161        let tensor = Array2::<f32>::ones((10, 10));
1162        assert!(c.store("test".to_string(), tensor).is_ok());
1163        assert!(c.contains("test"));
1164    }
1165
1166    /// F59: storing a tensor larger than the whole cache used to drain every
1167    /// existing entry and *then* report the error.
1168    #[test]
1169    fn an_oversized_tensor_is_rejected_without_evicting_anything() {
1170        let mut cache = MemoryCache::<f64>::new(4096, CacheEvictionStrategy::LRU).expect("cache");
1171        cache
1172            .store("keep_a".to_string(), Array2::<f64>::ones((4, 4)))
1173            .expect("store a");
1174        cache
1175            .store("keep_b".to_string(), Array2::<f64>::ones((4, 4)))
1176            .expect("store b");
1177        let before = cache.get_memory_usage();
1178        assert!(before > 0);
1179
1180        // 100 x 100 f64 = 80_000 bytes, far larger than the 4096-byte cache.
1181        let huge = Array2::<f64>::ones((100, 100));
1182        assert!(cache.store("huge".to_string(), huge).is_err());
1183
1184        assert!(cache.contains("keep_a"), "keep_a was evicted for nothing");
1185        assert!(cache.contains("keep_b"), "keep_b was evicted for nothing");
1186        assert_eq!(
1187            cache.get_memory_usage(),
1188            before,
1189            "the failed store changed the cache size"
1190        );
1191    }
1192
1193    /// F67: FIFO eviction must be *insertion* order. It used to read the same
1194    /// access-ordered queue LRU uses, making the two strategies identical.
1195    #[test]
1196    fn fifo_eviction_ignores_accesses_unlike_lru() {
1197        // Each 4x4 f64 tensor is 128 bytes; a 300-byte cache holds two.
1198        let build = |strategy: CacheEvictionStrategy| {
1199            let mut cache = MemoryCache::<f64>::new(300, strategy).expect("cache");
1200            cache
1201                .store("first".to_string(), Array2::<f64>::ones((4, 4)))
1202                .expect("store first");
1203            cache
1204                .store("second".to_string(), Array2::<f64>::ones((4, 4)))
1205                .expect("store second");
1206            // Touch "first" so it becomes the most recently used.
1207            assert!(cache.retrieve("first").expect("retrieve").is_some());
1208            cache
1209                .store("third".to_string(), Array2::<f64>::ones((4, 4)))
1210                .expect("store third");
1211            cache
1212        };
1213
1214        let lru = build(CacheEvictionStrategy::LRU);
1215        assert!(
1216            lru.contains("first"),
1217            "LRU must keep the recently accessed entry"
1218        );
1219        assert!(!lru.contains("second"), "LRU should drop 'second'");
1220
1221        let fifo = build(CacheEvictionStrategy::FIFO);
1222        assert!(
1223            !fifo.contains("first"),
1224            "FIFO must drop the first-inserted entry regardless of access"
1225        );
1226        assert!(fifo.contains("second"), "FIFO should keep 'second'");
1227        assert!(fifo.contains("third"));
1228    }
1229
1230    /// F67: `Random` eviction used to take `storage.keys().next()`, which is hash
1231    /// order — not random. Over many trials every candidate must get picked.
1232    #[test]
1233    fn random_eviction_actually_varies() {
1234        let mut victims = std::collections::HashSet::new();
1235        for _ in 0..200 {
1236            let mut cache =
1237                MemoryCache::<f64>::new(300, CacheEvictionStrategy::Random).expect("cache");
1238            for key in ["a", "b"] {
1239                cache
1240                    .store(key.to_string(), Array2::<f64>::ones((4, 4)))
1241                    .expect("store");
1242            }
1243            cache
1244                .store("c".to_string(), Array2::<f64>::ones((4, 4)))
1245                .expect("store c");
1246            for key in ["a", "b"] {
1247                if !cache.contains(key) {
1248                    victims.insert(key.to_string());
1249                }
1250            }
1251        }
1252        assert_eq!(
1253            victims.len(),
1254            2,
1255            "random eviction only ever chose {victims:?}"
1256        );
1257    }
1258
1259    /// F60: compression reported `bytes / 2` while storing the input verbatim.
1260    /// The ratio must now be a measurement of a payload that really did shrink,
1261    /// and the round trip must reconstruct within the quantization step.
1262    #[test]
1263    fn compression_reports_a_real_measured_ratio() {
1264        let comp = CompressionManager::<f64>::new().expect("manager");
1265        let tensor = Array2::from_shape_fn((8, 8), |(i, j)| (i as f64) - 0.5 * (j as f64));
1266        let compressed = comp.compress(&tensor).expect("compress");
1267
1268        assert_eq!(compressed.element_count(), 64);
1269        assert_eq!(compressed.original_size, 64 * std::mem::size_of::<f64>());
1270        // 64 code bytes + 2 f64 params + 2 usize shape entries.
1271        let expected = 64 + 2 * std::mem::size_of::<f64>() + 2 * std::mem::size_of::<usize>();
1272        assert_eq!(
1273            compressed.compressed_size, expected,
1274            "compressed_size is not the real payload size"
1275        );
1276        assert!(
1277            compressed.compressed_size < compressed.original_size,
1278            "the payload did not shrink: {} vs {}",
1279            compressed.compressed_size,
1280            compressed.original_size
1281        );
1282        // The old fabricated value was exactly half.
1283        let half = compressed.original_size / 2;
1284        assert_ne!(
1285            compressed.compressed_size, half,
1286            "compressed_size is still the fabricated original/2"
1287        );
1288
1289        let restored = comp.decompress(&compressed).expect("decompress");
1290        assert_eq!(restored.dim(), tensor.dim());
1291        let tolerance = compressed.max_reconstruction_error() + 1e-12;
1292        for (a, b) in tensor.iter().zip(restored.iter()) {
1293            assert!(
1294                (a - b).abs() <= tolerance,
1295                "reconstruction error {} exceeds the quantization bound {tolerance}",
1296                (a - b).abs()
1297            );
1298        }
1299    }
1300
1301    #[test]
1302    fn a_constant_tensor_round_trips_exactly() {
1303        let comp = CompressionManager::<f64>::new().expect("manager");
1304        let tensor = Array2::<f64>::from_elem((3, 4), 2.5);
1305        let compressed = comp.compress(&tensor).expect("compress");
1306        assert_eq!(compressed.quantization_step, 0.0);
1307        assert_eq!(compressed.max_reconstruction_error(), 0.0);
1308        let restored = comp.decompress(&compressed).expect("decompress");
1309        assert_eq!(restored, tensor);
1310    }
1311
1312    #[test]
1313    fn compression_rejects_degenerate_input() {
1314        let comp = CompressionManager::<f64>::new().expect("manager");
1315        assert!(comp.compress(&Array2::<f64>::zeros((0, 3))).is_err());
1316        let mut nan = Array2::<f64>::zeros((2, 2));
1317        nan[[0, 0]] = f64::NAN;
1318        assert!(comp.compress(&nan).is_err());
1319    }
1320
1321    #[test]
1322    fn test_compression_manager() {
1323        let compression = CompressionManager::<f32>::new();
1324        assert!(compression.is_ok());
1325
1326        let comp = compression.expect("CompressionManager::new should succeed");
1327        let tensor = Array2::<f32>::ones((5, 5));
1328        let compressed = comp.compress(&tensor);
1329        assert!(compressed.is_ok());
1330
1331        let decompressed = comp.decompress(&compressed.expect("decompress should succeed"));
1332        assert!(decompressed.is_ok());
1333    }
1334
1335    #[test]
1336    fn test_access_tracker() {
1337        let mut tracker = AccessTracker::new(100);
1338
1339        tracker.record_read("key1".to_string());
1340        tracker.record_write("key2".to_string());
1341
1342        let patterns = tracker.analyze_patterns();
1343        assert!(patterns.total_accesses > 0);
1344    }
1345
1346    #[test]
1347    fn test_memory_pressure_monitor() {
1348        let mut monitor = MemoryPressureMonitor::new();
1349
1350        monitor.update(500 * 1024 * 1024); // 500MB
1351        assert!(!monitor.is_high_pressure());
1352
1353        monitor.update(950 * 1024 * 1024); // 950MB
1354        assert!(monitor.is_high_pressure());
1355    }
1356}