1use crate::error::{CoreError, CoreResult};
8
9#[non_exhaustive]
15#[derive(Debug, Clone)]
16pub struct TopKConfig {
17 pub k_fraction: f64,
19 pub use_error_feedback: bool,
22}
23
24impl Default for TopKConfig {
25 fn default() -> Self {
26 TopKConfig {
27 k_fraction: 0.01,
28 use_error_feedback: true,
29 }
30 }
31}
32
33pub struct TopKCompressor {
39 config: TopKConfig,
40 error_feedback: Vec<f64>,
43}
44
45impl TopKCompressor {
46 pub fn new(n_params: usize, config: TopKConfig) -> Self {
48 TopKCompressor {
49 config,
50 error_feedback: vec![0.0; n_params],
51 }
52 }
53
54 pub fn compress(&mut self, gradient: &[f64]) -> CoreResult<(Vec<usize>, Vec<f64>)> {
61 if gradient.is_empty() {
62 return Ok((vec![], vec![]));
63 }
64 let n = gradient.len();
65 if n != self.error_feedback.len() {
66 return Err(CoreError::ShapeError(crate::error::ErrorContext::new(
67 format!(
68 "TopKCompressor: gradient len {} != initialised len {}",
69 n,
70 self.error_feedback.len()
71 ),
72 )));
73 }
74 let mut g: Vec<f64> = if self.config.use_error_feedback {
76 gradient
77 .iter()
78 .zip(self.error_feedback.iter())
79 .map(|(a, b)| a + b)
80 .collect()
81 } else {
82 gradient.to_vec()
83 };
84
85 let k = ((n as f64 * self.config.k_fraction).ceil() as usize)
86 .max(1)
87 .min(n);
88
89 let mut order: Vec<usize> = (0..n).collect();
91 order.sort_unstable_by(|&a, &b| {
92 g[b].abs()
93 .partial_cmp(&g[a].abs())
94 .unwrap_or(std::cmp::Ordering::Equal)
95 });
96
97 let top_k: Vec<usize> = order[..k].to_vec();
98
99 let mut indices: Vec<usize> = top_k.clone();
101 indices.sort_unstable();
102 let values: Vec<f64> = indices.iter().map(|&i| g[i]).collect();
103
104 if self.config.use_error_feedback {
106 for &i in &indices {
107 g[i] = 0.0;
108 }
109 self.error_feedback = g; }
111
112 Ok((indices, values))
113 }
114
115 pub fn decompress(indices: &[usize], values: &[f64], n_total: usize) -> CoreResult<Vec<f64>> {
117 if indices.len() != values.len() {
118 return Err(CoreError::InvalidArgument(crate::error::ErrorContext::new(
119 "decompress: indices and values length mismatch",
120 )));
121 }
122 let mut out = vec![0.0f64; n_total];
123 for (&i, &v) in indices.iter().zip(values.iter()) {
124 if i >= n_total {
125 return Err(CoreError::InvalidArgument(crate::error::ErrorContext::new(
126 format!(
127 "decompress: index {} out of bounds for n_total {}",
128 i, n_total
129 ),
130 )));
131 }
132 out[i] = v;
133 }
134 Ok(out)
135 }
136
137 pub fn compression_ratio(&self, n_total: usize) -> f64 {
139 let k = ((n_total as f64 * self.config.k_fraction).ceil() as usize)
140 .max(1)
141 .min(n_total);
142 n_total as f64 / k as f64
143 }
144}
145
146pub struct RandomKCompressor {
155 k_fraction: f64,
157}
158
159impl RandomKCompressor {
160 pub fn new(k_fraction: f64) -> Self {
162 RandomKCompressor { k_fraction }
163 }
164
165 pub fn compress(&self, gradient: &[f64], seed: u64) -> CoreResult<(Vec<usize>, Vec<f64>)> {
169 if gradient.is_empty() {
170 return Ok((vec![], vec![]));
171 }
172 let n = gradient.len();
173 let k = ((n as f64 * self.k_fraction).ceil() as usize).max(1).min(n);
174
175 let mut indices: Vec<usize> = (0..n).collect();
178 let mut rng_state = seed.wrapping_add(1);
179 let lcg_a: u64 = 6364136223846793005;
180 let lcg_c: u64 = 1442695040888963407;
181
182 for i in 0..k {
183 rng_state = rng_state.wrapping_mul(lcg_a).wrapping_add(lcg_c);
184 let j = (rng_state >> 33) as usize % (n - i) + i;
185 indices.swap(i, j);
186 }
187
188 let mut selected: Vec<usize> = indices[..k].to_vec();
189 selected.sort_unstable();
190 let values: Vec<f64> = selected.iter().map(|&i| gradient[i]).collect();
191
192 Ok((selected, values))
193 }
194
195 pub fn decompress(indices: &[usize], values: &[f64], n_total: usize) -> CoreResult<Vec<f64>> {
197 if indices.len() != values.len() {
198 return Err(CoreError::InvalidArgument(crate::error::ErrorContext::new(
199 "decompress: indices and values length mismatch",
200 )));
201 }
202 let mut out = vec![0.0f64; n_total];
203 for (&i, &v) in indices.iter().zip(values.iter()) {
204 if i >= n_total {
205 return Err(CoreError::InvalidArgument(crate::error::ErrorContext::new(
206 format!(
207 "decompress: index {} out of bounds for n_total {}",
208 i, n_total
209 ),
210 )));
211 }
212 out[i] = v;
213 }
214 Ok(out)
215 }
216}
217
218pub struct OneBitQuantizer;
226
227impl OneBitQuantizer {
228 pub fn quantize(gradient: &[f64]) -> CoreResult<(Vec<u64>, f64)> {
234 if gradient.is_empty() {
235 return Ok((vec![], 0.0));
236 }
237 let scale = gradient.iter().map(|x| x.abs()).sum::<f64>() / gradient.len() as f64;
238 let n_words = gradient.len().div_ceil(64);
239 let mut bits = vec![0u64; n_words];
240 for (i, &v) in gradient.iter().enumerate() {
241 if v >= 0.0 {
242 bits[i / 64] |= 1u64 << (i % 64);
243 }
244 }
245 Ok((bits, scale))
246 }
247
248 pub fn dequantize(bits: &[u64], scale: f64, n: usize) -> Vec<f64> {
253 (0..n)
254 .map(|i| {
255 if (bits[i / 64] >> (i % 64)) & 1 == 1 {
256 scale
257 } else {
258 -scale
259 }
260 })
261 .collect()
262 }
263
264 pub fn quantization_error(original: &[f64], quantized: &[f64]) -> f64 {
266 if original.is_empty() {
267 return 0.0;
268 }
269 let len = original.len().min(quantized.len());
270 let total: f64 = original[..len]
271 .iter()
272 .zip(quantized[..len].iter())
273 .map(|(a, b)| (a - b).abs())
274 .sum();
275 total / len as f64
276 }
277}
278
279#[non_exhaustive]
285#[derive(Debug, Clone)]
286pub struct PowerSgdConfig {
287 pub rank: usize,
289 pub n_power_iter: usize,
291 pub reuse_momentum: bool,
293}
294
295impl Default for PowerSgdConfig {
296 fn default() -> Self {
297 PowerSgdConfig {
298 rank: 4,
299 n_power_iter: 1,
300 reuse_momentum: true,
301 }
302 }
303}
304
305pub fn low_rank_compress(
320 gradient_matrix: &[Vec<f64>],
321 config: &PowerSgdConfig,
322) -> CoreResult<(Vec<Vec<f64>>, Vec<Vec<f64>>)> {
323 let m = gradient_matrix.len();
324 if m == 0 {
325 return Ok((vec![], vec![]));
326 }
327 let n = gradient_matrix[0].len();
328 if n == 0 {
329 return Ok((vec![vec![]; m], vec![]));
330 }
331 let r = config.rank.min(m.min(n));
332 if r == 0 {
333 return Err(CoreError::InvalidArgument(crate::error::ErrorContext::new(
334 "low_rank_compress: rank must be >= 1",
335 )));
336 }
337
338 let mut q = vec![vec![0.0f64; r]; n];
340 let mut rng: u64 = 0xDEAD_BEEF_1234_5678;
341 let lcg_a: u64 = 6364136223846793005;
342 let lcg_c: u64 = 1442695040888963407;
343 for row in q.iter_mut() {
344 for x in row.iter_mut() {
345 rng = rng.wrapping_mul(lcg_a).wrapping_add(lcg_c);
346 *x = (rng as i64 as f64) / (i64::MAX as f64);
348 }
349 }
350 orthonormalize_cols(&mut q)?;
351
352 let mut p = vec![vec![0.0f64; r]; m];
361 for _ in 0..config.n_power_iter.max(1) {
362 for i in 0..m {
364 for j in 0..r {
365 p[i][j] = gradient_matrix[i]
366 .iter()
367 .enumerate()
368 .map(|(k, &g)| g * q[k][j])
369 .sum();
370 }
371 }
372 orthonormalize_cols(&mut p)?;
373
374 for k in 0..n {
376 for j in 0..r {
377 q[k][j] = gradient_matrix
378 .iter()
379 .enumerate()
380 .map(|(i, row)| row[k] * p[i][j])
381 .sum();
382 }
383 }
384 orthonormalize_cols(&mut q)?;
386 }
387
388 for i in 0..m {
392 for j in 0..r {
393 p[i][j] = gradient_matrix[i]
394 .iter()
395 .enumerate()
396 .map(|(k, &g)| g * q[k][j])
397 .sum();
398 }
399 }
400
401 Ok((p, q))
402}
403
404pub fn low_rank_decompress(p: &[Vec<f64>], q: &[Vec<f64>]) -> Vec<Vec<f64>> {
408 let m = p.len();
409 if m == 0 {
410 return vec![];
411 }
412 let r = p[0].len();
413 let n = q.len();
414 let mut out = vec![vec![0.0f64; n]; m];
415 for i in 0..m {
416 for k in 0..n {
417 let dot: f64 = (0..r).map(|j| p[i][j] * q[k][j]).sum();
418 out[i][k] = dot;
419 }
420 }
421 out
422}
423
424fn orthonormalize_cols(mat: &mut Vec<Vec<f64>>) -> CoreResult<()> {
437 let rows = mat.len();
438 if rows == 0 {
439 return Ok(());
440 }
441 let cols = mat[0].len();
442 if cols == 0 {
443 return Ok(());
444 }
445
446 for j in 0..cols {
447 for k in 0..j {
450 let dot: f64 = (0..rows).map(|i| mat[i][j] * mat[i][k]).sum();
452 for i in 0..rows {
453 let prev = mat[i][k];
454 mat[i][j] -= dot * prev;
455 }
456 }
457 let norm: f64 = (0..rows).map(|i| mat[i][j] * mat[i][j]).sum::<f64>().sqrt();
459 if norm < 1e-10 {
460 let mut replaced = false;
463 'outer: for candidate in 0..rows {
464 for k in 0..j {
466 if mat[candidate][k].abs() > 0.9 {
468 continue 'outer;
469 }
470 }
471 for i in 0..rows {
472 mat[i][j] = if i == candidate { 1.0 } else { 0.0 };
473 }
474 replaced = true;
475 break;
476 }
477 if !replaced {
478 for i in 0..rows {
480 mat[i][j] = if i == j % rows { 1.0 } else { 0.0 };
481 }
482 }
483 } else {
484 for i in 0..rows {
485 mat[i][j] /= norm;
486 }
487 }
488 }
489 Ok(())
490}
491
492#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
503 fn test_topk_keeps_exactly_k_elements() {
504 let cfg = TopKConfig {
505 k_fraction: 0.25,
506 use_error_feedback: false,
507 };
508 let mut comp = TopKCompressor::new(8, cfg);
509 let grad = vec![0.1, 0.5, 0.3, 0.9, 0.2, 0.8, 0.4, 0.6];
510 let (indices, values) = comp.compress(&grad).expect("compress failed");
511 assert_eq!(indices.len(), 2);
513 assert_eq!(values.len(), 2);
514 assert!(indices.contains(&3));
516 assert!(indices.contains(&5));
517 }
518
519 #[test]
520 fn test_topk_error_feedback_reduces_over_rounds() {
521 let cfg = TopKConfig {
524 k_fraction: 0.5,
525 use_error_feedback: true,
526 };
527 let mut comp = TopKCompressor::new(4, cfg);
528 let grad = vec![1.0, 0.1, 0.1, 0.1];
529 let (_, v1) = comp.compress(&grad).expect("compress round 1 failed");
530 let (_, v2) = comp.compress(&grad).expect("compress round 2 failed");
531 let sum1: f64 = v1.iter().map(|x| x.abs()).sum();
533 let sum2: f64 = v2.iter().map(|x| x.abs()).sum();
534 assert!(sum1 > 0.0);
537 assert!(sum2 > 0.0);
538 }
539
540 #[test]
541 fn test_randomk_correct_size() {
542 let comp = RandomKCompressor::new(0.1);
543 let grad: Vec<f64> = (0..100).map(|i| i as f64).collect();
544 let (indices, values) = comp.compress(&grad, 42).expect("compress failed");
545 assert_eq!(indices.len(), 10);
547 assert_eq!(values.len(), 10);
548 let mut sorted = indices.clone();
550 sorted.dedup();
551 assert_eq!(sorted.len(), indices.len());
552 }
553
554 #[test]
555 fn test_1bit_quantize_dequantize_preserves_sign() {
556 let gradient = vec![-3.0, 1.5, -0.5, 2.0, -0.1, 0.8];
557 let (bits, scale) = OneBitQuantizer::quantize(&gradient).expect("quantize failed");
558 let dequantized = OneBitQuantizer::dequantize(&bits, scale, gradient.len());
559 for (orig, deq) in gradient.iter().zip(dequantized.iter()) {
560 let same_sign = (orig >= &0.0 && deq >= &0.0) || (orig < &0.0 && deq < &0.0);
562 assert!(same_sign, "sign mismatch: orig={} deq={}", orig, deq);
563 }
564 }
565
566 #[test]
567 fn test_low_rank_compress_decompress_close_for_full_rank() {
568 let m = 4;
573 let n = 4;
574 let u1 = [1.0, 2.0, 3.0, 4.0];
576 let v1 = [5.0, -1.0, 2.0, 0.5];
577 let u2 = [0.5, -1.0, 1.5, -2.0];
578 let v2 = [1.0, 3.0, -2.0, 4.0];
579 let grad: Vec<Vec<f64>> = (0..m)
580 .map(|i| (0..n).map(|j| u1[i] * v1[j] + u2[i] * v2[j]).collect())
581 .collect();
582 let cfg = PowerSgdConfig {
583 rank: 2, n_power_iter: 10, reuse_momentum: false,
586 };
587 let (p, q) = low_rank_compress(&grad, &cfg).expect("compress failed");
588 let approx = low_rank_decompress(&p, &q);
589 let mut max_err = 0.0f64;
590 for i in 0..m {
591 for j in 0..n {
592 let err = (grad[i][j] - approx[i][j]).abs();
593 max_err = max_err.max(err);
594 }
595 }
596 assert!(max_err < 1e-6, "max reconstruction error = {}", max_err);
597 }
598
599 #[test]
600 fn test_powersgd_config_defaults() {
601 let cfg = PowerSgdConfig::default();
602 assert_eq!(cfg.rank, 4);
603 assert_eq!(cfg.n_power_iter, 1);
604 assert!(cfg.reuse_momentum);
605 }
606
607 #[test]
608 fn test_compression_ratio_computation() {
609 let cfg = TopKConfig {
610 k_fraction: 0.01,
611 use_error_feedback: true,
612 };
613 let comp = TopKCompressor::new(1000, cfg);
614 let ratio = comp.compression_ratio(1000);
616 assert!((ratio - 100.0).abs() < 1e-9);
617 }
618
619 #[test]
620 fn test_empty_gradient_handling() {
621 let cfg = TopKConfig::default();
622 let mut comp = TopKCompressor::new(0, cfg);
623 let (idx, val) = comp.compress(&[]).expect("compress empty failed");
624 assert!(idx.is_empty());
625 assert!(val.is_empty());
626
627 let comp2 = RandomKCompressor::new(0.1);
628 let (idx2, val2) = comp2.compress(&[], 0).expect("compress empty failed");
629 assert!(idx2.is_empty());
630 assert!(val2.is_empty());
631
632 let (bits, scale) = OneBitQuantizer::quantize(&[]).expect("quantize empty failed");
633 assert!(bits.is_empty());
634 assert_eq!(scale, 0.0);
635 }
636}