Skip to main content

torsh_nn/quantization/
utils.rs

1//! Quantization utility functions and helpers
2
3use crate::quantization::{
4    BackendQuantConfig, CalibrationConfig, CalibrationMethod, DeploymentPlatform,
5    QuantizationConfig, QuantizationParams, QuantizationScheme,
6};
7use torsh_core::{
8    dtype::DType,
9    error::{Result, TorshError},
10};
11use torsh_tensor::Tensor;
12
13// Conditional imports for std/no_std compatibility
14#[cfg(feature = "std")]
15use std::collections::{BTreeMap, HashMap};
16
17#[cfg(not(feature = "std"))]
18use alloc::collections::BTreeMap;
19#[cfg(not(feature = "std"))]
20use hashbrown::HashMap;
21
22/// Calculate quantization error metrics
23pub fn calculate_quantization_error(
24    original: &Tensor,
25    quantized: &Tensor,
26    params: &QuantizationParams,
27) -> Result<QuantizationError> {
28    // Dequantize for comparison
29    let dequantized = params.dequantize(quantized)?;
30
31    let orig_data = original.to_vec()?;
32    let deq_data = dequantized.to_vec()?;
33
34    if orig_data.len() != deq_data.len() {
35        return Err(TorshError::InvalidArgument(
36            "Tensor sizes do not match for error calculation".to_string(),
37        ));
38    }
39
40    let mut mse = 0.0;
41    let mut mae = 0.0;
42    let mut max_error: f32 = 0.0;
43    let mut dot_product = 0.0;
44    let mut norm_orig = 0.0;
45    let mut norm_deq = 0.0;
46
47    for (&orig, &deq) in orig_data.iter().zip(deq_data.iter()) {
48        let error = orig - deq;
49        let abs_error = error.abs();
50
51        mse += error * error;
52        mae += abs_error;
53        max_error = max_error.max(abs_error);
54
55        dot_product += orig * deq;
56        norm_orig += orig * orig;
57        norm_deq += deq * deq;
58    }
59
60    let n = orig_data.len() as f32;
61    mse /= n;
62    mae /= n;
63
64    let cosine_similarity = if norm_orig > 0.0 && norm_deq > 0.0 {
65        dot_product / (norm_orig.sqrt() * norm_deq.sqrt())
66    } else {
67        0.0
68    };
69
70    let snr = if mse > 0.0 {
71        let signal_power = norm_orig / n;
72        10.0 * (signal_power / mse).log10()
73    } else {
74        f32::INFINITY
75    };
76
77    Ok(QuantizationError {
78        mse,
79        mae,
80        max_error,
81        cosine_similarity,
82        snr,
83    })
84}
85
86/// Quantization error metrics
87#[derive(Debug, Clone)]
88pub struct QuantizationError {
89    /// Mean squared error
90    pub mse: f32,
91    /// Mean absolute error
92    pub mae: f32,
93    /// Maximum absolute error
94    pub max_error: f32,
95    /// Cosine similarity
96    pub cosine_similarity: f32,
97    /// Signal-to-noise ratio in dB
98    pub snr: f32,
99}
100
101impl QuantizationError {
102    /// Check if quantization error is within acceptable bounds
103    pub fn is_acceptable(&self, max_mse: f32, min_cosine: f32, min_snr: f32) -> bool {
104        self.mse <= max_mse && self.cosine_similarity >= min_cosine && self.snr >= min_snr
105    }
106}
107
108/// Create quantization config for different deployment scenarios
109pub fn create_deployment_config(platform: DeploymentPlatform) -> QuantizationConfig {
110    match platform {
111        DeploymentPlatform::Mobile => {
112            QuantizationConfig {
113                dtype: DType::U8,
114                scheme: QuantizationScheme::Asymmetric,
115                backend_config: BackendQuantConfig {
116                    use_hardware_acceleration: true,
117                    enable_kernel_fusion: true,
118                    optimize_memory_layout: true,
119                    target_platform: DeploymentPlatform::Mobile,
120                },
121                calibration: CalibrationConfig {
122                    num_samples: 50, // Fewer samples for mobile
123                    method: CalibrationMethod::MinMax,
124                    outlier_percentile: 99.9,
125                    use_moving_average: true,
126                    momentum: 0.9,
127                },
128                per_channel: true,
129                quantize_weights: true,
130                quantize_activations: true,
131            }
132        }
133        DeploymentPlatform::Edge => {
134            QuantizationConfig {
135                dtype: DType::I8,
136                scheme: QuantizationScheme::Symmetric,
137                backend_config: BackendQuantConfig {
138                    use_hardware_acceleration: false, // Edge devices may not have specialized hardware
139                    enable_kernel_fusion: false,
140                    optimize_memory_layout: true,
141                    target_platform: DeploymentPlatform::Edge,
142                },
143                calibration: CalibrationConfig {
144                    num_samples: 25, // Very few samples for edge
145                    method: CalibrationMethod::MinMax,
146                    outlier_percentile: 99.5,
147                    use_moving_average: false,
148                    momentum: 0.9,
149                },
150                per_channel: false, // Simpler quantization
151                quantize_weights: true,
152                quantize_activations: false, // Keep activations in FP32 for accuracy
153            }
154        }
155        DeploymentPlatform::Server => {
156            QuantizationConfig {
157                dtype: DType::I8,
158                scheme: QuantizationScheme::KLDivergence,
159                backend_config: BackendQuantConfig {
160                    use_hardware_acceleration: true,
161                    enable_kernel_fusion: true,
162                    optimize_memory_layout: true,
163                    target_platform: DeploymentPlatform::Server,
164                },
165                calibration: CalibrationConfig {
166                    num_samples: 500, // More samples for better accuracy
167                    method: CalibrationMethod::Entropy,
168                    outlier_percentile: 99.99,
169                    use_moving_average: true,
170                    momentum: 0.95,
171                },
172                per_channel: true,
173                quantize_weights: true,
174                quantize_activations: true,
175            }
176        }
177        DeploymentPlatform::WASM => {
178            QuantizationConfig {
179                dtype: DType::U8,
180                scheme: QuantizationScheme::Asymmetric,
181                backend_config: BackendQuantConfig {
182                    use_hardware_acceleration: false, // WASM limitations
183                    enable_kernel_fusion: false,
184                    optimize_memory_layout: true,
185                    target_platform: DeploymentPlatform::WASM,
186                },
187                calibration: CalibrationConfig {
188                    num_samples: 100,
189                    method: CalibrationMethod::MinMax,
190                    outlier_percentile: 99.9,
191                    use_moving_average: true,
192                    momentum: 0.9,
193                },
194                per_channel: false, // Keep it simple for WASM
195                quantize_weights: true,
196                quantize_activations: true,
197            }
198        }
199        _ => QuantizationConfig::default(),
200    }
201}
202
203/// Analyze tensor for optimal quantization parameters
204pub fn analyze_tensor_distribution(tensor: &Tensor) -> Result<TensorDistributionStats> {
205    let data = tensor.to_vec()?;
206
207    let mut min_val = f32::INFINITY;
208    let mut max_val = f32::NEG_INFINITY;
209    let mut sum = 0.0;
210    let mut sum_squares = 0.0;
211
212    for &value in &data {
213        min_val = min_val.min(value);
214        max_val = max_val.max(value);
215        sum += value;
216        sum_squares += value * value;
217    }
218
219    let n = data.len() as f32;
220    let mean = sum / n;
221    let variance = (sum_squares / n) - (mean * mean);
222    let std_dev = variance.sqrt();
223
224    // Calculate percentiles
225    let mut sorted_data = data.clone();
226    sorted_data.sort_by(|a, b| {
227        a.partial_cmp(b)
228            .expect("data comparison should not involve NaN")
229    });
230
231    let percentiles = calculate_percentiles(&sorted_data);
232
233    // Check for sparsity
234    let zero_count = data.iter().filter(|&&x| x.abs() < 1e-7).count();
235    let sparsity = zero_count as f32 / n;
236
237    // Detect outliers using IQR method
238    let q1 = percentiles.get(&25).copied().unwrap_or(min_val);
239    let q3 = percentiles.get(&75).copied().unwrap_or(max_val);
240    let iqr = q3 - q1;
241    let lower_bound = q1 - 1.5 * iqr;
242    let upper_bound = q3 + 1.5 * iqr;
243
244    let outlier_count = data
245        .iter()
246        .filter(|&&x| x < lower_bound || x > upper_bound)
247        .count();
248    let outlier_ratio = outlier_count as f32 / n;
249
250    Ok(TensorDistributionStats {
251        min_val,
252        max_val,
253        mean,
254        std_dev,
255        percentiles,
256        sparsity,
257        outlier_ratio,
258        num_elements: data.len(),
259    })
260}
261
262/// Calculate percentiles for a sorted array
263fn calculate_percentiles(sorted_data: &[f32]) -> BTreeMap<i32, f32> {
264    let mut percentiles = BTreeMap::new();
265    let percentile_points = vec![1, 5, 10, 25, 50, 75, 90, 95, 99];
266
267    for p in percentile_points {
268        let p_frac = p as f32 / 100.0;
269        let index = (p_frac * (sorted_data.len() - 1) as f32) as usize;
270        let index = index.min(sorted_data.len() - 1);
271        percentiles.insert(p, sorted_data[index]);
272    }
273
274    percentiles
275}
276
277/// Tensor distribution statistics
278#[derive(Debug, Clone)]
279pub struct TensorDistributionStats {
280    pub min_val: f32,
281    pub max_val: f32,
282    pub mean: f32,
283    pub std_dev: f32,
284    pub percentiles: BTreeMap<i32, f32>,
285    pub sparsity: f32,
286    pub outlier_ratio: f32,
287    pub num_elements: usize,
288}
289
290impl TensorDistributionStats {
291    /// Recommend optimal quantization scheme based on distribution
292    pub fn recommend_quantization_scheme(&self) -> QuantizationScheme {
293        // If tensor has many outliers, use percentile-based quantization
294        if self.outlier_ratio > 0.05 {
295            return QuantizationScheme::Percentile(99.9);
296        }
297
298        // If tensor is sparse, dynamic quantization might be better
299        if self.sparsity > 0.7 {
300            return QuantizationScheme::Dynamic;
301        }
302
303        // If tensor has both positive and negative values around zero, use symmetric
304        if self.min_val < 0.0
305            && self.max_val > 0.0
306            && (self.min_val.abs() - self.max_val.abs()).abs() / self.max_val.abs() < 0.2
307        {
308            return QuantizationScheme::Symmetric;
309        }
310
311        // Otherwise, use asymmetric for better range utilization
312        QuantizationScheme::Asymmetric
313    }
314
315    /// Check if tensor is suitable for quantization
316    pub fn is_quantizable(&self, min_dynamic_range: f32) -> bool {
317        let dynamic_range = self.max_val - self.min_val;
318        dynamic_range > min_dynamic_range && self.std_dev > 1e-6
319    }
320}
321
322/// Batch process multiple tensors for quantization analysis
323pub fn batch_analyze_tensors(tensors: &[&Tensor]) -> Result<Vec<TensorDistributionStats>> {
324    tensors
325        .iter()
326        .map(|tensor| analyze_tensor_distribution(tensor))
327        .collect()
328}
329
330/// Find optimal bit-width for quantization given error constraints
331pub fn find_optimal_bitwidth(
332    tensor: &Tensor,
333    max_error: f32,
334    test_bitwidths: &[usize],
335) -> Result<Option<usize>> {
336    let mut best_bitwidth = None;
337
338    for &bitwidth in test_bitwidths {
339        let dtype = match bitwidth {
340            8 => DType::I8,
341            16 => DType::I16,
342            _ => continue, // Skip unsupported bit-widths
343        };
344
345        // Create quantization parameters
346        let data = tensor.to_vec()?;
347        let min_val = data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
348        let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
349
350        let scale = match dtype {
351            DType::I8 => max_val.abs().max(min_val.abs()) / 127.0,
352            DType::I16 => max_val.abs().max(min_val.abs()) / 32767.0,
353            _ => continue,
354        };
355
356        let params = QuantizationParams::symmetric(scale, DType::F32, dtype);
357
358        // Test quantization
359        let quantized = params.quantize(tensor)?;
360        let error = calculate_quantization_error(tensor, &quantized, &params)?;
361
362        if error.mse <= max_error * max_error {
363            best_bitwidth = Some(bitwidth);
364            break; // Found the smallest acceptable bit-width
365        }
366    }
367
368    Ok(best_bitwidth)
369}
370
371/// Create mixed precision configuration based on layer sensitivity analysis
372pub fn create_mixed_precision_config(
373    layer_sensitivities: &HashMap<String, f32>,
374    error_threshold: f32,
375) -> Vec<(String, DType)> {
376    let mut config = Vec::new();
377
378    for (layer_name, &sensitivity) in layer_sensitivities {
379        let dtype = if sensitivity > error_threshold * 2.0 {
380            DType::F16 // Keep high precision for sensitive layers
381        } else if sensitivity > error_threshold {
382            DType::I16 // Medium precision
383        } else {
384            DType::I8 // Low precision for robust layers
385        };
386
387        config.push((layer_name.clone(), dtype));
388    }
389
390    config
391}
392
393/// Estimate model size reduction from quantization
394pub fn estimate_size_reduction(
395    original_dtype: DType,
396    quantized_dtype: DType,
397    num_parameters: usize,
398) -> SizeReduction {
399    let original_bits = match original_dtype {
400        DType::F32 => 32,
401        DType::F16 => 16,
402        _ => 32,
403    };
404
405    let quantized_bits = match quantized_dtype {
406        DType::I8 | DType::U8 => 8,
407        DType::I16 => 16,
408        DType::F16 => 16,
409        DType::F32 => 32,
410        _ => 32,
411    };
412
413    let original_size = (num_parameters * original_bits) / 8; // bytes
414    let quantized_size = (num_parameters * quantized_bits) / 8; // bytes
415    let reduction_bytes = original_size - quantized_size;
416    let reduction_ratio = original_size as f32 / quantized_size as f32;
417
418    SizeReduction {
419        original_size_bytes: original_size,
420        quantized_size_bytes: quantized_size,
421        reduction_bytes,
422        reduction_ratio,
423        compression_percentage: (1.0 - (quantized_size as f32 / original_size as f32)) * 100.0,
424    }
425}
426
427/// Model size reduction statistics
428#[derive(Debug, Clone)]
429pub struct SizeReduction {
430    pub original_size_bytes: usize,
431    pub quantized_size_bytes: usize,
432    pub reduction_bytes: usize,
433    pub reduction_ratio: f32,
434    pub compression_percentage: f32,
435}
436
437impl SizeReduction {
438    /// Format size in human-readable units
439    pub fn format_size(&self) -> (String, String) {
440        let original = format_bytes(self.original_size_bytes);
441        let quantized = format_bytes(self.quantized_size_bytes);
442        (original, quantized)
443    }
444}
445
446/// Format bytes in human-readable units
447fn format_bytes(bytes: usize) -> String {
448    const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
449    let mut size = bytes as f64;
450    let mut unit_index = 0;
451
452    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
453        size /= 1024.0;
454        unit_index += 1;
455    }
456
457    format!("{:.2} {}", size, UNITS[unit_index])
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463    use torsh_tensor::Tensor;
464
465    #[test]
466    fn test_tensor_distribution_analysis() {
467        let data = vec![1.0, -1.0, 0.5, -0.5, 2.0, -2.0, 0.0, 0.1, -0.1];
468        let tensor = Tensor::from_data(data, vec![9], torsh_core::device::DeviceType::Cpu)
469            .expect("Tensor should succeed");
470
471        let stats = analyze_tensor_distribution(&tensor)
472            .expect("analyze tensor distribution should succeed");
473
474        assert!(stats.min_val <= -2.0);
475        assert!(stats.max_val >= 2.0);
476        assert!(stats.mean.abs() < 0.5); // Should be close to zero
477        assert!(stats.num_elements == 9);
478        assert!(stats.sparsity >= 0.0 && stats.sparsity <= 1.0);
479    }
480
481    #[test]
482    fn test_quantization_error_calculation() {
483        let original_data = vec![1.0, 2.0, 3.0, 4.0];
484        let original =
485            Tensor::from_data(original_data, vec![4], torsh_core::device::DeviceType::Cpu)
486                .expect("Tensor should succeed");
487
488        let params = QuantizationParams::symmetric(4.0 / 127.0, DType::F32, DType::I8);
489        let quantized = params
490            .quantize(&original)
491            .expect("quantization should succeed");
492
493        let error = calculate_quantization_error(&original, &quantized, &params)
494            .expect("calculate quantization error should succeed");
495
496        assert!(error.mse >= 0.0);
497        assert!(error.mae >= 0.0);
498        assert!(error.max_error >= 0.0);
499        assert!(error.cosine_similarity >= 0.0 && error.cosine_similarity <= 1.0);
500    }
501
502    #[test]
503    fn test_deployment_config_creation() {
504        let mobile_config = create_deployment_config(DeploymentPlatform::Mobile);
505        assert_eq!(mobile_config.dtype, DType::U8);
506        assert_eq!(mobile_config.scheme, QuantizationScheme::Asymmetric);
507        assert!(mobile_config.per_channel);
508
509        let edge_config = create_deployment_config(DeploymentPlatform::Edge);
510        assert_eq!(edge_config.dtype, DType::I8);
511        assert_eq!(edge_config.scheme, QuantizationScheme::Symmetric);
512        assert!(!edge_config.per_channel);
513    }
514
515    #[test]
516    fn test_size_reduction_estimation() {
517        let reduction = estimate_size_reduction(DType::F32, DType::I8, 1000);
518
519        assert_eq!(reduction.original_size_bytes, 4000); // 1000 * 32 bits / 8
520        assert_eq!(reduction.quantized_size_bytes, 1000); // 1000 * 8 bits / 8
521        assert_eq!(reduction.reduction_bytes, 3000);
522        assert_eq!(reduction.reduction_ratio, 4.0);
523        assert_eq!(reduction.compression_percentage, 75.0);
524    }
525
526    #[test]
527    fn test_optimal_bitwidth_finding() {
528        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
529        let tensor = Tensor::from_data(data, vec![5], torsh_core::device::DeviceType::Cpu)
530            .expect("Tensor should succeed");
531
532        let bitwidths = vec![8, 16];
533        let result = find_optimal_bitwidth(&tensor, 0.1, &bitwidths)
534            .expect("find optimal bitwidth should succeed");
535
536        // Should find some acceptable bit-width
537        assert!(result.is_some());
538    }
539}