Skip to main content

torsh_optim/
sparse_updates.rs

1use crate::{OptimizerError, OptimizerResult};
2use std::collections::{BTreeMap, HashMap, HashSet};
3use std::sync::{Arc, RwLock};
4use torsh_core::error::TorshError;
5
6/// Configuration for sparse parameter updates
7#[derive(Debug, Clone)]
8pub struct SparseUpdateConfig {
9    /// Sparsity threshold - gradients below this magnitude are considered zero
10    pub sparsity_threshold: f32,
11    /// Whether to use coordinate-wise sparsity detection
12    pub coordinate_wise_sparsity: bool,
13    /// Whether to track sparse patterns over time
14    pub track_sparse_patterns: bool,
15    /// Minimum sparsity ratio to trigger sparse updates (0.0 to 1.0)
16    pub min_sparsity_ratio: f32,
17    /// Whether to use compressed sparse representations
18    pub use_compression: bool,
19    /// Block size for block-sparse updates
20    pub block_size: usize,
21    /// Whether to use adaptive sparsity thresholds
22    pub adaptive_threshold: bool,
23}
24
25impl Default for SparseUpdateConfig {
26    fn default() -> Self {
27        Self {
28            sparsity_threshold: 1e-8,
29            coordinate_wise_sparsity: true,
30            track_sparse_patterns: true,
31            min_sparsity_ratio: 0.1, // At least 10% sparse to use sparse representation
32            use_compression: true,
33            block_size: 32,
34            adaptive_threshold: true,
35        }
36    }
37}
38
39/// Sparse gradient representation using Compressed Sparse Row (CSR) format
40#[derive(Debug, Clone)]
41pub struct SparseGradient {
42    /// Non-zero values
43    pub values: Vec<f32>,
44    /// Column indices of non-zero values
45    pub indices: Vec<usize>,
46    /// Row pointers (for 2D gradients)
47    pub row_ptr: Vec<usize>,
48    /// Shape of the original dense gradient
49    pub shape: Vec<usize>,
50    /// Total number of elements
51    pub total_elements: usize,
52    /// Sparsity ratio (0.0 = dense, 1.0 = completely sparse)
53    pub sparsity_ratio: f32,
54}
55
56impl SparseGradient {
57    /// Create a sparse gradient from a dense gradient
58    pub fn from_dense(gradient: &[f32], shape: Vec<usize>, threshold: f32) -> Self {
59        let mut values = Vec::new();
60        let mut indices = Vec::new();
61
62        for (i, &val) in gradient.iter().enumerate() {
63            if val.abs() > threshold {
64                values.push(val);
65                indices.push(i);
66            }
67        }
68
69        let total_elements = gradient.len();
70        let sparsity_ratio = 1.0 - (values.len() as f32 / total_elements as f32);
71
72        // For 1D gradients, create a simple row pointer
73        let row_ptr = if shape.len() == 1 {
74            vec![0, values.len()]
75        } else {
76            // For multi-dimensional gradients, compute proper row pointers
77            Self::compute_row_pointers(&indices, &shape)
78        };
79
80        Self {
81            values,
82            indices,
83            row_ptr,
84            shape,
85            total_elements,
86            sparsity_ratio,
87        }
88    }
89
90    /// Convert back to dense gradient
91    pub fn to_dense(&self) -> Vec<f32> {
92        let mut dense = vec![0.0; self.total_elements];
93
94        for (&idx, &val) in self.indices.iter().zip(self.values.iter()) {
95            dense[idx] = val;
96        }
97
98        dense
99    }
100
101    /// Check if this gradient is sparse enough to benefit from sparse representation
102    pub fn is_worth_sparse(&self, min_sparsity_ratio: f32) -> bool {
103        self.sparsity_ratio >= min_sparsity_ratio
104    }
105
106    /// Get memory footprint in bytes
107    pub fn memory_footprint(&self) -> usize {
108        self.values.len() * 4 + // f32 values
109        self.indices.len() * 8 + // usize indices  
110        self.row_ptr.len() * 8 + // usize row pointers
111        self.shape.len() * 8 + // usize shape
112        24 // other fields
113    }
114
115    /// Get compression ratio compared to dense representation
116    pub fn compression_ratio(&self) -> f32 {
117        let dense_size = self.total_elements * 4; // 4 bytes per f32
118        let sparse_size = self.memory_footprint();
119        dense_size as f32 / sparse_size as f32
120    }
121
122    /// Add another sparse gradient (element-wise addition)
123    pub fn add(&mut self, other: &SparseGradient) -> Result<(), OptimizerError> {
124        if self.shape != other.shape {
125            return Err(OptimizerError::InvalidParameter(
126                "Cannot add sparse gradients with different shapes".to_string(),
127            ));
128        }
129
130        // Convert both to dense, add, then convert back to sparse
131        let mut dense_self = self.to_dense();
132        let dense_other = other.to_dense();
133
134        for (a, b) in dense_self.iter_mut().zip(dense_other.iter()) {
135            *a += b;
136        }
137
138        // Update self with the result
139        let threshold = self
140            .values
141            .iter()
142            .chain(other.values.iter())
143            .map(|&x| x.abs())
144            .fold(0.0f32, |acc, x| acc.max(x))
145            * 1e-6;
146
147        let result = Self::from_dense(&dense_self, self.shape.clone(), threshold);
148
149        self.values = result.values;
150        self.indices = result.indices;
151        self.row_ptr = result.row_ptr;
152        self.sparsity_ratio = result.sparsity_ratio;
153
154        Ok(())
155    }
156
157    /// Scale the sparse gradient by a scalar
158    pub fn scale(&mut self, factor: f32) {
159        for val in &mut self.values {
160            *val *= factor;
161        }
162    }
163
164    /// Compute L2 norm of the sparse gradient
165    pub fn norm(&self) -> f32 {
166        self.values.iter().map(|&x| x * x).sum::<f32>().sqrt()
167    }
168
169    // Private helper methods
170
171    fn compute_row_pointers(indices: &[usize], shape: &[usize]) -> Vec<usize> {
172        if shape.len() != 2 {
173            // For non-2D shapes, return simple row pointer
174            return vec![0, indices.len()];
175        }
176
177        let rows = shape[0];
178        let cols = shape[1];
179        let mut row_ptr = vec![0; rows + 1];
180
181        for &idx in indices {
182            let row = idx / cols;
183            if row < rows {
184                row_ptr[row + 1] += 1;
185            }
186        }
187
188        // Convert counts to cumulative sums
189        for i in 1..row_ptr.len() {
190            row_ptr[i] += row_ptr[i - 1];
191        }
192
193        row_ptr
194    }
195}
196
197/// Block-sparse gradient for structured sparsity
198#[derive(Debug, Clone)]
199pub struct BlockSparseGradient {
200    /// Non-zero blocks
201    pub blocks: Vec<Vec<f32>>,
202    /// Block indices (row, col) for 2D or flat index for 1D
203    pub block_indices: Vec<(usize, usize)>,
204    /// Block size
205    pub block_size: usize,
206    /// Shape of the original gradient
207    pub shape: Vec<usize>,
208    /// Number of blocks in each dimension
209    pub block_shape: Vec<usize>,
210    /// Sparsity ratio at block level
211    pub block_sparsity_ratio: f32,
212}
213
214impl BlockSparseGradient {
215    /// Create block-sparse gradient from dense gradient
216    ///
217    /// Only rank-1 and rank-2 gradients have a block-sparse layout defined here;
218    /// higher-rank gradients (e.g. 4-D convolution weights) are reported as
219    /// unsupported so the caller can fall back to a dense update. A sparse-update
220    /// optimisation must degrade to correct dense behaviour, never abort.
221    ///
222    /// # Errors
223    /// Returns [`TorshError::UnsupportedOperation`] if `shape` is not rank 1 or 2.
224    pub fn from_dense(
225        gradient: &[f32],
226        shape: Vec<usize>,
227        block_size: usize,
228        threshold: f32,
229    ) -> torsh_core::error::Result<Self> {
230        match shape.len() {
231            1 => Ok(Self::from_dense_1d(gradient, shape, block_size, threshold)),
232            2 => Ok(Self::from_dense_2d(gradient, shape, block_size, threshold)),
233            rank => Err(TorshError::UnsupportedOperation {
234                op: "block-sparse gradient representation".to_string(),
235                dtype: format!("rank-{rank} gradient (only rank 1 and 2 are supported)"),
236            }),
237        }
238    }
239
240    /// Convert back to dense gradient
241    pub fn to_dense(&self) -> Vec<f32> {
242        let total_elements: usize = self.shape.iter().product();
243        let mut dense = vec![0.0; total_elements];
244
245        for (block, &(block_row, block_col)) in self.blocks.iter().zip(self.block_indices.iter()) {
246            if self.shape.len() == 1 {
247                let start_idx = block_row * self.block_size;
248                for (i, &val) in block.iter().enumerate() {
249                    if start_idx + i < dense.len() {
250                        dense[start_idx + i] = val;
251                    }
252                }
253            } else if self.shape.len() == 2 {
254                let rows = self.shape[0];
255                let cols = self.shape[1];
256                let start_row = block_row * self.block_size;
257                let start_col = block_col * self.block_size;
258
259                for (i, &val) in block.iter().enumerate() {
260                    let row = start_row + i / self.block_size;
261                    let col = start_col + i % self.block_size;
262                    if row < rows && col < cols {
263                        dense[row * cols + col] = val;
264                    }
265                }
266            }
267        }
268
269        dense
270    }
271
272    /// Check if block representation is beneficial
273    pub fn is_worth_block_sparse(&self, min_sparsity_ratio: f32) -> bool {
274        self.block_sparsity_ratio >= min_sparsity_ratio
275    }
276
277    // Private helper methods
278
279    fn from_dense_1d(
280        gradient: &[f32],
281        shape: Vec<usize>,
282        block_size: usize,
283        threshold: f32,
284    ) -> Self {
285        let total_elements = shape[0];
286        let num_blocks = (total_elements + block_size - 1) / block_size;
287
288        let mut blocks = Vec::new();
289        let mut block_indices = Vec::new();
290
291        for block_idx in 0..num_blocks {
292            let start_idx = block_idx * block_size;
293            let end_idx = (start_idx + block_size).min(total_elements);
294
295            let block_data: Vec<f32> = gradient[start_idx..end_idx].to_vec();
296
297            // Check if block has significant values
298            let block_norm = block_data.iter().map(|&x| x * x).sum::<f32>().sqrt();
299
300            if block_norm > threshold {
301                blocks.push(block_data);
302                block_indices.push((block_idx, 0));
303            }
304        }
305
306        let block_sparsity_ratio = 1.0 - (blocks.len() as f32 / num_blocks as f32);
307
308        Self {
309            blocks,
310            block_indices,
311            block_size,
312            shape,
313            block_shape: vec![num_blocks],
314            block_sparsity_ratio,
315        }
316    }
317
318    fn from_dense_2d(
319        gradient: &[f32],
320        shape: Vec<usize>,
321        block_size: usize,
322        threshold: f32,
323    ) -> Self {
324        let rows = shape[0];
325        let cols = shape[1];
326        let block_rows = (rows + block_size - 1) / block_size;
327        let block_cols = (cols + block_size - 1) / block_size;
328
329        let mut blocks = Vec::new();
330        let mut block_indices = Vec::new();
331
332        for block_row in 0..block_rows {
333            for block_col in 0..block_cols {
334                let mut block_data = Vec::new();
335
336                let start_row = block_row * block_size;
337                let end_row = (start_row + block_size).min(rows);
338                let start_col = block_col * block_size;
339                let end_col = (start_col + block_size).min(cols);
340
341                for row in start_row..end_row {
342                    for col in start_col..end_col {
343                        let idx = row * cols + col;
344                        block_data.push(gradient[idx]);
345                    }
346                }
347
348                // Check if block has significant values
349                let block_norm = block_data.iter().map(|&x| x * x).sum::<f32>().sqrt();
350
351                if block_norm > threshold {
352                    blocks.push(block_data);
353                    block_indices.push((block_row, block_col));
354                }
355            }
356        }
357
358        let total_blocks = block_rows * block_cols;
359        let block_sparsity_ratio = 1.0 - (blocks.len() as f32 / total_blocks as f32);
360
361        Self {
362            blocks,
363            block_indices,
364            block_size,
365            shape,
366            block_shape: vec![block_rows, block_cols],
367            block_sparsity_ratio,
368        }
369    }
370}
371
372/// Sparse pattern tracker for analyzing sparsity patterns over time
373#[derive(Debug, Clone)]
374pub struct SparsePatternTracker {
375    /// Parameter ID
376    pub parameter_id: String,
377    /// Historical sparsity patterns (indices of non-zero elements)
378    pub pattern_history: VecDeque<HashSet<usize>>,
379    /// Maximum history length
380    pub max_history: usize,
381    /// Stable sparse indices (consistently sparse)
382    pub stable_sparse_indices: HashSet<usize>,
383    /// Stable dense indices (consistently non-zero)
384    pub stable_dense_indices: HashSet<usize>,
385    /// Stability threshold (fraction of history where pattern must be consistent)
386    pub stability_threshold: f32,
387}
388
389use std::collections::VecDeque;
390
391impl SparsePatternTracker {
392    /// Create a new pattern tracker
393    pub fn new(parameter_id: String, max_history: usize, stability_threshold: f32) -> Self {
394        Self {
395            parameter_id,
396            pattern_history: VecDeque::with_capacity(max_history),
397            max_history,
398            stable_sparse_indices: HashSet::new(),
399            stable_dense_indices: HashSet::new(),
400            stability_threshold,
401        }
402    }
403
404    /// Update with a new sparse pattern
405    pub fn update(&mut self, sparse_gradient: &SparseGradient) {
406        let non_zero_indices: HashSet<usize> = sparse_gradient.indices.iter().cloned().collect();
407
408        // Add to history
409        self.pattern_history.push_back(non_zero_indices);
410
411        // Remove old history if necessary
412        if self.pattern_history.len() > self.max_history {
413            self.pattern_history.pop_front();
414        }
415
416        // Update stable patterns if we have enough history
417        if self.pattern_history.len() >= (self.max_history as f32 * 0.5) as usize {
418            self.update_stable_patterns(sparse_gradient.total_elements);
419        }
420    }
421
422    /// Get sparsity statistics
423    pub fn get_statistics(&self) -> SparsePatternStatistics {
424        let total_patterns = self.pattern_history.len();
425
426        let average_sparsity = if total_patterns > 0 {
427            let total_sparse: usize = self
428                .pattern_history
429                .iter()
430                .map(|pattern| pattern.len())
431                .sum();
432            total_sparse as f32 / total_patterns as f32
433        } else {
434            0.0
435        };
436
437        let pattern_stability = if total_patterns > 1 {
438            // Measure how much patterns change between consecutive updates
439            let mut stability_sum = 0.0;
440            for i in 1..total_patterns {
441                let prev = &self.pattern_history[i - 1];
442                let curr = &self.pattern_history[i];
443                let intersection = prev.intersection(curr).count();
444                let union = prev.union(curr).count();
445                stability_sum += intersection as f32 / union.max(1) as f32;
446            }
447            stability_sum / (total_patterns - 1) as f32
448        } else {
449            1.0
450        };
451
452        SparsePatternStatistics {
453            parameter_id: self.parameter_id.clone(),
454            average_sparsity,
455            pattern_stability,
456            stable_sparse_count: self.stable_sparse_indices.len(),
457            stable_dense_count: self.stable_dense_indices.len(),
458            total_patterns: total_patterns,
459        }
460    }
461
462    /// Check if an index is consistently sparse
463    pub fn is_consistently_sparse(&self, index: usize) -> bool {
464        self.stable_sparse_indices.contains(&index)
465    }
466
467    /// Check if an index is consistently dense
468    pub fn is_consistently_dense(&self, index: usize) -> bool {
469        self.stable_dense_indices.contains(&index)
470    }
471
472    // Private methods
473
474    fn update_stable_patterns(&mut self, total_elements: usize) {
475        let history_len = self.pattern_history.len();
476        let required_count = (history_len as f32 * self.stability_threshold) as usize;
477
478        // Count how often each index appears as non-zero
479        let mut index_counts: HashMap<usize, usize> = HashMap::new();
480        for pattern in &self.pattern_history {
481            for &idx in pattern {
482                *index_counts.entry(idx).or_insert(0) += 1;
483            }
484        }
485
486        // Update stable patterns
487        self.stable_dense_indices.clear();
488        self.stable_sparse_indices.clear();
489
490        for idx in 0..total_elements {
491            let count = index_counts.get(&idx).copied().unwrap_or(0);
492
493            if count >= required_count {
494                self.stable_dense_indices.insert(idx);
495            } else if count == 0 && history_len >= required_count {
496                self.stable_sparse_indices.insert(idx);
497            }
498        }
499    }
500}
501
502/// Statistics about sparse patterns
503#[derive(Debug, Clone)]
504pub struct SparsePatternStatistics {
505    pub parameter_id: String,
506    pub average_sparsity: f32,
507    pub pattern_stability: f32,
508    pub stable_sparse_count: usize,
509    pub stable_dense_count: usize,
510    pub total_patterns: usize,
511}
512
513impl std::fmt::Display for SparsePatternStatistics {
514    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515        writeln!(f, "Sparse Pattern Statistics for {}:", self.parameter_id)?;
516        writeln!(
517            f,
518            "  Average Sparsity: {:.1}%",
519            (1.0 - self.average_sparsity) * 100.0
520        )?;
521        writeln!(f, "  Pattern Stability: {:.3}", self.pattern_stability)?;
522        writeln!(f, "  Stable Sparse: {}", self.stable_sparse_count)?;
523        writeln!(f, "  Stable Dense: {}", self.stable_dense_count)?;
524        writeln!(f, "  Total Patterns: {}", self.total_patterns)?;
525        Ok(())
526    }
527}
528
529/// Sparse update manager
530pub struct SparseUpdateManager {
531    config: SparseUpdateConfig,
532    pattern_trackers: HashMap<String, SparsePatternTracker>,
533    adaptive_thresholds: HashMap<String, f32>,
534    compression_stats: HashMap<String, CompressionStats>,
535}
536
537#[derive(Debug, Clone)]
538pub struct CompressionStats {
539    pub total_updates: usize,
540    pub sparse_updates: usize,
541    pub average_compression_ratio: f32,
542    pub memory_saved_bytes: usize,
543}
544
545impl SparseUpdateManager {
546    /// Create a new sparse update manager
547    pub fn new(config: SparseUpdateConfig) -> Self {
548        Self {
549            config,
550            pattern_trackers: HashMap::new(),
551            adaptive_thresholds: HashMap::new(),
552            compression_stats: HashMap::new(),
553        }
554    }
555
556    /// Process a gradient and return sparse representation if beneficial
557    pub fn process_gradient(
558        &mut self,
559        parameter_id: String,
560        gradient: Vec<f32>,
561        shape: Vec<usize>,
562    ) -> SparseUpdateResult {
563        let threshold = self.get_threshold(&parameter_id, &gradient);
564
565        // Create sparse representation
566        let sparse_gradient = SparseGradient::from_dense(&gradient, shape.clone(), threshold);
567
568        // Update pattern tracking if enabled
569        if self.config.track_sparse_patterns {
570            let tracker = self
571                .pattern_trackers
572                .entry(parameter_id.clone())
573                .or_insert_with(|| SparsePatternTracker::new(parameter_id.clone(), 100, 0.8));
574            tracker.update(&sparse_gradient);
575        }
576
577        // Update compression statistics
578        self.update_compression_stats(&parameter_id, &sparse_gradient, gradient.len());
579
580        // Decide whether to use sparse representation
581        if sparse_gradient.is_worth_sparse(self.config.min_sparsity_ratio) {
582            if self.config.use_compression {
583                SparseUpdateResult::Sparse(sparse_gradient)
584            } else {
585                SparseUpdateResult::Dense(gradient)
586            }
587        } else {
588            SparseUpdateResult::Dense(gradient)
589        }
590    }
591
592    /// Process gradient with block-sparse representation
593    pub fn process_gradient_block_sparse(
594        &mut self,
595        parameter_id: String,
596        gradient: Vec<f32>,
597        shape: Vec<usize>,
598    ) -> SparseUpdateResult {
599        let threshold = self.get_threshold(&parameter_id, &gradient);
600
601        // Create block-sparse representation. Ranks without a block layout fall
602        // back to the dense update path rather than failing the step.
603        let block_sparse = match BlockSparseGradient::from_dense(
604            &gradient,
605            shape,
606            self.config.block_size,
607            threshold,
608        ) {
609            Ok(block_sparse) => block_sparse,
610            Err(_) => return SparseUpdateResult::Dense(gradient),
611        };
612
613        if block_sparse.is_worth_block_sparse(self.config.min_sparsity_ratio) {
614            SparseUpdateResult::BlockSparse(block_sparse)
615        } else {
616            SparseUpdateResult::Dense(gradient)
617        }
618    }
619
620    /// Get sparsity statistics for a parameter
621    pub fn get_parameter_statistics(&self, parameter_id: &str) -> Option<SparsePatternStatistics> {
622        self.pattern_trackers
623            .get(parameter_id)
624            .map(|tracker| tracker.get_statistics())
625    }
626
627    /// Get all compression statistics
628    pub fn get_compression_statistics(&self) -> &HashMap<String, CompressionStats> {
629        &self.compression_stats
630    }
631
632    /// Set custom threshold for a parameter
633    pub fn set_parameter_threshold(&mut self, parameter_id: String, threshold: f32) {
634        self.adaptive_thresholds.insert(parameter_id, threshold);
635    }
636
637    /// Get overall statistics
638    pub fn get_overall_statistics(&self) -> OverallSparseStatistics {
639        let total_parameters = self.compression_stats.len();
640
641        let total_updates: usize = self
642            .compression_stats
643            .values()
644            .map(|stats| stats.total_updates)
645            .sum();
646
647        let total_sparse_updates: usize = self
648            .compression_stats
649            .values()
650            .map(|stats| stats.sparse_updates)
651            .sum();
652
653        let average_compression_ratio = if total_parameters > 0 {
654            self.compression_stats
655                .values()
656                .map(|stats| stats.average_compression_ratio)
657                .sum::<f32>()
658                / total_parameters as f32
659        } else {
660            1.0
661        };
662
663        let total_memory_saved: usize = self
664            .compression_stats
665            .values()
666            .map(|stats| stats.memory_saved_bytes)
667            .sum();
668
669        let sparse_ratio = if total_updates > 0 {
670            total_sparse_updates as f32 / total_updates as f32
671        } else {
672            0.0
673        };
674
675        OverallSparseStatistics {
676            total_parameters,
677            total_updates,
678            total_sparse_updates,
679            sparse_ratio,
680            average_compression_ratio,
681            total_memory_saved,
682        }
683    }
684
685    // Private methods
686
687    fn get_threshold(&self, parameter_id: &str, gradient: &[f32]) -> f32 {
688        if let Some(&custom_threshold) = self.adaptive_thresholds.get(parameter_id) {
689            return custom_threshold;
690        }
691
692        if !self.config.adaptive_threshold {
693            return self.config.sparsity_threshold;
694        }
695
696        // Adaptive threshold based on gradient statistics
697        let gradient_magnitude = gradient.iter().map(|&x| x * x).sum::<f32>().sqrt();
698        let gradient_std = {
699            let mean = gradient.iter().sum::<f32>() / gradient.len() as f32;
700            let variance =
701                gradient.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / gradient.len() as f32;
702            variance.sqrt()
703        };
704
705        // Use a fraction of the standard deviation as threshold
706        let adaptive_threshold = gradient_std * 0.1;
707        adaptive_threshold
708            .max(self.config.sparsity_threshold)
709            .min(gradient_magnitude * 0.01)
710    }
711
712    fn update_compression_stats(
713        &mut self,
714        parameter_id: &str,
715        sparse_gradient: &SparseGradient,
716        original_size: usize,
717    ) {
718        let stats = self
719            .compression_stats
720            .entry(parameter_id.to_string())
721            .or_insert_with(|| CompressionStats {
722                total_updates: 0,
723                sparse_updates: 0,
724                average_compression_ratio: 1.0,
725                memory_saved_bytes: 0,
726            });
727
728        stats.total_updates += 1;
729
730        if sparse_gradient.is_worth_sparse(self.config.min_sparsity_ratio) {
731            stats.sparse_updates += 1;
732
733            let compression_ratio = sparse_gradient.compression_ratio();
734            stats.average_compression_ratio = (stats.average_compression_ratio
735                * (stats.sparse_updates - 1) as f32
736                + compression_ratio)
737                / stats.sparse_updates as f32;
738
739            let original_bytes = original_size * 4; // 4 bytes per f32
740            let compressed_bytes = sparse_gradient.memory_footprint();
741            stats.memory_saved_bytes += original_bytes.saturating_sub(compressed_bytes);
742        }
743    }
744}
745
746/// Result of sparse update processing
747#[derive(Debug)]
748pub enum SparseUpdateResult {
749    /// Use dense representation
750    Dense(Vec<f32>),
751    /// Use sparse representation
752    Sparse(SparseGradient),
753    /// Use block-sparse representation
754    BlockSparse(BlockSparseGradient),
755}
756
757/// Overall sparse update statistics
758#[derive(Debug, Clone)]
759pub struct OverallSparseStatistics {
760    pub total_parameters: usize,
761    pub total_updates: usize,
762    pub total_sparse_updates: usize,
763    pub sparse_ratio: f32,
764    pub average_compression_ratio: f32,
765    pub total_memory_saved: usize,
766}
767
768impl std::fmt::Display for OverallSparseStatistics {
769    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
770        writeln!(f, "Overall Sparse Update Statistics:")?;
771        writeln!(f, "  Total Parameters: {}", self.total_parameters)?;
772        writeln!(f, "  Total Updates: {}", self.total_updates)?;
773        writeln!(f, "  Sparse Updates: {}", self.total_sparse_updates)?;
774        writeln!(f, "  Sparse Ratio: {:.1}%", self.sparse_ratio * 100.0)?;
775        writeln!(
776            f,
777            "  Average Compression: {:.2}x",
778            self.average_compression_ratio
779        )?;
780        writeln!(
781            f,
782            "  Memory Saved: {:.2} MB",
783            self.total_memory_saved as f64 / 1024.0 / 1024.0
784        )?;
785        Ok(())
786    }
787}
788
789/// Trait for optimizers that support sparse updates
790pub trait SparseUpdateSupport {
791    /// Apply a sparse gradient update
792    fn apply_sparse_update(
793        &mut self,
794        parameter_id: &str,
795        sparse_gradient: &SparseGradient,
796    ) -> Result<(), OptimizerError>;
797
798    /// Apply a block-sparse gradient update
799    fn apply_block_sparse_update(
800        &mut self,
801        parameter_id: &str,
802        block_sparse: &BlockSparseGradient,
803    ) -> Result<(), OptimizerError>;
804
805    /// Apply a dense gradient update
806    fn apply_dense_update(
807        &mut self,
808        parameter_id: &str,
809        gradient: &[f32],
810    ) -> Result<(), OptimizerError>;
811
812    /// Get parameter shape for sparse processing
813    fn get_parameter_shape(&self, parameter_id: &str) -> Option<Vec<usize>>;
814}
815
816/// Wrapper optimizer that adds sparse update functionality
817pub struct SparseUpdateOptimizer<T> {
818    inner: T,
819    sparse_manager: SparseUpdateManager,
820    enabled: bool,
821}
822
823impl<T> SparseUpdateOptimizer<T>
824where
825    T: SparseUpdateSupport,
826{
827    /// Create a new sparse update optimizer wrapper
828    pub fn new(inner: T, config: SparseUpdateConfig) -> Self {
829        Self {
830            inner,
831            sparse_manager: SparseUpdateManager::new(config),
832            enabled: true,
833        }
834    }
835
836    /// Enable or disable sparse updates
837    pub fn set_enabled(&mut self, enabled: bool) {
838        self.enabled = enabled;
839    }
840
841    /// Get the inner optimizer
842    pub fn inner(&self) -> &T {
843        &self.inner
844    }
845
846    /// Get the inner optimizer mutably
847    pub fn inner_mut(&mut self) -> &mut T {
848        &mut self.inner
849    }
850
851    /// Get the sparse manager
852    pub fn sparse_manager(&self) -> &SparseUpdateManager {
853        &self.sparse_manager
854    }
855
856    /// Submit gradients for sparse processing
857    pub fn submit_gradients(
858        &mut self,
859        gradients: HashMap<String, Vec<f32>>,
860    ) -> Result<(), OptimizerError> {
861        for (parameter_id, gradient) in gradients {
862            if !self.enabled {
863                self.inner.apply_dense_update(&parameter_id, &gradient)?;
864                continue;
865            }
866
867            // Get parameter shape
868            let shape = self
869                .inner
870                .get_parameter_shape(&parameter_id)
871                .unwrap_or_else(|| vec![gradient.len()]);
872
873            // Process gradient for sparsity
874            match self
875                .sparse_manager
876                .process_gradient(parameter_id.clone(), gradient, shape)
877            {
878                SparseUpdateResult::Dense(dense_gradient) => {
879                    self.inner
880                        .apply_dense_update(&parameter_id, &dense_gradient)?;
881                }
882                SparseUpdateResult::Sparse(sparse_gradient) => {
883                    self.inner
884                        .apply_sparse_update(&parameter_id, &sparse_gradient)?;
885                }
886                SparseUpdateResult::BlockSparse(block_sparse) => {
887                    self.inner
888                        .apply_block_sparse_update(&parameter_id, &block_sparse)?;
889                }
890            }
891        }
892
893        Ok(())
894    }
895
896    /// Get statistics for a specific parameter
897    pub fn get_parameter_statistics(&self, parameter_id: &str) -> Option<SparsePatternStatistics> {
898        self.sparse_manager.get_parameter_statistics(parameter_id)
899    }
900
901    /// Get overall statistics
902    pub fn get_statistics(&self) -> OverallSparseStatistics {
903        self.sparse_manager.get_overall_statistics()
904    }
905}
906
907#[cfg(test)]
908mod tests {
909    use super::*;
910
911    #[test]
912    fn test_sparse_gradient() {
913        let dense = vec![0.0, 1.5, 0.0, 0.0, 2.3, 0.0, -1.1, 0.0];
914        let shape = vec![8];
915        let threshold = 0.1;
916
917        let sparse = SparseGradient::from_dense(&dense, shape, threshold);
918
919        assert_eq!(sparse.values, vec![1.5, 2.3, -1.1]);
920        assert_eq!(sparse.indices, vec![1, 4, 6]);
921        assert!(sparse.sparsity_ratio > 0.5);
922
923        let recovered = sparse.to_dense();
924        assert_eq!(recovered, dense);
925    }
926
927    #[test]
928    fn test_block_sparse_gradient() {
929        let dense = vec![
930            1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.5, 1.5, 0.0, 0.0, 0.5, 0.8,
931        ];
932        let shape = vec![4, 4];
933        let block_size = 2;
934        let threshold = 0.1;
935
936        let block_sparse = BlockSparseGradient::from_dense(&dense, shape, block_size, threshold)
937            .expect("rank-2 gradients are supported");
938
939        // Should have 2 non-zero blocks
940        assert_eq!(block_sparse.blocks.len(), 2);
941        assert!(block_sparse.block_sparsity_ratio > 0.0);
942
943        let recovered = block_sparse.to_dense();
944        // Check that significant values are preserved
945        assert!((recovered[0] - 1.0).abs() < 1e-6);
946        assert!((recovered[10] - 2.5).abs() < 1e-6);
947    }
948
949    #[test]
950    fn test_sparse_pattern_tracker() {
951        let mut tracker = SparsePatternTracker::new("test_param".to_string(), 5, 0.8);
952
953        // Simulate consistent pattern
954        for _ in 0..10 {
955            let dense = vec![1.0, 0.0, 2.0, 0.0, 0.0];
956            let sparse = SparseGradient::from_dense(&dense, vec![5], 0.1);
957            tracker.update(&sparse);
958        }
959
960        let stats = tracker.get_statistics();
961        assert!(stats.pattern_stability > 0.5);
962        assert_eq!(stats.stable_dense_count, 2); // indices 0 and 2
963        assert_eq!(stats.stable_sparse_count, 3); // indices 1, 3, and 4
964    }
965
966    #[derive(Debug)]
967    struct MockOptimizer {
968        updates_received: HashMap<String, usize>,
969    }
970
971    impl MockOptimizer {
972        fn new() -> Self {
973            Self {
974                updates_received: HashMap::new(),
975            }
976        }
977    }
978
979    impl SparseUpdateSupport for MockOptimizer {
980        fn apply_sparse_update(
981            &mut self,
982            parameter_id: &str,
983            _sparse_gradient: &SparseGradient,
984        ) -> Result<(), OptimizerError> {
985            *self
986                .updates_received
987                .entry(parameter_id.to_string())
988                .or_insert(0) += 1;
989            Ok(())
990        }
991
992        fn apply_block_sparse_update(
993            &mut self,
994            parameter_id: &str,
995            _block_sparse: &BlockSparseGradient,
996        ) -> Result<(), OptimizerError> {
997            *self
998                .updates_received
999                .entry(parameter_id.to_string())
1000                .or_insert(0) += 1;
1001            Ok(())
1002        }
1003
1004        fn apply_dense_update(
1005            &mut self,
1006            parameter_id: &str,
1007            _gradient: &[f32],
1008        ) -> Result<(), OptimizerError> {
1009            *self
1010                .updates_received
1011                .entry(parameter_id.to_string())
1012                .or_insert(0) += 1;
1013            Ok(())
1014        }
1015
1016        fn get_parameter_shape(&self, _parameter_id: &str) -> Option<Vec<usize>> {
1017            Some(vec![8])
1018        }
1019    }
1020
1021    #[test]
1022    fn test_sparse_update_optimizer() {
1023        let config = SparseUpdateConfig {
1024            sparsity_threshold: 0.1,
1025            min_sparsity_ratio: 0.3,
1026            ..Default::default()
1027        };
1028
1029        let optimizer = MockOptimizer::new();
1030        let mut sparse_optimizer = SparseUpdateOptimizer::new(optimizer, config);
1031
1032        // Submit gradients
1033        let mut gradients = HashMap::new();
1034        gradients.insert(
1035            "param1".to_string(),
1036            vec![0.0, 1.5, 0.0, 0.0, 2.3, 0.0, -1.1, 0.0],
1037        ); // Sparse
1038        gradients.insert(
1039            "param2".to_string(),
1040            vec![1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7],
1041        ); // Dense
1042
1043        sparse_optimizer.submit_gradients(gradients).unwrap();
1044
1045        // Check that updates were received
1046        assert_eq!(
1047            sparse_optimizer.inner().updates_received.get("param1"),
1048            Some(&1)
1049        );
1050        assert_eq!(
1051            sparse_optimizer.inner().updates_received.get("param2"),
1052            Some(&1)
1053        );
1054
1055        // Check statistics
1056        let stats = sparse_optimizer.get_statistics();
1057        assert_eq!(stats.total_parameters, 2);
1058        assert!(stats.total_updates > 0);
1059    }
1060}