1use crate::{Module, ModuleBase, Parameter};
31use torsh_core::device::DeviceType;
32use torsh_core::error::{Result, TorshError};
33use torsh_tensor::{creation::*, Tensor};
34
35#[cfg(feature = "std")]
37use std::collections::HashMap;
38
39#[cfg(not(feature = "std"))]
40use hashbrown::HashMap;
41
42use scirs2_core::slice_random::shuffle;
44
45#[derive(Debug, Clone, Copy)]
47pub enum SparsityPattern {
48 Random { sparsity: f32 },
50 Blocked { block_size: usize, sparsity: f32 },
52 Structured { channels_to_prune: usize },
54 MagnitudeBased { threshold: f32 },
56}
57
58#[derive(Debug, Clone)]
60pub struct SparseMask {
61 mask: Tensor,
63 sparsity: f32,
65 nnz: usize,
67}
68
69impl SparseMask {
70 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 let mut mask_data = vec![1.0_f32; total_elements];
84
85 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 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 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 let mut mask_data = vec![1.0_f32; rows * cols];
149
150 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 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 pub fn apply(&self, weights: &Tensor) -> Result<Tensor> {
181 weights.mul(&self.mask)
182 }
183
184 pub fn nnz(&self) -> usize {
186 self.nnz
187 }
188
189 pub fn sparsity(&self) -> f32 {
191 self.sparsity
192 }
193
194 pub fn mask(&self) -> &Tensor {
196 &self.mask
197 }
198}
199
200pub struct SparseLinear {
202 base: ModuleBase,
203 mask: SparseMask,
205 in_features: usize,
207 out_features: usize,
209 use_bias: bool,
211}
212
213impl SparseLinear {
214 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 let weight = crate::init::kaiming_uniform(&[in_features, out_features], "fan_in")
225 .expect("Failed to initialize sparse linear weight");
226
227 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 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 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 pub fn sparsity(&self) -> f32 {
270 self.mask.sparsity()
271 }
272
273 pub fn nnz(&self) -> usize {
275 self.mask.nnz()
276 }
277
278 pub fn prune_by_magnitude(&mut self, threshold: f32) -> Result<()> {
280 let weight = self.base.parameters["weight"].tensor().read().clone();
282
283 self.mask = SparseMask::from_magnitude(&weight, threshold)?;
285
286 let masked = self.mask.apply(&weight)?;
288 self.base
289 .register_parameter("weight".to_string(), Parameter::new(masked));
290
291 Ok(())
292 }
293
294 pub fn increase_sparsity(&mut self, target_sparsity: f32) -> Result<()> {
296 if target_sparsity <= self.mask.sparsity() {
297 return Ok(()); }
299
300 let weight = self.base.parameters["weight"].tensor().read().clone();
302
303 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 let weight = self.base.parameters["weight"].tensor().read().clone();
323
324 let sparse_weight = self.mask.apply(&weight)?;
326
327 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
378pub struct SparseConv2d {
380 base: ModuleBase,
381 mask: SparseMask,
383 in_channels: usize,
385 out_channels: usize,
386 kernel_size: usize,
387 stride: usize,
388 padding: usize,
389}
390
391impl SparseConv2d {
392 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 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 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 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 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 pub fn sparsity(&self) -> f32 {
454 self.mask.sparsity()
455 }
456
457 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 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), 1, )
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#[derive(Debug, Clone)]
530pub struct SparseTrainingConfig {
531 pub initial_sparsity: f32,
533 pub target_sparsity: f32,
535 pub pruning_steps: usize,
537 pub pruning_start_step: usize,
539 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
555pub struct GradualPruningScheduler {
557 config: SparseTrainingConfig,
558 current_step: usize,
559}
560
561impl GradualPruningScheduler {
562 pub fn new(config: SparseTrainingConfig) -> Self {
564 Self {
565 config,
566 current_step: 0,
567 }
568 }
569
570 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 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 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 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); 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 assert_eq!(scheduler.get_sparsity(), 0.0);
666 assert!(!scheduler.should_prune());
667
668 for _ in 0..10 {
670 scheduler.step();
671 }
672
673 assert!(scheduler.should_prune());
674 assert!(scheduler.get_sparsity() < 0.9);
675
676 for _ in 0..100 {
678 scheduler.step();
679 }
680
681 assert_eq!(scheduler.get_sparsity(), 0.9);
682 }
683}