1use serde::{Deserialize, Serialize};
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct SparsityConfig {
32 pub top_k: Option<usize>,
34 pub threshold: Option<f32>,
37 pub accumulate_residuals: bool,
40}
41
42impl Default for SparsityConfig {
43 fn default() -> Self {
44 Self {
45 top_k: None,
46 threshold: None,
47 accumulate_residuals: true,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct SparsifierStats {
57 pub total_rounds: u64,
59 pub total_elements_kept: u64,
61 pub total_elements_dropped: u64,
63 pub total_residual_applied: u64,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SparseGradient {
75 pub indices: Vec<u32>,
77 pub values: Vec<f32>,
79 pub original_len: usize,
81}
82
83impl SparseGradient {
84 pub fn sparsity_ratio(&self) -> f64 {
88 if self.original_len == 0 {
89 return 0.0;
90 }
91 1.0 - (self.indices.len() as f64 / self.original_len as f64)
92 }
93
94 pub fn to_dense(&self) -> Vec<f32> {
96 let mut dense = vec![0.0_f32; self.original_len];
97 for (&idx, &val) in self.indices.iter().zip(self.values.iter()) {
98 let pos = idx as usize;
99 if pos < self.original_len {
100 dense[pos] = val;
101 }
102 }
103 dense
104 }
105}
106
107pub struct GradientSparsifier {
115 pub config: SparsityConfig,
117 pub residual: Vec<f32>,
119 pub stats: SparsifierStats,
121}
122
123impl GradientSparsifier {
124 pub fn new(config: SparsityConfig, gradient_len: usize) -> Self {
130 Self {
131 config,
132 residual: vec![0.0_f32; gradient_len],
133 stats: SparsifierStats::default(),
134 }
135 }
136
137 pub fn sparsify(&mut self, gradient: &[f32]) -> SparseGradient {
148 let len = gradient.len();
149
150 if self.residual.len() != len {
152 self.residual.resize(len, 0.0);
153 }
154
155 let mut working: Vec<f32> = if self.config.accumulate_residuals {
157 let residual_applied = self.residual.iter().filter(|&&v| v != 0.0).count() as u64;
158 self.stats.total_residual_applied += residual_applied;
159
160 gradient
161 .iter()
162 .zip(self.residual.iter())
163 .map(|(&g, &r)| g + r)
164 .collect()
165 } else {
166 gradient.to_vec()
167 };
168
169 if let Some(thresh) = self.config.threshold {
171 for v in working.iter_mut() {
172 if v.abs() < thresh {
173 *v = 0.0;
174 }
175 }
176 }
177
178 let mut candidates: Vec<(usize, f32)> = working
181 .iter()
182 .enumerate()
183 .filter(|(_, &v)| v != 0.0)
184 .map(|(i, &v)| (i, v.abs()))
185 .collect();
186
187 let keep_count = match self.config.top_k {
188 Some(k) => k.min(candidates.len()),
189 None => candidates.len(),
190 };
191
192 let kept_indices: std::collections::HashSet<usize> = if keep_count < candidates.len() {
196 candidates.select_nth_unstable_by(keep_count, |a, b| {
198 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
200 });
201 candidates[..keep_count].iter().map(|&(i, _)| i).collect()
202 } else {
203 candidates.iter().map(|&(i, _)| i).collect()
204 };
205
206 let mut indices: Vec<u32> = Vec::with_capacity(keep_count);
208 let mut values: Vec<f32> = Vec::with_capacity(keep_count);
209
210 for (i, &val) in working.iter().enumerate() {
212 if kept_indices.contains(&i) {
213 indices.push(i as u32);
214 values.push(val);
215 self.residual[i] = 0.0; } else {
217 if self.config.accumulate_residuals {
220 self.residual[i] = val;
221 } else {
222 self.residual[i] = 0.0;
223 }
224 }
225 }
226
227 let kept = indices.len() as u64;
229 let dropped = (len as u64).saturating_sub(kept);
230 self.stats.total_rounds += 1;
231 self.stats.total_elements_kept += kept;
232 self.stats.total_elements_dropped += dropped;
233
234 SparseGradient {
235 indices,
236 values,
237 original_len: len,
238 }
239 }
240
241 pub fn reset_residual(&mut self) {
243 for v in self.residual.iter_mut() {
244 *v = 0.0;
245 }
246 }
247}
248
249#[derive(Debug, Clone, Default, Serialize, Deserialize)]
253pub struct DeltaStats {
254 pub total_encoded: u64,
256 pub total_full_sends: u64,
258 pub total_delta_sends: u64,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct GradientDelta {
268 pub values: Vec<f32>,
271 pub is_full: bool,
273 pub round: u64,
275}
276
277impl GradientDelta {
278 pub fn compression_ratio(&self, original_len: usize) -> f64 {
288 if self.is_full || self.values.is_empty() || original_len == 0 {
289 return 1.0;
290 }
291 let mean_abs_delta: f64 =
292 self.values.iter().map(|&v| v.abs() as f64).sum::<f64>() / self.values.len() as f64;
293
294 let max_possible: f64 = self
295 .values
296 .iter()
297 .map(|&v| v.abs() as f64)
298 .fold(0.0_f64, f64::max);
299
300 if max_possible == 0.0 {
301 return 0.0;
302 }
303 mean_abs_delta / max_possible
304 }
305}
306
307pub struct DeltaEncoder {
316 pub previous: Option<Vec<f32>>,
318 pub stats: DeltaStats,
320 round_counter: u64,
322}
323
324impl DeltaEncoder {
325 pub fn new() -> Self {
327 Self {
328 previous: None,
329 stats: DeltaStats::default(),
330 round_counter: 0,
331 }
332 }
333
334 pub fn encode_delta(&mut self, current: &[f32]) -> GradientDelta {
345 let round = self.round_counter;
346 self.round_counter += 1;
347 self.stats.total_encoded += 1;
348
349 match &self.previous {
350 None => {
351 self.stats.total_full_sends += 1;
352 let values = current.to_vec();
353 self.previous = Some(values.clone());
354 GradientDelta {
355 values,
356 is_full: true,
357 round,
358 }
359 }
360 Some(prev) if prev.len() != current.len() => {
361 self.stats.total_full_sends += 1;
363 let values = current.to_vec();
364 self.previous = Some(values.clone());
365 GradientDelta {
366 values,
367 is_full: true,
368 round,
369 }
370 }
371 Some(prev) => {
372 let delta: Vec<f32> = current
373 .iter()
374 .zip(prev.iter())
375 .map(|(&c, &p)| c - p)
376 .collect();
377 self.stats.total_delta_sends += 1;
378 self.previous = Some(current.to_vec());
379 GradientDelta {
380 values: delta,
381 is_full: false,
382 round,
383 }
384 }
385 }
386 }
387
388 pub fn decode_delta(&self, base: &[f32], delta: &GradientDelta) -> Vec<f32> {
396 if delta.is_full {
397 return delta.values.clone();
398 }
399 let len = base.len().min(delta.values.len());
400 let mut result = base.to_vec();
401 result.truncate(len);
402 for (r, &d) in result.iter_mut().zip(delta.values.iter()) {
403 *r += d;
404 }
405 result
406 }
407
408 pub fn reset(&mut self) {
411 self.previous = None;
412 }
413}
414
415impl Default for DeltaEncoder {
416 fn default() -> Self {
417 Self::new()
418 }
419}
420
421#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
430 fn test_sparse_gradient_sparsity_ratio_all_kept() {
431 let sg = SparseGradient {
432 indices: vec![0, 1, 2, 3],
433 values: vec![1.0, 2.0, 3.0, 4.0],
434 original_len: 4,
435 };
436 let ratio = sg.sparsity_ratio();
437 assert!((ratio - 0.0).abs() < 1e-9, "expected 0.0, got {}", ratio);
438 }
439
440 #[test]
441 fn test_sparse_gradient_sparsity_ratio_half() {
442 let sg = SparseGradient {
443 indices: vec![1, 3],
444 values: vec![0.5, 1.5],
445 original_len: 4,
446 };
447 let ratio = sg.sparsity_ratio();
448 assert!((ratio - 0.5).abs() < 1e-9, "expected 0.5, got {}", ratio);
449 }
450
451 #[test]
452 fn test_sparse_gradient_to_dense_basic() {
453 let sg = SparseGradient {
454 indices: vec![0, 2, 4],
455 values: vec![1.0, 3.0, 5.0],
456 original_len: 6,
457 };
458 let dense = sg.to_dense();
459 assert_eq!(dense, vec![1.0, 0.0, 3.0, 0.0, 5.0, 0.0]);
460 }
461
462 #[test]
463 fn test_sparse_gradient_to_dense_empty() {
464 let sg = SparseGradient {
465 indices: vec![],
466 values: vec![],
467 original_len: 5,
468 };
469 let dense = sg.to_dense();
470 assert_eq!(dense, vec![0.0; 5]);
471 }
472
473 #[test]
476 fn test_sparsifier_top_k_keeps_largest() {
477 let config = SparsityConfig {
478 top_k: Some(2),
479 threshold: None,
480 accumulate_residuals: false,
481 };
482 let mut sparsifier = GradientSparsifier::new(config, 5);
483 let gradient = vec![0.1_f32, 5.0, 0.2, 8.0, 0.3];
484 let sparse = sparsifier.sparsify(&gradient);
485
486 assert_eq!(sparse.indices.len(), 2);
488 assert!(sparse.values.contains(&8.0), "8.0 must be kept");
489 assert!(sparse.values.contains(&5.0), "5.0 must be kept");
490 }
491
492 #[test]
493 fn test_sparsifier_top_k_respects_absolute_value() {
494 let config = SparsityConfig {
495 top_k: Some(2),
496 threshold: None,
497 accumulate_residuals: false,
498 };
499 let mut sparsifier = GradientSparsifier::new(config, 4);
500 let gradient = vec![1.0_f32, -9.0, 7.0, 0.5];
502 let sparse = sparsifier.sparsify(&gradient);
503
504 assert_eq!(sparse.indices.len(), 2);
505 assert!(sparse.values.contains(&-9.0), "-9.0 must be kept");
506 assert!(sparse.values.contains(&7.0), "7.0 must be kept");
507 }
508
509 #[test]
512 fn test_sparsifier_threshold_drops_small() {
513 let config = SparsityConfig {
514 top_k: None,
515 threshold: Some(1.0),
516 accumulate_residuals: false,
517 };
518 let mut sparsifier = GradientSparsifier::new(config, 5);
519 let gradient = vec![0.1_f32, 5.0, 0.2, 8.0, 0.3];
520 let sparse = sparsifier.sparsify(&gradient);
521
522 assert_eq!(sparse.indices.len(), 2);
524 let dense = sparse.to_dense();
525 assert_eq!(dense[1], 5.0);
526 assert_eq!(dense[3], 8.0);
527 assert_eq!(dense[0], 0.0);
528 assert_eq!(dense[2], 0.0);
529 assert_eq!(dense[4], 0.0);
530 }
531
532 #[test]
533 fn test_sparsifier_threshold_keeps_all_above() {
534 let config = SparsityConfig {
535 top_k: None,
536 threshold: Some(0.0),
537 accumulate_residuals: false,
538 };
539 let mut sparsifier = GradientSparsifier::new(config, 3);
540 let gradient = vec![1.0_f32, 2.0, 3.0];
541 let sparse = sparsifier.sparsify(&gradient);
542
543 assert_eq!(sparse.indices.len(), 3);
545 }
546
547 #[test]
550 fn test_residual_accumulation_carries_forward() {
551 let config = SparsityConfig {
552 top_k: Some(1),
553 threshold: None,
554 accumulate_residuals: true,
555 };
556 let mut sparsifier = GradientSparsifier::new(config, 3);
557
558 let g1 = vec![0.5_f32, 0.4, 0.3];
560 let _s1 = sparsifier.sparsify(&g1);
561
562 assert!((sparsifier.residual[0] - 0.0).abs() < 1e-6);
564 assert!((sparsifier.residual[1] - 0.4).abs() < 1e-6);
565 assert!((sparsifier.residual[2] - 0.3).abs() < 1e-6);
566
567 let g2 = vec![0.1_f32, 0.1, 0.1];
570 let s2 = sparsifier.sparsify(&g2);
571
572 assert_eq!(s2.indices.len(), 1);
573 assert_eq!(s2.indices[0], 1, "index 1 should be kept in round 2");
574 assert!(
576 (s2.values[0] - 0.5).abs() < 1e-5,
577 "expected 0.5, got {}",
578 s2.values[0]
579 );
580 }
581
582 #[test]
583 fn test_residual_reset_clears_buffer() {
584 let config = SparsityConfig {
585 top_k: Some(1),
586 threshold: None,
587 accumulate_residuals: true,
588 };
589 let mut sparsifier = GradientSparsifier::new(config, 3);
590
591 sparsifier.sparsify(&[0.5_f32, 0.4, 0.3]);
592 assert!(sparsifier.residual.iter().any(|&v| v != 0.0));
594
595 sparsifier.reset_residual();
596 assert!(sparsifier.residual.iter().all(|&v| v == 0.0));
597 }
598
599 #[test]
602 fn test_sparsifier_stats_accumulation() {
603 let config = SparsityConfig {
604 top_k: Some(2),
605 threshold: None,
606 accumulate_residuals: false,
607 };
608 let mut sparsifier = GradientSparsifier::new(config, 4);
609
610 sparsifier.sparsify(&[1.0_f32, 2.0, 3.0, 4.0]);
611 sparsifier.sparsify(&[0.1_f32, 0.2, 0.3, 0.4]);
612
613 assert_eq!(sparsifier.stats.total_rounds, 2);
614 assert_eq!(sparsifier.stats.total_elements_kept, 4);
616 assert_eq!(sparsifier.stats.total_elements_dropped, 4);
617 }
618
619 #[test]
622 fn test_delta_encoder_first_call_is_full() {
623 let mut encoder = DeltaEncoder::new();
624 let g = vec![1.0_f32, 2.0, 3.0];
625 let delta = encoder.encode_delta(&g);
626
627 assert!(delta.is_full, "first call must be a full send");
628 assert_eq!(delta.values, g);
629 assert_eq!(delta.round, 0);
630 }
631
632 #[test]
633 fn test_delta_encoder_subsequent_call_is_delta() {
634 let mut encoder = DeltaEncoder::new();
635 let g1 = vec![1.0_f32, 2.0, 3.0];
636 let g2 = vec![1.5_f32, 2.5, 3.5];
637
638 encoder.encode_delta(&g1);
639 let delta = encoder.encode_delta(&g2);
640
641 assert!(!delta.is_full, "second call must be a delta");
642 assert_eq!(delta.values, vec![0.5, 0.5, 0.5]);
643 assert_eq!(delta.round, 1);
644 }
645
646 #[test]
647 fn test_delta_encoder_decode_full() {
648 let encoder = DeltaEncoder::new();
649 let base = vec![0.0_f32; 3];
650 let delta = GradientDelta {
651 values: vec![1.0, 2.0, 3.0],
652 is_full: true,
653 round: 0,
654 };
655 let result = encoder.decode_delta(&base, &delta);
656 assert_eq!(result, vec![1.0, 2.0, 3.0]);
657 }
658
659 #[test]
660 fn test_delta_encoder_decode_reconstructs_correctly() {
661 let mut encoder = DeltaEncoder::new();
662 let g1 = vec![1.0_f32, 2.0, 3.0];
663 let g2 = vec![1.5_f32, 2.0, 4.0];
664
665 let _full = encoder.encode_delta(&g1);
666 let delta = encoder.encode_delta(&g2);
667
668 let encoder2 = DeltaEncoder::new();
670 let reconstructed = encoder2.decode_delta(&g1, &delta);
671 assert_eq!(reconstructed.len(), g2.len());
672 for (r, &expected) in reconstructed.iter().zip(g2.iter()) {
673 assert!(
674 (r - expected).abs() < 1e-5,
675 "mismatch: {} vs {}",
676 r,
677 expected
678 );
679 }
680 }
681
682 #[test]
683 fn test_delta_encoder_reset_forces_full_send() {
684 let mut encoder = DeltaEncoder::new();
685 encoder.encode_delta(&[1.0_f32, 2.0]);
686 encoder.reset();
687
688 let delta = encoder.encode_delta(&[3.0_f32, 4.0]);
689 assert!(delta.is_full, "after reset, send must be full");
690 assert_eq!(delta.values, vec![3.0, 4.0]);
691 }
692
693 #[test]
694 fn test_delta_encoder_stats() {
695 let mut encoder = DeltaEncoder::new();
696 encoder.encode_delta(&[1.0_f32, 2.0]);
697 encoder.encode_delta(&[1.5_f32, 2.5]);
698 encoder.encode_delta(&[2.0_f32, 3.0]);
699
700 assert_eq!(encoder.stats.total_encoded, 3);
701 assert_eq!(encoder.stats.total_full_sends, 1);
702 assert_eq!(encoder.stats.total_delta_sends, 2);
703 }
704
705 #[test]
706 fn test_gradient_delta_compression_ratio_full() {
707 let delta = GradientDelta {
708 values: vec![1.0, 2.0, 3.0],
709 is_full: true,
710 round: 0,
711 };
712 assert!(
713 (delta.compression_ratio(3) - 1.0).abs() < 1e-9,
714 "full gradient compression ratio must be 1.0"
715 );
716 }
717
718 #[test]
719 fn test_gradient_delta_compression_ratio_delta() {
720 let delta = GradientDelta {
722 values: vec![0.1_f32, 0.1, 0.1],
723 is_full: false,
724 round: 1,
725 };
726 let ratio = delta.compression_ratio(3);
727 assert!(
728 (ratio - 1.0).abs() < 1e-5,
729 "uniform delta should give ratio 1.0, got {}",
730 ratio
731 );
732 }
733
734 #[test]
735 fn test_sparsity_ratio_zero_len() {
736 let sg = SparseGradient {
737 indices: vec![],
738 values: vec![],
739 original_len: 0,
740 };
741 assert_eq!(sg.sparsity_ratio(), 0.0);
742 }
743
744 #[test]
745 fn test_sparsifier_top_k_combined_with_threshold() {
746 let config = SparsityConfig {
748 top_k: Some(1),
749 threshold: Some(2.0),
750 accumulate_residuals: false,
751 };
752 let mut sparsifier = GradientSparsifier::new(config, 5);
753 let sparse = sparsifier.sparsify(&[0.1_f32, 5.0, 0.2, 8.0, 0.3]);
755
756 assert_eq!(sparse.indices.len(), 1);
757 assert_eq!(sparse.values[0], 8.0);
758 }
759}