Skip to main content

torsh_nn/
sparse.rs

1//! Sparse Neural Network Support
2//!
3//! This module provides efficient implementations of sparse neural network layers
4//! and operations, enabling training and inference with sparse weights and activations.
5//!
6//! # Features
7//!
8//! - **Sparse Linear Layers**: Efficient linear transformations with sparse weight matrices
9//! - **Sparse Convolutions**: Convolutional layers with structured sparsity
10//! - **Magnitude Pruning**: Remove small-magnitude weights during training
11//! - **Structured Sparsity**: Block-wise or channel-wise sparsity patterns
12//! - **Sparse Backpropagation**: Efficient gradient computation for sparse parameters
13//!
14//! # Example
15//!
16//! ```ignore
17//! use torsh_nn::sparse::{SparseLinear, SparsityPattern};
18//!
19//! // Create sparse linear layer with 90% sparsity
20//! let sparse_layer = SparseLinear::new(
21//!     512,
22//!     256,
23//!     SparsityPattern::Random { sparsity: 0.9 },
24//!     true,
25//! );
26//!
27//! let output = sparse_layer.forward(&input)?;
28//! ```
29
30use crate::{Module, ModuleBase, Parameter};
31use torsh_core::device::DeviceType;
32use torsh_core::error::{Result, TorshError};
33use torsh_tensor::{creation::*, Tensor};
34
35// Conditional imports for std/no_std compatibility
36#[cfg(feature = "std")]
37use std::collections::HashMap;
38
39#[cfg(not(feature = "std"))]
40use hashbrown::HashMap;
41
42// ✅ SciRS2 Policy Compliant
43use scirs2_core::slice_random::shuffle;
44
45/// Sparsity pattern for sparse layers
46#[derive(Debug, Clone, Copy)]
47pub enum SparsityPattern {
48    /// Random sparsity with specified fraction
49    Random { sparsity: f32 },
50    /// Block-wise sparsity with block size
51    Blocked { block_size: usize, sparsity: f32 },
52    /// Structured sparsity (channel-wise)
53    Structured { channels_to_prune: usize },
54    /// Magnitude-based pruning threshold
55    MagnitudeBased { threshold: f32 },
56}
57
58/// Sparse mask for weight matrices
59#[derive(Debug, Clone)]
60pub struct SparseMask {
61    /// Mask indicating which weights are active (1.0) or pruned (0.0)
62    mask: Tensor,
63    /// Current sparsity level (fraction of zero weights)
64    sparsity: f32,
65    /// Number of non-zero elements
66    nnz: usize,
67}
68
69impl SparseMask {
70    /// Create a new sparse mask with random sparsity
71    pub fn random(shape: &[usize], sparsity: f32) -> Result<Self> {
72        if !(0.0..=1.0).contains(&sparsity) {
73            return Err(TorshError::InvalidArgument(format!(
74                "Sparsity must be in [0, 1], got {}",
75                sparsity
76            )));
77        }
78
79        let total_elements: usize = shape.iter().product();
80        let num_zeros = (total_elements as f32 * sparsity) as usize;
81
82        // Create initial all-ones mask
83        let mut mask_data = vec![1.0_f32; total_elements];
84
85        // Randomly select positions to zero
86        let mut indices: Vec<usize> = (0..total_elements).collect();
87        shuffle(&mut indices);
88
89        for &idx in indices.iter().take(num_zeros) {
90            mask_data[idx] = 0.0;
91        }
92
93        let mask = Tensor::from_vec(mask_data, shape)?;
94        let nnz = total_elements - num_zeros;
95
96        Ok(Self {
97            mask,
98            sparsity,
99            nnz,
100        })
101    }
102
103    /// Create mask from magnitude-based pruning
104    pub fn from_magnitude(weights: &Tensor, threshold: f32) -> Result<Self> {
105        let shape = weights.shape().dims().to_vec();
106        let weight_data = weights.to_vec()?;
107
108        let mask_data: Vec<f32> = weight_data
109            .iter()
110            .map(|&w| if w.abs() >= threshold { 1.0 } else { 0.0 })
111            .collect();
112
113        let nnz = mask_data.iter().filter(|&&m| m > 0.0).count();
114        let total = mask_data.len();
115        let sparsity = 1.0 - (nnz as f32 / total as f32);
116
117        Ok(Self {
118            mask: Tensor::from_vec(mask_data, &shape)?,
119            sparsity,
120            nnz,
121        })
122    }
123
124    /// Create block-wise sparse mask
125    pub fn blocked(shape: &[usize], block_size: usize, sparsity: f32) -> Result<Self> {
126        if shape.len() != 2 {
127            return Err(TorshError::InvalidArgument(
128                "Block sparsity only supported for 2D tensors".to_string(),
129            ));
130        }
131
132        let rows = shape[0];
133        let cols = shape[1];
134
135        if rows % block_size != 0 || cols % block_size != 0 {
136            return Err(TorshError::InvalidArgument(format!(
137                "Shape {:?} must be divisible by block_size {}",
138                shape, block_size
139            )));
140        }
141
142        let num_blocks_row = rows / block_size;
143        let num_blocks_col = cols / block_size;
144        let total_blocks = num_blocks_row * num_blocks_col;
145        let blocks_to_zero = (total_blocks as f32 * sparsity) as usize;
146
147        // Create mask
148        let mut mask_data = vec![1.0_f32; rows * cols];
149
150        // Randomly select blocks to zero
151        let mut block_indices: Vec<usize> = (0..total_blocks).collect();
152        shuffle(&mut block_indices);
153
154        for &block_idx in block_indices.iter().take(blocks_to_zero) {
155            let block_row = block_idx / num_blocks_col;
156            let block_col = block_idx % num_blocks_col;
157
158            // Zero out the entire block
159            for r in 0..block_size {
160                for c in 0..block_size {
161                    let row = block_row * block_size + r;
162                    let col = block_col * block_size + c;
163                    let idx = row * cols + col;
164                    mask_data[idx] = 0.0;
165                }
166            }
167        }
168
169        let nnz = mask_data.iter().filter(|&&m| m > 0.0).count();
170        let actual_sparsity = 1.0 - (nnz as f32 / mask_data.len() as f32);
171
172        Ok(Self {
173            mask: Tensor::from_vec(mask_data, shape)?,
174            sparsity: actual_sparsity,
175            nnz,
176        })
177    }
178
179    /// Apply mask to weights
180    pub fn apply(&self, weights: &Tensor) -> Result<Tensor> {
181        weights.mul(&self.mask)
182    }
183
184    /// Get number of non-zero elements
185    pub fn nnz(&self) -> usize {
186        self.nnz
187    }
188
189    /// Get sparsity level
190    pub fn sparsity(&self) -> f32 {
191        self.sparsity
192    }
193
194    /// Get the mask tensor
195    pub fn mask(&self) -> &Tensor {
196        &self.mask
197    }
198}
199
200/// Sparse linear layer with efficient sparse matrix operations
201pub struct SparseLinear {
202    base: ModuleBase,
203    /// Sparsity mask
204    mask: SparseMask,
205    /// Input features
206    in_features: usize,
207    /// Output features
208    out_features: usize,
209    /// Whether bias is used
210    use_bias: bool,
211}
212
213impl SparseLinear {
214    /// Create a new sparse linear layer
215    pub fn new(
216        in_features: usize,
217        out_features: usize,
218        pattern: SparsityPattern,
219        bias: bool,
220    ) -> Self {
221        let mut base = ModuleBase::new();
222
223        // Initialize dense weights first
224        let weight = crate::init::kaiming_uniform(&[in_features, out_features], "fan_in")
225            .expect("Failed to initialize sparse linear weight");
226
227        // Create sparsity mask
228        let mask = match pattern {
229            SparsityPattern::Random { sparsity } => {
230                SparseMask::random(&[in_features, out_features], sparsity)
231                    .expect("Failed to create random sparsity mask")
232            }
233            SparsityPattern::Blocked {
234                block_size,
235                sparsity,
236            } => SparseMask::blocked(&[in_features, out_features], block_size, sparsity)
237                .expect("Failed to create blocked sparsity mask"),
238            SparsityPattern::MagnitudeBased { threshold } => {
239                SparseMask::from_magnitude(&weight, threshold)
240                    .expect("Failed to create magnitude-based mask")
241            }
242            SparsityPattern::Structured { channels_to_prune } => {
243                // Create structured sparsity (prune entire output channels)
244                let sparsity = channels_to_prune as f32 / out_features as f32;
245                SparseMask::random(&[in_features, out_features], sparsity)
246                    .expect("Failed to create structured sparsity mask")
247            }
248        };
249
250        // Apply initial mask to weights
251        let masked_weight = mask.apply(&weight).expect("Failed to apply mask");
252        base.register_parameter("weight".to_string(), Parameter::new(masked_weight));
253
254        if bias {
255            let bias_tensor = zeros(&[out_features]).expect("Failed to create bias tensor");
256            base.register_parameter("bias".to_string(), Parameter::new(bias_tensor));
257        }
258
259        Self {
260            base,
261            mask,
262            in_features,
263            out_features,
264            use_bias: bias,
265        }
266    }
267
268    /// Get current sparsity level
269    pub fn sparsity(&self) -> f32 {
270        self.mask.sparsity()
271    }
272
273    /// Get number of non-zero parameters
274    pub fn nnz(&self) -> usize {
275        self.mask.nnz()
276    }
277
278    /// Update sparsity by magnitude-based pruning
279    pub fn prune_by_magnitude(&mut self, threshold: f32) -> Result<()> {
280        // Get current weights
281        let weight = self.base.parameters["weight"].tensor().read().clone();
282
283        // Create new mask based on current weights
284        self.mask = SparseMask::from_magnitude(&weight, threshold)?;
285
286        // Apply mask to weights
287        let masked = self.mask.apply(&weight)?;
288        self.base
289            .register_parameter("weight".to_string(), Parameter::new(masked));
290
291        Ok(())
292    }
293
294    /// Increase sparsity gradually
295    pub fn increase_sparsity(&mut self, target_sparsity: f32) -> Result<()> {
296        if target_sparsity <= self.mask.sparsity() {
297            return Ok(()); // Already sparse enough
298        }
299
300        // Get current weights
301        let weight = self.base.parameters["weight"].tensor().read().clone();
302
303        // Calculate threshold to achieve target sparsity
304        let weight_data = weight.to_vec()?;
305        let mut abs_weights: Vec<f32> = weight_data.iter().map(|&w| w.abs()).collect();
306        abs_weights.sort_by(|a, b| {
307            a.partial_cmp(b)
308                .expect("weight comparison should not involve NaN")
309        });
310
311        let num_to_prune = (abs_weights.len() as f32 * target_sparsity) as usize
312            - (abs_weights.len() - self.mask.nnz());
313        let threshold = abs_weights[num_to_prune];
314
315        self.prune_by_magnitude(threshold)
316    }
317}
318
319impl Module for SparseLinear {
320    fn forward(&self, input: &Tensor) -> Result<Tensor> {
321        // Get weight parameter
322        let weight = self.base.parameters["weight"].tensor().read().clone();
323
324        // Ensure weights remain sparse during forward pass
325        let sparse_weight = self.mask.apply(&weight)?;
326
327        // Compute input @ sparse_weight
328        let output = input.matmul(&sparse_weight)?;
329
330        if self.use_bias {
331            let bias = self.base.parameters["bias"].tensor().read().clone();
332            Ok(output.add(&bias)?)
333        } else {
334            Ok(output)
335        }
336    }
337
338    fn parameters(&self) -> HashMap<String, Parameter> {
339        self.base.parameters.clone()
340    }
341
342    fn named_parameters(&self) -> HashMap<String, Parameter> {
343        self.base.named_parameters()
344    }
345
346    fn training(&self) -> bool {
347        self.base.training()
348    }
349
350    fn train(&mut self) {
351        self.base.set_training(true);
352    }
353
354    fn eval(&mut self) {
355        self.base.set_training(false);
356    }
357
358    fn set_training(&mut self, training: bool) {
359        self.base.set_training(training);
360    }
361
362    fn to_device(&mut self, device: DeviceType) -> Result<()> {
363        self.base.to_device(device)
364    }
365}
366
367impl core::fmt::Debug for SparseLinear {
368    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
369        f.debug_struct("SparseLinear")
370            .field("in_features", &self.in_features)
371            .field("out_features", &self.out_features)
372            .field("sparsity", &self.mask.sparsity())
373            .field("nnz", &self.mask.nnz())
374            .finish()
375    }
376}
377
378/// Sparse convolutional layer with structured sparsity
379pub struct SparseConv2d {
380    base: ModuleBase,
381    /// Sparsity mask
382    mask: SparseMask,
383    /// Convolution parameters
384    in_channels: usize,
385    out_channels: usize,
386    kernel_size: usize,
387    stride: usize,
388    padding: usize,
389}
390
391impl SparseConv2d {
392    /// Create a new sparse conv2d layer
393    pub fn new(
394        in_channels: usize,
395        out_channels: usize,
396        kernel_size: usize,
397        stride: usize,
398        padding: usize,
399        pattern: SparsityPattern,
400        bias: bool,
401    ) -> Self {
402        let mut base = ModuleBase::new();
403        let weight_shape = [out_channels, in_channels, kernel_size, kernel_size];
404
405        let weight = crate::init::kaiming_uniform(&weight_shape, "fan_in")
406            .expect("Failed to initialize sparse conv2d weight");
407
408        // Create sparsity mask
409        let mask = match pattern {
410            SparsityPattern::Random { sparsity } => SparseMask::random(&weight_shape, sparsity)
411                .expect("Failed to create random sparsity mask"),
412            SparsityPattern::Blocked {
413                block_size: _block_size,
414                sparsity,
415            } => {
416                // For conv, we typically prune entire filters
417                SparseMask::random(&weight_shape, sparsity)
418                    .expect("Failed to create blocked sparsity mask")
419            }
420            SparsityPattern::MagnitudeBased { threshold } => {
421                SparseMask::from_magnitude(&weight, threshold)
422                    .expect("Failed to create magnitude-based mask")
423            }
424            SparsityPattern::Structured { channels_to_prune } => {
425                // Prune entire output channels
426                let sparsity = channels_to_prune as f32 / out_channels as f32;
427                SparseMask::random(&weight_shape, sparsity)
428                    .expect("Failed to create structured sparsity mask")
429            }
430        };
431
432        // Apply mask
433        let masked_weight = mask.apply(&weight).expect("Failed to apply mask");
434        base.register_parameter("weight".to_string(), Parameter::new(masked_weight));
435
436        if bias {
437            let bias_tensor = zeros(&[out_channels]).expect("Failed to create bias tensor");
438            base.register_parameter("bias".to_string(), Parameter::new(bias_tensor));
439        }
440
441        Self {
442            base,
443            mask,
444            in_channels,
445            out_channels,
446            kernel_size,
447            stride,
448            padding,
449        }
450    }
451
452    /// Get current sparsity level
453    pub fn sparsity(&self) -> f32 {
454        self.mask.sparsity()
455    }
456
457    /// Get number of non-zero parameters
458    pub fn nnz(&self) -> usize {
459        self.mask.nnz()
460    }
461}
462
463impl Module for SparseConv2d {
464    fn forward(&self, input: &Tensor) -> Result<Tensor> {
465        use crate::functional as F;
466
467        // Get weight and apply mask
468        let weight = self.base.parameters["weight"].tensor().read().clone();
469        let sparse_weight = self.mask.apply(&weight)?;
470
471        let bias = if self.base.parameters.contains_key("bias") {
472            Some(self.base.parameters["bias"].tensor().read().clone())
473        } else {
474            None
475        };
476
477        F::conv2d(
478            input,
479            &sparse_weight,
480            bias.as_ref(),
481            (self.stride, self.stride),
482            (self.padding, self.padding),
483            (1, 1), // dilation
484            1,      // groups
485        )
486    }
487
488    fn parameters(&self) -> HashMap<String, Parameter> {
489        self.base.parameters.clone()
490    }
491
492    fn named_parameters(&self) -> HashMap<String, Parameter> {
493        self.base.named_parameters()
494    }
495
496    fn training(&self) -> bool {
497        self.base.training()
498    }
499
500    fn train(&mut self) {
501        self.base.set_training(true);
502    }
503
504    fn eval(&mut self) {
505        self.base.set_training(false);
506    }
507
508    fn set_training(&mut self, training: bool) {
509        self.base.set_training(training);
510    }
511
512    fn to_device(&mut self, device: DeviceType) -> Result<()> {
513        self.base.to_device(device)
514    }
515}
516
517impl core::fmt::Debug for SparseConv2d {
518    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
519        f.debug_struct("SparseConv2d")
520            .field("in_channels", &self.in_channels)
521            .field("out_channels", &self.out_channels)
522            .field("kernel_size", &self.kernel_size)
523            .field("sparsity", &self.mask.sparsity())
524            .finish()
525    }
526}
527
528/// Sparse training configuration
529#[derive(Debug, Clone)]
530pub struct SparseTrainingConfig {
531    /// Initial sparsity level
532    pub initial_sparsity: f32,
533    /// Target sparsity level
534    pub target_sparsity: f32,
535    /// Number of steps to reach target sparsity
536    pub pruning_steps: usize,
537    /// Start pruning at this step
538    pub pruning_start_step: usize,
539    /// Pruning frequency (every N steps)
540    pub pruning_frequency: usize,
541}
542
543impl Default for SparseTrainingConfig {
544    fn default() -> Self {
545        Self {
546            initial_sparsity: 0.0,
547            target_sparsity: 0.9,
548            pruning_steps: 1000,
549            pruning_start_step: 0,
550            pruning_frequency: 100,
551        }
552    }
553}
554
555/// Gradual magnitude pruning scheduler
556pub struct GradualPruningScheduler {
557    config: SparseTrainingConfig,
558    current_step: usize,
559}
560
561impl GradualPruningScheduler {
562    /// Create a new pruning scheduler
563    pub fn new(config: SparseTrainingConfig) -> Self {
564        Self {
565            config,
566            current_step: 0,
567        }
568    }
569
570    /// Get current target sparsity for this step
571    pub fn get_sparsity(&self) -> f32 {
572        if self.current_step < self.config.pruning_start_step {
573            return self.config.initial_sparsity;
574        }
575
576        let steps_since_start = self.current_step - self.config.pruning_start_step;
577
578        if steps_since_start >= self.config.pruning_steps {
579            return self.config.target_sparsity;
580        }
581
582        // Linear interpolation
583        let progress = steps_since_start as f32 / self.config.pruning_steps as f32;
584        self.config.initial_sparsity
585            + (self.config.target_sparsity - self.config.initial_sparsity) * progress
586    }
587
588    /// Check if we should prune at this step
589    pub fn should_prune(&self) -> bool {
590        if self.current_step < self.config.pruning_start_step {
591            return false;
592        }
593
594        (self.current_step - self.config.pruning_start_step) % self.config.pruning_frequency == 0
595    }
596
597    /// Increment step counter
598    pub fn step(&mut self) {
599        self.current_step += 1;
600    }
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    #[test]
608    fn test_sparse_mask_random() {
609        let mask = SparseMask::random(&[10, 10], 0.5).unwrap();
610        assert!((mask.sparsity() - 0.5).abs() < 0.1); // Allow some variance
611        assert_eq!(mask.nnz() + (100.0 * mask.sparsity()) as usize, 100);
612    }
613
614    #[test]
615    fn test_sparse_mask_blocked() {
616        let mask = SparseMask::blocked(&[8, 8], 2, 0.5).unwrap();
617        assert!(mask.sparsity() >= 0.4 && mask.sparsity() <= 0.6);
618    }
619
620    #[test]
621    fn test_sparse_linear() {
622        let layer = SparseLinear::new(10, 5, SparsityPattern::Random { sparsity: 0.8 }, true);
623
624        assert_eq!(layer.in_features, 10);
625        assert_eq!(layer.out_features, 5);
626        assert!((layer.sparsity() - 0.8).abs() < 0.1);
627
628        let input = randn(&[2, 10]).unwrap();
629        let output = layer.forward(&input).unwrap();
630        assert_eq!(output.shape().dims(), &[2, 5]);
631    }
632
633    #[test]
634    fn test_sparse_conv2d() {
635        let layer = SparseConv2d::new(
636            3,
637            16,
638            3,
639            1,
640            1,
641            SparsityPattern::Random { sparsity: 0.7 },
642            true,
643        );
644
645        assert!((layer.sparsity() - 0.7).abs() < 0.1);
646
647        let input = randn(&[2, 3, 32, 32]).unwrap();
648        let output = layer.forward(&input).unwrap();
649        assert_eq!(output.shape().dims(), &[2, 16, 32, 32]);
650    }
651
652    #[test]
653    fn test_gradual_pruning_scheduler() {
654        let config = SparseTrainingConfig {
655            initial_sparsity: 0.0,
656            target_sparsity: 0.9,
657            pruning_steps: 100,
658            pruning_start_step: 10,
659            pruning_frequency: 10,
660        };
661
662        let mut scheduler = GradualPruningScheduler::new(config);
663
664        // Before start
665        assert_eq!(scheduler.get_sparsity(), 0.0);
666        assert!(!scheduler.should_prune());
667
668        // Advance to start
669        for _ in 0..10 {
670            scheduler.step();
671        }
672
673        assert!(scheduler.should_prune());
674        assert!(scheduler.get_sparsity() < 0.9);
675
676        // Advance to end
677        for _ in 0..100 {
678            scheduler.step();
679        }
680
681        assert_eq!(scheduler.get_sparsity(), 0.9);
682    }
683}