Skip to main content

sklears_neural/
quantization.rs

1//! Model Quantization utilities for neural network compression
2//!
3//! This module provides comprehensive quantization techniques to reduce the precision
4//! of neural network weights and activations, enabling deployment on resource-constrained
5//! devices while maintaining model performance. Quantization reduces memory footprint
6//! and improves inference speed, making it crucial for mobile and edge deployment.
7//!
8//! # Theory
9//!
10//! Quantization maps high-precision floating-point values to lower-precision
11//! representations (typically 8-bit integers). The key equation is:
12//!
13//! quantized_value = round((float_value - zero_point) / scale)
14//! dequantized_value = scale * quantized_value + zero_point
15//!
16//! Where:
17//! - scale: Quantization scale factor
18//! - zero_point: Zero point offset for asymmetric quantization
19//!
20//! # Supported Quantization Types
21//!
22//! - Post-Training Quantization (PTQ): Quantize pre-trained models
23//! - Quantization-Aware Training (QAT): Train with quantization in mind
24//! - Dynamic Quantization: Quantize weights statically, activations dynamically
25//! - Static Quantization: Quantize both weights and activations statically
26//!
27//! # Example
28//!
29//! ```rust,ignore
30//! use sklears_neural::quantization::{Quantizer, QuantizationConfig, QuantizationType};
31//!
32//! let config = QuantizationConfig::default()
33//!     .quantization_type(QuantizationType::INT8)
34//!     .symmetric(false);
35//!
36//! let quantizer = Quantizer::new(config);
37//! ```
38
39use crate::{NeuralResult, SklearsError};
40use scirs2_core::ndarray::Array2;
41use sklears_core::types::{Float, FloatBounds};
42use std::collections::HashMap;
43use std::marker::PhantomData;
44
45#[cfg(feature = "serde")]
46use serde::{Deserialize, Serialize};
47
48/// Quantization data type
49#[derive(Debug, Clone, PartialEq, Default)]
50#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
51pub enum QuantizationType {
52    /// 8-bit integer quantization
53    #[default]
54    INT8,
55    /// 16-bit integer quantization
56    INT16,
57    /// 4-bit integer quantization
58    INT4,
59    /// Binary quantization (1-bit)
60    Binary,
61    /// Ternary quantization (3 values: -1, 0, 1)
62    Ternary,
63}
64
65/// Quantization strategy
66#[derive(Debug, Clone, PartialEq, Default)]
67#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
68pub enum QuantizationStrategy {
69    /// Post-training quantization
70    #[default]
71    PostTraining,
72    /// Quantization-aware training
73    QuantizationAware,
74    /// Dynamic quantization
75    Dynamic,
76    /// Static quantization
77    Static,
78}
79
80/// Quantization granularity
81#[derive(Debug, Clone, PartialEq, Default)]
82#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
83pub enum Granularity {
84    /// Per-tensor quantization (single scale/zero-point for entire tensor)
85    #[default]
86    PerTensor,
87    /// Per-channel quantization (different scale/zero-point per output channel)
88    PerChannel,
89    /// Per-group quantization (different scale/zero-point per group of channels)
90    PerGroup {
91        /// Number of channels per quantization group
92        group_size: usize,
93    },
94}
95
96/// Calibration method for determining quantization parameters
97#[derive(Debug, Clone, PartialEq, Default)]
98#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
99pub enum CalibrationMethod {
100    /// Use min/max values from calibration data
101    #[default]
102    MinMax,
103    /// Use percentile values (e.g., 99.9th percentile)
104    Percentile {
105        /// Percentile value for calibration (e.g., 99.9)
106        percentile: f64,
107    },
108    /// Use entropy-based calibration (KL divergence)
109    Entropy,
110    /// Use mean squared error for optimal quantization parameters
111    MSE,
112}
113
114/// Quantization configuration
115#[derive(Debug, Clone)]
116#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
117pub struct QuantizationConfig {
118    /// Type of quantization
119    pub quantization_type: QuantizationType,
120    /// Quantization strategy
121    pub strategy: QuantizationStrategy,
122    /// Granularity of quantization
123    pub granularity: Granularity,
124    /// Calibration method
125    pub calibration_method: CalibrationMethod,
126    /// Whether to use symmetric quantization (zero_point = 0)
127    pub symmetric: bool,
128    /// Whether to quantize weights
129    pub quantize_weights: bool,
130    /// Whether to quantize activations
131    pub quantize_activations: bool,
132    /// Layers to skip quantization
133    pub skip_layers: Vec<String>,
134    /// Number of quantization-aware training epochs
135    pub qat_epochs: usize,
136    /// Learning rate used during quantization-aware training
137    pub qat_learning_rate: Float,
138    /// Fake quantization during training
139    pub fake_quantize: bool,
140    /// Observer momentum for running statistics
141    pub observer_momentum: Float,
142    /// Number of calibration samples
143    pub calibration_samples: usize,
144    /// Random seed
145    pub random_state: Option<u64>,
146}
147
148impl Default for QuantizationConfig {
149    fn default() -> Self {
150        Self {
151            quantization_type: QuantizationType::INT8,
152            strategy: QuantizationStrategy::PostTraining,
153            granularity: Granularity::PerTensor,
154            calibration_method: CalibrationMethod::MinMax,
155            symmetric: true,
156            quantize_weights: true,
157            quantize_activations: true,
158            skip_layers: vec!["input".to_string(), "output".to_string()],
159            qat_epochs: 10,
160            qat_learning_rate: 0.0001,
161            fake_quantize: false,
162            observer_momentum: 0.1,
163            calibration_samples: 1000,
164            random_state: None,
165        }
166    }
167}
168
169impl QuantizationConfig {
170    /// Set quantization type
171    pub fn quantization_type(mut self, qtype: QuantizationType) -> Self {
172        self.quantization_type = qtype;
173        self
174    }
175
176    /// Set quantization strategy
177    pub fn strategy(mut self, strategy: QuantizationStrategy) -> Self {
178        self.strategy = strategy;
179        self
180    }
181
182    /// Set granularity
183    pub fn granularity(mut self, granularity: Granularity) -> Self {
184        self.granularity = granularity;
185        self
186    }
187
188    /// Set calibration method
189    pub fn calibration_method(mut self, method: CalibrationMethod) -> Self {
190        self.calibration_method = method;
191        self
192    }
193
194    /// Set symmetric quantization
195    pub fn symmetric(mut self, symmetric: bool) -> Self {
196        self.symmetric = symmetric;
197        self
198    }
199
200    /// Configure what to quantize
201    pub fn quantize_components(mut self, weights: bool, activations: bool) -> Self {
202        self.quantize_weights = weights;
203        self.quantize_activations = activations;
204        self
205    }
206
207    /// Set layers to skip
208    pub fn skip_layers(mut self, layers: Vec<String>) -> Self {
209        self.skip_layers = layers;
210        self
211    }
212
213    /// Configure quantization-aware training
214    pub fn qat_config(mut self, epochs: usize, lr: Float) -> Self {
215        self.strategy = QuantizationStrategy::QuantizationAware;
216        self.qat_epochs = epochs;
217        self.qat_learning_rate = lr;
218        self
219    }
220}
221
222/// Quantization parameters
223#[derive(Debug, Clone)]
224#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
225pub struct QuantizationParams {
226    /// Quantization scale
227    pub scale: f64,
228    /// Zero point (for asymmetric quantization)
229    pub zero_point: i32,
230    /// Minimum value
231    pub min_val: f64,
232    /// Maximum value
233    pub max_val: f64,
234    /// Number of quantization levels
235    pub n_levels: u32,
236}
237
238impl QuantizationParams {
239    /// Create a new set of quantization parameters with the given scale, zero-point, and value range
240    pub fn new(scale: f64, zero_point: i32, min_val: f64, max_val: f64, n_levels: u32) -> Self {
241        Self {
242            scale,
243            zero_point,
244            min_val,
245            max_val,
246            n_levels,
247        }
248    }
249
250    /// Create symmetric quantization parameters
251    pub fn symmetric(max_abs: f64, n_levels: u32) -> Self {
252        let scale = 2.0 * max_abs / (n_levels - 1) as f64;
253        Self {
254            scale,
255            zero_point: 0,
256            min_val: -max_abs,
257            max_val: max_abs,
258            n_levels,
259        }
260    }
261
262    /// Create asymmetric quantization parameters
263    pub fn asymmetric(min_val: f64, max_val: f64, n_levels: u32) -> Self {
264        let scale = (max_val - min_val) / (n_levels - 1) as f64;
265        let zero_point = (-min_val / scale).round() as i32;
266        Self {
267            scale,
268            zero_point,
269            min_val,
270            max_val,
271            n_levels,
272        }
273    }
274}
275
276/// Quantized tensor representation
277#[derive(Debug, Clone)]
278pub struct QuantizedTensor {
279    /// Quantized integer values
280    pub data: Array2<i32>,
281    /// Quantization parameters
282    pub params: QuantizationParams,
283    /// Original shape
284    pub original_shape: Vec<usize>,
285}
286
287impl QuantizedTensor {
288    /// Create new quantized tensor
289    pub fn new(data: Array2<i32>, params: QuantizationParams, original_shape: Vec<usize>) -> Self {
290        Self {
291            data,
292            params,
293            original_shape,
294        }
295    }
296
297    /// Dequantize to floating point
298    pub fn dequantize<T: FloatBounds>(&self) -> NeuralResult<Array2<T>> {
299        let mut result = Array2::zeros(self.data.dim());
300
301        for (i, &quantized_val) in self.data.iter().enumerate() {
302            let dequantized = self.params.scale * (quantized_val - self.params.zero_point) as f64;
303            result.as_slice_mut().expect("non-contiguous array")[i] =
304                T::from(dequantized).unwrap_or_else(|| T::zero());
305        }
306
307        Ok(result)
308    }
309
310    /// Get compression ratio
311    pub fn compression_ratio(&self) -> f64 {
312        // Assuming original was 32-bit float and quantized is 8-bit int
313        32.0 / 8.0
314    }
315}
316
317/// Observer for collecting statistics during calibration
318#[derive(Debug, Clone)]
319#[allow(dead_code)] // momentum retained for EMA calibration; will be used in update_ema method
320pub struct Observer {
321    /// Running minimum
322    min_val: f64,
323    /// Running maximum
324    max_val: f64,
325    /// Running count
326    count: usize,
327    /// Momentum for exponential moving average
328    momentum: f64,
329    /// Histogram for entropy calibration
330    histogram: Vec<u64>,
331    histogram_bins: usize,
332}
333
334impl Observer {
335    /// Create a new observer with the given EMA momentum and histogram resolution
336    pub fn new(momentum: f64, histogram_bins: usize) -> Self {
337        Self {
338            min_val: f64::INFINITY,
339            max_val: f64::NEG_INFINITY,
340            count: 0,
341            momentum,
342            histogram: vec![0; histogram_bins],
343            histogram_bins,
344        }
345    }
346
347    /// Update observer with new data
348    pub fn update<T: FloatBounds>(&mut self, data: &Array2<T>) {
349        let is_first_update = self.count == 0;
350
351        for &val in data.iter() {
352            let val_f64 = val.to_f64().unwrap_or(0.0);
353
354            if is_first_update && self.min_val == f64::INFINITY {
355                self.min_val = val_f64;
356                self.max_val = val_f64;
357            } else {
358                // Update min/max directly
359                self.min_val = self.min_val.min(val_f64);
360                self.max_val = self.max_val.max(val_f64);
361            }
362
363            // Update histogram
364            self.update_histogram(val_f64);
365        }
366
367        self.count += 1;
368    }
369
370    fn update_histogram(&mut self, val: f64) {
371        if self.min_val < self.max_val {
372            let range = self.max_val - self.min_val;
373            let normalized = (val - self.min_val) / range;
374            let bin_idx = ((normalized * (self.histogram_bins - 1) as f64).floor() as usize)
375                .min(self.histogram_bins - 1);
376            self.histogram[bin_idx] += 1;
377        }
378    }
379
380    /// Get quantization parameters based on calibration method
381    pub fn get_quantization_params(
382        &self,
383        method: &CalibrationMethod,
384        symmetric: bool,
385        n_levels: u32,
386    ) -> QuantizationParams {
387        match method {
388            CalibrationMethod::MinMax => {
389                if symmetric {
390                    let max_abs = self.min_val.abs().max(self.max_val.abs());
391                    QuantizationParams::symmetric(max_abs, n_levels)
392                } else {
393                    QuantizationParams::asymmetric(self.min_val, self.max_val, n_levels)
394                }
395            }
396            CalibrationMethod::Percentile { percentile } => {
397                // Use histogram to compute percentile
398                let (min_p, max_p) = self.compute_percentile(*percentile);
399                if symmetric {
400                    let max_abs = min_p.abs().max(max_p.abs());
401                    QuantizationParams::symmetric(max_abs, n_levels)
402                } else {
403                    QuantizationParams::asymmetric(min_p, max_p, n_levels)
404                }
405            }
406            CalibrationMethod::Entropy => {
407                // Use KL divergence to find optimal clipping values
408                let (min_opt, max_opt) = self.compute_optimal_clipping(n_levels);
409                QuantizationParams::asymmetric(min_opt, max_opt, n_levels)
410            }
411            CalibrationMethod::MSE => {
412                // Use MSE optimization for quantization parameters
413                let (min_mse, max_mse) = self.compute_mse_optimal();
414                QuantizationParams::asymmetric(min_mse, max_mse, n_levels)
415            }
416        }
417    }
418
419    fn compute_percentile(&self, percentile: f64) -> (f64, f64) {
420        let total_count: u64 = self.histogram.iter().sum();
421        if total_count == 0 {
422            return (self.min_val, self.max_val);
423        }
424
425        let target_low = ((100.0 - percentile) / 2.0 / 100.0 * total_count as f64) as u64;
426        let target_high = (((100.0 + percentile) / 2.0 / 100.0) * total_count as f64) as u64;
427
428        let mut cumsum = 0;
429        let mut min_p = self.min_val;
430        let mut max_p = self.max_val;
431
432        let bin_width = (self.max_val - self.min_val) / self.histogram_bins as f64;
433
434        for (i, &count) in self.histogram.iter().enumerate() {
435            cumsum += count;
436            if cumsum >= target_low && min_p == self.min_val {
437                min_p = self.min_val + i as f64 * bin_width;
438            }
439            if cumsum >= target_high {
440                max_p = self.min_val + (i + 1) as f64 * bin_width;
441                break;
442            }
443        }
444
445        (min_p, max_p)
446    }
447
448    fn compute_optimal_clipping(&self, _n_levels: u32) -> (f64, f64) {
449        // Simplified implementation - in practice would use KL divergence
450        let (min_p, max_p) = self.compute_percentile(99.9);
451        (min_p, max_p)
452    }
453
454    fn compute_mse_optimal(&self) -> (f64, f64) {
455        // Simplified implementation - in practice would minimize MSE
456        (self.min_val, self.max_val)
457    }
458}
459
460/// Main quantization engine
461#[allow(dead_code)] // calibration_data retained for replay calibration and future cross-layer optimization
462pub struct Quantizer<T: FloatBounds> {
463    config: QuantizationConfig,
464    /// Layer-specific quantization parameters
465    layer_params: HashMap<String, QuantizationParams>,
466    /// Observers for calibration
467    observers: HashMap<String, Observer>,
468    /// Calibration data storage
469    calibration_data: HashMap<String, Vec<Array2<T>>>,
470    _phantom: PhantomData<T>,
471}
472
473impl<T: FloatBounds> Quantizer<T> {
474    /// Create new quantizer
475    pub fn new(config: QuantizationConfig) -> Self {
476        Self {
477            config,
478            layer_params: HashMap::new(),
479            observers: HashMap::new(),
480            calibration_data: HashMap::new(),
481            _phantom: PhantomData,
482        }
483    }
484
485    /// Calibrate quantization parameters using sample data
486    pub fn calibrate(&mut self, model_data: &HashMap<String, Array2<T>>) -> NeuralResult<()> {
487        // Initialize observers
488        for layer_name in model_data.keys() {
489            if !self.config.skip_layers.contains(layer_name) {
490                let observer = Observer::new(self.config.observer_momentum, 256);
491                self.observers.insert(layer_name.clone(), observer);
492            }
493        }
494
495        // Collect statistics from calibration data
496        for (layer_name, data) in model_data {
497            if let Some(observer) = self.observers.get_mut(layer_name) {
498                observer.update(data);
499            }
500        }
501
502        // Generate quantization parameters
503        for (layer_name, observer) in &self.observers {
504            let n_levels = self.get_quantization_levels();
505            let params = observer.get_quantization_params(
506                &self.config.calibration_method,
507                self.config.symmetric,
508                n_levels,
509            );
510            self.layer_params.insert(layer_name.clone(), params);
511        }
512
513        Ok(())
514    }
515
516    /// Quantize a tensor
517    pub fn quantize_tensor(
518        &self,
519        tensor: &Array2<T>,
520        layer_name: &str,
521    ) -> NeuralResult<QuantizedTensor> {
522        let params =
523            self.layer_params
524                .get(layer_name)
525                .ok_or_else(|| SklearsError::InvalidParameter {
526                    name: "layer_name".to_string(),
527                    reason: format!("No quantization parameters found for layer {}", layer_name),
528                })?;
529
530        let quantized_data = self.apply_quantization(tensor, params)?;
531        let original_shape = tensor.shape().to_vec();
532
533        Ok(QuantizedTensor::new(
534            quantized_data,
535            params.clone(),
536            original_shape,
537        ))
538    }
539
540    /// Apply quantization to tensor data
541    fn apply_quantization(
542        &self,
543        tensor: &Array2<T>,
544        params: &QuantizationParams,
545    ) -> NeuralResult<Array2<i32>> {
546        let mut quantized = Array2::zeros(tensor.dim());
547
548        for (i, &val) in tensor.iter().enumerate() {
549            let val_f64 = val.to_f64().unwrap_or(0.0);
550
551            // Apply quantization formula: q = round(x / scale) + zero_point
552            let quantized_val = (val_f64 / params.scale).round() as i32 + params.zero_point;
553
554            // Clamp to valid range
555            let clamped = quantized_val
556                .max(-(params.n_levels as i32 / 2))
557                .min(params.n_levels as i32 / 2 - 1);
558
559            quantized.as_slice_mut().expect("non-contiguous array")[i] = clamped;
560        }
561
562        Ok(quantized)
563    }
564
565    /// Dequantize a quantized tensor
566    pub fn dequantize_tensor(&self, quantized: &QuantizedTensor) -> NeuralResult<Array2<T>> {
567        quantized.dequantize()
568    }
569
570    /// Quantize model weights
571    pub fn quantize_weights(
572        &mut self,
573        weights: &HashMap<String, Array2<T>>,
574    ) -> NeuralResult<HashMap<String, QuantizedTensor>> {
575        if !self.config.quantize_weights {
576            return Err(SklearsError::InvalidParameter {
577                name: "quantize_weights".to_string(),
578                reason: "Weight quantization is disabled".to_string(),
579            });
580        }
581
582        // Calibrate if not already done
583        if self.layer_params.is_empty() {
584            self.calibrate(weights)?;
585        }
586
587        let mut quantized_weights = HashMap::new();
588        for (layer_name, weight) in weights {
589            if !self.config.skip_layers.contains(layer_name) {
590                let quantized = self.quantize_tensor(weight, layer_name)?;
591                quantized_weights.insert(layer_name.clone(), quantized);
592            }
593        }
594
595        Ok(quantized_weights)
596    }
597
598    /// Apply fake quantization (for QAT)
599    pub fn fake_quantize_tensor(
600        &self,
601        tensor: &Array2<T>,
602        layer_name: &str,
603    ) -> NeuralResult<Array2<T>> {
604        if !self.config.fake_quantize {
605            return Ok(tensor.clone());
606        }
607
608        // Quantize then immediately dequantize
609        let quantized = self.quantize_tensor(tensor, layer_name)?;
610        self.dequantize_tensor(&quantized)
611    }
612
613    /// Get number of quantization levels based on type
614    fn get_quantization_levels(&self) -> u32 {
615        match self.config.quantization_type {
616            QuantizationType::INT8 => 256,
617            QuantizationType::INT16 => 65536,
618            QuantizationType::INT4 => 16,
619            QuantizationType::Binary => 2,
620            QuantizationType::Ternary => 3,
621        }
622    }
623
624    /// Compute quantization error
625    pub fn compute_quantization_error(
626        &self,
627        original: &Array2<T>,
628        quantized: &QuantizedTensor,
629    ) -> NeuralResult<QuantizationMetrics> {
630        let reconstructed = self.dequantize_tensor(quantized)?;
631
632        // Mean Squared Error
633        let diff = original - &reconstructed;
634        let mse = diff
635            .mapv(|x| x * x)
636            .mean()
637            .expect("mean should not fail on non-empty array")
638            .to_f64()
639            .unwrap_or(0.0);
640
641        // Signal-to-Noise Ratio
642        let signal_power = original
643            .mapv(|x| x * x)
644            .mean()
645            .expect("value should be present")
646            .to_f64()
647            .unwrap_or(0.0);
648        let snr_db = if mse > 0.0 {
649            10.0 * (signal_power / mse).log10()
650        } else {
651            f64::INFINITY
652        };
653
654        // Peak Signal-to-Noise Ratio
655        let max_val = original
656            .iter()
657            .map(|&x| x.abs())
658            .fold(T::from(0.0).unwrap_or_else(|| T::zero()), |a, b| a.max(b));
659        let max_val_f64 = max_val.to_f64().unwrap_or(0.0);
660        let psnr_db = if mse > 0.0 {
661            20.0 * (max_val_f64 / mse.sqrt()).log10()
662        } else {
663            f64::INFINITY
664        };
665
666        // Compression ratio
667        let compression_ratio = quantized.compression_ratio();
668
669        Ok(QuantizationMetrics {
670            mse,
671            snr_db,
672            psnr_db,
673            compression_ratio,
674            original_size: original.len() * std::mem::size_of::<T>(),
675            quantized_size: quantized.data.len() * std::mem::size_of::<i32>(),
676        })
677    }
678
679    /// Analyze quantization sensitivity
680    pub fn analyze_layer_sensitivity(
681        &mut self,
682        layers_data: &HashMap<String, Array2<T>>,
683    ) -> NeuralResult<HashMap<String, f64>> {
684        let mut sensitivity_scores = HashMap::new();
685
686        for (layer_name, data) in layers_data {
687            if self.config.skip_layers.contains(layer_name) {
688                continue;
689            }
690
691            // Compute quantization error for this layer
692            let quantized = self.quantize_tensor(data, layer_name)?;
693            let metrics = self.compute_quantization_error(data, &quantized)?;
694
695            // Use MSE as sensitivity score
696            sensitivity_scores.insert(layer_name.clone(), metrics.mse);
697        }
698
699        Ok(sensitivity_scores)
700    }
701}
702
703/// Quantization performance metrics
704#[derive(Debug, Clone)]
705#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
706pub struct QuantizationMetrics {
707    /// Mean Squared Error
708    pub mse: f64,
709    /// Signal-to-Noise Ratio (dB)
710    pub snr_db: f64,
711    /// Peak Signal-to-Noise Ratio (dB)
712    pub psnr_db: f64,
713    /// Compression ratio
714    pub compression_ratio: f64,
715    /// Original tensor size in bytes
716    pub original_size: usize,
717    /// Quantized tensor size in bytes
718    pub quantized_size: usize,
719}
720
721impl QuantizationMetrics {
722    /// Check if quantization quality is acceptable
723    pub fn is_acceptable(&self, min_snr_db: f64, max_mse: f64) -> bool {
724        self.snr_db >= min_snr_db && self.mse <= max_mse
725    }
726
727    /// Get memory savings ratio
728    pub fn memory_savings(&self) -> f64 {
729        1.0 - (self.quantized_size as f64 / self.original_size as f64)
730    }
731}
732
733/// Utility functions for quantization
734pub mod utils {
735    use super::*;
736
737    /// Create INT8 post-training quantization config
738    pub fn int8_ptq_config() -> QuantizationConfig {
739        QuantizationConfig::default()
740            .quantization_type(QuantizationType::INT8)
741            .strategy(QuantizationStrategy::PostTraining)
742            .symmetric(true)
743    }
744
745    /// Create INT8 quantization-aware training config
746    pub fn int8_qat_config(epochs: usize, lr: f64) -> QuantizationConfig {
747        QuantizationConfig::default()
748            .quantization_type(QuantizationType::INT8)
749            .qat_config(epochs, lr)
750    }
751
752    /// Create dynamic quantization config
753    pub fn dynamic_quantization_config() -> QuantizationConfig {
754        QuantizationConfig::default()
755            .strategy(QuantizationStrategy::Dynamic)
756            .quantize_components(true, false) // Only quantize weights
757    }
758
759    /// Create per-channel quantization config
760    pub fn per_channel_config() -> QuantizationConfig {
761        QuantizationConfig::default()
762            .granularity(Granularity::PerChannel)
763            .symmetric(false)
764    }
765
766    /// Create binary quantization config
767    pub fn binary_quantization_config() -> QuantizationConfig {
768        QuantizationConfig::default()
769            .quantization_type(QuantizationType::Binary)
770            .symmetric(true)
771    }
772
773    /// Evaluate quantization impact on model accuracy
774    pub fn evaluate_accuracy_impact<T: FloatBounds>(
775        original_accuracy: f64,
776        quantized_accuracy: f64,
777    ) -> f64 {
778        (original_accuracy - quantized_accuracy) / original_accuracy
779    }
780
781    /// Select optimal quantization parameters based on sensitivity analysis
782    pub fn select_optimal_parameters(
783        sensitivity_scores: &HashMap<String, f64>,
784        target_compression: f64,
785    ) -> Vec<String> {
786        let mut layers: Vec<(String, f64)> = sensitivity_scores
787            .iter()
788            .map(|(name, &score)| (name.clone(), score))
789            .collect();
790
791        // Sort by sensitivity (ascending - less sensitive first)
792        layers.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
793
794        // Select layers to quantize based on target compression
795        let n_layers_to_quantize = (layers.len() as f64 * target_compression) as usize;
796        layers
797            .into_iter()
798            .take(n_layers_to_quantize)
799            .map(|(name, _)| name)
800            .collect()
801    }
802}
803
804#[allow(non_snake_case)]
805#[cfg(test)]
806mod tests {
807    use super::*;
808    use approx;
809    use scirs2_core::ndarray::arr2;
810
811    #[test]
812    fn test_quantization_config() {
813        let config = QuantizationConfig::default()
814            .quantization_type(QuantizationType::INT8)
815            .strategy(QuantizationStrategy::PostTraining)
816            .symmetric(false);
817
818        assert_eq!(config.quantization_type, QuantizationType::INT8);
819        assert_eq!(config.strategy, QuantizationStrategy::PostTraining);
820        assert!(!config.symmetric);
821    }
822
823    #[test]
824    fn test_quantization_params() {
825        let params = QuantizationParams::symmetric(1.0, 256);
826        assert_eq!(params.zero_point, 0);
827        approx::assert_abs_diff_eq!(params.scale, 2.0 / 255.0, epsilon = 1e-10);
828
829        let params = QuantizationParams::asymmetric(-1.0, 2.0, 256);
830        approx::assert_abs_diff_eq!(params.scale, 3.0 / 255.0, epsilon = 1e-10);
831        assert!(params.zero_point > 0);
832    }
833
834    #[test]
835    fn test_observer() {
836        let mut observer = Observer::new(0.1, 100);
837
838        let data = arr2(&[[1.0, 2.0], [3.0, 4.0]]);
839        observer.update(&data);
840
841        assert_eq!(observer.min_val, 1.0);
842        assert_eq!(observer.max_val, 4.0);
843
844        let params = observer.get_quantization_params(&CalibrationMethod::MinMax, true, 256);
845        assert_eq!(params.zero_point, 0); // Symmetric
846    }
847
848    #[test]
849    fn test_quantization_dequantization() {
850        let config = QuantizationConfig::default();
851        let mut quantizer = Quantizer::<f64>::new(config);
852
853        let data = arr2(&[[1.0, -1.0], [0.5, -0.5]]);
854        let mut layer_data = HashMap::new();
855        layer_data.insert("test_layer".to_string(), data.clone());
856
857        quantizer
858            .calibrate(&layer_data)
859            .expect("operation should succeed");
860
861        let quantized = quantizer
862            .quantize_tensor(&data, "test_layer")
863            .expect("operation should succeed");
864        let reconstructed = quantizer
865            .dequantize_tensor(&quantized)
866            .expect("operation should succeed");
867
868        // Check that reconstruction is close to original
869        for (orig, recon) in data.iter().zip(reconstructed.iter()) {
870            approx::assert_abs_diff_eq!(orig, recon, epsilon = 0.1);
871        }
872    }
873
874    #[test]
875    fn test_quantization_metrics() {
876        let original = arr2(&[[1.0, 2.0], [3.0, 4.0]]);
877        let _reconstructed = arr2(&[[1.1, 1.9], [3.1, 3.9]]);
878
879        let config = QuantizationConfig::default();
880        let quantizer = Quantizer::<f64>::new(config);
881
882        // Create a mock quantized tensor for testing
883        let quantized_data = arr2(&[[110, 190], [310, 390]]);
884        let params = QuantizationParams::symmetric(4.0, 256);
885        let quantized = QuantizedTensor::new(quantized_data, params, vec![2, 2]);
886
887        let metrics = quantizer
888            .compute_quantization_error(&original, &quantized)
889            .expect("operation should succeed");
890        assert!(metrics.mse >= 0.0);
891        assert!(metrics.compression_ratio > 1.0);
892    }
893
894    #[test]
895    fn test_utility_functions() {
896        let config = utils::int8_ptq_config();
897        assert_eq!(config.quantization_type, QuantizationType::INT8);
898        assert_eq!(config.strategy, QuantizationStrategy::PostTraining);
899
900        let qat_config = utils::int8_qat_config(10, 0.001);
901        assert_eq!(qat_config.strategy, QuantizationStrategy::QuantizationAware);
902        assert_eq!(qat_config.qat_epochs, 10);
903
904        let accuracy_impact = utils::evaluate_accuracy_impact::<f64>(0.95, 0.92);
905        approx::assert_abs_diff_eq!(accuracy_impact, 0.0315789, epsilon = 1e-6);
906    }
907
908    #[test]
909    fn test_sensitivity_analysis() {
910        let mut sensitivity_scores = HashMap::new();
911        sensitivity_scores.insert("layer1".to_string(), 0.1);
912        sensitivity_scores.insert("layer2".to_string(), 0.05);
913        sensitivity_scores.insert("layer3".to_string(), 0.2);
914
915        let selected = utils::select_optimal_parameters(&sensitivity_scores, 0.67);
916        assert_eq!(selected.len(), 2); // 2 out of 3 layers
917        assert!(selected.contains(&"layer2".to_string())); // Least sensitive should be first
918    }
919
920    #[test]
921    fn test_quantized_tensor() {
922        let data = arr2(&[[100, 150], [200, 250]]);
923        let params = QuantizationParams::symmetric(2.0, 256);
924        let quantized = QuantizedTensor::new(data, params, vec![2, 2]);
925
926        let dequantized = quantized
927            .dequantize::<f64>()
928            .expect("operation should succeed");
929        assert!(dequantized.dim() == (2, 2));
930
931        let ratio = quantized.compression_ratio();
932        assert_eq!(ratio, 4.0); // 32-bit to 8-bit
933    }
934}