1use thiserror::Error;
29
30#[derive(Debug, Error, Clone, PartialEq)]
36pub enum QuantizerError {
37 #[error("Input tensor is empty")]
39 EmptyInput,
40
41 #[error("Dimension mismatch: values.len()={values_len} != product(dims)={dims_product}")]
43 DimensionMismatch {
44 values_len: usize,
46 dims_product: usize,
48 },
49
50 #[error("Invalid percentile {0}: must be in [0, 100]")]
52 InvalidPercentile(f64),
53
54 #[error("Dims must be non-empty (scalar tensors are not supported)")]
56 InvalidDims,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum QuantizationMode {
66 Int8Symmetric,
68 Int8Asymmetric,
70 Int4,
72 Fp16,
74 Bf16,
76}
77
78impl QuantizationMode {
79 pub fn name(&self) -> &'static str {
81 match self {
82 Self::Int8Symmetric => "Int8Symmetric",
83 Self::Int8Asymmetric => "Int8Asymmetric",
84 Self::Int4 => "Int4",
85 Self::Fp16 => "Fp16",
86 Self::Bf16 => "Bf16",
87 }
88 }
89
90 pub fn bits_per_element(&self) -> f64 {
92 match self {
93 Self::Int8Symmetric | Self::Int8Asymmetric => 8.0,
94 Self::Int4 => 4.0,
95 Self::Fp16 | Self::Bf16 => 16.0,
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
106pub struct QuantizerConfig {
107 pub mode: QuantizationMode,
109 pub per_channel: bool,
111 pub channel_dim: usize,
113 pub calibration_percentile: f64,
116}
117
118impl Default for QuantizerConfig {
119 fn default() -> Self {
120 Self {
121 mode: QuantizationMode::Int8Symmetric,
122 per_channel: false,
123 channel_dim: 0,
124 calibration_percentile: 99.9,
125 }
126 }
127}
128
129#[derive(Debug, Clone)]
139pub struct QuantizedTensor {
140 pub mode: QuantizationMode,
142 pub data: Vec<i32>,
144 pub scale: f64,
146 pub zero_point: i32,
148 pub original_dims: Vec<usize>,
150 pub original_min: f64,
152 pub original_max: f64,
154 pub(crate) channel_scales: Vec<f64>,
156 pub(crate) channel_zero_points: Vec<i32>,
158}
159
160#[derive(Debug, Clone)]
162pub struct DequantizedTensor {
163 pub values: Vec<f64>,
165 pub dims: Vec<usize>,
167}
168
169#[derive(Debug, Clone, Default)]
175pub struct QuantizerStats {
176 pub elements_quantized: usize,
178 pub avg_compression_ratio: f64,
180 pub avg_quantization_error: f64,
182 pub modes_used: Vec<String>,
184
185 total_cr_weight: f64,
187 total_cr_sum: f64,
188 total_err_weight: f64,
189 total_err_sum: f64,
190}
191
192impl QuantizerStats {
193 fn record(&mut self, n: usize, cr: f64, err: f64, mode_name: &str) {
194 self.elements_quantized += n;
195
196 let w = n as f64;
197
198 self.total_cr_sum += cr * w;
199 self.total_cr_weight += w;
200 self.avg_compression_ratio = if self.total_cr_weight > 0.0 {
201 self.total_cr_sum / self.total_cr_weight
202 } else {
203 0.0
204 };
205
206 self.total_err_sum += err * w;
207 self.total_err_weight += w;
208 self.avg_quantization_error = if self.total_err_weight > 0.0 {
209 self.total_err_sum / self.total_err_weight
210 } else {
211 0.0
212 };
213
214 let mode_str = mode_name.to_string();
215 if !self.modes_used.contains(&mode_str) {
216 self.modes_used.push(mode_str);
217 }
218 }
219}
220
221pub fn percentile(values: &[f64], p: f64) -> Result<f64, QuantizerError> {
230 if !(0.0..=100.0).contains(&p) {
231 return Err(QuantizerError::InvalidPercentile(p));
232 }
233 if values.is_empty() {
234 return Err(QuantizerError::EmptyInput);
235 }
236 let mut sorted = values.to_vec();
237 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
238 let n = sorted.len();
239 if n == 1 {
240 return Ok(sorted[0]);
241 }
242 let idx = if p == 0.0 {
244 0
245 } else {
246 let raw = (p / 100.0 * n as f64).ceil() as usize;
247 raw.saturating_sub(1).min(n - 1)
248 };
249 Ok(sorted[idx])
250}
251
252struct ScaleZp {
257 scale: f64,
258 zero_point: i32,
259}
260
261fn compute_scale_zp(
263 abs_values: &[f64],
264 mode: QuantizationMode,
265 calib_pct: f64,
266) -> Result<ScaleZp, QuantizerError> {
267 let p = percentile(abs_values, calib_pct)?;
268 match mode {
269 QuantizationMode::Int8Symmetric => {
270 let scale = if p == 0.0 { 1.0 } else { p / 127.0 };
271 Ok(ScaleZp {
272 scale,
273 zero_point: 0,
274 })
275 }
276 QuantizationMode::Int8Asymmetric => {
277 let max_val = p;
278 let min_val = -p;
279 let range = max_val - min_val;
280 let scale = if range == 0.0 { 1.0 } else { range / 255.0 };
281 let zero_point = (-min_val / scale).round().clamp(0.0, 255.0) as i32;
282 Ok(ScaleZp { scale, zero_point })
283 }
284 QuantizationMode::Int4 => {
285 let scale = if p == 0.0 { 1.0 } else { p / 7.0 };
286 Ok(ScaleZp {
287 scale,
288 zero_point: 0,
289 })
290 }
291 QuantizationMode::Fp16 | QuantizationMode::Bf16 => Ok(ScaleZp {
293 scale: 1.0,
294 zero_point: 0,
295 }),
296 }
297}
298
299fn quantize_element(x: f64, mode: QuantizationMode, scale: f64, zero_point: i32) -> i32 {
300 match mode {
301 QuantizationMode::Int8Symmetric => (x / scale).round().clamp(-127.0, 127.0) as i32,
302 QuantizationMode::Int8Asymmetric => {
303 ((x / scale).round() + zero_point as f64).clamp(0.0, 255.0) as i32
304 }
305 QuantizationMode::Int4 => (x / scale).round().clamp(-7.0, 7.0) as i32,
306 QuantizationMode::Fp16 => {
307 let clamped = x.clamp(-65504.0, 65504.0);
310 let quantized = (clamped * 1024.0).round();
311 quantized as i32
313 }
314 QuantizationMode::Bf16 => {
315 let bits = (x as f32).to_bits();
317 let bf16_bits = (bits >> 16) as u16;
318 bf16_bits as i32
319 }
320 }
321}
322
323fn dequantize_element(q: i32, mode: QuantizationMode, scale: f64, zero_point: i32) -> f64 {
324 match mode {
325 QuantizationMode::Int8Symmetric => q as f64 * scale,
326 QuantizationMode::Int8Asymmetric => (q - zero_point) as f64 * scale,
327 QuantizationMode::Int4 => q as f64 * scale,
328 QuantizationMode::Fp16 => {
329 q as f64 / 1024.0
331 }
332 QuantizationMode::Bf16 => {
333 let bf16_bits = q as u16;
335 let f32_bits = (bf16_bits as u32) << 16;
336 f32::from_bits(f32_bits) as f64
337 }
338 }
339}
340
341pub struct TensorQuantizer {
350 config: QuantizerConfig,
351 stats: QuantizerStats,
352}
353
354impl TensorQuantizer {
355 pub fn new(config: QuantizerConfig) -> Self {
357 Self {
358 config,
359 stats: QuantizerStats::default(),
360 }
361 }
362
363 pub fn stats(&self) -> &QuantizerStats {
365 &self.stats
366 }
367
368 pub fn reset_stats(&mut self) {
370 self.stats = QuantizerStats::default();
371 }
372
373 pub fn quantize(
378 &mut self,
379 values: &[f64],
380 dims: &[usize],
381 ) -> Result<QuantizedTensor, QuantizerError> {
382 if dims.is_empty() {
384 return Err(QuantizerError::InvalidDims);
385 }
386 if values.is_empty() {
387 return Err(QuantizerError::EmptyInput);
388 }
389 let expected: usize = dims.iter().product();
390 if values.len() != expected {
391 return Err(QuantizerError::DimensionMismatch {
392 values_len: values.len(),
393 dims_product: expected,
394 });
395 }
396
397 let original_min = values.iter().cloned().fold(f64::INFINITY, f64::min);
398 let original_max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
399
400 let qt = if self.config.per_channel {
401 self.quantize_per_channel(values, dims, original_min, original_max)?
402 } else {
403 self.quantize_per_tensor(values, dims, original_min, original_max)?
404 };
405
406 let n = values.len();
408 let cr = Self::compression_ratio(n, &self.config.mode);
409 let err = self.quantization_error_internal(values, &qt).unwrap_or(0.0);
411 self.stats.record(n, cr, err, self.config.mode.name());
412
413 Ok(qt)
414 }
415
416 fn quantize_per_tensor(
417 &self,
418 values: &[f64],
419 dims: &[usize],
420 original_min: f64,
421 original_max: f64,
422 ) -> Result<QuantizedTensor, QuantizerError> {
423 let abs_values: Vec<f64> = values.iter().map(|x| x.abs()).collect();
424 let szp = compute_scale_zp(
425 &abs_values,
426 self.config.mode,
427 self.config.calibration_percentile,
428 )?;
429
430 let data: Vec<i32> = values
431 .iter()
432 .map(|&x| quantize_element(x, self.config.mode, szp.scale, szp.zero_point))
433 .collect();
434
435 Ok(QuantizedTensor {
436 mode: self.config.mode,
437 data,
438 scale: szp.scale,
439 zero_point: szp.zero_point,
440 original_dims: dims.to_vec(),
441 original_min,
442 original_max,
443 channel_scales: Vec::new(),
444 channel_zero_points: Vec::new(),
445 })
446 }
447
448 fn quantize_per_channel(
449 &self,
450 values: &[f64],
451 dims: &[usize],
452 original_min: f64,
453 original_max: f64,
454 ) -> Result<QuantizedTensor, QuantizerError> {
455 let channel_dim = self.config.channel_dim;
456 if channel_dim >= dims.len() {
457 return self.quantize_per_tensor(values, dims, original_min, original_max);
459 }
460
461 let num_channels = dims[channel_dim];
462 let total = values.len();
464 let per_channel = total / num_channels;
465
466 let inner: usize = dims[channel_dim + 1..].iter().product();
470
471 let mut channel_scales = vec![1.0f64; num_channels];
472 let mut channel_zero_points = vec![0i32; num_channels];
473 let mut data = vec![0i32; total];
474
475 for c in 0..num_channels {
476 let channel_vals: Vec<f64> = (0..total)
478 .filter(|&idx| {
479 let stride: usize = if channel_dim + 1 < dims.len() {
481 inner
482 } else {
483 1
484 };
485 (idx / stride) % num_channels == c
486 })
487 .map(|idx| values[idx])
488 .collect();
489
490 if channel_vals.is_empty() {
491 continue;
492 }
493
494 let abs_vals: Vec<f64> = channel_vals.iter().map(|x| x.abs()).collect();
495 let szp = compute_scale_zp(
496 &abs_vals,
497 self.config.mode,
498 self.config.calibration_percentile,
499 )?;
500 channel_scales[c] = szp.scale;
501 channel_zero_points[c] = szp.zero_point;
502
503 let stride = inner;
505 let mut local_idx = 0usize;
506 for (idx, slot) in data.iter_mut().enumerate() {
507 let ch_idx = (idx / stride) % num_channels;
508 if ch_idx == c {
509 *slot = quantize_element(
510 channel_vals[local_idx],
511 self.config.mode,
512 szp.scale,
513 szp.zero_point,
514 );
515 local_idx += 1;
516 }
517 }
518 }
519
520 let global_scale = if num_channels > 0 {
522 channel_scales.iter().sum::<f64>() / num_channels as f64
523 } else {
524 1.0
525 };
526 let global_zp = if num_channels > 0 {
527 (channel_zero_points.iter().map(|&z| z as i64).sum::<i64>() / num_channels as i64)
528 as i32
529 } else {
530 0
531 };
532
533 let _ = per_channel; Ok(QuantizedTensor {
537 mode: self.config.mode,
538 data,
539 scale: global_scale,
540 zero_point: global_zp,
541 original_dims: dims.to_vec(),
542 original_min,
543 original_max,
544 channel_scales,
545 channel_zero_points,
546 })
547 }
548
549 pub fn dequantize(&self, qt: &QuantizedTensor) -> Result<DequantizedTensor, QuantizerError> {
551 if qt.data.is_empty() {
552 return Err(QuantizerError::EmptyInput);
553 }
554
555 let values = if !qt.channel_scales.is_empty() {
556 let num_channels = qt.channel_scales.len();
558 let inner: usize = if qt.original_dims.len() > 1 {
559 let channel_dim = self.config.channel_dim.min(qt.original_dims.len() - 1);
560 qt.original_dims[channel_dim + 1..].iter().product()
561 } else {
562 1
563 };
564 let stride = inner;
565
566 qt.data
567 .iter()
568 .enumerate()
569 .map(|(idx, &q)| {
570 let ch_idx = (idx / stride) % num_channels;
571 let s = qt.channel_scales.get(ch_idx).copied().unwrap_or(qt.scale);
572 let zp = qt
573 .channel_zero_points
574 .get(ch_idx)
575 .copied()
576 .unwrap_or(qt.zero_point);
577 dequantize_element(q, qt.mode, s, zp)
578 })
579 .collect()
580 } else {
581 qt.data
583 .iter()
584 .map(|&q| dequantize_element(q, qt.mode, qt.scale, qt.zero_point))
585 .collect()
586 };
587
588 Ok(DequantizedTensor {
589 values,
590 dims: qt.original_dims.clone(),
591 })
592 }
593
594 pub fn quantization_error(
597 &self,
598 original: &[f64],
599 qt: &QuantizedTensor,
600 ) -> Result<f64, QuantizerError> {
601 self.quantization_error_internal(original, qt)
602 }
603
604 fn quantization_error_internal(
605 &self,
606 original: &[f64],
607 qt: &QuantizedTensor,
608 ) -> Result<f64, QuantizerError> {
609 if original.is_empty() {
610 return Err(QuantizerError::EmptyInput);
611 }
612 if original.len() != qt.data.len() {
613 return Err(QuantizerError::DimensionMismatch {
614 values_len: original.len(),
615 dims_product: qt.data.len(),
616 });
617 }
618 let dq = self.dequantize(qt)?;
619 let mse = original
620 .iter()
621 .zip(dq.values.iter())
622 .map(|(&a, &b)| (a - b).powi(2))
623 .sum::<f64>()
624 / original.len() as f64;
625 Ok(mse)
626 }
627
628 pub fn compression_ratio(original_len: usize, mode: &QuantizationMode) -> f64 {
632 let _ = original_len; 64.0 / mode.bits_per_element()
634 }
635
636 pub fn clamp_to_range(x: f64, mode: &QuantizationMode) -> f64 {
638 match mode {
639 QuantizationMode::Int8Symmetric => x.clamp(-127.0, 127.0),
640 QuantizationMode::Int8Asymmetric => x.clamp(0.0, 255.0),
641 QuantizationMode::Int4 => x.clamp(-7.0, 7.0),
642 QuantizationMode::Fp16 => x.clamp(-65504.0, 65504.0),
643 QuantizationMode::Bf16 => x.clamp(f32::MIN as f64, f32::MAX as f64),
645 }
646 }
647}
648
649#[cfg(test)]
654mod tests {
655 use super::{percentile, QuantizationMode, QuantizerConfig, QuantizerError, TensorQuantizer};
656
657 fn default_quantizer(mode: QuantizationMode) -> TensorQuantizer {
661 TensorQuantizer::new(QuantizerConfig {
662 mode,
663 per_channel: false,
664 channel_dim: 0,
665 calibration_percentile: 99.9,
666 })
667 }
668
669 fn mse(a: &[f64], b: &[f64]) -> f64 {
670 assert_eq!(a.len(), b.len());
671 a.iter()
672 .zip(b.iter())
673 .map(|(&x, &y)| (x - y).powi(2))
674 .sum::<f64>()
675 / a.len() as f64
676 }
677
678 #[test]
683 fn test_percentile_single_element() {
684 let v = vec![42.0];
685 assert_eq!(percentile(&v, 50.0).expect("test: should succeed"), 42.0);
686 assert_eq!(percentile(&v, 0.0).expect("test: should succeed"), 42.0);
687 assert_eq!(percentile(&v, 100.0).expect("test: should succeed"), 42.0);
688 }
689
690 #[test]
691 fn test_percentile_sorted_five() {
692 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
693 assert_eq!(percentile(&v, 0.0).expect("test: should succeed"), 1.0);
695 assert_eq!(percentile(&v, 100.0).expect("test: should succeed"), 5.0);
697 assert_eq!(percentile(&v, 50.0).expect("test: should succeed"), 3.0);
699 }
700
701 #[test]
702 fn test_percentile_unsorted() {
703 let v = vec![5.0, 1.0, 3.0, 2.0, 4.0];
704 assert_eq!(percentile(&v, 100.0).expect("test: should succeed"), 5.0);
705 assert_eq!(percentile(&v, 0.0).expect("test: should succeed"), 1.0);
706 }
707
708 #[test]
709 fn test_percentile_invalid() {
710 let v = vec![1.0, 2.0];
711 assert_eq!(
712 percentile(&v, -1.0).unwrap_err(),
713 QuantizerError::InvalidPercentile(-1.0)
714 );
715 assert_eq!(
716 percentile(&v, 101.0).unwrap_err(),
717 QuantizerError::InvalidPercentile(101.0)
718 );
719 }
720
721 #[test]
722 fn test_percentile_empty() {
723 assert_eq!(
724 percentile(&[], 50.0).unwrap_err(),
725 QuantizerError::EmptyInput
726 );
727 }
728
729 #[test]
734 fn test_empty_input_error() {
735 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
736 assert_eq!(
737 q.quantize(&[], &[0]).unwrap_err(),
738 QuantizerError::EmptyInput
739 );
740 }
741
742 #[test]
743 fn test_empty_dims_error() {
744 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
745 assert_eq!(
746 q.quantize(&[1.0], &[]).unwrap_err(),
747 QuantizerError::InvalidDims
748 );
749 }
750
751 #[test]
752 fn test_dimension_mismatch_error() {
753 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
754 let err = q.quantize(&[1.0, 2.0, 3.0], &[2]).unwrap_err();
755 assert!(matches!(err, QuantizerError::DimensionMismatch { .. }));
756 }
757
758 #[test]
763 fn test_int8sym_quantize_roundtrip() {
764 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
765 let values = vec![1.0_f64, -1.0, 0.5, -0.5, 0.0];
766 let qt = q.quantize(&values, &[5]).expect("test: should succeed");
767 assert_eq!(qt.mode, QuantizationMode::Int8Symmetric);
768 assert_eq!(qt.data.len(), 5);
769 let dq = q.dequantize(&qt).expect("test: should succeed");
770 assert_eq!(dq.values.len(), 5);
771 assert!((dq.values[4] - 0.0).abs() < 0.02);
773 }
774
775 #[test]
776 fn test_int8sym_scale_calculation() {
777 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
778 let values: Vec<f64> = (1..=127).map(|x| x as f64).collect();
779 let qt = q.quantize(&values, &[127]).expect("test: should succeed");
780 assert!((qt.scale - 1.0).abs() < 0.01, "scale={}", qt.scale);
782 }
783
784 #[test]
785 fn test_int8sym_clamp() {
786 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
787 let values = vec![100.0, -100.0, 1000.0, -1000.0];
789 let qt = q.quantize(&values, &[4]).expect("test: should succeed");
790 for &v in &qt.data {
791 assert!((-127..=127).contains(&v), "out of range: {v}");
792 }
793 }
794
795 #[test]
796 fn test_int8sym_zero_point_is_zero() {
797 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
798 let values = vec![0.1, 0.5, -0.3];
799 let qt = q.quantize(&values, &[3]).expect("test: should succeed");
800 assert_eq!(qt.zero_point, 0);
801 }
802
803 #[test]
804 fn test_int8sym_mse_low() {
805 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
806 let values: Vec<f64> = (0..256).map(|i| (i as f64 / 128.0) - 1.0).collect();
807 let qt = q.quantize(&values, &[256]).expect("test: should succeed");
808 let err = q
809 .quantization_error(&values, &qt)
810 .expect("test: should succeed");
811 assert!(err < 1e-4, "MSE too high: {err}");
813 }
814
815 #[test]
820 fn test_int8asym_roundtrip() {
821 let mut q = default_quantizer(QuantizationMode::Int8Asymmetric);
822 let values = vec![0.2_f64, 0.5, -0.5, 0.0, 0.8];
823 let qt = q.quantize(&values, &[5]).expect("test: should succeed");
824 assert_eq!(qt.mode, QuantizationMode::Int8Asymmetric);
825 let dq = q.dequantize(&qt).expect("test: should succeed");
826 let err = mse(&values, &dq.values);
827 assert!(err < 1e-4, "MSE too high: {err}");
828 }
829
830 #[test]
831 fn test_int8asym_data_range() {
832 let mut q = default_quantizer(QuantizationMode::Int8Asymmetric);
833 let values = vec![-1.0, 0.0, 0.5, 1.0];
834 let qt = q.quantize(&values, &[4]).expect("test: should succeed");
835 for &v in &qt.data {
836 assert!((0..=255).contains(&v), "out of [0,255]: {v}");
837 }
838 }
839
840 #[test]
841 fn test_int8asym_zero_point_nonzero() {
842 let mut q = default_quantizer(QuantizationMode::Int8Asymmetric);
843 let values = vec![-1.0, 1.0];
844 let qt = q.quantize(&values, &[2]).expect("test: should succeed");
845 assert!(qt.zero_point > 0, "zero_point={}", qt.zero_point);
847 }
848
849 #[test]
854 fn test_int4_data_range() {
855 let mut q = default_quantizer(QuantizationMode::Int4);
856 let values = vec![-1.0, 0.0, 0.5, -0.5, 1.0, 0.25];
857 let qt = q.quantize(&values, &[6]).expect("test: should succeed");
858 for &v in &qt.data {
859 assert!((-7..=7).contains(&v), "out of [-7,7]: {v}");
860 }
861 }
862
863 #[test]
864 fn test_int4_roundtrip() {
865 let mut q = default_quantizer(QuantizationMode::Int4);
866 let values: Vec<f64> = (-7..=7).map(|x| x as f64 * 0.1).collect();
867 let qt = q.quantize(&values, &[15]).expect("test: should succeed");
868 let dq = q.dequantize(&qt).expect("test: should succeed");
869 let err = mse(&values, &dq.values);
870 assert!(err < 1e-3, "MSE={err}");
871 }
872
873 #[test]
874 fn test_int4_scale() {
875 let mut q = default_quantizer(QuantizationMode::Int4);
876 let values = vec![7.0, -7.0, 3.5];
877 let qt = q.quantize(&values, &[3]).expect("test: should succeed");
878 assert!((qt.scale - 1.0).abs() < 0.01, "scale={}", qt.scale);
880 }
881
882 #[test]
887 fn test_fp16_roundtrip_small() {
888 let mut q = default_quantizer(QuantizationMode::Fp16);
889 let values = vec![1.0_f64, 0.5, -0.5, 0.25, -0.25];
890 let qt = q.quantize(&values, &[5]).expect("test: should succeed");
891 assert_eq!(qt.mode, QuantizationMode::Fp16);
892 let dq = q.dequantize(&qt).expect("test: should succeed");
893 for (&orig, &rec) in values.iter().zip(dq.values.iter()) {
894 assert!((orig - rec).abs() < 0.002, "orig={orig} rec={rec}");
896 }
897 }
898
899 #[test]
900 fn test_fp16_clamp_large() {
901 let mut q = default_quantizer(QuantizationMode::Fp16);
902 let values = vec![1e6_f64, -1e6];
904 let qt = q.quantize(&values, &[2]).expect("test: should succeed");
905 let dq = q.dequantize(&qt).expect("test: should succeed");
906 assert!(dq.values[0] <= 65504.1);
908 assert!(dq.values[1] >= -65504.1);
909 }
910
911 #[test]
912 fn test_fp16_zero() {
913 let mut q = default_quantizer(QuantizationMode::Fp16);
914 let values = vec![0.0_f64];
915 let qt = q.quantize(&values, &[1]).expect("test: should succeed");
916 let dq = q.dequantize(&qt).expect("test: should succeed");
917 assert_eq!(dq.values[0], 0.0);
918 }
919
920 #[test]
921 fn test_fp16_data_stored_as_scaled_int() {
922 let mut q = default_quantizer(QuantizationMode::Fp16);
923 let values = vec![1.0_f64];
924 let qt = q.quantize(&values, &[1]).expect("test: should succeed");
925 assert_eq!(qt.data[0], 1024);
927 }
928
929 #[test]
934 fn test_bf16_roundtrip() {
935 let mut q = default_quantizer(QuantizationMode::Bf16);
936 let values = vec![1.0_f64, 0.5, -0.5, std::f64::consts::PI, -2.71];
937 let qt = q.quantize(&values, &[5]).expect("test: should succeed");
938 assert_eq!(qt.mode, QuantizationMode::Bf16);
939 let dq = q.dequantize(&qt).expect("test: should succeed");
940 for (&orig, &rec) in values.iter().zip(dq.values.iter()) {
942 let rel_err = if orig.abs() > 1e-9 {
943 (orig - rec).abs() / orig.abs()
944 } else {
945 (orig - rec).abs()
946 };
947 assert!(rel_err < 0.02, "orig={orig} rec={rec} rel_err={rel_err}");
948 }
949 }
950
951 #[test]
952 fn test_bf16_stores_u16_bits() {
953 let mut q = default_quantizer(QuantizationMode::Bf16);
954 let values = vec![1.0_f64];
955 let qt = q.quantize(&values, &[1]).expect("test: should succeed");
956 assert_eq!(qt.data[0], 0x3F80i32, "bf16 bits={}", qt.data[0]);
958 }
959
960 #[test]
961 fn test_bf16_zero() {
962 let mut q = default_quantizer(QuantizationMode::Bf16);
963 let values = vec![0.0_f64];
964 let qt = q.quantize(&values, &[1]).expect("test: should succeed");
965 let dq = q.dequantize(&qt).expect("test: should succeed");
966 assert_eq!(dq.values[0], 0.0);
967 }
968
969 #[test]
974 fn test_compression_ratio_int8() {
975 let cr = TensorQuantizer::compression_ratio(100, &QuantizationMode::Int8Symmetric);
976 assert!((cr - 8.0).abs() < 1e-10, "cr={cr}");
977 }
978
979 #[test]
980 fn test_compression_ratio_int4() {
981 let cr = TensorQuantizer::compression_ratio(100, &QuantizationMode::Int4);
982 assert!((cr - 16.0).abs() < 1e-10, "cr={cr}");
983 }
984
985 #[test]
986 fn test_compression_ratio_fp16() {
987 let cr = TensorQuantizer::compression_ratio(100, &QuantizationMode::Fp16);
988 assert!((cr - 4.0).abs() < 1e-10, "cr={cr}");
989 }
990
991 #[test]
992 fn test_compression_ratio_bf16() {
993 let cr = TensorQuantizer::compression_ratio(100, &QuantizationMode::Bf16);
994 assert!((cr - 4.0).abs() < 1e-10, "cr={cr}");
995 }
996
997 #[test]
1002 fn test_clamp_int8sym() {
1003 assert_eq!(
1004 TensorQuantizer::clamp_to_range(200.0, &QuantizationMode::Int8Symmetric),
1005 127.0
1006 );
1007 assert_eq!(
1008 TensorQuantizer::clamp_to_range(-200.0, &QuantizationMode::Int8Symmetric),
1009 -127.0
1010 );
1011 }
1012
1013 #[test]
1014 fn test_clamp_int8asym() {
1015 assert_eq!(
1016 TensorQuantizer::clamp_to_range(-1.0, &QuantizationMode::Int8Asymmetric),
1017 0.0
1018 );
1019 assert_eq!(
1020 TensorQuantizer::clamp_to_range(300.0, &QuantizationMode::Int8Asymmetric),
1021 255.0
1022 );
1023 }
1024
1025 #[test]
1026 fn test_clamp_int4() {
1027 assert_eq!(
1028 TensorQuantizer::clamp_to_range(10.0, &QuantizationMode::Int4),
1029 7.0
1030 );
1031 assert_eq!(
1032 TensorQuantizer::clamp_to_range(-10.0, &QuantizationMode::Int4),
1033 -7.0
1034 );
1035 }
1036
1037 #[test]
1038 fn test_clamp_fp16() {
1039 assert_eq!(
1040 TensorQuantizer::clamp_to_range(1e10, &QuantizationMode::Fp16),
1041 65504.0
1042 );
1043 assert_eq!(
1044 TensorQuantizer::clamp_to_range(-1e10, &QuantizationMode::Fp16),
1045 -65504.0
1046 );
1047 }
1048
1049 #[test]
1054 fn test_quantization_error_zero_tensor() {
1055 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
1056 let values = vec![0.0_f64; 16];
1057 let qt = q.quantize(&values, &[16]).expect("test: should succeed");
1058 let err = q
1059 .quantization_error(&values, &qt)
1060 .expect("test: should succeed");
1061 assert_eq!(err, 0.0);
1062 }
1063
1064 #[test]
1065 fn test_quantization_error_length_mismatch() {
1066 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
1067 let values = vec![1.0, 2.0, 3.0];
1068 let qt = q.quantize(&values, &[3]).expect("test: should succeed");
1069 let err = q.quantization_error(&[1.0, 2.0], &qt);
1070 assert!(matches!(err, Err(QuantizerError::DimensionMismatch { .. })));
1071 }
1072
1073 #[test]
1074 fn test_quantization_error_bf16_low() {
1075 let mut q = default_quantizer(QuantizationMode::Bf16);
1076 let values: Vec<f64> = (0..64).map(|i| i as f64 * 0.01).collect();
1077 let qt = q.quantize(&values, &[64]).expect("test: should succeed");
1078 let err = q
1079 .quantization_error(&values, &qt)
1080 .expect("test: should succeed");
1081 assert!(err < 1e-4, "MSE={err}");
1083 }
1084
1085 #[test]
1090 fn test_per_channel_produces_channel_scales() {
1091 let config = QuantizerConfig {
1092 mode: QuantizationMode::Int8Symmetric,
1093 per_channel: true,
1094 channel_dim: 0,
1095 calibration_percentile: 99.9,
1096 };
1097 let mut q = TensorQuantizer::new(config);
1098 let values = vec![1.0, 2.0, 3.0, 4.0, 0.1, 0.2, 0.3, 0.4];
1100 let qt = q.quantize(&values, &[2, 4]).expect("test: should succeed");
1101 assert_eq!(qt.channel_scales.len(), 2);
1102 assert!(
1104 (qt.channel_scales[0] - qt.channel_scales[1]).abs() > 0.01,
1105 "scales equal: {:?}",
1106 qt.channel_scales
1107 );
1108 }
1109
1110 #[test]
1111 fn test_per_channel_dequantize() {
1112 let config = QuantizerConfig {
1113 mode: QuantizationMode::Int8Symmetric,
1114 per_channel: true,
1115 channel_dim: 0,
1116 calibration_percentile: 100.0,
1117 };
1118 let mut q = TensorQuantizer::new(config);
1119 let values = vec![10.0, 20.0, 30.0, 40.0, 1.0, 2.0, 3.0, 4.0];
1120 let qt = q.quantize(&values, &[2, 4]).expect("test: should succeed");
1121 let dq = q.dequantize(&qt).expect("test: should succeed");
1122 assert_eq!(dq.values.len(), 8);
1123 let err = mse(&values, &dq.values);
1125 assert!(err < 1.0, "MSE={err}");
1126 }
1127
1128 #[test]
1133 fn test_stats_accumulate() {
1134 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
1135 let v1 = vec![1.0, 2.0, 3.0];
1136 let v2 = vec![4.0, 5.0, 6.0, 7.0, 8.0];
1137 q.quantize(&v1, &[3]).expect("test: should succeed");
1138 q.quantize(&v2, &[5]).expect("test: should succeed");
1139 let stats = q.stats();
1140 assert_eq!(stats.elements_quantized, 8);
1141 assert_eq!(stats.modes_used, vec!["Int8Symmetric"]);
1142 assert!(stats.avg_compression_ratio > 0.0);
1143 }
1144
1145 #[test]
1146 fn test_stats_reset() {
1147 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
1148 q.quantize(&[1.0, 2.0], &[2]).expect("test: should succeed");
1149 q.reset_stats();
1150 let stats = q.stats();
1151 assert_eq!(stats.elements_quantized, 0);
1152 assert!(stats.modes_used.is_empty());
1153 }
1154
1155 #[test]
1156 fn test_stats_multiple_modes_if_changed() {
1157 let mut q = default_quantizer(QuantizationMode::Int4);
1159 q.quantize(&[1.0, 2.0], &[2]).expect("test: should succeed");
1160 q.quantize(&[3.0, 4.0], &[2]).expect("test: should succeed");
1161 assert_eq!(q.stats().modes_used.len(), 1);
1162 assert_eq!(q.stats().modes_used[0], "Int4");
1163 }
1164
1165 #[test]
1170 fn test_2d_tensor_int8sym() {
1171 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
1172 let values: Vec<f64> = (0..12).map(|i| i as f64 * 0.1).collect();
1173 let qt = q.quantize(&values, &[3, 4]).expect("test: should succeed");
1174 assert_eq!(qt.original_dims, vec![3, 4]);
1175 assert_eq!(qt.data.len(), 12);
1176 let dq = q.dequantize(&qt).expect("test: should succeed");
1177 assert_eq!(dq.dims, vec![3, 4]);
1178 }
1179
1180 #[test]
1185 fn test_all_zeros_int8sym() {
1186 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
1187 let values = vec![0.0_f64; 8];
1188 let qt = q.quantize(&values, &[8]).expect("test: should succeed");
1189 assert_eq!(qt.scale, 1.0);
1191 let dq = q.dequantize(&qt).expect("test: should succeed");
1192 for v in &dq.values {
1193 assert_eq!(*v, 0.0);
1194 }
1195 }
1196
1197 #[test]
1198 fn test_all_zeros_bf16() {
1199 let mut q = default_quantizer(QuantizationMode::Bf16);
1200 let values = vec![0.0_f64; 4];
1201 let qt = q.quantize(&values, &[4]).expect("test: should succeed");
1202 let dq = q.dequantize(&qt).expect("test: should succeed");
1203 for v in &dq.values {
1204 assert_eq!(*v, 0.0);
1205 }
1206 }
1207
1208 #[test]
1213 fn test_original_min_max_preserved() {
1214 let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
1215 let values = vec![-3.5_f64, 0.0, 7.2];
1216 let qt = q.quantize(&values, &[3]).expect("test: should succeed");
1217 assert!((qt.original_min - (-3.5)).abs() < 1e-10);
1218 assert!((qt.original_max - 7.2).abs() < 1e-10);
1219 }
1220
1221 #[test]
1226 fn test_calibration_percentile_effect() {
1227 let values: Vec<f64> = (1..=100).map(|x| x as f64).collect();
1228
1229 let mut q99 = TensorQuantizer::new(QuantizerConfig {
1230 mode: QuantizationMode::Int8Symmetric,
1231 calibration_percentile: 99.9,
1232 ..QuantizerConfig::default()
1233 });
1234 let mut q50 = TensorQuantizer::new(QuantizerConfig {
1235 mode: QuantizationMode::Int8Symmetric,
1236 calibration_percentile: 50.0,
1237 ..QuantizerConfig::default()
1238 });
1239
1240 let qt99 = q99.quantize(&values, &[100]).expect("test: should succeed");
1241 let qt50 = q50.quantize(&values, &[100]).expect("test: should succeed");
1242
1243 assert!(
1244 qt50.scale < qt99.scale,
1245 "scale_50={} scale_99={}",
1246 qt50.scale,
1247 qt99.scale
1248 );
1249 }
1250}