1use anyhow::{anyhow, Result};
2use std::collections::HashMap;
3use trustformers_core::tensor::Tensor;
4
5#[derive(Debug, Clone)]
11pub enum CompressionMethod {
12 TopK { k: usize },
14 RandomK { k: usize },
16 Threshold { threshold: f32 },
18 Quantization { bits: u8 },
20 SignSGD,
22 ErrorFeedback { base_method: Box<CompressionMethod> },
24}
25
26#[derive(Debug)]
27pub struct GradientCompressor {
28 method: CompressionMethod,
29 compression_ratio: f32,
30 error_buffer: HashMap<String, Vec<f32>>, }
32
33#[derive(Debug, Clone)]
44pub struct CompressedGradient {
45 pub indices: Vec<usize>,
47 pub values: Vec<f32>,
49 pub original_size: usize,
51 pub compression_ratio: f32,
53 pub value_bits: u8,
56}
57
58impl CompressedGradient {
59 pub fn payload_bytes(&self) -> usize {
64 let index_bytes = self.indices.len() * std::mem::size_of::<usize>();
65 let value_bits = self.values.len() * self.value_bits as usize;
66 index_bytes + value_bits.div_ceil(8)
67 }
68
69 pub fn dense_bytes(&self) -> usize {
71 self.original_size * std::mem::size_of::<f32>()
72 }
73}
74
75impl GradientCompressor {
76 pub fn new(method: CompressionMethod) -> Self {
77 Self {
78 method,
79 compression_ratio: 0.0,
80 error_buffer: HashMap::new(),
81 }
82 }
83
84 pub fn compress(
85 &mut self,
86 gradients: &HashMap<String, Tensor>,
87 ) -> Result<HashMap<String, CompressedGradient>> {
88 let mut compressed = HashMap::new();
89
90 for (name, gradient) in gradients.iter() {
91 let grad_data = gradient.data()?;
92 let compressed_grad = self.compress_single(&grad_data, name)?;
93 compressed.insert(name.clone(), compressed_grad);
94 }
95
96 Ok(compressed)
97 }
98
99 pub fn decompress(
100 &self,
101 compressed: &HashMap<String, CompressedGradient>,
102 ) -> Result<HashMap<String, Tensor>> {
103 let mut decompressed = HashMap::new();
104
105 for (name, compressed_grad) in compressed.iter() {
106 let grad_data = self.decompress_single(compressed_grad)?;
107 decompressed.insert(name.clone(), Tensor::new(grad_data)?);
108 }
109
110 Ok(decompressed)
111 }
112
113 fn compress_single(
114 &mut self,
115 gradient: &[f32],
116 param_name: &str,
117 ) -> Result<CompressedGradient> {
118 match self.method.clone() {
119 CompressionMethod::TopK { k } => self.compress_topk(gradient, k),
120 CompressionMethod::RandomK { k } => self.compress_randomk(gradient, k),
121 CompressionMethod::Threshold { threshold } => {
122 self.compress_threshold(gradient, threshold)
123 },
124 CompressionMethod::Quantization { bits } => self.compress_quantized(gradient, bits),
125 CompressionMethod::SignSGD => self.compress_signsgd(gradient),
126 CompressionMethod::ErrorFeedback { base_method } => {
127 self.compress_with_error_feedback(gradient, param_name, &base_method)
128 },
129 }
130 }
131
132 fn compress_topk(&self, gradient: &[f32], k: usize) -> Result<CompressedGradient> {
133 let mut indexed_grads: Vec<(usize, f32)> =
134 gradient.iter().enumerate().map(|(i, &val)| (i, val.abs())).collect();
135
136 indexed_grads.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
138
139 let k = k.min(gradient.len());
140 let mut indices = Vec::with_capacity(k);
141 let mut values = Vec::with_capacity(k);
142
143 for i in 0..k {
144 let (idx, _) = indexed_grads[i];
145 indices.push(idx);
146 values.push(gradient[idx]);
147 }
148
149 Ok(CompressedGradient {
150 indices,
151 values,
152 original_size: gradient.len(),
153 compression_ratio: k as f32 / gradient.len() as f32,
154 value_bits: 32,
155 })
156 }
157
158 fn compress_randomk(&self, gradient: &[f32], k: usize) -> Result<CompressedGradient> {
159 use std::collections::HashSet;
160
161 let k = k.min(gradient.len());
162 let mut indices = Vec::with_capacity(k);
163 let mut values = Vec::with_capacity(k);
164 let mut selected_indices = HashSet::new();
165
166 let step = gradient.len() / k.max(1);
168 for i in (0..gradient.len()).step_by(step) {
169 if indices.len() < k && !selected_indices.contains(&i) {
170 indices.push(i);
171 values.push(gradient[i]);
172 selected_indices.insert(i);
173 }
174 }
175
176 Ok(CompressedGradient {
177 indices,
178 values,
179 original_size: gradient.len(),
180 compression_ratio: k as f32 / gradient.len() as f32,
181 value_bits: 32,
182 })
183 }
184
185 fn compress_threshold(&self, gradient: &[f32], threshold: f32) -> Result<CompressedGradient> {
186 let mut indices = Vec::new();
187 let mut values = Vec::new();
188
189 for (i, &val) in gradient.iter().enumerate() {
190 if val.abs() > threshold {
191 indices.push(i);
192 values.push(val);
193 }
194 }
195
196 let compression_ratio = indices.len() as f32 / gradient.len() as f32;
197
198 Ok(CompressedGradient {
199 indices,
200 values,
201 original_size: gradient.len(),
202 compression_ratio,
203 value_bits: 32,
204 })
205 }
206
207 fn compress_quantized(&self, gradient: &[f32], bits: u8) -> Result<CompressedGradient> {
208 let levels = (1 << bits) - 1;
209 let min_val = gradient.iter().fold(f32::INFINITY, |a, &b| a.min(b));
210 let max_val = gradient.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
211 let scale = (max_val - min_val) / levels as f32;
212
213 let mut quantized_values = Vec::with_capacity(gradient.len());
214
215 for &val in gradient.iter() {
216 let quantized = if scale > 0.0 { ((val - min_val) / scale).round() } else { 0.0 };
217 quantized_values.push(min_val + quantized * scale);
218 }
219
220 Ok(CompressedGradient {
221 indices: Vec::new(),
223 values: quantized_values,
224 original_size: gradient.len(),
225 compression_ratio: (bits as f32) / 32.0, value_bits: bits,
227 })
228 }
229
230 fn compress_signsgd(&self, gradient: &[f32]) -> Result<CompressedGradient> {
231 let values: Vec<f32> =
232 gradient.iter().map(|&val| if val >= 0.0 { 1.0 } else { -1.0 }).collect();
233
234 Ok(CompressedGradient {
235 indices: Vec::new(),
237 values,
238 original_size: gradient.len(),
239 compression_ratio: 1.0 / 32.0, value_bits: 1,
241 })
242 }
243
244 fn compress_with_error_feedback(
245 &mut self,
246 gradient: &[f32],
247 param_name: &str,
248 base_method: &Box<CompressionMethod>,
249 ) -> Result<CompressedGradient> {
250 let mut corrected_gradient = gradient.to_vec();
252
253 if let Some(error) = self.error_buffer.get(param_name) {
254 for i in 0..corrected_gradient.len().min(error.len()) {
255 corrected_gradient[i] += error[i];
256 }
257 }
258
259 let mut temp_compressor = GradientCompressor::new((**base_method).clone());
261 let compressed = temp_compressor.compress_single(&corrected_gradient, param_name)?;
262
263 let decompressed = self.decompress_single(&compressed)?;
265 let mut new_error = vec![0.0; corrected_gradient.len()];
266
267 for i in 0..new_error.len() {
268 new_error[i] = corrected_gradient[i] - decompressed.get(i).copied().unwrap_or(0.0);
269 }
270
271 self.error_buffer.insert(param_name.to_string(), new_error);
272
273 Ok(compressed)
274 }
275
276 fn decompress_single(&self, compressed: &CompressedGradient) -> Result<Vec<f32>> {
277 let mut gradient = vec![0.0; compressed.original_size];
278
279 if compressed.indices.is_empty() {
280 if compressed.values.len() != compressed.original_size && !compressed.values.is_empty()
282 {
283 return Err(anyhow!(
284 "dense compressed payload has {} values but the gradient has {}",
285 compressed.values.len(),
286 compressed.original_size
287 ));
288 }
289 gradient[..compressed.values.len()].copy_from_slice(&compressed.values);
290 return Ok(gradient);
291 }
292
293 for (&i, &value) in compressed.indices.iter().zip(compressed.values.iter()) {
294 if i < gradient.len() {
295 gradient[i] = value;
296 }
297 }
298
299 Ok(gradient)
300 }
301
302 pub fn get_compression_ratio(&self) -> f32 {
303 self.compression_ratio
304 }
305
306 pub fn reset_error_buffer(&mut self) {
307 self.error_buffer.clear();
308 }
309}
310
311#[derive(Debug)]
313pub struct CompressedAllReduce {
314 compressor: GradientCompressor,
315 world_size: usize,
316}
317
318impl CompressedAllReduce {
319 pub fn new(compression_method: CompressionMethod, world_size: usize) -> Self {
320 Self {
321 compressor: GradientCompressor::new(compression_method),
322 world_size,
323 }
324 }
325
326 pub fn all_reduce(
345 &mut self,
346 gradients: &HashMap<String, Tensor>,
347 ) -> Result<HashMap<String, Tensor>> {
348 if self.world_size != 1 {
349 return Err(anyhow!(
350 "CompressedAllReduce has no communicator: cannot aggregate across {} ranks. \
351 Use all_reduce_with_peers to supply the other ranks' compressed gradients.",
352 self.world_size
353 ));
354 }
355
356 let compressed = self.compressor.compress(gradients)?;
357 self.compressor.decompress(&compressed)
358 }
359
360 pub fn all_reduce_with_peers(
372 &mut self,
373 gradients: &HashMap<String, Tensor>,
374 peers: &[HashMap<String, CompressedGradient>],
375 ) -> Result<HashMap<String, Tensor>> {
376 if peers.len() + 1 != self.world_size {
377 return Err(anyhow!(
378 "expected {} peer contributions for world_size {}, got {}",
379 self.world_size - 1,
380 self.world_size,
381 peers.len()
382 ));
383 }
384
385 let local = self.compressor.compress(gradients)?;
386 let mut summed: HashMap<String, Vec<f32>> = HashMap::new();
387
388 for contribution in std::iter::once(&local).chain(peers.iter()) {
389 for (name, compressed) in contribution {
390 let dense = self.compressor.decompress_single(compressed)?;
391 match summed.get_mut(name) {
392 Some(accumulator) => {
393 if accumulator.len() != dense.len() {
394 return Err(anyhow!(
395 "rank contributions for '{name}' disagree on length: {} vs {}",
396 accumulator.len(),
397 dense.len()
398 ));
399 }
400 for (slot, value) in accumulator.iter_mut().zip(dense.iter()) {
401 *slot += value;
402 }
403 },
404 None => {
405 summed.insert(name.clone(), dense);
406 },
407 }
408 }
409 }
410
411 let divisor = self.world_size as f32;
412 let mut result = HashMap::new();
413 for (name, mut values) in summed {
414 for value in values.iter_mut() {
415 *value /= divisor;
416 }
417 let shape = gradients
418 .get(&name)
419 .map(|t| t.shape().to_vec())
420 .unwrap_or_else(|| vec![values.len()]);
421 result.insert(name, Tensor::from_vec(values, &shape)?);
422 }
423
424 Ok(result)
425 }
426
427 pub fn compress_for_transmission(
433 &mut self,
434 gradients: &HashMap<String, Tensor>,
435 ) -> Result<HashMap<String, CompressedGradient>> {
436 self.compressor.compress(gradients)
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_topk_compression() {
446 let mut compressor = GradientCompressor::new(CompressionMethod::TopK { k: 3 });
447 let gradient = vec![0.1, 0.8, 0.2, -0.9, 0.3, -0.1];
448
449 let compressed =
450 compressor.compress_single(&gradient, "test").expect("Operation failed in test");
451
452 assert_eq!(compressed.indices.len(), 3);
453 assert_eq!(compressed.values.len(), 3);
454 assert_eq!(compressed.original_size, 6);
455 assert!(compressed.compression_ratio < 1.0);
456
457 assert!(compressed.values.contains(&-0.9));
459 assert!(compressed.values.contains(&0.8));
460 assert!(compressed.values.contains(&0.3));
461 }
462
463 #[test]
464 fn test_threshold_compression() {
465 let mut compressor =
466 GradientCompressor::new(CompressionMethod::Threshold { threshold: 0.5 });
467 let gradient = vec![0.1, 0.8, 0.2, -0.9, 0.3, -0.1];
468
469 let compressed =
470 compressor.compress_single(&gradient, "test").expect("Operation failed in test");
471
472 assert_eq!(compressed.values.len(), 2);
474 assert!(compressed.values.contains(&0.8));
475 assert!(compressed.values.contains(&-0.9));
476 }
477
478 #[test]
479 fn test_signsgd_compression() {
480 let mut compressor = GradientCompressor::new(CompressionMethod::SignSGD);
481 let gradient = vec![0.1, -0.8, 0.2, -0.9, 0.3, -0.1];
482
483 let compressed =
484 compressor.compress_single(&gradient, "test").expect("Operation failed in test");
485
486 assert_eq!(compressed.values.len(), gradient.len());
487 assert_eq!(compressed.compression_ratio, 1.0 / 32.0);
488
489 for &val in &compressed.values {
491 assert!(val == 1.0 || val == -1.0);
492 }
493 }
494
495 #[test]
496 fn test_compression_decompression_roundtrip() {
497 let mut compressor = GradientCompressor::new(CompressionMethod::TopK { k: 3 });
498 let mut gradients = HashMap::new();
499
500 let grad_data = vec![0.1, 0.8, 0.2, -0.9, 0.3, -0.1];
501 gradients.insert(
502 "param1".to_string(),
503 Tensor::new(grad_data.clone()).expect("Failed to create tensor"),
504 );
505
506 let compressed = compressor.compress(&gradients).expect("Operation failed in test");
507 let decompressed = compressor.decompress(&compressed).expect("Operation failed in test");
508
509 let result_data = decompressed
510 .get("param1")
511 .expect("Key not found")
512 .data()
513 .expect("Operation failed in test");
514 assert_eq!(result_data.len(), grad_data.len());
515
516 assert!(result_data.contains(&0.8));
518 assert!(result_data.contains(&-0.9));
519 }
520
521 #[test]
522 fn test_compressed_all_reduce() {
523 let mut all_reduce = CompressedAllReduce::new(CompressionMethod::TopK { k: 2 }, 4);
526
527 let mut gradients = HashMap::new();
528 gradients.insert(
529 "param1".to_string(),
530 Tensor::from_vec(vec![0.4_f32, 0.8, 0.2, -0.6], &[4]).expect("tensor"),
531 );
532
533 assert!(
534 all_reduce.all_reduce(&gradients).is_err(),
535 "aggregating across 4 ranks with no communicator must be an error"
536 );
537 }
538
539 #[test]
540 fn test_single_rank_all_reduce_is_a_round_trip() {
541 let mut all_reduce = CompressedAllReduce::new(CompressionMethod::SignSGD, 1);
542
543 let mut gradients = HashMap::new();
544 gradients.insert(
545 "param1".to_string(),
546 Tensor::from_vec(vec![0.4_f32, -0.8, 0.2, -0.6], &[4]).expect("tensor"),
547 );
548
549 let result = all_reduce.all_reduce(&gradients).expect("single-rank all-reduce");
550 let values = result.get("param1").expect("param1").data_f32().expect("data");
551 assert_eq!(
552 values,
553 vec![1.0, -1.0, 1.0, -1.0],
554 "SignSGD keeps only the sign"
555 );
556 }
557
558 #[test]
559 fn test_all_reduce_with_peers_sums_and_averages() {
560 let mut all_reduce = CompressedAllReduce::new(CompressionMethod::SignSGD, 3);
561
562 let mut local = HashMap::new();
563 local.insert(
564 "w".to_string(),
565 Tensor::from_vec(vec![1.0_f32, 1.0], &[2]).expect("tensor"),
566 );
567
568 let mut peer = GradientCompressor::new(CompressionMethod::SignSGD);
570 let mut peer_gradients = HashMap::new();
571 peer_gradients.insert(
572 "w".to_string(),
573 Tensor::from_vec(vec![1.0_f32, -1.0], &[2]).expect("tensor"),
574 );
575 let peer_payload = peer.compress(&peer_gradients).expect("compress");
576
577 let result = all_reduce
578 .all_reduce_with_peers(&local, &[peer_payload.clone(), peer_payload])
579 .expect("all reduce");
580 let values = result.get("w").expect("w").data_f32().expect("data");
581
582 assert!((values[0] - 1.0).abs() < 1e-6, "{}", values[0]);
584 assert!((values[1] + 1.0 / 3.0).abs() < 1e-6, "{}", values[1]);
585 }
586
587 #[test]
588 fn test_payload_bytes_reflects_the_method() {
589 let mut compressor = GradientCompressor::new(CompressionMethod::SignSGD);
590 let gradient = vec![0.1_f32; 64];
591 let compressed = compressor.compress_single(&gradient, "w").expect("compress");
592
593 assert!(compressed.indices.is_empty());
595 assert_eq!(compressed.payload_bytes(), 8);
596 assert_eq!(compressed.dense_bytes(), 256);
597
598 let mut sparse = GradientCompressor::new(CompressionMethod::TopK { k: 4 });
599 let compressed = sparse.compress_single(&gradient, "w").expect("compress");
600 assert_eq!(compressed.payload_bytes(), 4 * 8 + 4 * 4);
602 }
603
604 #[test]
605 fn test_dense_round_trip_preserves_positions() {
606 let mut compressor = GradientCompressor::new(CompressionMethod::Quantization { bits: 8 });
607 let gradient = vec![-1.0_f32, -0.5, 0.0, 0.5, 1.0];
608 let compressed = compressor.compress_single(&gradient, "w").expect("compress");
609 assert!(
610 compressed.indices.is_empty(),
611 "a dense method needs no index list"
612 );
613
614 let restored = compressor.decompress_single(&compressed).expect("decompress");
615 assert_eq!(restored.len(), gradient.len());
616 for (a, b) in restored.iter().zip(gradient.iter()) {
617 assert!((a - b).abs() < 0.02, "8-bit round trip: {a} vs {b}");
618 }
619 }
620}