Skip to main content

sklears_svm/
thread_safe_cache.rs

1//! Thread-safe kernel caching for parallel SVM processing
2//!
3//! This module provides thread-safe caching implementations for kernel computations
4//! that enable efficient parallel processing of SVM algorithms while maintaining
5//! cache coherence and avoiding race conditions.
6
7use crate::kernels::Kernel;
8use dashmap::DashMap;
9use parking_lot::Mutex as ParkingMutex;
10use scirs2_core::ndarray::{Array2, ArrayView1};
11use scirs2_core::rand_prelude::IndexedRandom;
12use sklears_core::error::{Result, SklearsError};
13use sklears_core::types::Float;
14use std::collections::HashMap;
15use std::hash::{Hash, Hasher};
16use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
17use std::sync::Arc;
18
19/// Configuration for thread-safe kernel cache
20#[derive(Debug, Clone)]
21pub struct ThreadSafeKernelCacheConfig {
22    /// Maximum number of cached kernel values
23    pub max_cache_size: usize,
24    /// Number of cache shards for reducing contention
25    pub num_shards: usize,
26    /// Cache eviction strategy
27    pub eviction_strategy: EvictionStrategy,
28    /// Whether to enable cache statistics
29    pub enable_stats: bool,
30    /// Preallocation size for cache maps
31    pub prealloc_size: usize,
32    /// Concurrency level for DashMap
33    pub concurrency_level: usize,
34}
35
36impl Default for ThreadSafeKernelCacheConfig {
37    fn default() -> Self {
38        #[cfg(feature = "parallel")]
39        let default_threads = rayon::current_num_threads();
40        #[cfg(not(feature = "parallel"))]
41        let default_threads = num_cpus::get();
42
43        Self {
44            max_cache_size: 100000,
45            num_shards: default_threads,
46            eviction_strategy: EvictionStrategy::LeastRecentlyUsed,
47            enable_stats: true,
48            prealloc_size: 1000,
49            concurrency_level: default_threads,
50        }
51    }
52}
53
54/// Cache eviction strategies
55#[derive(Debug, Clone, Copy)]
56pub enum EvictionStrategy {
57    /// Least Recently Used eviction
58    LeastRecentlyUsed,
59    /// Least Frequently Used eviction
60    LeastFrequentlyUsed,
61    /// Random eviction
62    Random,
63    /// First In First Out eviction
64    FirstInFirstOut,
65}
66
67/// Thread-safe kernel cache using multiple strategies
68pub trait ThreadSafeKernelCache: Send + Sync {
69    /// Get cached kernel value
70    fn get(&self, key: &KernelCacheKey) -> Option<Float>;
71
72    /// Insert kernel value into cache
73    fn insert(&self, key: KernelCacheKey, value: Float);
74
75    /// Clear the cache
76    fn clear(&self);
77
78    /// Get cache statistics
79    fn stats(&self) -> CacheStatistics;
80
81    /// Get current cache size
82    fn size(&self) -> usize;
83}
84
85/// Cache key for kernel values
86#[derive(Debug, Clone, PartialEq, Eq, Hash)]
87pub struct KernelCacheKey {
88    /// First sample index (always <= second_index for canonical ordering)
89    pub first_index: usize,
90    /// Second sample index
91    pub second_index: usize,
92    /// Hash of kernel parameters for differentiation
93    pub kernel_hash: u64,
94}
95
96impl KernelCacheKey {
97    /// Create a new cache key with canonical ordering
98    pub fn new(i: usize, j: usize, kernel_hash: u64) -> Self {
99        let (first_index, second_index) = if i <= j { (i, j) } else { (j, i) };
100        Self {
101            first_index,
102            second_index,
103            kernel_hash,
104        }
105    }
106}
107
108/// Cache statistics for monitoring performance
109#[derive(Debug, Clone)]
110pub struct CacheStatistics {
111    pub hits: u64,
112    pub misses: u64,
113    pub insertions: u64,
114    pub evictions: u64,
115    pub current_size: usize,
116    pub max_size: usize,
117    pub hit_rate: f64,
118}
119
120impl Default for CacheStatistics {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl CacheStatistics {
127    pub fn new() -> Self {
128        Self {
129            hits: 0,
130            misses: 0,
131            insertions: 0,
132            evictions: 0,
133            current_size: 0,
134            max_size: 0,
135            hit_rate: 0.0,
136        }
137    }
138
139    pub fn update_hit_rate(&mut self) {
140        let total = self.hits + self.misses;
141        self.hit_rate = if total > 0 {
142            self.hits as f64 / total as f64
143        } else {
144            0.0
145        };
146    }
147}
148
149/// DashMap-based thread-safe cache with high concurrency
150pub struct DashMapKernelCache {
151    cache: DashMap<KernelCacheKey, CacheEntry>,
152    config: ThreadSafeKernelCacheConfig,
153    stats: Arc<ParkingMutex<CacheStatistics>>,
154    current_size: AtomicUsize,
155}
156
157#[derive(Debug)]
158struct CacheEntry {
159    value: Float,
160    access_count: AtomicU64,
161    insertion_order: u64,
162    last_access: AtomicU64,
163}
164
165impl Clone for CacheEntry {
166    fn clone(&self) -> Self {
167        Self {
168            value: self.value,
169            access_count: AtomicU64::new(self.access_count.load(Ordering::Relaxed)),
170            insertion_order: self.insertion_order,
171            last_access: AtomicU64::new(self.last_access.load(Ordering::Relaxed)),
172        }
173    }
174}
175
176impl DashMapKernelCache {
177    /// Create a new DashMap-based kernel cache
178    pub fn new(config: ThreadSafeKernelCacheConfig) -> Self {
179        let cache = DashMap::with_capacity_and_hasher(
180            config.prealloc_size,
181            std::collections::hash_map::RandomState::new(),
182        );
183
184        Self {
185            cache,
186            config,
187            stats: Arc::new(ParkingMutex::new(CacheStatistics::new())),
188            current_size: AtomicUsize::new(0),
189        }
190    }
191
192    /// Evict entries based on the configured strategy
193    fn evict_if_needed(&self) {
194        let current_size = self.current_size.load(Ordering::Relaxed);
195        if current_size >= self.config.max_cache_size {
196            let entries_to_evict = current_size - self.config.max_cache_size + 1;
197            self.evict_entries(entries_to_evict);
198        }
199    }
200
201    /// Evict specified number of entries
202    fn evict_entries(&self, count: usize) {
203        match self.config.eviction_strategy {
204            EvictionStrategy::LeastRecentlyUsed => self.evict_lru(count),
205            EvictionStrategy::LeastFrequentlyUsed => self.evict_lfu(count),
206            EvictionStrategy::Random => self.evict_random(count),
207            EvictionStrategy::FirstInFirstOut => self.evict_fifo(count),
208        }
209    }
210
211    /// Evict least recently used entries
212    fn evict_lru(&self, count: usize) {
213        let mut entries_to_remove = Vec::new();
214
215        // Collect entries with their last access times
216        for entry in self.cache.iter() {
217            let last_access = entry.value().last_access.load(Ordering::Relaxed);
218            entries_to_remove.push((entry.key().clone(), last_access));
219        }
220
221        // Sort by last access time (ascending) and remove oldest
222        entries_to_remove.sort_by_key(|(_, last_access)| *last_access);
223
224        let mut evicted = 0;
225        for (key, _) in entries_to_remove.into_iter().take(count) {
226            if self.cache.remove(&key).is_some() {
227                self.current_size.fetch_sub(1, Ordering::Relaxed);
228                evicted += 1;
229            }
230        }
231
232        if self.config.enable_stats {
233            let mut stats = self.stats.lock();
234            stats.evictions += evicted;
235        }
236    }
237
238    /// Evict least frequently used entries
239    fn evict_lfu(&self, count: usize) {
240        let mut entries_to_remove = Vec::new();
241
242        // Collect entries with their access counts
243        for entry in self.cache.iter() {
244            let access_count = entry.value().access_count.load(Ordering::Relaxed);
245            entries_to_remove.push((entry.key().clone(), access_count));
246        }
247
248        // Sort by access count (ascending) and remove least used
249        entries_to_remove.sort_by_key(|(_, access_count)| *access_count);
250
251        let mut evicted = 0;
252        for (key, _) in entries_to_remove.into_iter().take(count) {
253            if self.cache.remove(&key).is_some() {
254                self.current_size.fetch_sub(1, Ordering::Relaxed);
255                evicted += 1;
256            }
257        }
258
259        if self.config.enable_stats {
260            let mut stats = self.stats.lock();
261            stats.evictions += evicted;
262        }
263    }
264
265    /// Evict random entries
266    fn evict_random(&self, count: usize) {
267        let keys: Vec<_> = self.cache.iter().map(|entry| entry.key().clone()).collect();
268        let mut rng = scirs2_core::random::thread_rng();
269        let keys_to_remove: Vec<_> = keys.as_slice().sample(&mut rng, count).cloned().collect();
270
271        let mut evicted = 0;
272        for key in keys_to_remove {
273            if self.cache.remove(&key).is_some() {
274                self.current_size.fetch_sub(1, Ordering::Relaxed);
275                evicted += 1;
276            }
277        }
278
279        if self.config.enable_stats {
280            let mut stats = self.stats.lock();
281            stats.evictions += evicted;
282        }
283    }
284
285    /// Evict first in first out entries
286    fn evict_fifo(&self, count: usize) {
287        let mut entries_to_remove = Vec::new();
288
289        // Collect entries with their insertion order
290        for entry in self.cache.iter() {
291            let insertion_order = entry.value().insertion_order;
292            entries_to_remove.push((entry.key().clone(), insertion_order));
293        }
294
295        // Sort by insertion order (ascending) and remove oldest
296        entries_to_remove.sort_by_key(|(_, insertion_order)| *insertion_order);
297
298        let mut evicted = 0;
299        for (key, _) in entries_to_remove.into_iter().take(count) {
300            if self.cache.remove(&key).is_some() {
301                self.current_size.fetch_sub(1, Ordering::Relaxed);
302                evicted += 1;
303            }
304        }
305
306        if self.config.enable_stats {
307            let mut stats = self.stats.lock();
308            stats.evictions += evicted;
309        }
310    }
311}
312
313impl ThreadSafeKernelCache for DashMapKernelCache {
314    fn get(&self, key: &KernelCacheKey) -> Option<Float> {
315        if let Some(entry) = self.cache.get(key) {
316            entry.access_count.fetch_add(1, Ordering::Relaxed);
317            entry.last_access.store(
318                std::time::SystemTime::now()
319                    .duration_since(std::time::UNIX_EPOCH)
320                    .expect("value should be present")
321                    .as_nanos() as u64,
322                Ordering::Relaxed,
323            );
324
325            if self.config.enable_stats {
326                let mut stats = self.stats.lock();
327                stats.hits += 1;
328                stats.update_hit_rate();
329            }
330
331            Some(entry.value)
332        } else {
333            if self.config.enable_stats {
334                let mut stats = self.stats.lock();
335                stats.misses += 1;
336                stats.update_hit_rate();
337            }
338            None
339        }
340    }
341
342    fn insert(&self, key: KernelCacheKey, value: Float) {
343        self.evict_if_needed();
344
345        let entry = CacheEntry {
346            value,
347            access_count: AtomicU64::new(1),
348            insertion_order: std::time::SystemTime::now()
349                .duration_since(std::time::UNIX_EPOCH)
350                .expect("value should be present")
351                .as_nanos() as u64,
352            last_access: AtomicU64::new(
353                std::time::SystemTime::now()
354                    .duration_since(std::time::UNIX_EPOCH)
355                    .expect("value should be present")
356                    .as_nanos() as u64,
357            ),
358        };
359
360        if self.cache.insert(key, entry).is_none() {
361            self.current_size.fetch_add(1, Ordering::Relaxed);
362        }
363
364        if self.config.enable_stats {
365            let mut stats = self.stats.lock();
366            stats.insertions += 1;
367            stats.current_size = self.current_size.load(Ordering::Relaxed);
368            stats.max_size = self.config.max_cache_size;
369        }
370    }
371
372    fn clear(&self) {
373        self.cache.clear();
374        self.current_size.store(0, Ordering::Relaxed);
375
376        if self.config.enable_stats {
377            let mut stats = self.stats.lock();
378            *stats = CacheStatistics::new();
379        }
380    }
381
382    fn stats(&self) -> CacheStatistics {
383        if self.config.enable_stats {
384            let mut stats = self.stats.lock();
385            stats.current_size = self.current_size.load(Ordering::Relaxed);
386            stats.max_size = self.config.max_cache_size;
387            stats.clone()
388        } else {
389            CacheStatistics::new()
390        }
391    }
392
393    fn size(&self) -> usize {
394        self.current_size.load(Ordering::Relaxed)
395    }
396}
397
398/// Sharded kernel cache for reduced contention
399pub struct ShardedKernelCache {
400    shards: Vec<Arc<DashMapKernelCache>>,
401    num_shards: usize,
402}
403
404impl ShardedKernelCache {
405    /// Create a new sharded kernel cache
406    pub fn new(config: ThreadSafeKernelCacheConfig) -> Self {
407        let shard_size = config.max_cache_size / config.num_shards;
408        let mut shard_config = config.clone();
409        shard_config.max_cache_size = shard_size;
410
411        let shards = (0..config.num_shards)
412            .map(|_| Arc::new(DashMapKernelCache::new(shard_config.clone())))
413            .collect();
414
415        Self {
416            shards,
417            num_shards: config.num_shards,
418        }
419    }
420
421    /// Get shard index for a key
422    fn get_shard_index(&self, key: &KernelCacheKey) -> usize {
423        use std::collections::hash_map::DefaultHasher;
424
425        let mut hasher = DefaultHasher::new();
426        key.hash(&mut hasher);
427        (hasher.finish() as usize) % self.num_shards
428    }
429}
430
431impl ThreadSafeKernelCache for ShardedKernelCache {
432    fn get(&self, key: &KernelCacheKey) -> Option<Float> {
433        let shard_index = self.get_shard_index(key);
434        self.shards[shard_index].get(key)
435    }
436
437    fn insert(&self, key: KernelCacheKey, value: Float) {
438        let shard_index = self.get_shard_index(&key);
439        self.shards[shard_index].insert(key, value);
440    }
441
442    fn clear(&self) {
443        for shard in &self.shards {
444            shard.clear();
445        }
446    }
447
448    fn stats(&self) -> CacheStatistics {
449        let mut combined_stats = CacheStatistics::new();
450
451        for shard in &self.shards {
452            let shard_stats = shard.stats();
453            combined_stats.hits += shard_stats.hits;
454            combined_stats.misses += shard_stats.misses;
455            combined_stats.insertions += shard_stats.insertions;
456            combined_stats.evictions += shard_stats.evictions;
457            combined_stats.current_size += shard_stats.current_size;
458            combined_stats.max_size += shard_stats.max_size;
459        }
460
461        combined_stats.update_hit_rate();
462        combined_stats
463    }
464
465    fn size(&self) -> usize {
466        self.shards.iter().map(|shard| shard.size()).sum()
467    }
468}
469
470/// Cached kernel wrapper that automatically handles caching
471pub struct CachedKernel {
472    inner_kernel: Box<dyn Kernel>,
473    cache: Arc<dyn ThreadSafeKernelCache>,
474    kernel_hash: u64,
475    x_data: Option<Array2<Float>>, // Store data for index-based access
476}
477
478impl std::fmt::Debug for CachedKernel {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        f.debug_struct("CachedKernel")
481            .field("inner_kernel", &self.inner_kernel)
482            .field("kernel_hash", &self.kernel_hash)
483            .field("x_data", &self.x_data.as_ref().map(|x| x.dim()))
484            .finish()
485    }
486}
487
488impl CachedKernel {
489    /// Create a new cached kernel
490    pub fn new(kernel: Box<dyn Kernel>, cache: Arc<dyn ThreadSafeKernelCache>) -> Self {
491        // Generate hash for kernel parameters
492        let kernel_debug = format!("{kernel:?}");
493        let kernel_hash = Self::compute_kernel_hash_from_string(&kernel_debug);
494
495        Self {
496            inner_kernel: kernel,
497            cache,
498            kernel_hash,
499            x_data: None,
500        }
501    }
502
503    /// Set data for index-based caching
504    pub fn set_data(&mut self, x: Array2<Float>) {
505        self.x_data = Some(x);
506    }
507
508    /// Compute kernel value with caching by indices
509    pub fn compute_by_indices(&self, i: usize, j: usize) -> Result<Float> {
510        let key = KernelCacheKey::new(i, j, self.kernel_hash);
511
512        if let Some(cached_value) = self.cache.get(&key) {
513            return Ok(cached_value);
514        }
515
516        if let Some(ref x) = self.x_data {
517            let value = self
518                .inner_kernel
519                .compute(x.row(i).to_owned().view(), x.row(j).to_owned().view());
520            self.cache.insert(key, value);
521            Ok(value)
522        } else {
523            Err(SklearsError::InvalidInput(
524                "No data set for index-based computation".to_string(),
525            ))
526        }
527    }
528
529    /// Get cache statistics
530    pub fn cache_stats(&self) -> CacheStatistics {
531        self.cache.stats()
532    }
533
534    /// Clear cache
535    pub fn clear_cache(&self) {
536        self.cache.clear();
537    }
538
539    /// Compute hash for kernel parameters from string
540    fn compute_kernel_hash_from_string(kernel_debug: &str) -> u64 {
541        use std::collections::hash_map::DefaultHasher;
542        use std::hash::Hasher;
543
544        let mut hasher = DefaultHasher::new();
545        kernel_debug.hash(&mut hasher);
546        hasher.finish()
547    }
548}
549
550impl Kernel for CachedKernel {
551    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
552        // For direct vector computation, we can't use index-based caching
553        // So we compute directly
554        self.inner_kernel.compute(x, y)
555    }
556
557    fn compute_matrix(&self, x: &Array2<f64>, y: &Array2<f64>) -> Array2<f64> {
558        let n_x = x.nrows();
559        let n_y = y.nrows();
560        let mut kernel_matrix = Array2::<f64>::zeros((n_x, n_y));
561
562        // Use cached computation for matrix
563        for i in 0..n_x {
564            for j in 0..n_y {
565                let key = KernelCacheKey::new(i, j, self.kernel_hash);
566
567                let k_val = if let Some(cached_value) = self.cache.get(&key) {
568                    cached_value
569                } else {
570                    let value = self.inner_kernel.compute(x.row(i), y.row(j));
571                    self.cache.insert(key, value);
572                    value
573                };
574
575                kernel_matrix[[i, j]] = k_val;
576            }
577        }
578
579        kernel_matrix
580    }
581
582    fn parameters(&self) -> HashMap<String, f64> {
583        // Delegate to inner kernel
584        self.inner_kernel.parameters()
585    }
586}
587
588#[allow(non_snake_case)]
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use crate::kernels::LinearKernel;
593    use scirs2_core::ndarray::Array1;
594
595    #[test]
596    fn test_dashmap_cache_basic_operations() {
597        let config = ThreadSafeKernelCacheConfig::default();
598        let cache = DashMapKernelCache::new(config);
599
600        let key = KernelCacheKey::new(0, 1, 12345);
601        cache.insert(key.clone(), 1.5);
602
603        assert_eq!(cache.get(&key), Some(1.5));
604        assert_eq!(cache.size(), 1);
605    }
606
607    #[test]
608    fn test_sharded_cache() {
609        let config = ThreadSafeKernelCacheConfig {
610            num_shards: 4,
611            ..ThreadSafeKernelCacheConfig::default()
612        };
613        let cache = ShardedKernelCache::new(config);
614
615        let key = KernelCacheKey::new(0, 1, 12345);
616        cache.insert(key.clone(), 2.5);
617
618        assert_eq!(cache.get(&key), Some(2.5));
619    }
620
621    #[test]
622    fn test_cached_kernel() {
623        let kernel = Box::new(LinearKernel);
624        let cache_config = ThreadSafeKernelCacheConfig::default();
625        let cache = Arc::new(DashMapKernelCache::new(cache_config));
626
627        let cached_kernel = CachedKernel::new(kernel, cache);
628
629        let x1 = Array1::from(vec![1.0, 2.0, 3.0]);
630        let x2 = Array1::from(vec![2.0, 3.0, 4.0]);
631
632        let result1 = cached_kernel.compute(x1.view(), x2.view());
633        let result2 = cached_kernel.compute(x1.view(), x2.view());
634
635        assert_eq!(result1, result2);
636    }
637
638    #[test]
639    fn test_cache_eviction() {
640        let config = ThreadSafeKernelCacheConfig {
641            max_cache_size: 2,
642            ..ThreadSafeKernelCacheConfig::default()
643        };
644        let cache = DashMapKernelCache::new(config);
645
646        // Insert 3 items to trigger eviction
647        cache.insert(KernelCacheKey::new(0, 1, 1), 1.0);
648        cache.insert(KernelCacheKey::new(1, 2, 1), 2.0);
649        cache.insert(KernelCacheKey::new(2, 3, 1), 3.0);
650
651        // Cache should not exceed max size
652        assert!(cache.size() <= 2);
653    }
654}