1use 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#[derive(Debug, Clone)]
21pub struct ThreadSafeKernelCacheConfig {
22 pub max_cache_size: usize,
24 pub num_shards: usize,
26 pub eviction_strategy: EvictionStrategy,
28 pub enable_stats: bool,
30 pub prealloc_size: usize,
32 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#[derive(Debug, Clone, Copy)]
56pub enum EvictionStrategy {
57 LeastRecentlyUsed,
59 LeastFrequentlyUsed,
61 Random,
63 FirstInFirstOut,
65}
66
67pub trait ThreadSafeKernelCache: Send + Sync {
69 fn get(&self, key: &KernelCacheKey) -> Option<Float>;
71
72 fn insert(&self, key: KernelCacheKey, value: Float);
74
75 fn clear(&self);
77
78 fn stats(&self) -> CacheStatistics;
80
81 fn size(&self) -> usize;
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Hash)]
87pub struct KernelCacheKey {
88 pub first_index: usize,
90 pub second_index: usize,
92 pub kernel_hash: u64,
94}
95
96impl KernelCacheKey {
97 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#[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
149pub 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 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 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 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 fn evict_lru(&self, count: usize) {
213 let mut entries_to_remove = Vec::new();
214
215 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 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 fn evict_lfu(&self, count: usize) {
240 let mut entries_to_remove = Vec::new();
241
242 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 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 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 fn evict_fifo(&self, count: usize) {
287 let mut entries_to_remove = Vec::new();
288
289 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 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
398pub struct ShardedKernelCache {
400 shards: Vec<Arc<DashMapKernelCache>>,
401 num_shards: usize,
402}
403
404impl ShardedKernelCache {
405 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 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
470pub struct CachedKernel {
472 inner_kernel: Box<dyn Kernel>,
473 cache: Arc<dyn ThreadSafeKernelCache>,
474 kernel_hash: u64,
475 x_data: Option<Array2<Float>>, }
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 pub fn new(kernel: Box<dyn Kernel>, cache: Arc<dyn ThreadSafeKernelCache>) -> Self {
491 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 pub fn set_data(&mut self, x: Array2<Float>) {
505 self.x_data = Some(x);
506 }
507
508 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 pub fn cache_stats(&self) -> CacheStatistics {
531 self.cache.stats()
532 }
533
534 pub fn clear_cache(&self) {
536 self.cache.clear();
537 }
538
539 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 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 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 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 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 assert!(cache.size() <= 2);
653 }
654}