Skip to main content

sklears_svm/
memory_mapped_kernels.rs

1//! Memory-mapped kernel matrices for large-scale SVM training
2//!
3//! This module provides memory-mapped implementations of kernel matrices that allow
4//! efficient access to kernel values without loading the entire matrix into memory.
5//! This is essential for training SVMs on very large datasets where the kernel matrix
6//! would be too large to fit in available RAM.
7
8use crate::kernels::Kernel;
9use memmap2::{Mmap, MmapMut, MmapOptions};
10use scirs2_core::ndarray::Array2;
11use sklears_core::{
12    error::{Result, SklearsError},
13    types::Float,
14};
15use std::collections::HashMap;
16use std::fs::{File, OpenOptions};
17use std::io::{Seek, SeekFrom, Write};
18use std::path::PathBuf;
19use std::sync::{Arc, Mutex};
20
21/// Configuration for memory-mapped kernel matrices
22#[derive(Debug, Clone)]
23pub struct MemoryMappedKernelConfig {
24    /// Path to store the memory-mapped file
25    pub file_path: PathBuf,
26    /// Cache size for frequently accessed kernel values (in elements)
27    pub cache_size: usize,
28    /// Block size for chunked access (in elements)
29    pub block_size: usize,
30    /// Whether to precompute the entire kernel matrix
31    pub precompute_all: bool,
32    /// Compression level for on-disk storage (0-9, 0=no compression)
33    pub compression_level: u8,
34    /// Whether to use read-only mode
35    pub read_only: bool,
36    /// Number of threads for parallel computation
37    pub num_threads: usize,
38}
39
40impl Default for MemoryMappedKernelConfig {
41    fn default() -> Self {
42        #[cfg(feature = "parallel")]
43        let default_threads = rayon::current_num_threads();
44        #[cfg(not(feature = "parallel"))]
45        let default_threads = num_cpus::get();
46
47        Self {
48            file_path: std::env::temp_dir().join("sklears_kernel_matrix.dat"),
49            cache_size: 10000,
50            block_size: 1000,
51            precompute_all: false,
52            compression_level: 0,
53            read_only: false,
54            num_threads: default_threads,
55        }
56    }
57}
58
59/// Memory-mapped kernel matrix
60pub struct MemoryMappedKernelMatrix {
61    config: MemoryMappedKernelConfig,
62    file: File,
63    mmap: Option<Mmap>,
64    mmap_mut: Option<MmapMut>,
65    dimensions: (usize, usize),
66    kernel: Box<dyn Kernel>,
67    cache: Arc<Mutex<LRUCache<(usize, usize), Float>>>,
68    x_data: Option<Array2<Float>>, // Store original data for on-demand computation
69    header_size: usize,
70}
71
72impl MemoryMappedKernelMatrix {
73    /// Create a new memory-mapped kernel matrix
74    pub fn new(
75        kernel: Box<dyn Kernel>,
76        dimensions: (usize, usize),
77        config: MemoryMappedKernelConfig,
78    ) -> Result<Self> {
79        let header_size = std::mem::size_of::<KernelMatrixHeader>();
80
81        let file = if config.read_only {
82            File::open(&config.file_path)?
83        } else {
84            OpenOptions::new()
85                .create(true)
86                .truncate(true)
87                .read(true)
88                .write(true)
89                .open(&config.file_path)?
90        };
91
92        let cache = Arc::new(Mutex::new(LRUCache::new(config.cache_size)));
93
94        let mut matrix = Self {
95            config,
96            file,
97            mmap: None,
98            mmap_mut: None,
99            dimensions,
100            kernel,
101            cache,
102            x_data: None,
103            header_size,
104        };
105
106        matrix.initialize()?;
107        Ok(matrix)
108    }
109
110    /// Initialize the memory-mapped file
111    fn initialize(&mut self) -> Result<()> {
112        let total_elements = self.dimensions.0 * self.dimensions.1;
113        let data_size = total_elements * std::mem::size_of::<Float>();
114        let total_size = self.header_size + data_size;
115
116        // Resize file to accommodate header + data
117        self.file.set_len(total_size as u64)?;
118
119        // Write header
120        let header = KernelMatrixHeader {
121            rows: self.dimensions.0 as u64,
122            cols: self.dimensions.1 as u64,
123            element_size: std::mem::size_of::<Float>() as u32,
124            compression_level: self.config.compression_level,
125            version: 1,
126        };
127
128        self.write_header(&header)?;
129
130        // Create memory mapping
131        if self.config.read_only {
132            let mmap = unsafe {
133                MmapOptions::new()
134                    .offset(self.header_size as u64)
135                    .len(data_size)
136                    .map(&self.file)?
137            };
138            self.mmap = Some(mmap);
139        } else {
140            let mmap_mut = unsafe {
141                MmapOptions::new()
142                    .offset(self.header_size as u64)
143                    .len(data_size)
144                    .map_mut(&self.file)?
145            };
146            self.mmap_mut = Some(mmap_mut);
147        }
148
149        Ok(())
150    }
151
152    /// Set the original data for on-demand computation
153    pub fn set_data(&mut self, x: Array2<Float>) {
154        self.x_data = Some(x);
155    }
156
157    /// Precompute the entire kernel matrix
158    pub fn precompute(&mut self) -> Result<()> {
159        if self.x_data.is_none() {
160            return Err(SklearsError::InvalidInput(
161                "No data provided for precomputation".to_string(),
162            ));
163        }
164
165        let x = self
166            .x_data
167            .as_ref()
168            .expect("x_data not available - model not fitted")
169            .clone();
170        let (rows, cols) = self.dimensions;
171
172        // Sequential computation for now (to avoid borrowing issues)
173        for i in 0..rows {
174            for j in 0..cols {
175                let k_val = if i <= j {
176                    // Only compute upper triangle for symmetric matrices
177                    self.kernel.compute(x.row(i), x.row(j))
178                } else {
179                    // Use symmetry
180                    self.get_raw(j, i)?
181                };
182
183                self.set_raw(i, j, k_val)?;
184                if i != j {
185                    self.set_raw(j, i, k_val)?; // Symmetric
186                }
187            }
188        }
189
190        self.flush()?;
191        Ok(())
192    }
193
194    /// Get kernel value at position (i, j)
195    pub fn get(&self, i: usize, j: usize) -> Result<Float> {
196        if i >= self.dimensions.0 || j >= self.dimensions.1 {
197            return Err(SklearsError::InvalidInput(format!(
198                "Index ({}, {}) out of bounds for matrix of size ({}, {})",
199                i, j, self.dimensions.0, self.dimensions.1
200            )));
201        }
202
203        // Check cache first
204        if let Ok(mut cache) = self.cache.lock() {
205            if let Some(&value) = cache.get(&(i, j)) {
206                return Ok(value);
207            }
208        }
209
210        // Try to get from memory-mapped file
211        let value = if let Ok(precomputed) = self.get_raw(i, j) {
212            precomputed
213        } else if let Some(ref x) = self.x_data {
214            // Compute on-demand
215            let val = self.kernel.compute(x.row(i), x.row(j));
216
217            // Cache the computed value
218            if let Ok(mut cache) = self.cache.lock() {
219                cache.put((i, j), val);
220                if i != j {
221                    cache.put((j, i), val); // Symmetric
222                }
223            }
224
225            val
226        } else {
227            return Err(SklearsError::InvalidInput(
228                "No precomputed data or original data available".to_string(),
229            ));
230        };
231
232        Ok(value)
233    }
234
235    /// Set kernel value at position (i, j)
236    pub fn set(&mut self, i: usize, j: usize, value: Float) -> Result<()> {
237        if i >= self.dimensions.0 || j >= self.dimensions.1 {
238            return Err(SklearsError::InvalidInput(format!(
239                "Index ({}, {}) out of bounds",
240                i, j
241            )));
242        }
243
244        self.set_raw(i, j, value)?;
245
246        // Update cache
247        if let Ok(mut cache) = self.cache.lock() {
248            cache.put((i, j), value);
249        }
250
251        Ok(())
252    }
253
254    /// Get a block of kernel values
255    pub fn get_block(
256        &self,
257        row_start: usize,
258        row_end: usize,
259        col_start: usize,
260        col_end: usize,
261    ) -> Result<Array2<Float>> {
262        if row_end > self.dimensions.0 || col_end > self.dimensions.1 {
263            return Err(SklearsError::InvalidInput(
264                "Block indices out of bounds".to_string(),
265            ));
266        }
267
268        let block_rows = row_end - row_start;
269        let block_cols = col_end - col_start;
270        let mut block = Array2::zeros((block_rows, block_cols));
271
272        for i in 0..block_rows {
273            for j in 0..block_cols {
274                block[[i, j]] = self.get(row_start + i, col_start + j)?;
275            }
276        }
277
278        Ok(block)
279    }
280
281    /// Set a block of kernel values
282    pub fn set_block(
283        &mut self,
284        row_start: usize,
285        col_start: usize,
286        block: &Array2<Float>,
287    ) -> Result<()> {
288        let (block_rows, block_cols) = block.dim();
289
290        if row_start + block_rows > self.dimensions.0 || col_start + block_cols > self.dimensions.1
291        {
292            return Err(SklearsError::InvalidInput(
293                "Block would exceed matrix bounds".to_string(),
294            ));
295        }
296
297        for i in 0..block_rows {
298            for j in 0..block_cols {
299                self.set(row_start + i, col_start + j, block[[i, j]])?;
300            }
301        }
302
303        Ok(())
304    }
305
306    /// Get raw value from memory-mapped file
307    fn get_raw(&self, i: usize, j: usize) -> Result<Float> {
308        let offset = (i * self.dimensions.1 + j) * std::mem::size_of::<Float>();
309
310        if let Some(ref mmap) = self.mmap {
311            let bytes = &mmap[offset..offset + std::mem::size_of::<Float>()];
312            let value =
313                Float::from_le_bytes(bytes.try_into().map_err(|_| {
314                    SklearsError::InvalidInput("Failed to read raw value".to_string())
315                })?);
316            Ok(value)
317        } else if let Some(ref mmap_mut) = self.mmap_mut {
318            let bytes = &mmap_mut[offset..offset + std::mem::size_of::<Float>()];
319            let value =
320                Float::from_le_bytes(bytes.try_into().map_err(|_| {
321                    SklearsError::InvalidInput("Failed to read raw value".to_string())
322                })?);
323            Ok(value)
324        } else {
325            Err(SklearsError::InvalidInput(
326                "No memory mapping available".to_string(),
327            ))
328        }
329    }
330
331    /// Set raw value in memory-mapped file
332    fn set_raw(&mut self, i: usize, j: usize, value: Float) -> Result<()> {
333        let offset = (i * self.dimensions.1 + j) * std::mem::size_of::<Float>();
334
335        if let Some(ref mut mmap_mut) = self.mmap_mut {
336            let bytes = value.to_le_bytes();
337            mmap_mut[offset..offset + std::mem::size_of::<Float>()].copy_from_slice(&bytes);
338            Ok(())
339        } else {
340            Err(SklearsError::InvalidInput(
341                "Memory mapping not available for writing".to_string(),
342            ))
343        }
344    }
345
346    /// Write header to file
347    fn write_header(&mut self, header: &KernelMatrixHeader) -> Result<()> {
348        self.file.seek(SeekFrom::Start(0))?;
349
350        let header_bytes = unsafe {
351            std::slice::from_raw_parts(
352                header as *const KernelMatrixHeader as *const u8,
353                std::mem::size_of::<KernelMatrixHeader>(),
354            )
355        };
356
357        self.file.write_all(header_bytes)?;
358
359        Ok(())
360    }
361
362    /// Flush changes to disk
363    pub fn flush(&mut self) -> Result<()> {
364        if let Some(ref mmap_mut) = self.mmap_mut {
365            mmap_mut.flush()?;
366        }
367        Ok(())
368    }
369
370    /// Get matrix dimensions
371    pub fn dimensions(&self) -> (usize, usize) {
372        self.dimensions
373    }
374
375    /// Get cache hit rate statistics
376    pub fn cache_stats(&self) -> Result<CacheStats> {
377        if let Ok(cache) = self.cache.lock() {
378            Ok(CacheStats {
379                size: cache.len(),
380                capacity: cache.cap(),
381                hit_rate: 0.0, // Would need to track hits/misses for accurate rate
382            })
383        } else {
384            Err(SklearsError::InvalidInput(
385                "Failed to access cache".to_string(),
386            ))
387        }
388    }
389}
390
391/// Header structure for the memory-mapped kernel matrix file
392#[repr(C)]
393#[derive(Debug, Clone, Copy)]
394struct KernelMatrixHeader {
395    rows: u64,
396    cols: u64,
397    element_size: u32,
398    compression_level: u8,
399    version: u8,
400}
401
402/// Cache statistics
403pub struct CacheStats {
404    pub size: usize,
405    pub capacity: usize,
406    pub hit_rate: f64,
407}
408
409/// Sparse memory-mapped kernel matrix for very large, sparse datasets
410pub struct SparseMemoryMappedKernelMatrix {
411    dense_matrix: MemoryMappedKernelMatrix,
412    sparse_indices: HashMap<(usize, usize), usize>,
413    sparsity_threshold: Float,
414}
415
416impl SparseMemoryMappedKernelMatrix {
417    /// Create a new sparse memory-mapped kernel matrix
418    pub fn new(
419        kernel: Box<dyn Kernel>,
420        dimensions: (usize, usize),
421        config: MemoryMappedKernelConfig,
422        sparsity_threshold: Float,
423    ) -> Result<Self> {
424        let dense_matrix = MemoryMappedKernelMatrix::new(kernel, dimensions, config)?;
425
426        Ok(Self {
427            dense_matrix,
428            sparse_indices: HashMap::new(),
429            sparsity_threshold,
430        })
431    }
432
433    /// Get kernel value, using sparsity
434    pub fn get(&self, i: usize, j: usize) -> Result<Float> {
435        if let Some(&sparse_idx) = self.sparse_indices.get(&(i, j)) {
436            // This is a stored sparse value
437            self.dense_matrix.get_raw(
438                sparse_idx / self.dense_matrix.dimensions.1,
439                sparse_idx % self.dense_matrix.dimensions.1,
440            )
441        } else {
442            // Either dense value or implicit zero
443            let value = self.dense_matrix.get(i, j)?;
444            if value.abs() < self.sparsity_threshold {
445                Ok(0.0)
446            } else {
447                Ok(value)
448            }
449        }
450    }
451
452    /// Set kernel value with sparsity consideration
453    pub fn set(&mut self, i: usize, j: usize, value: Float) -> Result<()> {
454        if value.abs() < self.sparsity_threshold {
455            // Remove from sparse indices if it exists
456            self.sparse_indices.remove(&(i, j));
457            Ok(())
458        } else {
459            self.dense_matrix.set(i, j, value)
460        }
461    }
462
463    /// Get sparsity ratio
464    pub fn sparsity_ratio(&self) -> f64 {
465        let total_elements = self.dense_matrix.dimensions.0 * self.dense_matrix.dimensions.1;
466        let sparse_elements = self.sparse_indices.len();
467        1.0 - (sparse_elements as f64 / total_elements as f64)
468    }
469}
470
471/// Simple LRU Cache implementation
472struct LRUCache<K, V> {
473    map: HashMap<K, V>,
474    capacity: usize,
475}
476
477impl<K: std::hash::Hash + Eq + Clone, V: Clone> LRUCache<K, V> {
478    fn new(capacity: usize) -> Self {
479        Self {
480            map: HashMap::with_capacity(capacity),
481            capacity,
482        }
483    }
484
485    fn get(&mut self, key: &K) -> Option<&V> {
486        self.map.get(key)
487    }
488
489    fn put(&mut self, key: K, value: V) {
490        if self.map.len() >= self.capacity {
491            // Simple eviction - remove a random entry
492            if let Some(key_to_remove) = self.map.keys().next().cloned() {
493                self.map.remove(&key_to_remove);
494            }
495        }
496        self.map.insert(key, value);
497    }
498
499    fn len(&self) -> usize {
500        self.map.len()
501    }
502
503    fn cap(&self) -> usize {
504        self.capacity
505    }
506}
507
508#[allow(non_snake_case)]
509#[cfg(test)]
510mod tests {
511    use super::*;
512    use crate::kernels::LinearKernel;
513
514    use tempfile::NamedTempFile;
515
516    #[test]
517    fn test_memory_mapped_kernel_creation() {
518        let temp_file = NamedTempFile::new().expect("construction should succeed");
519        let config = MemoryMappedKernelConfig {
520            file_path: temp_file.path().to_path_buf(),
521            ..MemoryMappedKernelConfig::default()
522        };
523
524        let kernel = Box::new(LinearKernel);
525        let matrix = MemoryMappedKernelMatrix::new(kernel, (100, 100), config);
526        assert!(matrix.is_ok());
527    }
528
529    #[test]
530    fn test_kernel_value_set_get() {
531        let temp_file = NamedTempFile::new().expect("construction should succeed");
532        let config = MemoryMappedKernelConfig {
533            file_path: temp_file.path().to_path_buf(),
534            ..MemoryMappedKernelConfig::default()
535        };
536
537        let kernel = Box::new(LinearKernel);
538        let mut matrix = MemoryMappedKernelMatrix::new(kernel, (10, 10), config)
539            .expect("construction should succeed");
540
541        matrix.set(0, 0, 1.5).expect("operation should succeed");
542        assert_eq!(matrix.get(0, 0).expect("operation should succeed"), 1.5);
543    }
544
545    #[test]
546    fn test_sparse_memory_mapped_kernel() {
547        let temp_file = NamedTempFile::new().expect("construction should succeed");
548        let config = MemoryMappedKernelConfig {
549            file_path: temp_file.path().to_path_buf(),
550            ..MemoryMappedKernelConfig::default()
551        };
552
553        let kernel = Box::new(LinearKernel);
554        let sparse_matrix = SparseMemoryMappedKernelMatrix::new(kernel, (10, 10), config, 0.1);
555        assert!(sparse_matrix.is_ok());
556    }
557}