Skip to main content

torsh_sparse/layers/
linear.rs

1//! Sparse linear and embedding layers
2//!
3//! This module provides sparse linear transformations including
4//! sparse fully connected layers and embedding layers optimized for sparse tensors.
5
6use crate::{CooTensor, CsrTensor, SparseTensor, TorshResult};
7
8use torsh_core::{Shape, TorshError};
9use torsh_tensor::{
10    creation::{randn, zeros},
11    Tensor,
12};
13
14/// Sparse linear layer (also known as sparse fully connected layer)
15#[derive(Debug, Clone)]
16pub struct SparseLinear {
17    /// Weight matrix in sparse format
18    weight: CsrTensor,
19    /// Optional bias vector
20    bias: Option<Tensor>,
21    /// Input features
22    in_features: usize,
23    /// Output features
24    out_features: usize,
25    /// Sparsity level (fraction of zero weights)
26    sparsity: f32,
27}
28
29impl SparseLinear {
30    /// Create a new sparse linear layer
31    pub fn new(
32        in_features: usize,
33        out_features: usize,
34        sparsity: f32,
35        use_bias: bool,
36    ) -> TorshResult<Self> {
37        if !(0.0..=1.0).contains(&sparsity) {
38            return Err(TorshError::InvalidArgument(
39                "Sparsity must be between 0.0 and 1.0".to_string(),
40            ));
41        }
42
43        // Generate sparse weight matrix
44        let weight = Self::generate_sparse_weights(in_features, out_features, sparsity)?;
45
46        // Generate bias if requested
47        let bias = if use_bias {
48            Some(randn::<f32>(&[out_features])?)
49        } else {
50            None
51        };
52
53        Ok(Self {
54            weight,
55            bias,
56            in_features,
57            out_features,
58            sparsity,
59        })
60    }
61
62    /// Create from existing sparse weight matrix
63    pub fn from_weight(weight: CsrTensor, bias: Option<Tensor>) -> TorshResult<Self> {
64        let shape = weight.shape();
65        if shape.ndim() != 2 {
66            return Err(TorshError::InvalidArgument(
67                "Weight matrix must be 2D".to_string(),
68            ));
69        }
70
71        let out_features = shape.dims()[0];
72        let in_features = shape.dims()[1];
73
74        if let Some(ref bias_tensor) = bias {
75            if bias_tensor.shape().dims() != [out_features] {
76                return Err(TorshError::InvalidArgument(
77                    "Bias dimension must match output features".to_string(),
78                ));
79            }
80        }
81
82        let total_elements = out_features * in_features;
83        let nnz = weight.nnz();
84        let sparsity = 1.0 - (nnz as f32 / total_elements as f32);
85
86        Ok(Self {
87            weight,
88            bias,
89            in_features,
90            out_features,
91            sparsity,
92        })
93    }
94
95    /// Forward pass
96    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
97        // Validate input shape
98        let input_shape = input.shape();
99        let batch_size = if input_shape.ndim() == 1 {
100            1
101        } else if input_shape.ndim() == 2 {
102            input_shape.dims()[0]
103        } else {
104            return Err(TorshError::InvalidArgument(
105                "Input must be 1D or 2D tensor".to_string(),
106            ));
107        };
108
109        let input_features = if input_shape.ndim() == 1 {
110            input_shape.dims()[0]
111        } else {
112            input_shape.dims()[1]
113        };
114
115        if input_features != self.in_features {
116            return Err(TorshError::InvalidArgument(format!(
117                "Input features {} don't match layer input features {}",
118                input_features, self.in_features
119            )));
120        }
121
122        // Handle batch processing
123        let output = if input_shape.ndim() == 1 {
124            // Single sample
125
126            self.weight.matvec(input)?
127        } else {
128            // Batch processing
129            let batch_output = zeros::<f32>(&[batch_size, self.out_features])?;
130
131            for b in 0..batch_size {
132                // Extract single sample from batch
133                let sample = zeros::<f32>(&[self.in_features])?;
134                for i in 0..self.in_features {
135                    sample.set(&[i], input.get(&[b, i])?)?;
136                }
137
138                // Process sample
139                let sample_output = self.weight.matvec(&sample)?;
140
141                // Store in batch output
142                for i in 0..self.out_features {
143                    batch_output.set(&[b, i], sample_output.get(&[i])?)?;
144                }
145            }
146
147            batch_output
148        };
149
150        // Add bias if present
151        if let Some(ref bias) = self.bias {
152            if input_shape.ndim() == 1 {
153                // Single sample
154                for i in 0..self.out_features {
155                    output.set(&[i], output.get(&[i])? + bias.get(&[i])?)?;
156                }
157            } else {
158                // Batch processing
159                for b in 0..batch_size {
160                    for i in 0..self.out_features {
161                        let current = output.get(&[b, i])?;
162                        output.set(&[b, i], current + bias.get(&[i])?)?;
163                    }
164                }
165            }
166        }
167
168        Ok(output)
169    }
170
171    /// Get weight sparsity
172    pub fn sparsity(&self) -> f32 {
173        self.sparsity
174    }
175
176    /// Get number of parameters (including bias)
177    pub fn num_parameters(&self) -> usize {
178        let weight_params = self.weight.nnz();
179        let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
180        weight_params + bias_params
181    }
182
183    /// Apply structured pruning (remove entire rows/columns)
184    pub fn structured_prune(&mut self, ratio: f32, dimension: usize) -> TorshResult<()> {
185        if !(0.0..=1.0).contains(&ratio) {
186            return Err(TorshError::InvalidArgument(
187                "Pruning ratio must be between 0.0 and 1.0".to_string(),
188            ));
189        }
190
191        if dimension > 1 {
192            return Err(TorshError::InvalidArgument(
193                "Dimension must be 0 (rows) or 1 (columns)".to_string(),
194            ));
195        }
196
197        // Convert to COO for easier manipulation
198        let coo = self.weight.to_coo()?;
199        let triplets = coo.triplets();
200
201        // Calculate importance scores (L2 norm of rows/columns)
202        let importance_scores = if dimension == 0 {
203            // Row-wise pruning
204            let mut scores = vec![0.0; self.out_features];
205            for (row, _, val) in &triplets {
206                scores[*row] += val * val;
207            }
208            scores.iter_mut().for_each(|x| *x = x.sqrt());
209            scores
210        } else {
211            // Column-wise pruning
212            let mut scores = vec![0.0; self.in_features];
213            for (_, col, val) in &triplets {
214                scores[*col] += val * val;
215            }
216            scores.iter_mut().for_each(|x| *x = x.sqrt());
217            scores
218        };
219
220        // Select indices to keep
221        let num_to_prune = (importance_scores.len() as f32 * ratio) as usize;
222        let mut indexed_scores: Vec<(usize, f32)> =
223            importance_scores.into_iter().enumerate().collect();
224        indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); // Descending order
225
226        let keep_indices: std::collections::HashSet<usize> = indexed_scores
227            .into_iter()
228            .skip(num_to_prune)
229            .map(|(idx, _)| idx)
230            .collect();
231
232        // Filter triplets
233        let filtered_triplets: Vec<_> = triplets
234            .into_iter()
235            .filter(|(row, col, _)| {
236                if dimension == 0 {
237                    keep_indices.contains(row)
238                } else {
239                    keep_indices.contains(col)
240                }
241            })
242            .collect();
243
244        // Rebuild sparse matrix
245        let (row_indices, col_indices, values): (Vec<_>, Vec<_>, Vec<_>) =
246            filtered_triplets.into_iter().fold(
247                (Vec::new(), Vec::new(), Vec::new()),
248                |(mut rows, mut cols, mut vals), (r, c, v)| {
249                    rows.push(r);
250                    cols.push(c);
251                    vals.push(v);
252                    (rows, cols, vals)
253                },
254            );
255
256        let pruned_coo = CooTensor::new(
257            row_indices,
258            col_indices,
259            values,
260            self.weight.shape().clone(),
261        )?;
262        self.weight = CsrTensor::from_coo(&pruned_coo)?;
263
264        // Update sparsity
265        let total_elements = self.out_features * self.in_features;
266        let nnz = self.weight.nnz();
267        self.sparsity = 1.0 - (nnz as f32 / total_elements as f32);
268
269        Ok(())
270    }
271
272    /// Apply magnitude-based unstructured pruning
273    pub fn magnitude_prune(&mut self, ratio: f32) -> TorshResult<()> {
274        if !(0.0..=1.0).contains(&ratio) {
275            return Err(TorshError::InvalidArgument(
276                "Pruning ratio must be between 0.0 and 1.0".to_string(),
277            ));
278        }
279
280        let coo = self.weight.to_coo()?;
281        let triplets = coo.triplets();
282
283        // Sort by magnitude
284        let mut magnitude_triplets: Vec<_> = triplets
285            .into_iter()
286            .map(|(r, c, v)| (r, c, v, v.abs()))
287            .collect();
288        magnitude_triplets
289            .sort_by(|a, b| b.3.partial_cmp(&a.3).unwrap_or(std::cmp::Ordering::Equal)); // Descending order
290
291        // Keep top (1 - ratio) fraction
292        let num_to_keep = ((magnitude_triplets.len() as f32) * (1.0 - ratio)) as usize;
293        let kept_triplets = &magnitude_triplets[..num_to_keep];
294
295        // Rebuild sparse matrix
296        let (row_indices, col_indices, values): (Vec<_>, Vec<_>, Vec<_>) =
297            kept_triplets.iter().map(|(r, c, v, _)| (*r, *c, *v)).fold(
298                (Vec::new(), Vec::new(), Vec::new()),
299                |(mut rows, mut cols, mut vals), (r, c, v)| {
300                    rows.push(r);
301                    cols.push(c);
302                    vals.push(v);
303                    (rows, cols, vals)
304                },
305            );
306
307        let pruned_coo = CooTensor::new(
308            row_indices,
309            col_indices,
310            values,
311            self.weight.shape().clone(),
312        )?;
313        self.weight = CsrTensor::from_coo(&pruned_coo)?;
314
315        // Update sparsity
316        let total_elements = self.out_features * self.in_features;
317        let nnz = self.weight.nnz();
318        self.sparsity = 1.0 - (nnz as f32 / total_elements as f32);
319
320        Ok(())
321    }
322
323    /// Generate sparse weight matrix
324    fn generate_sparse_weights(
325        in_features: usize,
326        out_features: usize,
327        sparsity: f32,
328    ) -> TorshResult<CsrTensor> {
329        let total_elements = in_features * out_features;
330        let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
331
332        let mut row_indices = Vec::with_capacity(nnz);
333        let mut col_indices = Vec::with_capacity(nnz);
334        let mut values = Vec::with_capacity(nnz);
335
336        // Generate random sparse pattern
337        let mut positions = std::collections::HashSet::new();
338        while positions.len() < nnz {
339            let mut rng = scirs2_core::random::thread_rng();
340            let row = rng.gen_range(0..out_features);
341            let col = rng.gen_range(0..in_features);
342            positions.insert((row, col));
343        }
344
345        // Convert to COO format with random values
346        for (row, col) in positions {
347            row_indices.push(row);
348            col_indices.push(col);
349            // Xavier/Glorot initialization
350            let std_dev = (2.0 / (in_features + out_features) as f32).sqrt();
351            let mut rng = scirs2_core::random::thread_rng();
352            values.push(rng.random::<f32>() * 2.0 * std_dev - std_dev);
353        }
354
355        let shape = Shape::new(vec![out_features, in_features]);
356        let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
357        CsrTensor::from_coo(&coo)
358    }
359}
360
361/// Sparse embedding layer
362pub struct SparseEmbedding {
363    /// Embedding weight matrix (vocab_size x embedding_dim)
364    weight: CsrTensor,
365    /// Vocabulary size
366    vocab_size: usize,
367    /// Embedding dimension
368    embedding_dim: usize,
369    /// Sparsity level
370    sparsity: f32,
371}
372
373impl SparseEmbedding {
374    /// Create a new sparse embedding layer
375    pub fn new(vocab_size: usize, embedding_dim: usize, sparsity: f32) -> TorshResult<Self> {
376        if !(0.0..=1.0).contains(&sparsity) {
377            return Err(TorshError::InvalidArgument(
378                "Sparsity must be between 0.0 and 1.0".to_string(),
379            ));
380        }
381
382        // Generate sparse embedding matrix
383        let weight = Self::generate_sparse_embeddings(vocab_size, embedding_dim, sparsity)?;
384
385        Ok(Self {
386            weight,
387            vocab_size,
388            embedding_dim,
389            sparsity,
390        })
391    }
392
393    /// Forward pass - lookup embeddings for given indices
394    pub fn forward(&self, indices: &[usize]) -> TorshResult<Tensor> {
395        // Validate indices
396        for &idx in indices {
397            if idx >= self.vocab_size {
398                return Err(TorshError::InvalidArgument(format!(
399                    "Index {} out of vocabulary range [0, {})",
400                    idx, self.vocab_size
401                )));
402            }
403        }
404
405        let batch_size = indices.len();
406        let output = zeros::<f32>(&[batch_size, self.embedding_dim])?;
407
408        // Look up each embedding
409        for (batch_idx, &vocab_idx) in indices.iter().enumerate() {
410            let (cols, vals) = self.weight.get_row(vocab_idx)?;
411
412            // Set the embedding values
413            for (&col, &val) in cols.iter().zip(vals.iter()) {
414                output.set(&[batch_idx, col], val)?;
415            }
416        }
417
418        Ok(output)
419    }
420
421    /// Get embedding for a single index
422    pub fn get_embedding(&self, index: usize) -> TorshResult<Tensor> {
423        if index >= self.vocab_size {
424            return Err(TorshError::InvalidArgument(format!(
425                "Index {} out of vocabulary range [0, {})",
426                index, self.vocab_size
427            )));
428        }
429
430        let embedding = zeros::<f32>(&[self.embedding_dim])?;
431        let (cols, vals) = self.weight.get_row(index)?;
432
433        for (&col, &val) in cols.iter().zip(vals.iter()) {
434            embedding.set(&[col], val)?;
435        }
436
437        Ok(embedding)
438    }
439
440    /// Get sparsity level
441    pub fn sparsity(&self) -> f32 {
442        self.sparsity
443    }
444
445    /// Get number of parameters
446    pub fn num_parameters(&self) -> usize {
447        self.weight.nnz()
448    }
449
450    /// Generate sparse embedding matrix
451    fn generate_sparse_embeddings(
452        vocab_size: usize,
453        embedding_dim: usize,
454        sparsity: f32,
455    ) -> TorshResult<CsrTensor> {
456        let total_elements = vocab_size * embedding_dim;
457        let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
458
459        let mut row_indices = Vec::with_capacity(nnz);
460        let mut col_indices = Vec::with_capacity(nnz);
461        let mut values = Vec::with_capacity(nnz);
462
463        // Generate random sparse pattern
464        let mut positions = std::collections::HashSet::new();
465        while positions.len() < nnz {
466            let mut rng = scirs2_core::random::thread_rng();
467            let row = rng.gen_range(0..vocab_size);
468            let col = rng.gen_range(0..embedding_dim);
469            positions.insert((row, col));
470        }
471
472        // Convert to COO format with random values
473        for (row, col) in positions {
474            row_indices.push(row);
475            col_indices.push(col);
476            // Normal initialization with std=1.0
477            let mut rng = scirs2_core::random::thread_rng();
478            values.push(rng.random::<f32>() * 2.0 - 1.0);
479        }
480
481        let shape = Shape::new(vec![vocab_size, embedding_dim]);
482        let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
483        CsrTensor::from_coo(&coo)
484    }
485}