1use crate::{CooTensor, CsrTensor, SparseTensor, TorshResult};
9
10use torsh_core::{Shape, TorshError};
11use torsh_tensor::{creation::zeros, Tensor};
12
13#[derive(Debug, Clone)]
28pub struct SparseConv2d {
29 kernel: CsrTensor,
31 bias: Option<Tensor>,
33 in_channels: usize,
35 out_channels: usize,
37 kernel_size: (usize, usize),
39 stride: (usize, usize),
41 padding: (usize, usize),
43 dilation: (usize, usize),
45}
46
47impl SparseConv2d {
48 #[allow(clippy::too_many_arguments)]
75 pub fn new(
76 in_channels: usize,
77 out_channels: usize,
78 kernel_size: (usize, usize),
79 stride: Option<(usize, usize)>,
80 padding: Option<(usize, usize)>,
81 dilation: Option<(usize, usize)>,
82 sparsity: f32,
83 use_bias: bool,
84 ) -> TorshResult<Self> {
85 if !(0.0..=1.0).contains(&sparsity) {
86 return Err(TorshError::InvalidArgument(
87 "Sparsity must be between 0.0 and 1.0".to_string(),
88 ));
89 }
90
91 if kernel_size.0 == 0 || kernel_size.1 == 0 {
92 return Err(TorshError::InvalidArgument(
93 "Kernel size must be greater than 0".to_string(),
94 ));
95 }
96
97 let stride = stride.unwrap_or((1, 1));
98 let padding = padding.unwrap_or((0, 0));
99 let dilation = dilation.unwrap_or((1, 1));
100
101 if stride.0 == 0 || stride.1 == 0 {
102 return Err(TorshError::InvalidArgument(
103 "Stride must be greater than 0".to_string(),
104 ));
105 }
106
107 if dilation.0 == 0 || dilation.1 == 0 {
108 return Err(TorshError::InvalidArgument(
109 "Dilation must be greater than 0".to_string(),
110 ));
111 }
112
113 let kernel =
115 Self::generate_sparse_kernel(out_channels, in_channels, kernel_size, sparsity)?;
116
117 let bias = if use_bias {
119 Some(zeros::<f32>(&[out_channels])?)
120 } else {
121 None
122 };
123
124 Ok(Self {
125 kernel,
126 bias,
127 in_channels,
128 out_channels,
129 kernel_size,
130 stride,
131 padding,
132 dilation,
133 })
134 }
135
136 pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
144 let input_shape = input.shape();
146 if input_shape.ndim() != 4 {
147 return Err(TorshError::InvalidArgument(
148 "Input must be 4D tensor (batch_size, in_channels, height, width)".to_string(),
149 ));
150 }
151
152 let batch_size = input_shape.dims()[0];
153 let input_channels = input_shape.dims()[1];
154 let input_height = input_shape.dims()[2];
155 let input_width = input_shape.dims()[3];
156
157 if input_channels != self.in_channels {
158 return Err(TorshError::InvalidArgument(format!(
159 "Input channels {} don't match layer input channels {}",
160 input_channels, self.in_channels
161 )));
162 }
163
164 let output_height =
166 (input_height + 2 * self.padding.0 - self.dilation.0 * (self.kernel_size.0 - 1) - 1)
167 / self.stride.0
168 + 1;
169 let output_width =
170 (input_width + 2 * self.padding.1 - self.dilation.1 * (self.kernel_size.1 - 1) - 1)
171 / self.stride.1
172 + 1;
173
174 let mut output =
175 zeros::<f32>(&[batch_size, self.out_channels, output_height, output_width])?;
176
177 for b in 0..batch_size {
179 self.conv2d_single(
180 input,
181 &mut output,
182 b,
183 input_height,
184 input_width,
185 output_height,
186 output_width,
187 )?;
188 }
189
190 if let Some(ref bias) = self.bias {
192 for b in 0..batch_size {
193 for c in 0..self.out_channels {
194 let bias_val = bias.get(&[c])?;
195 for h in 0..output_height {
196 for w in 0..output_width {
197 let current = output.get(&[b, c, h, w])?;
198 output.set(&[b, c, h, w], current + bias_val)?;
199 }
200 }
201 }
202 }
203 }
204
205 Ok(output)
206 }
207
208 #[allow(clippy::too_many_arguments)]
210 fn conv2d_single(
211 &self,
212 input: &Tensor,
213 output: &mut Tensor,
214 batch_idx: usize,
215 input_height: usize,
216 input_width: usize,
217 output_height: usize,
218 output_width: usize,
219 ) -> TorshResult<()> {
220 let kernel_coo = self.kernel.to_coo()?;
222 let kernel_triplets = kernel_coo.triplets();
223
224 for (kernel_flat_idx, _, kernel_val) in kernel_triplets {
226 let (out_c, in_c, kh, kw) = self.flat_to_4d_kernel(kernel_flat_idx);
228
229 for out_h in 0..output_height {
231 for out_w in 0..output_width {
232 let in_h = out_h * self.stride.0 + kh * self.dilation.0;
234 let in_w = out_w * self.stride.1 + kw * self.dilation.1;
235
236 if in_h >= self.padding.0 && in_w >= self.padding.1 {
238 let padded_in_h = in_h - self.padding.0;
239 let padded_in_w = in_w - self.padding.1;
240
241 if padded_in_h < input_height && padded_in_w < input_width {
243 let input_val =
244 input.get(&[batch_idx, in_c, padded_in_h, padded_in_w])?;
245 let current_out = output.get(&[batch_idx, out_c, out_h, out_w])?;
246 output.set(
247 &[batch_idx, out_c, out_h, out_w],
248 current_out + kernel_val * input_val,
249 )?;
250 }
251 }
252 }
253 }
254 }
255
256 Ok(())
257 }
258
259 fn flat_to_4d_kernel(&self, flat_idx: usize) -> (usize, usize, usize, usize) {
261 let kernel_size_total = self.kernel_size.0 * self.kernel_size.1;
262 let channel_size = self.in_channels * kernel_size_total;
263
264 let out_c = flat_idx / channel_size;
265 let remaining = flat_idx % channel_size;
266 let in_c = remaining / kernel_size_total;
267 let remaining = remaining % kernel_size_total;
268 let kh = remaining / self.kernel_size.1;
269 let kw = remaining % self.kernel_size.1;
270
271 (out_c, in_c, kh, kw)
272 }
273
274 #[allow(dead_code)]
276 fn kernel_4d_to_flat(&self, out_c: usize, in_c: usize, kh: usize, kw: usize) -> usize {
277 let kernel_size_total = self.kernel_size.0 * self.kernel_size.1;
278 let channel_size = self.in_channels * kernel_size_total;
279
280 out_c * channel_size + in_c * kernel_size_total + kh * self.kernel_size.1 + kw
281 }
282
283 fn generate_sparse_kernel(
285 out_channels: usize,
286 in_channels: usize,
287 kernel_size: (usize, usize),
288 sparsity: f32,
289 ) -> TorshResult<CsrTensor> {
290 let total_elements = out_channels * in_channels * kernel_size.0 * kernel_size.1;
291 let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
292
293 let mut row_indices = Vec::with_capacity(nnz);
294 let mut col_indices = Vec::with_capacity(nnz);
295 let mut values = Vec::with_capacity(nnz);
296
297 let mut positions = std::collections::HashSet::new();
299 while positions.len() < nnz {
300 let mut rng = scirs2_core::random::thread_rng();
301 let flat_idx = rng.gen_range(0..total_elements);
302 positions.insert(flat_idx);
303 }
304
305 let kernel_size_total = kernel_size.0 * kernel_size.1;
309 let channel_size = in_channels * kernel_size_total;
310
311 for flat_idx in positions {
312 let out_c = flat_idx / channel_size;
313 let col_idx = flat_idx % channel_size; row_indices.push(out_c);
316 col_indices.push(col_idx);
317
318 let fan_in = in_channels * kernel_size.0 * kernel_size.1;
320 let std_dev = (2.0 / fan_in as f32).sqrt();
321 let mut rng = scirs2_core::random::thread_rng();
322 values.push((rng.random::<f32>() * 2.0 - 1.0) * std_dev);
323 }
324
325 let shape = Shape::new(vec![out_channels, channel_size]);
326 let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
327 CsrTensor::from_coo(&coo)
328 }
329
330 pub fn num_parameters(&self) -> usize {
332 let kernel_params = self.kernel.nnz();
333 let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
334 kernel_params + bias_params
335 }
336
337 pub fn kernel_sparsity(&self) -> f32 {
339 let total_elements =
340 self.out_channels * self.in_channels * self.kernel_size.0 * self.kernel_size.1;
341 let nnz = self.kernel.nnz();
342 1.0 - (nnz as f32 / total_elements as f32)
343 }
344
345 pub fn in_channels(&self) -> usize {
347 self.in_channels
348 }
349
350 pub fn out_channels(&self) -> usize {
352 self.out_channels
353 }
354
355 pub fn kernel_size(&self) -> (usize, usize) {
357 self.kernel_size
358 }
359
360 pub fn stride(&self) -> (usize, usize) {
362 self.stride
363 }
364
365 pub fn padding(&self) -> (usize, usize) {
367 self.padding
368 }
369
370 pub fn dilation(&self) -> (usize, usize) {
372 self.dilation
373 }
374}
375
376#[derive(Debug, Clone)]
382pub struct SparseConv1d {
383 kernel: CsrTensor,
385 bias: Option<Tensor>,
387 in_channels: usize,
389 out_channels: usize,
391 kernel_size: usize,
393 stride: usize,
395 padding: usize,
397 dilation: usize,
399}
400
401impl SparseConv1d {
402 #[allow(clippy::too_many_arguments)]
425 pub fn new(
426 in_channels: usize,
427 out_channels: usize,
428 kernel_size: usize,
429 stride: Option<usize>,
430 padding: Option<usize>,
431 dilation: Option<usize>,
432 sparsity: f32,
433 use_bias: bool,
434 ) -> TorshResult<Self> {
435 if !(0.0..=1.0).contains(&sparsity) {
436 return Err(TorshError::InvalidArgument(
437 "Sparsity must be between 0.0 and 1.0".to_string(),
438 ));
439 }
440
441 if kernel_size == 0 {
442 return Err(TorshError::InvalidArgument(
443 "Kernel size must be greater than 0".to_string(),
444 ));
445 }
446
447 let stride = stride.unwrap_or(1);
448 let padding = padding.unwrap_or(0);
449 let dilation = dilation.unwrap_or(1);
450
451 if stride == 0 {
452 return Err(TorshError::InvalidArgument(
453 "Stride must be greater than 0".to_string(),
454 ));
455 }
456
457 if dilation == 0 {
458 return Err(TorshError::InvalidArgument(
459 "Dilation must be greater than 0".to_string(),
460 ));
461 }
462
463 let kernel =
465 Self::generate_sparse_kernel_1d(out_channels, in_channels, kernel_size, sparsity)?;
466
467 let bias = if use_bias {
469 Some(zeros::<f32>(&[out_channels])?)
470 } else {
471 None
472 };
473
474 Ok(Self {
475 kernel,
476 bias,
477 in_channels,
478 out_channels,
479 kernel_size,
480 stride,
481 padding,
482 dilation,
483 })
484 }
485
486 pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
494 let input_shape = input.shape();
496 if input_shape.ndim() != 3 {
497 return Err(TorshError::InvalidArgument(
498 "Input must be 3D tensor (batch_size, in_channels, length)".to_string(),
499 ));
500 }
501
502 let batch_size = input_shape.dims()[0];
503 let input_channels = input_shape.dims()[1];
504 let input_length = input_shape.dims()[2];
505
506 if input_channels != self.in_channels {
507 return Err(TorshError::InvalidArgument(format!(
508 "Input channels {} don't match layer input channels {}",
509 input_channels, self.in_channels
510 )));
511 }
512
513 let output_length =
515 (input_length + 2 * self.padding - self.dilation * (self.kernel_size - 1) - 1)
516 / self.stride
517 + 1;
518
519 let mut output = zeros::<f32>(&[batch_size, self.out_channels, output_length])?;
520
521 for b in 0..batch_size {
523 self.conv1d_single(input, &mut output, b, input_length, output_length)?;
524 }
525
526 if let Some(ref bias) = self.bias {
528 for b in 0..batch_size {
529 for c in 0..self.out_channels {
530 let bias_val = bias.get(&[c])?;
531 for l in 0..output_length {
532 let current = output.get(&[b, c, l])?;
533 output.set(&[b, c, l], current + bias_val)?;
534 }
535 }
536 }
537 }
538
539 Ok(output)
540 }
541
542 fn conv1d_single(
544 &self,
545 input: &Tensor,
546 output: &mut Tensor,
547 batch_idx: usize,
548 input_length: usize,
549 output_length: usize,
550 ) -> TorshResult<()> {
551 let kernel_coo = self.kernel.to_coo()?;
553 let kernel_triplets = kernel_coo.triplets();
554
555 for (kernel_flat_idx, _, kernel_val) in kernel_triplets {
557 let (out_c, in_c, k_pos) = self.flat_to_3d_kernel(kernel_flat_idx);
559
560 for out_pos in 0..output_length {
562 let in_pos = out_pos * self.stride + k_pos * self.dilation;
564
565 if in_pos >= self.padding {
567 let padded_in_pos = in_pos - self.padding;
568
569 if padded_in_pos < input_length {
571 let input_val = input.get(&[batch_idx, in_c, padded_in_pos])?;
572 let current_out = output.get(&[batch_idx, out_c, out_pos])?;
573 output.set(
574 &[batch_idx, out_c, out_pos],
575 current_out + kernel_val * input_val,
576 )?;
577 }
578 }
579 }
580 }
581
582 Ok(())
583 }
584
585 fn flat_to_3d_kernel(&self, flat_idx: usize) -> (usize, usize, usize) {
587 let channel_size = self.in_channels * self.kernel_size;
588
589 let out_c = flat_idx / channel_size;
590 let remaining = flat_idx % channel_size;
591 let in_c = remaining / self.kernel_size;
592 let k_pos = remaining % self.kernel_size;
593
594 (out_c, in_c, k_pos)
595 }
596
597 fn generate_sparse_kernel_1d(
599 out_channels: usize,
600 in_channels: usize,
601 kernel_size: usize,
602 sparsity: f32,
603 ) -> TorshResult<CsrTensor> {
604 let total_elements = out_channels * in_channels * kernel_size;
605 let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
606
607 let mut row_indices = Vec::with_capacity(nnz);
608 let mut col_indices = Vec::with_capacity(nnz);
609 let mut values = Vec::with_capacity(nnz);
610
611 let mut positions = std::collections::HashSet::new();
613 while positions.len() < nnz {
614 let mut rng = scirs2_core::random::thread_rng();
615 let flat_idx = rng.gen_range(0..total_elements);
616 positions.insert(flat_idx);
617 }
618
619 let channel_size = in_channels * kernel_size;
623
624 for flat_idx in positions {
625 let out_c = flat_idx / channel_size;
626 let col_idx = flat_idx % channel_size; row_indices.push(out_c);
629 col_indices.push(col_idx);
630
631 let fan_in = in_channels * kernel_size;
633 let std_dev = (2.0 / fan_in as f32).sqrt();
634 let mut rng = scirs2_core::random::thread_rng();
635 values.push((rng.random::<f32>() * 2.0 - 1.0) * std_dev);
636 }
637
638 let shape = Shape::new(vec![out_channels, channel_size]);
639 let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
640 CsrTensor::from_coo(&coo)
641 }
642
643 pub fn num_parameters(&self) -> usize {
645 let kernel_params = self.kernel.nnz();
646 let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
647 kernel_params + bias_params
648 }
649
650 pub fn kernel_sparsity(&self) -> f32 {
652 let total_elements = self.out_channels * self.in_channels * self.kernel_size;
653 let nnz = self.kernel.nnz();
654 1.0 - (nnz as f32 / total_elements as f32)
655 }
656
657 pub fn in_channels(&self) -> usize {
659 self.in_channels
660 }
661
662 pub fn out_channels(&self) -> usize {
664 self.out_channels
665 }
666
667 pub fn kernel_size(&self) -> usize {
669 self.kernel_size
670 }
671
672 pub fn stride(&self) -> usize {
674 self.stride
675 }
676
677 pub fn padding(&self) -> usize {
679 self.padding
680 }
681
682 pub fn dilation(&self) -> usize {
684 self.dilation
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use torsh_tensor::creation::ones;
692
693 #[test]
694 fn test_sparse_conv2d_creation() {
695 let conv = SparseConv2d::new(3, 16, (3, 3), Some((1, 1)), Some((1, 1)), None, 0.5, true)
696 .expect("operation should succeed");
697 assert_eq!(conv.in_channels(), 3);
698 assert_eq!(conv.out_channels(), 16);
699 assert_eq!(conv.kernel_size(), (3, 3));
700 assert!(conv.num_parameters() > 0);
701 }
702
703 #[test]
704 fn test_sparse_conv2d_forward() {
705 let conv = SparseConv2d::new(2, 4, (3, 3), None, Some((1, 1)), None, 0.3, false)
706 .expect("operation should succeed");
707 let input = ones::<f32>(&[1, 2, 5, 5]).expect("operation should succeed");
708 let output = conv.forward(&input).expect("forward pass should succeed");
709 assert_eq!(output.shape().dims(), &[1, 4, 5, 5]); }
711
712 #[test]
713 fn test_sparse_conv1d_creation() {
714 let conv = SparseConv1d::new(8, 16, 5, None, None, None, 0.7, true)
715 .expect("Sparse Conv1d should succeed");
716 assert_eq!(conv.in_channels(), 8);
717 assert_eq!(conv.out_channels(), 16);
718 assert_eq!(conv.kernel_size(), 5);
719 assert!(conv.num_parameters() > 0);
720 }
721
722 #[test]
723 fn test_sparse_conv1d_forward() {
724 let conv = SparseConv1d::new(4, 8, 3, None, Some(1), None, 0.4, false)
725 .expect("operation should succeed");
726 let input = ones::<f32>(&[2, 4, 10]).expect("operation should succeed");
727 let output = conv.forward(&input).expect("forward pass should succeed");
728 assert_eq!(output.shape().dims(), &[2, 8, 10]); }
730
731 #[test]
732 fn test_output_size_calculation() {
733 let conv = SparseConv2d::new(1, 1, (3, 3), Some((2, 2)), None, None, 0.0, false)
735 .expect("operation should succeed");
736 let input = ones::<f32>(&[1, 1, 8, 8]).expect("operation should succeed");
737 let output = conv.forward(&input).expect("forward pass should succeed");
738 assert_eq!(output.shape().dims(), &[1, 1, 3, 3]); let conv1d = SparseConv1d::new(1, 1, 3, Some(2), None, None, 0.0, false)
742 .expect("operation should succeed");
743 let input1d = ones::<f32>(&[1, 1, 10]).expect("operation should succeed");
744 let output1d = conv1d
745 .forward(&input1d)
746 .expect("forward pass should succeed");
747 assert_eq!(output1d.shape().dims(), &[1, 1, 4]); }
749
750 #[test]
751 fn test_invalid_parameters() {
752 assert!(SparseConv2d::new(1, 1, (0, 3), None, None, None, 0.5, false).is_err());
753 assert!(SparseConv2d::new(1, 1, (3, 3), Some((0, 1)), None, None, 0.5, false).is_err());
754 assert!(SparseConv2d::new(1, 1, (3, 3), None, None, None, 1.5, false).is_err());
755
756 assert!(SparseConv1d::new(1, 1, 0, None, None, None, 0.5, false).is_err());
757 assert!(SparseConv1d::new(1, 1, 3, Some(0), None, None, 0.5, false).is_err());
758 assert!(SparseConv1d::new(1, 1, 3, None, None, None, -0.1, false).is_err());
759 }
760
761 #[test]
762 fn test_dimension_validation() {
763 let conv = SparseConv2d::new(3, 16, (3, 3), None, None, None, 0.5, false)
764 .expect("operation should succeed");
765 let wrong_input = ones::<f32>(&[1, 2, 5, 5]).expect("operation should succeed"); assert!(conv.forward(&wrong_input).is_err());
767
768 let conv1d = SparseConv1d::new(4, 8, 3, None, None, None, 0.5, false)
769 .expect("Sparse Conv1d should succeed");
770 let wrong_input1d = ones::<f32>(&[1, 3, 10]).expect("operation should succeed"); assert!(conv1d.forward(&wrong_input1d).is_err());
772 }
773
774 #[test]
775 fn test_sparsity_measurement() {
776 let conv = SparseConv2d::new(2, 4, (3, 3), None, None, None, 0.8, false)
777 .expect("operation should succeed");
778 let sparsity = conv.kernel_sparsity();
779 assert!(sparsity >= 0.7 && sparsity <= 0.9); let conv1d = SparseConv1d::new(2, 4, 5, None, None, None, 0.6, false)
782 .expect("Sparse Conv1d should succeed");
783 let sparsity1d = conv1d.kernel_sparsity();
784 assert!(sparsity1d >= 0.5 && sparsity1d <= 0.7); }
786
787 #[test]
788 fn test_bias_addition() {
789 let conv = SparseConv2d::new(1, 2, (1, 1), None, None, None, 0.0, true)
790 .expect("operation should succeed");
791 let input = ones::<f32>(&[1, 1, 3, 3]).expect("operation should succeed");
792 let output = conv.forward(&input).expect("forward pass should succeed");
793 assert_eq!(output.shape().dims(), &[1, 2, 3, 3]);
794
795 let conv1d = SparseConv1d::new(1, 2, 1, None, None, None, 0.0, true)
796 .expect("Sparse Conv1d should succeed");
797 let input1d = ones::<f32>(&[1, 1, 5]).expect("operation should succeed");
798 let output1d = conv1d
799 .forward(&input1d)
800 .expect("forward pass should succeed");
801 assert_eq!(output1d.shape().dims(), &[1, 2, 5]);
802 }
803}