Skip to main content

ipfrs_tensorlogic/gradient/
tensor.rs

1//! Core gradient tensor types and operations.
2//!
3//! This module provides the fundamental gradient data structures:
4//! - [`SparseGradient`] — sparse representation with index/value pairs
5//! - [`QuantizedGradient`] — int8-quantized representation for compression
6//! - [`LayerGradient`] — enum over dense / sparse / quantized
7//! - [`GradientDelta`] — per-layer gradient bundle linked to a base model CID
8//! - [`GradientCompressor`] — top-k, threshold, random, and quantization compression
9//! - [`GradientAggregator`] — average, weighted-average, and momentum helpers
10//! - [`GradientVerifier`] — shape, finiteness, outlier, clipping utilities
11
12use crate::arrow::{TensorDtype, TensorMetadata};
13use ipfrs_core::Cid;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16
17use super::GradientError;
18
19// ── SparseGradient ─────────────────────────────────────────────────────────
20
21/// Sparse gradient representation
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SparseGradient {
24    /// Indices of non-zero elements (flattened)
25    pub indices: Vec<usize>,
26    /// Non-zero gradient values
27    pub values: Vec<f32>,
28    /// Original tensor shape
29    pub shape: Vec<usize>,
30    /// Metadata
31    pub metadata: TensorMetadata,
32}
33
34impl SparseGradient {
35    /// Create a new sparse gradient
36    pub fn new(indices: Vec<usize>, values: Vec<f32>, shape: Vec<usize>) -> Self {
37        let metadata = TensorMetadata {
38            name: "sparse_gradient".to_string(),
39            shape: shape.clone(),
40            dtype: TensorDtype::Float32,
41            strides: None,
42            custom: HashMap::new(),
43        };
44
45        Self {
46            indices,
47            values,
48            shape,
49            metadata,
50        }
51    }
52
53    /// Get the number of non-zero elements
54    pub fn nnz(&self) -> usize {
55        self.indices.len()
56    }
57
58    /// Get the total number of elements
59    pub fn total_elements(&self) -> usize {
60        self.shape.iter().product()
61    }
62
63    /// Get the sparsity ratio (0.0 = dense, 1.0 = all zeros)
64    pub fn sparsity_ratio(&self) -> f32 {
65        1.0 - (self.nnz() as f32 / self.total_elements() as f32)
66    }
67
68    /// Convert to dense representation
69    pub fn to_dense(&self) -> Vec<f32> {
70        let total = self.total_elements();
71        let mut dense = vec![0.0; total];
72
73        for (&idx, &val) in self.indices.iter().zip(&self.values) {
74            if idx < total {
75                dense[idx] = val;
76            }
77        }
78
79        dense
80    }
81
82    /// Verify shape consistency
83    pub fn verify_shape(&self) -> Result<(), GradientError> {
84        let total = self.total_elements();
85
86        for &idx in &self.indices {
87            if idx >= total {
88                return Err(GradientError::InvalidGradient(format!(
89                    "Index {} out of bounds for shape {:?}",
90                    idx, self.shape
91                )));
92            }
93        }
94
95        if self.indices.len() != self.values.len() {
96            return Err(GradientError::InvalidGradient(format!(
97                "Indices length {} != values length {}",
98                self.indices.len(),
99                self.values.len()
100            )));
101        }
102
103        Ok(())
104    }
105}
106
107// ── QuantizedGradient ──────────────────────────────────────────────────────
108
109/// Quantized gradient (reduced precision)
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct QuantizedGradient {
112    /// Quantized values (e.g., int8)
113    pub quantized_values: Vec<i8>,
114    /// Scale factor for dequantization
115    pub scale: f32,
116    /// Minimum value for dequantization
117    pub min_val: f32,
118    /// Original tensor shape
119    pub shape: Vec<usize>,
120    /// Metadata
121    pub metadata: TensorMetadata,
122}
123
124impl QuantizedGradient {
125    /// Quantize a dense gradient to int8
126    pub fn from_dense(values: &[f32], shape: Vec<usize>) -> Self {
127        let (quantized_values, scale, min_val) = Self::quantize_i8(values);
128
129        let metadata = TensorMetadata {
130            name: "quantized_gradient".to_string(),
131            shape: shape.clone(),
132            dtype: TensorDtype::Int8,
133            strides: None,
134            custom: HashMap::new(),
135        };
136
137        Self {
138            quantized_values,
139            scale,
140            min_val,
141            shape,
142            metadata,
143        }
144    }
145
146    /// Quantize f32 values to i8
147    fn quantize_i8(values: &[f32]) -> (Vec<i8>, f32, f32) {
148        if values.is_empty() {
149            return (Vec::new(), 1.0, 0.0);
150        }
151
152        let min_val = values.iter().copied().fold(f32::INFINITY, f32::min);
153        let max_val = values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
154
155        // Avoid division by zero
156        let scale = if (max_val - min_val).abs() < 1e-8 {
157            1.0
158        } else {
159            (max_val - min_val) / 255.0
160        };
161
162        let quantized = values
163            .iter()
164            .map(|&v| {
165                // Map [min_val, max_val] to [0, 255], then shift to [-128, 127]
166                let normalized = (v - min_val) / scale;
167                (normalized - 128.0).round().clamp(-128.0, 127.0) as i8
168            })
169            .collect();
170
171        (quantized, scale, min_val)
172    }
173
174    /// Dequantize to f32 values
175    pub fn to_dense(&self) -> Vec<f32> {
176        self.quantized_values
177            .iter()
178            .map(|&q| {
179                // Shift from [-128, 127] to [0, 255], then scale back
180                let normalized = (q as f32) + 128.0;
181                normalized * self.scale + self.min_val
182            })
183            .collect()
184    }
185
186    /// Get compression ratio
187    pub fn compression_ratio(&self) -> f32 {
188        // f32 = 4 bytes, i8 = 1 byte, plus scale and min_val
189        let original_size = self.quantized_values.len() * 4;
190        let compressed_size = self.quantized_values.len() + 8; // 4 bytes scale + 4 bytes min_val
191        original_size as f32 / compressed_size as f32
192    }
193}
194
195// ── LayerGradient ──────────────────────────────────────────────────────────
196
197/// Gradient for a single layer
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub enum LayerGradient {
200    /// Dense gradient
201    Dense { values: Vec<f32>, shape: Vec<usize> },
202    /// Sparse gradient
203    Sparse(SparseGradient),
204    /// Quantized gradient
205    Quantized(QuantizedGradient),
206}
207
208impl LayerGradient {
209    /// Get the shape of the gradient
210    pub fn shape(&self) -> &[usize] {
211        match self {
212            LayerGradient::Dense { shape, .. } => shape,
213            LayerGradient::Sparse(sg) => &sg.shape,
214            LayerGradient::Quantized(qg) => &qg.shape,
215        }
216    }
217
218    /// Convert to dense representation
219    pub fn to_dense(&self) -> Vec<f32> {
220        match self {
221            LayerGradient::Dense { values, .. } => values.clone(),
222            LayerGradient::Sparse(sg) => sg.to_dense(),
223            LayerGradient::Quantized(qg) => qg.to_dense(),
224        }
225    }
226
227    /// Get memory size in bytes
228    pub fn memory_size(&self) -> usize {
229        match self {
230            LayerGradient::Dense { values, .. } => values.len() * 4,
231            LayerGradient::Sparse(sg) => sg.indices.len() * 4 + sg.values.len() * 4,
232            LayerGradient::Quantized(qg) => qg.quantized_values.len() + 8,
233        }
234    }
235}
236
237// ── GradientDelta ──────────────────────────────────────────────────────────
238
239/// Gradient delta (difference from base model)
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct GradientDelta {
242    /// Base model CID
243    #[serde(serialize_with = "crate::serialize_cid")]
244    #[serde(deserialize_with = "crate::deserialize_cid")]
245    pub base_model: Cid,
246    /// Layer name to gradient mapping
247    pub layer_gradients: HashMap<String, LayerGradient>,
248    /// Checksum for verification
249    pub checksum: u64,
250    /// Timestamp
251    pub timestamp: i64,
252}
253
254impl GradientDelta {
255    /// Create a new gradient delta
256    pub fn new(base_model: Cid) -> Self {
257        Self {
258            base_model,
259            layer_gradients: HashMap::new(),
260            checksum: 0,
261            timestamp: chrono::Utc::now().timestamp(),
262        }
263    }
264
265    /// Add a dense gradient for a layer
266    pub fn add_dense_gradient(&mut self, layer_name: String, values: Vec<f32>, shape: Vec<usize>) {
267        self.layer_gradients
268            .insert(layer_name, LayerGradient::Dense { values, shape });
269        self.update_checksum();
270    }
271
272    /// Add a sparse gradient for a layer
273    pub fn add_sparse_gradient(&mut self, layer_name: String, gradient: SparseGradient) {
274        self.layer_gradients
275            .insert(layer_name, LayerGradient::Sparse(gradient));
276        self.update_checksum();
277    }
278
279    /// Add a quantized gradient for a layer
280    pub fn add_quantized_gradient(&mut self, layer_name: String, gradient: QuantizedGradient) {
281        self.layer_gradients
282            .insert(layer_name, LayerGradient::Quantized(gradient));
283        self.update_checksum();
284    }
285
286    /// Compute checksum for verification
287    fn update_checksum(&mut self) {
288        use std::collections::hash_map::DefaultHasher;
289        use std::hash::{Hash, Hasher};
290
291        let mut hasher = DefaultHasher::new();
292
293        // Hash layer count
294        self.layer_gradients.len().hash(&mut hasher);
295
296        // Hash each layer's data
297        let mut sorted_layers: Vec<_> = self.layer_gradients.iter().collect();
298        sorted_layers.sort_by_key(|(name, _)| *name);
299
300        for (name, gradient) in sorted_layers {
301            name.hash(&mut hasher);
302            gradient.shape().hash(&mut hasher);
303
304            // Hash a sample of values for efficiency
305            let dense = gradient.to_dense();
306            let sample_size = dense.len().min(100);
307            for &v in dense.iter().take(sample_size) {
308                v.to_bits().hash(&mut hasher);
309            }
310        }
311
312        self.checksum = hasher.finish();
313    }
314
315    /// Verify checksum
316    pub fn verify_checksum(&self) -> Result<(), GradientError> {
317        let mut temp = self.clone();
318        temp.update_checksum();
319
320        if temp.checksum == self.checksum {
321            Ok(())
322        } else {
323            Err(GradientError::ChecksumFailed)
324        }
325    }
326
327    /// Get total memory size in bytes
328    pub fn total_memory_size(&self) -> usize {
329        self.layer_gradients.values().map(|g| g.memory_size()).sum()
330    }
331}
332
333// ── GradientCompressor ─────────────────────────────────────────────────────
334
335/// Gradient compression utilities
336pub struct GradientCompressor;
337
338impl GradientCompressor {
339    /// Compress gradient using top-k sparsification
340    pub fn top_k(
341        values: &[f32],
342        shape: Vec<usize>,
343        k: usize,
344    ) -> Result<SparseGradient, GradientError> {
345        if k == 0 || k > values.len() {
346            return Err(GradientError::InvalidCompressionRatio(
347                k as f32 / values.len() as f32,
348            ));
349        }
350
351        // Get indices of top-k absolute values
352        let mut indexed_values: Vec<(usize, f32)> = values
353            .iter()
354            .enumerate()
355            .map(|(i, &v)| (i, v.abs()))
356            .collect();
357
358        indexed_values.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
359        indexed_values.truncate(k);
360
361        let mut indices = Vec::with_capacity(k);
362        let mut sparse_values = Vec::with_capacity(k);
363
364        for (idx, _) in indexed_values {
365            indices.push(idx);
366            sparse_values.push(values[idx]);
367        }
368
369        Ok(SparseGradient::new(indices, sparse_values, shape))
370    }
371
372    /// Compress gradient using threshold-based sparsification
373    pub fn threshold(values: &[f32], shape: Vec<usize>, threshold: f32) -> SparseGradient {
374        let mut indices = Vec::new();
375        let mut sparse_values = Vec::new();
376
377        for (i, &v) in values.iter().enumerate() {
378            if v.abs() >= threshold {
379                indices.push(i);
380                sparse_values.push(v);
381            }
382        }
383
384        SparseGradient::new(indices, sparse_values, shape)
385    }
386
387    /// Compress gradient using quantization
388    pub fn quantize(values: &[f32], shape: Vec<usize>) -> QuantizedGradient {
389        QuantizedGradient::from_dense(values, shape)
390    }
391
392    /// Compress gradient using random sparsification
393    pub fn random_sparsification(
394        values: &[f32],
395        shape: Vec<usize>,
396        keep_ratio: f32,
397    ) -> Result<SparseGradient, GradientError> {
398        use rand::RngExt;
399
400        if keep_ratio <= 0.0 || keep_ratio > 1.0 {
401            return Err(GradientError::InvalidCompressionRatio(keep_ratio));
402        }
403
404        let mut rng = rand::rng();
405        let mut indices = Vec::new();
406        let mut sparse_values = Vec::new();
407
408        for (i, &v) in values.iter().enumerate() {
409            if rng.random::<f32>() < keep_ratio {
410                indices.push(i);
411                sparse_values.push(v / keep_ratio); // Compensate for dropout
412            }
413        }
414
415        Ok(SparseGradient::new(indices, sparse_values, shape))
416    }
417}
418
419// ── GradientAggregator ─────────────────────────────────────────────────────
420
421/// Gradient aggregation for federated learning
422pub struct GradientAggregator;
423
424impl GradientAggregator {
425    /// Average multiple gradients (unweighted)
426    pub fn average(gradients: &[Vec<f32>]) -> Result<Vec<f32>, GradientError> {
427        if gradients.is_empty() {
428            return Err(GradientError::EmptyGradientSet);
429        }
430
431        let len = gradients[0].len();
432
433        // Verify all gradients have the same length
434        for g in gradients.iter() {
435            if g.len() != len {
436                return Err(GradientError::ShapeMismatch {
437                    expected: vec![len],
438                    actual: vec![g.len()],
439                });
440            }
441        }
442
443        let mut result = vec![0.0; len];
444        let count = gradients.len() as f32;
445
446        for gradient in gradients {
447            for (i, &v) in gradient.iter().enumerate() {
448                result[i] += v / count;
449            }
450        }
451
452        Ok(result)
453    }
454
455    /// Weighted average of gradients
456    pub fn weighted_average(
457        gradients: &[Vec<f32>],
458        weights: &[f32],
459    ) -> Result<Vec<f32>, GradientError> {
460        if gradients.is_empty() {
461            return Err(GradientError::EmptyGradientSet);
462        }
463
464        if gradients.len() != weights.len() {
465            return Err(GradientError::InvalidGradient(format!(
466                "Gradient count {} != weight count {}",
467                gradients.len(),
468                weights.len()
469            )));
470        }
471
472        let len = gradients[0].len();
473
474        // Verify all gradients have the same length
475        for g in gradients.iter() {
476            if g.len() != len {
477                return Err(GradientError::ShapeMismatch {
478                    expected: vec![len],
479                    actual: vec![g.len()],
480                });
481            }
482        }
483
484        let weight_sum: f32 = weights.iter().sum();
485        if weight_sum == 0.0 {
486            return Err(GradientError::InvalidGradient(
487                "Sum of weights is zero".to_string(),
488            ));
489        }
490
491        let mut result = vec![0.0; len];
492
493        for (gradient, &weight) in gradients.iter().zip(weights) {
494            let normalized_weight = weight / weight_sum;
495            for (i, &v) in gradient.iter().enumerate() {
496                result[i] += v * normalized_weight;
497            }
498        }
499
500        Ok(result)
501    }
502
503    /// Apply momentum to gradient
504    pub fn apply_momentum(
505        current_gradient: &[f32],
506        previous_momentum: &[f32],
507        momentum_factor: f32,
508    ) -> Result<Vec<f32>, GradientError> {
509        if current_gradient.len() != previous_momentum.len() {
510            return Err(GradientError::ShapeMismatch {
511                expected: vec![previous_momentum.len()],
512                actual: vec![current_gradient.len()],
513            });
514        }
515
516        let result = current_gradient
517            .iter()
518            .zip(previous_momentum)
519            .map(|(&g, &m)| momentum_factor * m + g)
520            .collect();
521
522        Ok(result)
523    }
524}
525
526// ── GradientVerifier ───────────────────────────────────────────────────────
527
528/// Gradient verification utilities
529pub struct GradientVerifier;
530
531impl GradientVerifier {
532    /// Verify gradient shape matches expected shape
533    pub fn verify_shape(gradient: &[f32], expected_shape: &[usize]) -> Result<(), GradientError> {
534        let expected_size: usize = expected_shape.iter().product();
535
536        if gradient.len() != expected_size {
537            return Err(GradientError::ShapeMismatch {
538                expected: expected_shape.to_vec(),
539                actual: vec![gradient.len()],
540            });
541        }
542
543        Ok(())
544    }
545
546    /// Detect outliers in gradient (values beyond threshold standard deviations)
547    pub fn detect_outliers(gradient: &[f32], std_threshold: f32) -> Result<(), GradientError> {
548        if gradient.is_empty() {
549            return Ok(());
550        }
551
552        // Calculate mean
553        let mean = gradient.iter().sum::<f32>() / gradient.len() as f32;
554
555        // Calculate standard deviation
556        let variance =
557            gradient.iter().map(|&v| (v - mean).powi(2)).sum::<f32>() / gradient.len() as f32;
558        let std_dev = variance.sqrt();
559
560        // Check for outliers
561        for (i, &v) in gradient.iter().enumerate() {
562            let z_score = (v - mean).abs() / std_dev;
563            if z_score > std_threshold {
564                return Err(GradientError::OutlierDetected { index: i, value: v });
565            }
566        }
567
568        Ok(())
569    }
570
571    /// Verify gradient is not NaN or Inf
572    pub fn verify_finite(gradient: &[f32]) -> Result<(), GradientError> {
573        for (i, &v) in gradient.iter().enumerate() {
574            if !v.is_finite() {
575                return Err(GradientError::InvalidGradient(format!(
576                    "Non-finite value at index {}: {}",
577                    i, v
578                )));
579            }
580        }
581
582        Ok(())
583    }
584
585    /// Compute L2 norm of gradient
586    pub fn l2_norm(gradient: &[f32]) -> f32 {
587        gradient.iter().map(|&v| v * v).sum::<f32>().sqrt()
588    }
589
590    /// Clip gradient by norm
591    pub fn clip_by_norm(gradient: &mut [f32], max_norm: f32) {
592        let norm = Self::l2_norm(gradient);
593
594        if norm > max_norm {
595            let scale = max_norm / norm;
596            for v in gradient.iter_mut() {
597                *v *= scale;
598            }
599        }
600    }
601}