1use 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#[derive(Debug, Clone, PartialEq, Default)]
50#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
51pub enum QuantizationType {
52 #[default]
54 INT8,
55 INT16,
57 INT4,
59 Binary,
61 Ternary,
63}
64
65#[derive(Debug, Clone, PartialEq, Default)]
67#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
68pub enum QuantizationStrategy {
69 #[default]
71 PostTraining,
72 QuantizationAware,
74 Dynamic,
76 Static,
78}
79
80#[derive(Debug, Clone, PartialEq, Default)]
82#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
83pub enum Granularity {
84 #[default]
86 PerTensor,
87 PerChannel,
89 PerGroup {
91 group_size: usize,
93 },
94}
95
96#[derive(Debug, Clone, PartialEq, Default)]
98#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
99pub enum CalibrationMethod {
100 #[default]
102 MinMax,
103 Percentile {
105 percentile: f64,
107 },
108 Entropy,
110 MSE,
112}
113
114#[derive(Debug, Clone)]
116#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
117pub struct QuantizationConfig {
118 pub quantization_type: QuantizationType,
120 pub strategy: QuantizationStrategy,
122 pub granularity: Granularity,
124 pub calibration_method: CalibrationMethod,
126 pub symmetric: bool,
128 pub quantize_weights: bool,
130 pub quantize_activations: bool,
132 pub skip_layers: Vec<String>,
134 pub qat_epochs: usize,
136 pub qat_learning_rate: Float,
138 pub fake_quantize: bool,
140 pub observer_momentum: Float,
142 pub calibration_samples: usize,
144 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 pub fn quantization_type(mut self, qtype: QuantizationType) -> Self {
172 self.quantization_type = qtype;
173 self
174 }
175
176 pub fn strategy(mut self, strategy: QuantizationStrategy) -> Self {
178 self.strategy = strategy;
179 self
180 }
181
182 pub fn granularity(mut self, granularity: Granularity) -> Self {
184 self.granularity = granularity;
185 self
186 }
187
188 pub fn calibration_method(mut self, method: CalibrationMethod) -> Self {
190 self.calibration_method = method;
191 self
192 }
193
194 pub fn symmetric(mut self, symmetric: bool) -> Self {
196 self.symmetric = symmetric;
197 self
198 }
199
200 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 pub fn skip_layers(mut self, layers: Vec<String>) -> Self {
209 self.skip_layers = layers;
210 self
211 }
212
213 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#[derive(Debug, Clone)]
224#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
225pub struct QuantizationParams {
226 pub scale: f64,
228 pub zero_point: i32,
230 pub min_val: f64,
232 pub max_val: f64,
234 pub n_levels: u32,
236}
237
238impl QuantizationParams {
239 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 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 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#[derive(Debug, Clone)]
278pub struct QuantizedTensor {
279 pub data: Array2<i32>,
281 pub params: QuantizationParams,
283 pub original_shape: Vec<usize>,
285}
286
287impl QuantizedTensor {
288 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 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 pub fn compression_ratio(&self) -> f64 {
312 32.0 / 8.0
314 }
315}
316
317#[derive(Debug, Clone)]
319#[allow(dead_code)] pub struct Observer {
321 min_val: f64,
323 max_val: f64,
325 count: usize,
327 momentum: f64,
329 histogram: Vec<u64>,
331 histogram_bins: usize,
332}
333
334impl Observer {
335 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 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 self.min_val = self.min_val.min(val_f64);
360 self.max_val = self.max_val.max(val_f64);
361 }
362
363 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 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 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 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 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 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 (self.min_val, self.max_val)
457 }
458}
459
460#[allow(dead_code)] pub struct Quantizer<T: FloatBounds> {
463 config: QuantizationConfig,
464 layer_params: HashMap<String, QuantizationParams>,
466 observers: HashMap<String, Observer>,
468 calibration_data: HashMap<String, Vec<Array2<T>>>,
470 _phantom: PhantomData<T>,
471}
472
473impl<T: FloatBounds> Quantizer<T> {
474 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 pub fn calibrate(&mut self, model_data: &HashMap<String, Array2<T>>) -> NeuralResult<()> {
487 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 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 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 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 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 let quantized_val = (val_f64 / params.scale).round() as i32 + params.zero_point;
553
554 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 pub fn dequantize_tensor(&self, quantized: &QuantizedTensor) -> NeuralResult<Array2<T>> {
567 quantized.dequantize()
568 }
569
570 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 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 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 let quantized = self.quantize_tensor(tensor, layer_name)?;
610 self.dequantize_tensor(&quantized)
611 }
612
613 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 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 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 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 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 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 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 let quantized = self.quantize_tensor(data, layer_name)?;
693 let metrics = self.compute_quantization_error(data, &quantized)?;
694
695 sensitivity_scores.insert(layer_name.clone(), metrics.mse);
697 }
698
699 Ok(sensitivity_scores)
700 }
701}
702
703#[derive(Debug, Clone)]
705#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
706pub struct QuantizationMetrics {
707 pub mse: f64,
709 pub snr_db: f64,
711 pub psnr_db: f64,
713 pub compression_ratio: f64,
715 pub original_size: usize,
717 pub quantized_size: usize,
719}
720
721impl QuantizationMetrics {
722 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 pub fn memory_savings(&self) -> f64 {
729 1.0 - (self.quantized_size as f64 / self.original_size as f64)
730 }
731}
732
733pub mod utils {
735 use super::*;
736
737 pub fn int8_ptq_config() -> QuantizationConfig {
739 QuantizationConfig::default()
740 .quantization_type(QuantizationType::INT8)
741 .strategy(QuantizationStrategy::PostTraining)
742 .symmetric(true)
743 }
744
745 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 pub fn dynamic_quantization_config() -> QuantizationConfig {
754 QuantizationConfig::default()
755 .strategy(QuantizationStrategy::Dynamic)
756 .quantize_components(true, false) }
758
759 pub fn per_channel_config() -> QuantizationConfig {
761 QuantizationConfig::default()
762 .granularity(Granularity::PerChannel)
763 .symmetric(false)
764 }
765
766 pub fn binary_quantization_config() -> QuantizationConfig {
768 QuantizationConfig::default()
769 .quantization_type(QuantizationType::Binary)
770 .symmetric(true)
771 }
772
773 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 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 layers.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
793
794 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); }
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 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 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); assert!(selected.contains(&"layer2".to_string())); }
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); }
934}