Skip to main content

optirs_core/distributed/
compression.rs

1use crate::error::{OptimError, Result};
2use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
3use scirs2_core::numeric::Float;
4use scirs2_core::random::thread_rng;
5use std::fmt::Debug;
6
7/// Gradient compression strategies for communication optimization
8#[derive(Debug, Clone, PartialEq)]
9pub enum CompressionStrategy {
10    /// No compression
11    None,
12    /// Top-K sparsification (keep only top K largest gradients)
13    TopK {
14        /// Number of top gradients to keep
15        k: usize,
16    },
17    /// Random-K sparsification (keep K random gradients)
18    RandomK {
19        /// Number of random gradients to keep
20        k: usize,
21    },
22    /// Threshold-based sparsification (keep gradients above threshold)
23    Threshold {
24        /// Threshold value for gradient magnitude
25        threshold: f64,
26    },
27    /// Quantization to fewer bits
28    Quantization {
29        /// Number of bits for quantization
30        bits: u8,
31    },
32    /// Error feedback compression (maintain error state)
33    ErrorFeedback {
34        /// Base compression strategy to apply
35        base_strategy: Box<CompressionStrategy>,
36        /// Whether to enable error compensation
37        error_compensation: bool,
38    },
39    /// Gradient clipping before compression
40    ClippedCompression {
41        /// Base compression strategy to apply after clipping
42        base_strategy: Box<CompressionStrategy>,
43        /// Value to clip gradients to
44        clip_value: f64,
45    },
46}
47
48/// Read a little-endian `f64` out of a byte slice, returning an honest error
49/// instead of panicking on a truncated/corrupted buffer (e.g. from a
50/// tampered or short network payload).
51fn read_f64_le(bytes: &[u8]) -> Result<f64> {
52    let arr: [u8; 8] = bytes.try_into().map_err(|_| {
53        OptimError::InvalidConfig(
54            "corrupted compressed data: expected 8 bytes for an f64".to_string(),
55        )
56    })?;
57    Ok(f64::from_le_bytes(arr))
58}
59
60/// Read a little-endian `u32` out of a byte slice, returning an honest error
61/// instead of panicking on a truncated/corrupted buffer.
62fn read_u32_le(bytes: &[u8]) -> Result<u32> {
63    let arr: [u8; 4] = bytes.try_into().map_err(|_| {
64        OptimError::InvalidConfig(
65            "corrupted compressed data: expected 4 bytes for a u32".to_string(),
66        )
67    })?;
68    Ok(u32::from_le_bytes(arr))
69}
70
71/// Read a little-endian `u16` out of a byte slice, returning an honest error
72/// instead of panicking on a truncated/corrupted buffer.
73fn read_u16_le(bytes: &[u8]) -> Result<u16> {
74    let arr: [u8; 2] = bytes.try_into().map_err(|_| {
75        OptimError::InvalidConfig(
76            "corrupted compressed data: expected 2 bytes for a u16".to_string(),
77        )
78    })?;
79    Ok(u16::from_le_bytes(arr))
80}
81
82/// Compressed gradient representation
83#[derive(Debug, Clone)]
84pub struct CompressedGradient<A: Float> {
85    /// Compressed data
86    pub data: Vec<u8>,
87    /// Compression metadata
88    pub metadata: CompressionMetadata<A>,
89    /// Original shape information
90    pub shapes: Vec<Vec<usize>>,
91}
92
93/// Compression metadata
94#[derive(Debug, Clone)]
95pub struct CompressionMetadata<A: Float> {
96    /// Compression strategy used
97    pub strategy: CompressionStrategy,
98    /// Compression ratio achieved
99    pub compression_ratio: f64,
100    /// Number of non-zero elements (for sparse methods)
101    pub nnz_count: usize,
102    /// Quantization scale factors (for quantization methods)
103    pub scale_factors: Vec<A>,
104    /// Additional strategy-specific data
105    pub extra_data: Vec<u8>,
106}
107
108/// Gradient compression engine
109#[derive(Debug)]
110pub struct GradientCompressor<A: Float, D: Dimension> {
111    /// Compression strategy
112    strategy: CompressionStrategy,
113    /// Error feedback state for error compensation
114    error_state: Option<Vec<Array<A, D>>>,
115    /// Compression statistics
116    stats: CompressionStats,
117}
118
119impl<A: Float + ScalarOperand + Debug + Send + Sync, D: Dimension + Send + Sync>
120    GradientCompressor<A, D>
121{
122    /// Create a new gradient compressor
123    pub fn new(strategy: CompressionStrategy) -> Self {
124        Self {
125            strategy,
126            error_state: None,
127            stats: CompressionStats::new(),
128        }
129    }
130
131    /// Initialize error state for error feedback compression
132    pub fn initialize_error_state(&mut self, gradientshapes: &[Array<A, D>]) {
133        self.error_state = Some(
134            gradientshapes
135                .iter()
136                .map(|g| Array::zeros(g.raw_dim()))
137                .collect(),
138        );
139    }
140
141    /// Compress gradients
142    pub fn compress(&mut self, gradients: &[Array<A, D>]) -> Result<CompressedGradient<A>> {
143        // Lazily initialize the error-feedback residual the first time a caller
144        // selects ErrorFeedback with compensation enabled but never called
145        // `initialize_error_state` themselves. Previously this silently
146        // degraded to plain (uncompensated) compression with no signal.
147        let needs_lazy_init = matches!(
148            &self.strategy,
149            CompressionStrategy::ErrorFeedback {
150                error_compensation: true,
151                ..
152            }
153        ) && self.error_state.is_none();
154        if needs_lazy_init {
155            self.initialize_error_state(gradients);
156        }
157
158        // Apply error feedback if enabled: working = gradient + accumulated residual
159        let mut working_gradients: Vec<Array<A, D>> =
160            if let Some(ref error_state) = self.error_state {
161                gradients
162                    .iter()
163                    .zip(error_state.iter())
164                    .map(|(grad, error)| grad + error)
165                    .collect()
166            } else {
167                gradients.to_vec()
168            };
169
170        let (compressed_data, metadata) = match &self.strategy {
171            CompressionStrategy::None => self.compress_none(&working_gradients)?,
172            CompressionStrategy::TopK { k } => self.compress_topk(&working_gradients, *k)?,
173            CompressionStrategy::RandomK { k } => self.compress_randomk(&working_gradients, *k)?,
174            CompressionStrategy::Threshold { threshold } => self.compress_threshold(
175                &working_gradients,
176                A::from(*threshold).ok_or_else(|| {
177                    OptimError::InvalidConfig(format!(
178                        "threshold {threshold} could not be represented in the parameter type"
179                    ))
180                })?,
181            )?,
182            CompressionStrategy::Quantization { bits } => {
183                self.compress_quantization(&working_gradients, *bits)?
184            }
185            CompressionStrategy::ErrorFeedback {
186                base_strategy,
187                error_compensation,
188            } => {
189                // Recursively apply base strategy
190                let mut temp_compressor = GradientCompressor::new((**base_strategy).clone());
191                let compressed = temp_compressor.compress(&working_gradients)?;
192
193                // Honour the `error_compensation` flag: when disabled, no
194                // residual should be tracked or applied on future calls.
195                if *error_compensation {
196                    // EF-SGD residual: e_new = working - decompress(compress(working)).
197                    // Using the raw input (`original`) here instead of `working`
198                    // (which already folds in the previous residual `e_old`) would
199                    // pin the residual at a constant and coordinates that never
200                    // individually clear the Top-K threshold would never be sent.
201                    let decompressed = temp_compressor.decompress(&compressed)?;
202                    if let Some(ref mut error_state) = self.error_state {
203                        for ((working, decompressed), error) in working_gradients
204                            .iter()
205                            .zip(decompressed.iter())
206                            .zip(error_state.iter_mut())
207                        {
208                            *error = working - decompressed;
209                        }
210                    }
211                }
212
213                (compressed.data, compressed.metadata)
214            }
215            CompressionStrategy::ClippedCompression {
216                base_strategy,
217                clip_value,
218            } => {
219                // Clip gradients first
220                let clip_val = A::from(*clip_value).ok_or_else(|| {
221                    OptimError::InvalidConfig(format!(
222                        "clip value {clip_value} could not be represented in the parameter type"
223                    ))
224                })?;
225                for grad in &mut working_gradients {
226                    grad.mapv_inplace(|x| {
227                        if x > clip_val {
228                            clip_val
229                        } else if x < -clip_val {
230                            -clip_val
231                        } else {
232                            x
233                        }
234                    });
235                }
236
237                // Apply base compression strategy
238                let mut temp_compressor = GradientCompressor::new((**base_strategy).clone());
239                let compressed = temp_compressor.compress(&working_gradients)?;
240                (compressed.data, compressed.metadata)
241            }
242        };
243
244        // Collect shape information
245        let shapes = gradients.iter().map(|g| g.shape().to_vec()).collect();
246
247        let result = CompressedGradient {
248            data: compressed_data,
249            metadata,
250            shapes,
251        };
252
253        // Update statistics
254        let original_size = self.calculate_size(gradients);
255        let compressed_size = result.data.len();
256        self.stats
257            .record_compression(original_size, compressed_size);
258
259        Ok(result)
260    }
261
262    /// Decompress gradients
263    pub fn decompress(&self, compressed: &CompressedGradient<A>) -> Result<Vec<Array<A, D>>> {
264        match &compressed.metadata.strategy {
265            CompressionStrategy::None => self.decompress_none(compressed),
266            CompressionStrategy::TopK { .. } => self.decompress_sparse(compressed),
267            CompressionStrategy::RandomK { .. } => self.decompress_sparse(compressed),
268            CompressionStrategy::Threshold { .. } => self.decompress_sparse(compressed),
269            CompressionStrategy::Quantization { bits } => {
270                self.decompress_quantization(compressed, *bits)
271            }
272            CompressionStrategy::ErrorFeedback { base_strategy, .. } => {
273                let temp_compressor = GradientCompressor::new((**base_strategy).clone());
274                temp_compressor.decompress(compressed)
275            }
276            CompressionStrategy::ClippedCompression { base_strategy, .. } => {
277                let temp_compressor = GradientCompressor::new((**base_strategy).clone());
278                temp_compressor.decompress(compressed)
279            }
280        }
281    }
282
283    /// Compress with no compression (passthrough)
284    fn compress_none(
285        &self,
286        gradients: &[Array<A, D>],
287    ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
288        let mut data = Vec::new();
289
290        // Simple serialization: store all gradient values sequentially
291        for grad in gradients {
292            for &val in grad.iter() {
293                let bits = val.to_f64().ok_or_else(|| {
294                    OptimError::InvalidConfig(
295                        "gradient value could not be converted to f64 for serialization"
296                            .to_string(),
297                    )
298                })?;
299                data.extend_from_slice(&bits.to_le_bytes());
300            }
301        }
302
303        let metadata = CompressionMetadata {
304            strategy: CompressionStrategy::None,
305            compression_ratio: 1.0,
306            nnz_count: gradients.iter().map(|g| g.len()).sum(),
307            scale_factors: Vec::new(),
308            extra_data: Vec::new(),
309        };
310
311        Ok((data, metadata))
312    }
313
314    /// Compress using Top-K sparsification
315    fn compress_topk(
316        &self,
317        gradients: &[Array<A, D>],
318        k: usize,
319    ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
320        let mut indices = Vec::new();
321        let mut values = Vec::new();
322        let mut total_elements = 0;
323
324        for (grad_idx, grad) in gradients.iter().enumerate() {
325            total_elements += grad.len();
326
327            // Collect (signed value, index) pairs once -- capturing the signed
328            // value up front avoids an O(n) `.nth()` re-lookup per selected
329            // element below (previously O(n*k) per gradient).
330            let mut value_indices: Vec<(A, usize)> =
331                grad.iter().enumerate().map(|(i, &val)| (val, i)).collect();
332
333            // Sort by absolute value (descending). NaN gradients (e.g. from a
334            // diverged run) must not panic a comparator run by `sort_by` --
335            // treat incomparable pairs as equal rather than unwrapping.
336            value_indices.sort_by(|a, b| {
337                b.0.abs()
338                    .partial_cmp(&a.0.abs())
339                    .unwrap_or(std::cmp::Ordering::Equal)
340            });
341
342            // Take top k elements
343            let k_local = k.min(value_indices.len());
344            for &(val, orig_idx) in value_indices.iter().take(k_local) {
345                indices.push((grad_idx as u32, orig_idx as u32));
346                values.push(val);
347            }
348        }
349
350        // Serialize sparse representation
351        let mut data = Vec::new();
352
353        // Store number of sparse elements
354        data.extend_from_slice(&(indices.len() as u32).to_le_bytes());
355
356        // Store indices and values
357        for ((grad_idx, elem_idx), value) in indices.iter().zip(values.iter()) {
358            data.extend_from_slice(&grad_idx.to_le_bytes());
359            data.extend_from_slice(&elem_idx.to_le_bytes());
360            let bits = value.to_f64().ok_or_else(|| {
361                OptimError::InvalidConfig(
362                    "gradient value could not be converted to f64 for serialization".to_string(),
363                )
364            })?;
365            data.extend_from_slice(&bits.to_le_bytes());
366        }
367
368        let metadata = CompressionMetadata {
369            strategy: CompressionStrategy::TopK { k },
370            compression_ratio: data.len() as f64
371                / (total_elements.max(1) * std::mem::size_of::<A>()) as f64,
372            nnz_count: indices.len(),
373            scale_factors: Vec::new(),
374            extra_data: Vec::new(),
375        };
376
377        Ok((data, metadata))
378    }
379
380    /// Compress using Random-K sparsification
381    fn compress_randomk(
382        &self,
383        gradients: &[Array<A, D>],
384        k: usize,
385    ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
386        let mut indices = Vec::new();
387        let mut values = Vec::new();
388        let mut total_elements = 0;
389        let mut rng = thread_rng();
390
391        for (grad_idx, grad) in gradients.iter().enumerate() {
392            total_elements += grad.len();
393
394            // Random sampling of k indices via a genuine partial Fisher-Yates
395            // shuffle. The previous implementation picked a swap index that
396            // was a pure function of (grad_idx, i) -- every node selected the
397            // identical index set every round (losing Random-K's unbiased-
398            // estimator property), and it divided by `grad.len() - i`, which
399            // is unreachable-but-fragile when i approaches grad.len().
400            let k_local = k.min(grad.len());
401            let mut selected_indices: Vec<usize> = (0..grad.len()).collect();
402            for i in 0..k_local {
403                let remaining = grad.len() - i;
404                let swap_idx = i + rng.gen_range(0..remaining);
405                selected_indices.swap(i, swap_idx);
406            }
407
408            // Flatten once so per-element access below is O(1) instead of the
409            // previous O(n) `.nth()` walk (O(n*k) total per gradient).
410            let flat: Vec<A> = grad.iter().copied().collect();
411            for &idx in selected_indices.iter().take(k_local) {
412                indices.push((grad_idx as u32, idx as u32));
413                values.push(flat[idx]);
414            }
415        }
416
417        // Serialize sparse representation (same format as Top-K)
418        let mut data = Vec::new();
419        data.extend_from_slice(&(indices.len() as u32).to_le_bytes());
420
421        for ((grad_idx, elem_idx), value) in indices.iter().zip(values.iter()) {
422            data.extend_from_slice(&grad_idx.to_le_bytes());
423            data.extend_from_slice(&elem_idx.to_le_bytes());
424            let bits = value.to_f64().ok_or_else(|| {
425                OptimError::InvalidConfig(
426                    "gradient value could not be converted to f64 for serialization".to_string(),
427                )
428            })?;
429            data.extend_from_slice(&bits.to_le_bytes());
430        }
431
432        let metadata = CompressionMetadata {
433            strategy: CompressionStrategy::RandomK { k },
434            compression_ratio: data.len() as f64
435                / (total_elements.max(1) * std::mem::size_of::<A>()) as f64,
436            nnz_count: indices.len(),
437            scale_factors: Vec::new(),
438            extra_data: Vec::new(),
439        };
440
441        Ok((data, metadata))
442    }
443
444    /// Compress using threshold-based sparsification
445    fn compress_threshold(
446        &self,
447        gradients: &[Array<A, D>],
448        threshold: A,
449    ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
450        let mut indices = Vec::new();
451        let mut values = Vec::new();
452        let mut total_elements = 0;
453
454        for (grad_idx, grad) in gradients.iter().enumerate() {
455            total_elements += grad.len();
456
457            for (elem_idx, &val) in grad.iter().enumerate() {
458                if val.abs() > threshold {
459                    indices.push((grad_idx as u32, elem_idx as u32));
460                    values.push(val);
461                }
462            }
463        }
464
465        // Serialize sparse representation
466        let mut data = Vec::new();
467        data.extend_from_slice(&(indices.len() as u32).to_le_bytes());
468
469        for ((grad_idx, elem_idx), value) in indices.iter().zip(values.iter()) {
470            data.extend_from_slice(&grad_idx.to_le_bytes());
471            data.extend_from_slice(&elem_idx.to_le_bytes());
472            let bits = value.to_f64().ok_or_else(|| {
473                OptimError::InvalidConfig(
474                    "gradient value could not be converted to f64 for serialization".to_string(),
475                )
476            })?;
477            data.extend_from_slice(&bits.to_le_bytes());
478        }
479
480        let metadata = CompressionMetadata {
481            strategy: CompressionStrategy::Threshold {
482                threshold: threshold.to_f64().ok_or_else(|| {
483                    OptimError::InvalidConfig(
484                        "threshold could not be converted to f64 for metadata".to_string(),
485                    )
486                })?,
487            },
488            compression_ratio: data.len() as f64
489                / (total_elements.max(1) * std::mem::size_of::<A>()) as f64,
490            nnz_count: indices.len(),
491            scale_factors: Vec::new(),
492            extra_data: Vec::new(),
493        };
494
495        Ok((data, metadata))
496    }
497
498    /// Compress using quantization
499    fn compress_quantization(
500        &self,
501        gradients: &[Array<A, D>],
502        bits: u8,
503    ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
504        if bits == 0 || bits > 32 {
505            return Err(OptimError::InvalidConfig(
506                "Quantization bits must be in 1..=32".to_string(),
507            ));
508        }
509
510        let mut data = Vec::new();
511        let mut scale_factors = Vec::new();
512        let levels = (1u64 << bits) - 1;
513        let levels_a = A::from(levels).ok_or_else(|| {
514            OptimError::InvalidConfig(format!(
515                "quantization level count {levels} could not be represented in the parameter type"
516            ))
517        })?;
518
519        for grad in gradients {
520            // Reject non-finite gradients up front: NaN/inf would otherwise
521            // corrupt the min/max fold below (whose behaviour on NaN is
522            // unspecified) and could drive `normalized` negative or NaN,
523            // which used to panic in `to_u64().expect(...)`.
524            if grad.iter().any(|v| !v.is_finite()) {
525                return Err(OptimError::InvalidConfig(
526                    "gradient contains non-finite (NaN/inf) values; cannot quantize".to_string(),
527                ));
528            }
529
530            // Find min and max values for this gradient
531            let min_val = grad.iter().fold(A::infinity(), |acc, &x| acc.min(x));
532            let max_val = grad.iter().fold(A::neg_infinity(), |acc, &x| acc.max(x));
533
534            let range = max_val - min_val;
535            let scale = if range > A::zero() {
536                range / levels_a
537            } else {
538                A::one()
539            };
540
541            scale_factors.push(scale);
542
543            // Quantize each value, clamping into [0, levels] so the u64
544            // conversion below can never fail (previously an unclamped
545            // negative/NaN `normalized` would panic).
546            for &val in grad.iter() {
547                let normalized = ((val - min_val) / scale)
548                    .max(A::zero())
549                    .min(levels_a)
550                    .round();
551                let quantized = normalized.to_u64().unwrap_or(levels).min(levels) as u32;
552
553                // Store quantized value
554                match bits {
555                    1..=8 => data.push(quantized as u8),
556                    9..=16 => data.extend_from_slice(&(quantized as u16).to_le_bytes()),
557                    17..=32 => data.extend_from_slice(&quantized.to_le_bytes()),
558                    _ => unreachable!(),
559                }
560            }
561
562            // Store min value AND scale inline for reconstruction. Carrying
563            // both in the byte stream (rather than trusting that the
564            // separately-returned `scale_factors[grad_idx]` stays aligned by
565            // position) means decompression never depends on a parallel
566            // array matching this stream's gradient order.
567            let min_bits = min_val.to_f64().ok_or_else(|| {
568                OptimError::InvalidConfig(
569                    "min value could not be converted to f64 for serialization".to_string(),
570                )
571            })?;
572            let scale_bits = scale.to_f64().ok_or_else(|| {
573                OptimError::InvalidConfig(
574                    "scale factor could not be converted to f64 for serialization".to_string(),
575                )
576            })?;
577            data.extend_from_slice(&min_bits.to_le_bytes());
578            data.extend_from_slice(&scale_bits.to_le_bytes());
579        }
580
581        let total_elements: usize = gradients.iter().map(|g| g.len()).sum();
582        let metadata = CompressionMetadata {
583            strategy: CompressionStrategy::Quantization { bits },
584            compression_ratio: data.len() as f64
585                / (total_elements.max(1) * std::mem::size_of::<A>()) as f64,
586            nnz_count: total_elements,
587            scale_factors,
588            extra_data: Vec::new(),
589        };
590
591        Ok((data, metadata))
592    }
593
594    /// Decompress uncompressed data
595    fn decompress_none(&self, compressed: &CompressedGradient<A>) -> Result<Vec<Array<A, D>>> {
596        let mut result = Vec::new();
597        let mut data_offset = 0;
598
599        for shape in &compressed.shapes {
600            let num_elements: usize = shape.iter().product();
601            let mut values = Vec::with_capacity(num_elements);
602
603            for _ in 0..num_elements {
604                if data_offset + 8 > compressed.data.len() {
605                    return Err(OptimError::InvalidConfig(
606                        "Insufficient data for decompression".to_string(),
607                    ));
608                }
609
610                let value = read_f64_le(&compressed.data[data_offset..data_offset + 8])?;
611                values.push(A::from(value).ok_or_else(|| {
612                    OptimError::InvalidConfig(
613                        "decompressed value could not be represented in the parameter type"
614                            .to_string(),
615                    )
616                })?);
617                data_offset += 8;
618            }
619
620            // Create a dynamic array first, then convert to the target dimension type
621            let dynamic_array = Array::from_shape_vec(shape.as_slice(), values).map_err(|_| {
622                OptimError::InvalidConfig("Invalid shape for reconstruction".to_string())
623            })?;
624            let array = dynamic_array.into_dimensionality::<D>().map_err(|_| {
625                OptimError::InvalidConfig("Dimension conversion failed".to_string())
626            })?;
627            result.push(array);
628        }
629
630        Ok(result)
631    }
632
633    /// Decompress sparse representation
634    fn decompress_sparse(&self, compressed: &CompressedGradient<A>) -> Result<Vec<Array<A, D>>> {
635        let mut result = Vec::new();
636
637        // Initialize zero arrays
638        for shape in &compressed.shapes {
639            let dynamic_array = Array::zeros(shape.as_slice());
640            let array = dynamic_array.into_dimensionality::<D>().map_err(|_| {
641                OptimError::InvalidConfig("Dimension conversion failed for zero array".to_string())
642            })?;
643            result.push(array);
644        }
645
646        // Read number of sparse elements
647        if compressed.data.len() < 4 {
648            return Err(OptimError::InvalidConfig(
649                "Invalid compressed data format".to_string(),
650            ));
651        }
652
653        let num_elements = read_u32_le(&compressed.data[0..4])? as usize;
654        let mut data_offset = 4;
655
656        // Restore sparse elements
657        for _ in 0..num_elements {
658            if data_offset + 16 > compressed.data.len() {
659                return Err(OptimError::InvalidConfig(
660                    "Insufficient data for sparse decompression".to_string(),
661                ));
662            }
663
664            let grad_idx = read_u32_le(&compressed.data[data_offset..data_offset + 4])? as usize;
665            let elem_idx =
666                read_u32_le(&compressed.data[data_offset + 4..data_offset + 8])? as usize;
667            let value_f64 = read_f64_le(&compressed.data[data_offset + 8..data_offset + 16])?;
668            let value = A::from(value_f64).ok_or_else(|| {
669                OptimError::InvalidConfig(
670                    "decompressed value could not be represented in the parameter type".to_string(),
671                )
672            })?;
673
674            data_offset += 16;
675
676            if grad_idx >= result.len() {
677                return Err(OptimError::InvalidConfig(
678                    "Invalid gradient index in compressed data".to_string(),
679                ));
680            }
681
682            // Write via a flat slice (O(1) indexed access) instead of
683            // `.iter_mut().nth(elem_idx)`, which re-walks from the start of
684            // the array for every restored element.
685            let target = result[grad_idx].as_slice_mut().ok_or_else(|| {
686                OptimError::InvalidConfig(
687                    "target array is not contiguous; cannot write decompressed element".to_string(),
688                )
689            })?;
690            match target.get_mut(elem_idx) {
691                Some(elem) => *elem = value,
692                None => {
693                    return Err(OptimError::InvalidConfig(
694                        "Invalid element index in compressed data".to_string(),
695                    ));
696                }
697            }
698        }
699
700        Ok(result)
701    }
702
703    /// Decompress quantized data
704    fn decompress_quantization(
705        &self,
706        compressed: &CompressedGradient<A>,
707        bits: u8,
708    ) -> Result<Vec<Array<A, D>>> {
709        let mut result = Vec::new();
710        let mut data_offset = 0;
711
712        for shape in compressed.shapes.iter() {
713            let num_elements: usize = shape.iter().product();
714            let mut values = Vec::with_capacity(num_elements);
715
716            // Read quantized values
717            for _ in 0..num_elements {
718                let quantized = match bits {
719                    1..=8 => {
720                        if data_offset >= compressed.data.len() {
721                            return Err(OptimError::InvalidConfig(
722                                "Insufficient quantized data".to_string(),
723                            ));
724                        }
725                        let val = compressed.data[data_offset] as u32;
726                        data_offset += 1;
727                        val
728                    }
729                    9..=16 => {
730                        if data_offset + 2 > compressed.data.len() {
731                            return Err(OptimError::InvalidConfig(
732                                "Insufficient quantized data".to_string(),
733                            ));
734                        }
735                        let val =
736                            read_u16_le(&compressed.data[data_offset..data_offset + 2])? as u32;
737                        data_offset += 2;
738                        val
739                    }
740                    17..=32 => {
741                        if data_offset + 4 > compressed.data.len() {
742                            return Err(OptimError::InvalidConfig(
743                                "Insufficient quantized data".to_string(),
744                            ));
745                        }
746                        let val = read_u32_le(&compressed.data[data_offset..data_offset + 4])?;
747                        data_offset += 4;
748                        val
749                    }
750                    _ => {
751                        return Err(OptimError::InvalidConfig(
752                            "Invalid quantization bits".to_string(),
753                        ))
754                    }
755                };
756
757                values.push(quantized);
758            }
759
760            // Read min value and scale, stored inline by `compress_quantization`
761            // right after each gradient's quantized block. Reading them from
762            // the stream itself (rather than indexing into the separately
763            // carried `metadata.scale_factors` by position) means
764            // reconstruction never depends on that parallel array staying
765            // aligned with this one.
766            if data_offset + 16 > compressed.data.len() {
767                return Err(OptimError::InvalidConfig(
768                    "Missing min value/scale for quantization".to_string(),
769                ));
770            }
771            let min_val_f64 = read_f64_le(&compressed.data[data_offset..data_offset + 8])?;
772            let scale_f64 = read_f64_le(&compressed.data[data_offset + 8..data_offset + 16])?;
773            let min_val = A::from(min_val_f64).ok_or_else(|| {
774                OptimError::InvalidConfig(
775                    "min value could not be represented in the parameter type".to_string(),
776                )
777            })?;
778            let scale = A::from(scale_f64).ok_or_else(|| {
779                OptimError::InvalidConfig(
780                    "scale factor could not be represented in the parameter type".to_string(),
781                )
782            })?;
783            data_offset += 16;
784
785            // Dequantize values
786            let dequantized_values: Vec<A> = values
787                .into_iter()
788                .map(|q| -> Result<A> {
789                    let q_a = A::from(q).ok_or_else(|| {
790                        OptimError::InvalidConfig(
791                            "quantized value could not be represented in the parameter type"
792                                .to_string(),
793                        )
794                    })?;
795                    Ok(min_val + q_a * scale)
796                })
797                .collect::<Result<Vec<A>>>()?;
798
799            let dynamic_array = Array::from_shape_vec(shape.as_slice(), dequantized_values)
800                .map_err(|_| {
801                    OptimError::InvalidConfig(
802                        "Invalid shape for quantized reconstruction".to_string(),
803                    )
804                })?;
805            let array = dynamic_array.into_dimensionality::<D>().map_err(|_| {
806                OptimError::InvalidConfig(
807                    "Dimension conversion failed for quantized array".to_string(),
808                )
809            })?;
810            result.push(array);
811        }
812
813        Ok(result)
814    }
815
816    /// Calculate size of gradients in bytes
817    fn calculate_size(&self, gradients: &[Array<A, D>]) -> usize {
818        gradients
819            .iter()
820            .map(|g| g.len() * std::mem::size_of::<A>())
821            .sum()
822    }
823
824    /// Get compression statistics
825    pub fn stats(&self) -> &CompressionStats {
826        &self.stats
827    }
828
829    /// Reset compression statistics
830    pub fn reset_stats(&mut self) {
831        self.stats = CompressionStats::new();
832    }
833}
834
835/// Compression statistics
836#[derive(Debug, Clone)]
837pub struct CompressionStats {
838    /// Total compressions performed
839    pub compressions_count: usize,
840    /// Total original bytes
841    pub total_original_bytes: usize,
842    /// Total compressed bytes
843    pub total_compressed_bytes: usize,
844    /// Average compression ratio
845    pub average_compression_ratio: f64,
846    /// Best compression ratio achieved
847    pub best_compression_ratio: f64,
848    /// Worst compression ratio achieved
849    pub worst_compression_ratio: f64,
850}
851
852impl CompressionStats {
853    /// Create new compression statistics
854    pub fn new() -> Self {
855        Self {
856            compressions_count: 0,
857            total_original_bytes: 0,
858            total_compressed_bytes: 0,
859            average_compression_ratio: 0.0,
860            best_compression_ratio: f64::INFINITY,
861            worst_compression_ratio: 0.0,
862        }
863    }
864
865    /// Record a compression operation
866    pub fn record_compression(&mut self, original_bytes: usize, compressedbytes: usize) {
867        self.compressions_count += 1;
868        self.total_original_bytes += original_bytes;
869        self.total_compressed_bytes += compressedbytes;
870
871        let ratio = if original_bytes > 0 {
872            compressedbytes as f64 / original_bytes as f64
873        } else {
874            1.0
875        };
876
877        self.best_compression_ratio = self.best_compression_ratio.min(ratio);
878        self.worst_compression_ratio = self.worst_compression_ratio.max(ratio);
879
880        self.average_compression_ratio = if self.total_original_bytes > 0 {
881            self.total_compressed_bytes as f64 / self.total_original_bytes as f64
882        } else {
883            0.0
884        };
885    }
886
887    /// Get overall compression ratio
888    pub fn overall_compression_ratio(&self) -> f64 {
889        self.average_compression_ratio
890    }
891
892    /// Get bandwidth savings (as percentage)
893    pub fn bandwidth_savings(&self) -> f64 {
894        (1.0 - self.average_compression_ratio) * 100.0
895    }
896}
897
898impl Default for CompressionStats {
899    fn default() -> Self {
900        Self::new()
901    }
902}