1use crate::{CooTensor, CsrTensor, SparseTensor, TorshResult};
7
8use torsh_core::{Shape, TorshError};
9use torsh_tensor::{
10 creation::{randn, zeros},
11 Tensor,
12};
13
14#[derive(Debug, Clone)]
16pub struct SparseLinear {
17 weight: CsrTensor,
19 bias: Option<Tensor>,
21 in_features: usize,
23 out_features: usize,
25 sparsity: f32,
27}
28
29impl SparseLinear {
30 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 let weight = Self::generate_sparse_weights(in_features, out_features, sparsity)?;
45
46 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 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 pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
97 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 let output = if input_shape.ndim() == 1 {
124 self.weight.matvec(input)?
127 } else {
128 let batch_output = zeros::<f32>(&[batch_size, self.out_features])?;
130
131 for b in 0..batch_size {
132 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 let sample_output = self.weight.matvec(&sample)?;
140
141 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 if let Some(ref bias) = self.bias {
152 if input_shape.ndim() == 1 {
153 for i in 0..self.out_features {
155 output.set(&[i], output.get(&[i])? + bias.get(&[i])?)?;
156 }
157 } else {
158 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 pub fn sparsity(&self) -> f32 {
173 self.sparsity
174 }
175
176 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 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 let coo = self.weight.to_coo()?;
199 let triplets = coo.triplets();
200
201 let importance_scores = if dimension == 0 {
203 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 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 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)); 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 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 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 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 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 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)); 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 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 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 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 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 for (row, col) in positions {
347 row_indices.push(row);
348 col_indices.push(col);
349 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
361pub struct SparseEmbedding {
363 weight: CsrTensor,
365 vocab_size: usize,
367 embedding_dim: usize,
369 sparsity: f32,
371}
372
373impl SparseEmbedding {
374 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 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 pub fn forward(&self, indices: &[usize]) -> TorshResult<Tensor> {
395 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 for (batch_idx, &vocab_idx) in indices.iter().enumerate() {
410 let (cols, vals) = self.weight.get_row(vocab_idx)?;
411
412 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 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 pub fn sparsity(&self) -> f32 {
442 self.sparsity
443 }
444
445 pub fn num_parameters(&self) -> usize {
447 self.weight.nnz()
448 }
449
450 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 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 for (row, col) in positions {
474 row_indices.push(row);
475 col_indices.push(col);
476 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}