1use crate::errors::{QuantizeError, Result};
8
9#[derive(Debug, Clone)]
16pub struct QuantConfig {
17 pub bits: u8,
19 pub per_channel: bool,
21 pub symmetric: bool,
25 pub calibration_method: Option<crate::calibration::methods::CalibrationMethod>,
27 pub excluded_layers: Vec<String>,
29 pub layer_bits: std::collections::HashMap<String, u8>,
31 pub min_elements: usize,
34}
35
36impl Default for QuantConfig {
37 fn default() -> Self {
38 Self {
39 bits: 8,
40 per_channel: false,
41 symmetric: false,
42 calibration_method: None,
43 excluded_layers: Vec::new(),
44 layer_bits: std::collections::HashMap::new(),
45 min_elements: 0,
46 }
47 }
48}
49
50impl QuantConfig {
51 pub fn int8() -> Self {
53 Self::default()
54 }
55
56 pub fn with_per_channel(mut self, enabled: bool) -> Self {
58 self.per_channel = enabled;
59 self
60 }
61
62 pub fn with_symmetric(mut self, enabled: bool) -> Self {
64 self.symmetric = enabled;
65 self
66 }
67
68 pub fn with_calibration(
70 mut self,
71 method: crate::calibration::methods::CalibrationMethod,
72 ) -> Self {
73 self.calibration_method = Some(method);
74 self
75 }
76
77 pub fn should_quantize(&self, name: &str, num_elements: usize) -> bool {
83 if self.excluded_layers.iter().any(|e| e == name) {
84 return false;
85 }
86 if self.min_elements > 0 && num_elements < self.min_elements {
87 return false;
88 }
89 true
90 }
91
92 pub fn bits_for_layer(&self, name: &str) -> u8 {
97 self.layer_bits.get(name).copied().unwrap_or(self.bits)
98 }
99}
100
101mod sealed {
109 pub trait Sealed {}
110 impl Sealed for super::Int8Range {}
111 impl Sealed for super::Int4Range {}
112}
113
114pub trait QuantRange: sealed::Sealed + Clone + std::fmt::Debug + Send + Sync + 'static {
119 const QMIN: f32;
121 const QMAX: f32;
123 const BITS: u8;
125}
126
127#[derive(Debug, Clone)]
129pub struct Int8Range;
130impl QuantRange for Int8Range {
131 const QMIN: f32 = -128.0;
132 const QMAX: f32 = 127.0;
133 const BITS: u8 = 8;
134}
135
136#[derive(Debug, Clone)]
138pub struct Int4Range;
139impl QuantRange for Int4Range {
140 const QMIN: f32 = -8.0;
141 const QMAX: f32 = 7.0;
142 const BITS: u8 = 4;
143}
144
145#[derive(Debug, Clone)]
155pub struct QuantParamsGeneric<R: QuantRange> {
156 scale: f32,
157 zero_point: i8,
158 _marker: std::marker::PhantomData<R>,
159}
160
161pub type QuantParams = QuantParamsGeneric<Int8Range>;
163pub type QuantParamsInt4 = QuantParamsGeneric<Int4Range>;
165
166impl<R: QuantRange> QuantParamsGeneric<R> {
167 pub fn scale(&self) -> f32 {
169 self.scale
170 }
171 pub fn zero_point(&self) -> i8 {
173 self.zero_point
174 }
175
176 pub fn from_range(min: f32, max: f32) -> Self {
184 let min = min.min(0.0);
185 let max = max.max(0.0);
186
187 let (min, max) = if (max - min).abs() < 1e-8 {
190 let abs = min.abs().max(max.abs()).max(1e-8);
191 (-abs, abs)
192 } else {
193 (min, max)
194 };
195
196 let scale = (max - min) / (R::QMAX - R::QMIN);
197 let scale = scale.max(1e-8);
198
199 let initial_zero_point = R::QMIN - min / scale;
200 let zero_point = if initial_zero_point.is_finite() {
203 initial_zero_point.round().clamp(R::QMIN, R::QMAX) as i8
204 } else {
205 0i8
206 };
207
208 QuantParamsGeneric {
209 scale,
210 zero_point,
211 _marker: std::marker::PhantomData,
212 }
213 }
214
215 pub fn from_range_symmetric(min: f32, max: f32) -> Self {
225 let abs_max = min.abs().max(max.abs()).max(1e-8);
226 let scale = (abs_max / R::QMAX).max(1e-8);
230 QuantParamsGeneric {
231 scale,
232 zero_point: 0,
233 _marker: std::marker::PhantomData,
234 }
235 }
236
237 pub fn quantize(&self, value: f32) -> i8 {
239 if !value.is_finite() {
240 return self.zero_point;
241 }
242 let quantized = (value / self.scale).round() + (self.zero_point as f32);
243 quantized.clamp(R::QMIN, R::QMAX) as i8
244 }
245
246 pub fn dequantize(&self, value: i8) -> f32 {
248 ((value as i32) - (self.zero_point as i32)) as f32 * self.scale
249 }
250}
251
252#[derive(Debug, Clone)]
261pub struct QuantizedTensorGeneric<R: QuantRange> {
262 pub(crate) data: Vec<i8>,
263 pub(crate) packed_data: Option<Vec<u8>>,
265 pub(crate) shape: Vec<usize>,
266 pub(crate) params: QuantParamsGeneric<R>,
267 pub(crate) per_channel: bool,
268 pub(crate) channel_params: Option<Vec<QuantParamsGeneric<R>>>,
269}
270
271pub type QuantizedTensor = QuantizedTensorGeneric<Int8Range>;
273
274pub type QuantizedTensorInt4 = QuantizedTensorGeneric<Int4Range>;
279
280impl<R: QuantRange> QuantizedTensorGeneric<R> {
285 pub fn shape(&self) -> &[usize] {
287 &self.shape
288 }
289 pub fn params(&self) -> &QuantParamsGeneric<R> {
291 &self.params
292 }
293 pub fn is_per_channel(&self) -> bool {
295 self.per_channel
296 }
297
298 pub fn from_f32(data: &[f32], shape: Vec<usize>) -> Result<Self> {
304 Self::from_f32_with_mode(data, shape, false)
305 }
306
307 pub fn from_f32_symmetric(data: &[f32], shape: Vec<usize>) -> Result<Self> {
312 Self::from_f32_with_mode(data, shape, true)
313 }
314
315 fn from_f32_with_mode(data: &[f32], shape: Vec<usize>, symmetric: bool) -> Result<Self> {
316 if data.is_empty() {
317 return Err(QuantizeError::InvalidTensor {
318 reason: "Cannot quantize empty tensor".into(),
319 });
320 }
321
322 let expected_len: usize = shape.iter().product();
323 if expected_len != data.len() {
324 return Err(QuantizeError::InvalidTensor {
325 reason: format!(
326 "Shape {:?} expects {} elements but got {}",
327 shape,
328 expected_len,
329 data.len()
330 ),
331 });
332 }
333
334 let min = data
335 .iter()
336 .copied()
337 .filter(|v| v.is_finite())
338 .fold(f32::INFINITY, f32::min);
339 let max = data
340 .iter()
341 .copied()
342 .filter(|v| v.is_finite())
343 .fold(f32::NEG_INFINITY, f32::max);
344
345 if !min.is_finite() || !max.is_finite() {
346 return Err(QuantizeError::InvalidTensor {
347 reason: "Tensor contains only non-finite values (NaN/Inf)".into(),
348 });
349 }
350
351 let params = if symmetric {
352 QuantParamsGeneric::<R>::from_range_symmetric(min, max)
353 } else {
354 QuantParamsGeneric::<R>::from_range(min, max)
355 };
356
357 let quantized_data: Vec<i8> = data.iter().map(|&v| params.quantize(v)).collect();
358
359 Ok(QuantizedTensorGeneric {
360 data: quantized_data,
361 packed_data: None,
362 shape,
363 params,
364 per_channel: false,
365 channel_params: None,
366 })
367 }
368
369 pub fn from_f32_with_range(
375 data: &[f32],
376 shape: Vec<usize>,
377 min: f32,
378 max: f32,
379 ) -> Result<Self> {
380 Self::from_f32_with_range_and_mode(data, shape, min, max, false)
381 }
382
383 pub fn from_f32_with_range_symmetric(
386 data: &[f32],
387 shape: Vec<usize>,
388 min: f32,
389 max: f32,
390 ) -> Result<Self> {
391 Self::from_f32_with_range_and_mode(data, shape, min, max, true)
392 }
393
394 fn from_f32_with_range_and_mode(
395 data: &[f32],
396 shape: Vec<usize>,
397 min: f32,
398 max: f32,
399 symmetric: bool,
400 ) -> Result<Self> {
401 if data.is_empty() {
402 return Err(QuantizeError::InvalidTensor {
403 reason: "Cannot quantize empty tensor".into(),
404 });
405 }
406
407 let expected_len: usize = shape.iter().product();
408 if expected_len != data.len() {
409 return Err(QuantizeError::InvalidTensor {
410 reason: format!(
411 "Shape {:?} expects {} elements but got {}",
412 shape,
413 expected_len,
414 data.len()
415 ),
416 });
417 }
418
419 let params = if symmetric {
420 QuantParamsGeneric::<R>::from_range_symmetric(min, max)
421 } else {
422 QuantParamsGeneric::<R>::from_range(min, max)
423 };
424
425 let quantized_data: Vec<i8> = data.iter().map(|&v| params.quantize(v)).collect();
426
427 Ok(QuantizedTensorGeneric {
428 data: quantized_data,
429 packed_data: None,
430 shape,
431 params,
432 per_channel: false,
433 channel_params: None,
434 })
435 }
436
437 pub fn from_f32_per_channel(data: &[f32], shape: Vec<usize>) -> Result<Self> {
444 Self::from_f32_per_channel_with_mode(data, shape, false)
445 }
446
447 pub fn from_f32_per_channel_symmetric(data: &[f32], shape: Vec<usize>) -> Result<Self> {
451 Self::from_f32_per_channel_with_mode(data, shape, true)
452 }
453
454 fn from_f32_per_channel_with_mode(
455 data: &[f32],
456 shape: Vec<usize>,
457 symmetric: bool,
458 ) -> Result<Self> {
459 if data.is_empty() {
460 return Err(QuantizeError::InvalidTensor {
461 reason: "Cannot quantize empty tensor".into(),
462 });
463 }
464
465 if shape.is_empty() {
466 return Err(QuantizeError::InvalidTensor {
467 reason: "Cannot do per-channel quantization on scalar".into(),
468 });
469 }
470
471 let expected_len: usize = shape.iter().product();
472 if expected_len != data.len() {
473 return Err(QuantizeError::InvalidTensor {
474 reason: format!(
475 "Shape {:?} expects {} elements but got {}",
476 shape,
477 expected_len,
478 data.len()
479 ),
480 });
481 }
482
483 let num_channels = shape[0];
484 if num_channels == 0 {
485 return Err(QuantizeError::InvalidTensor {
486 reason: "Number of channels is 0".into(),
487 });
488 }
489 if !data.len().is_multiple_of(num_channels) {
490 return Err(QuantizeError::InvalidTensor {
491 reason: format!(
492 "Data length {} not evenly divisible by {} channels",
493 data.len(),
494 num_channels
495 ),
496 });
497 }
498 let elements_per_channel = data.len() / num_channels;
499
500 let mut channel_params = Vec::with_capacity(num_channels);
501 let mut quantized_data = Vec::with_capacity(data.len());
502
503 for (channel_idx, channel_slice) in data.chunks_exact(elements_per_channel).enumerate() {
507 let mut min = f32::INFINITY;
508 let mut max = f32::NEG_INFINITY;
509 for &v in channel_slice {
510 if v.is_finite() {
511 if v < min {
512 min = v;
513 }
514 if v > max {
515 max = v;
516 }
517 }
518 }
519
520 if !min.is_finite() || !max.is_finite() {
521 return Err(QuantizeError::InvalidTensor {
522 reason: format!(
523 "Channel {} contains only non-finite values (NaN/Inf)",
524 channel_idx
525 ),
526 });
527 }
528
529 let params = if symmetric {
530 QuantParamsGeneric::<R>::from_range_symmetric(min, max)
531 } else {
532 QuantParamsGeneric::<R>::from_range(min, max)
533 };
534
535 quantized_data.extend(channel_slice.iter().map(|&v| params.quantize(v)));
536 channel_params.push(params);
537 }
538
539 let params = channel_params[0].clone();
541
542 Ok(QuantizedTensorGeneric {
543 data: quantized_data,
544 packed_data: None,
545 shape,
546 params,
547 per_channel: true,
548 channel_params: Some(channel_params),
549 })
550 }
551
552 pub fn to_f32(&self) -> Vec<f32> {
554 let data_owned;
556 let data: &[i8] = if let Some(ref packed) = self.packed_data {
557 data_owned = unpack_int4(packed, self.data.len());
558 &data_owned
559 } else {
560 &self.data
561 };
562
563 if self.per_channel {
564 if let Some(ref channel_params) = self.channel_params {
565 if channel_params.is_empty() {
566 return data.iter().map(|&v| self.params.dequantize(v)).collect();
567 }
568 let elements_per_channel = data.len() / channel_params.len();
573 if elements_per_channel == 0 {
574 return data.iter().map(|&v| self.params.dequantize(v)).collect();
577 }
578 let mut out = Vec::with_capacity(data.len());
579 for (chunk, params) in data
580 .chunks_exact(elements_per_channel)
581 .zip(channel_params.iter())
582 {
583 out.extend(chunk.iter().map(|&v| params.dequantize(v)));
584 }
585 out
586 } else {
587 data.iter().map(|&v| self.params.dequantize(v)).collect()
588 }
589 } else {
590 data.iter().map(|&v| self.params.dequantize(v)).collect()
591 }
592 }
593
594 pub fn size_bytes(&self) -> usize {
596 if let Some(ref packed) = self.packed_data {
597 packed.len()
598 } else {
599 self.data.len() * std::mem::size_of::<i8>()
600 }
601 }
602
603 pub fn quantization_error(&self, original: &[f32]) -> f32 {
605 if original.is_empty() {
606 return 0.0;
607 }
608
609 let dequantized = self.to_f32();
610
611 let sum: f32 = original
612 .iter()
613 .zip(dequantized.iter())
614 .map(|(a, b)| (a - b).powi(2))
615 .sum();
616
617 sum / original.len() as f32
618 }
619}
620
621impl QuantizedTensorGeneric<Int4Range> {
626 pub fn pack(&mut self) {
628 self.packed_data = Some(pack_int4(&self.data));
629 }
630
631 pub fn ensure_unpacked(&self) -> Vec<i8> {
633 if let Some(ref packed) = self.packed_data {
634 unpack_int4(packed, self.data.len())
635 } else {
636 self.data.clone()
637 }
638 }
639
640 pub fn is_packed(&self) -> bool {
642 self.packed_data.is_some()
643 }
644
645 pub fn packed_size_bytes(&self) -> usize {
647 if let Some(ref packed) = self.packed_data {
648 packed.len()
649 } else {
650 self.data.len().div_ceil(2)
651 }
652 }
653
654 pub fn unpacked_size_bytes(&self) -> usize {
656 self.data.len() * std::mem::size_of::<i8>()
657 }
658}
659
660fn pack_int4_pair(val1: i8, val2: i8) -> u8 {
665 debug_assert!((-8..=7).contains(&val1), "val1 out of INT4 range: {}", val1);
666 debug_assert!((-8..=7).contains(&val2), "val2 out of INT4 range: {}", val2);
667
668 let nibble1 = (val1 & 0x0F) as u8;
670 let nibble2 = (val2 & 0x0F) as u8;
671
672 (nibble1 << 4) | nibble2
674}
675
676fn unpack_int4_pair(byte: u8) -> (i8, i8) {
677 let nibble1 = (byte >> 4) & 0x0F;
678 let nibble2 = byte & 0x0F;
679
680 let val1 = if nibble1 >= 8 {
682 (nibble1 as i8) | !0x0F
683 } else {
684 nibble1 as i8
685 };
686
687 let val2 = if nibble2 >= 8 {
688 (nibble2 as i8) | !0x0F
689 } else {
690 nibble2 as i8
691 };
692
693 (val1, val2)
694}
695
696pub fn pack_int4(values: &[i8]) -> Vec<u8> {
698 let mut packed = Vec::with_capacity(values.len().div_ceil(2));
699
700 for chunk in values.chunks(2) {
701 let val1 = chunk[0];
702 let val2 = if chunk.len() > 1 { chunk[1] } else { 0 };
703
704 packed.push(pack_int4_pair(val1, val2));
705 }
706
707 packed
708}
709
710pub fn unpack_int4(packed: &[u8], num_values: usize) -> Vec<i8> {
712 let mut values = Vec::with_capacity(num_values);
713
714 for &byte in packed {
715 let (val1, val2) = unpack_int4_pair(byte);
716 values.push(val1);
717 if values.len() < num_values {
718 values.push(val2);
719 }
720 }
721
722 values.truncate(num_values);
724 values
725}
726
727#[derive(Debug, Clone)]
736#[non_exhaustive]
737pub enum QuantizedTensorType {
738 Int8(QuantizedTensor),
739 Int4(QuantizedTensorInt4),
740}
741
742impl QuantizedTensorType {
743 pub fn to_f32(&self) -> Vec<f32> {
745 match self {
746 QuantizedTensorType::Int8(t) => t.to_f32(),
747 QuantizedTensorType::Int4(t) => t.to_f32(),
748 }
749 }
750
751 pub fn size_bytes(&self) -> usize {
753 match self {
754 QuantizedTensorType::Int8(t) => t.size_bytes(),
755 QuantizedTensorType::Int4(t) => t.size_bytes(),
756 }
757 }
758
759 #[must_use]
760 pub fn quantization_error(&self, original: &[f32]) -> f32 {
761 match self {
762 QuantizedTensorType::Int8(t) => t.quantization_error(original),
763 QuantizedTensorType::Int4(t) => t.quantization_error(original),
764 }
765 }
766
767 #[must_use]
768 pub fn data(&self) -> Vec<i8> {
769 match self {
770 QuantizedTensorType::Int8(t) => t.data.clone(),
771 QuantizedTensorType::Int4(t) => t.ensure_unpacked(),
772 }
773 }
774
775 pub fn get_scale_zero_point(&self) -> (f32, i8) {
777 match self {
778 QuantizedTensorType::Int8(t) => (t.params.scale, t.params.zero_point),
779 QuantizedTensorType::Int4(t) => (t.params.scale, t.params.zero_point),
780 }
781 }
782
783 pub fn get_all_scales_zero_points(&self) -> (Vec<f32>, Vec<i8>) {
788 match self {
789 QuantizedTensorType::Int8(t) => {
790 if let Some(ref cp) = t.channel_params {
791 (
792 cp.iter().map(|p| p.scale).collect(),
793 cp.iter().map(|p| p.zero_point).collect(),
794 )
795 } else {
796 (vec![t.params.scale], vec![t.params.zero_point])
797 }
798 }
799 QuantizedTensorType::Int4(t) => {
800 if let Some(ref cp) = t.channel_params {
801 (
802 cp.iter().map(|p| p.scale).collect(),
803 cp.iter().map(|p| p.zero_point).collect(),
804 )
805 } else {
806 (vec![t.params.scale], vec![t.params.zero_point])
807 }
808 }
809 }
810 }
811
812 pub fn is_per_channel(&self) -> bool {
814 match self {
815 QuantizedTensorType::Int8(t) => t.per_channel,
816 QuantizedTensorType::Int4(t) => t.per_channel,
817 }
818 }
819
820 #[must_use]
821 pub fn bits(&self) -> u8 {
822 match self {
823 QuantizedTensorType::Int8(_) => 8,
824 QuantizedTensorType::Int4(_) => 4,
825 }
826 }
827
828 pub fn is_int8(&self) -> bool {
830 matches!(self, QuantizedTensorType::Int8(_))
831 }
832
833 pub fn is_int4(&self) -> bool {
835 matches!(self, QuantizedTensorType::Int4(_))
836 }
837
838 pub fn data_ref(&self) -> Option<&[i8]> {
842 match self {
843 QuantizedTensorType::Int8(t) => Some(&t.data),
844 QuantizedTensorType::Int4(t) => {
845 if t.packed_data.is_some() {
846 None } else {
848 Some(&t.data)
849 }
850 }
851 }
852 }
853}
854
855pub struct Quantizer {
861 config: QuantConfig,
862 calibration_stats:
863 Option<std::collections::HashMap<String, crate::calibration::stats::ActivationStats>>,
864}
865
866impl std::fmt::Debug for Quantizer {
867 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
868 let stats_count = self.calibration_stats.as_ref().map(|m| m.len());
869 f.debug_struct("Quantizer")
870 .field("config", &self.config)
871 .field("calibration_stats_count", &stats_count)
872 .finish()
873 }
874}
875
876impl Quantizer {
877 pub fn new(config: QuantConfig) -> Self {
879 Self {
880 config,
881 calibration_stats: None,
882 }
883 }
884
885 pub fn with_calibration(
887 config: QuantConfig,
888 stats: std::collections::HashMap<String, crate::calibration::stats::ActivationStats>,
889 ) -> Self {
890 Self {
891 config,
892 calibration_stats: Some(stats),
893 }
894 }
895
896 pub fn quantize_tensor_with_name(
898 &self,
899 name: &str,
900 data: &[f32],
901 shape: Vec<usize>,
902 ) -> Result<QuantizedTensorType> {
903 let (min, max) = if let Some(ref stats_map) = self.calibration_stats {
904 if let Some(stats) = stats_map.get(name) {
905 if let Some(method) = self.config.calibration_method {
906 use crate::calibration::stats::calculate_optimal_range_from_stats;
909 calculate_optimal_range_from_stats(stats, method)
910 } else {
911 (stats.min(), stats.max())
912 }
913 } else {
914 finite_min_max(data, name)?
915 }
916 } else {
917 finite_min_max(data, name)?
918 };
919
920 self.quantize_with_range(data, shape, min, max)
921 }
922
923 pub fn quantize_tensor(&self, data: &[f32], shape: Vec<usize>) -> Result<QuantizedTensorType> {
929 self.build_tensor_with_optional_range(data, shape, None)
930 }
931
932 fn quantize_with_range(
939 &self,
940 data: &[f32],
941 shape: Vec<usize>,
942 min: f32,
943 max: f32,
944 ) -> Result<QuantizedTensorType> {
945 self.build_tensor_with_optional_range(data, shape, Some((min, max)))
946 }
947
948 fn build_tensor_with_optional_range(
950 &self,
951 data: &[f32],
952 shape: Vec<usize>,
953 range: Option<(f32, f32)>,
954 ) -> Result<QuantizedTensorType> {
955 let pc = self.config.per_channel && shape.len() >= 2;
956 let sym = self.config.symmetric;
957 match self.config.bits {
958 8 => {
959 let t = match (pc, range, sym) {
960 (true, _, true) => {
961 QuantizedTensor::from_f32_per_channel_symmetric(data, shape)?
962 }
963 (true, _, false) => QuantizedTensor::from_f32_per_channel(data, shape)?,
964 (false, Some((min, max)), true) => {
965 QuantizedTensor::from_f32_with_range_symmetric(data, shape, min, max)?
966 }
967 (false, Some((min, max)), false) => {
968 QuantizedTensor::from_f32_with_range(data, shape, min, max)?
969 }
970 (false, None, true) => QuantizedTensor::from_f32_symmetric(data, shape)?,
971 (false, None, false) => QuantizedTensor::from_f32(data, shape)?,
972 };
973 Ok(QuantizedTensorType::Int8(t))
974 }
975 4 => {
976 let mut t = match (pc, range, sym) {
977 (true, _, true) => {
978 QuantizedTensorInt4::from_f32_per_channel_symmetric(data, shape)?
979 }
980 (true, _, false) => QuantizedTensorInt4::from_f32_per_channel(data, shape)?,
981 (false, Some((min, max)), true) => {
982 QuantizedTensorInt4::from_f32_with_range_symmetric(data, shape, min, max)?
983 }
984 (false, Some((min, max)), false) => {
985 QuantizedTensorInt4::from_f32_with_range(data, shape, min, max)?
986 }
987 (false, None, true) => QuantizedTensorInt4::from_f32_symmetric(data, shape)?,
988 (false, None, false) => QuantizedTensorInt4::from_f32(data, shape)?,
989 };
990 t.pack();
991 Ok(QuantizedTensorType::Int4(t))
992 }
993 b => Err(QuantizeError::UnsupportedConfig {
994 reason: format!("bits must be 4 or 8, got {b}"),
995 }),
996 }
997 }
998
999 pub fn quantize_model(
1009 &self,
1010 model: &crate::onnx_utils::OnnxModel,
1011 ) -> Result<Vec<QuantizedWeightOutput>> {
1012 let weights = model.extract_weights();
1013 self.quantize_weights(&weights)
1014 }
1015
1016 pub fn quantize_weights(
1027 &self,
1028 weights: &[crate::onnx_utils::WeightTensor],
1029 ) -> Result<Vec<QuantizedWeightOutput>> {
1030 use rayon::prelude::*;
1031
1032 let to_quantize: Vec<_> = weights
1033 .iter()
1034 .filter(|w| self.config.should_quantize(&w.name, w.num_elements()))
1035 .collect();
1036
1037 to_quantize
1038 .par_iter()
1039 .map(|w| self.quantize_weight_to_output(w))
1040 .collect()
1041 }
1042
1043 fn quantize_weight_to_output(
1044 &self,
1045 weight: &crate::onnx_utils::WeightTensor,
1046 ) -> Result<QuantizedWeightOutput> {
1047 let layer_bits = self.config.bits_for_layer(&weight.name);
1048
1049 let quantized = if layer_bits == self.config.bits {
1054 self.quantize_tensor_with_name(&weight.name, &weight.data, weight.shape.clone())?
1055 } else {
1056 let layer_config = QuantConfig {
1057 bits: layer_bits,
1058 per_channel: self.config.per_channel,
1059 symmetric: self.config.symmetric,
1060 ..Default::default()
1061 };
1062 Quantizer::new(layer_config).quantize_tensor(&weight.data, weight.shape.clone())?
1063 };
1064
1065 let mse = quantized.quantization_error(&weight.data);
1066 let (scales, zero_points) = quantized.get_all_scales_zero_points();
1067 let is_per_channel = quantized.is_per_channel();
1068 let bits_used = quantized.bits();
1069 let quantized_size_bytes = quantized.size_bytes();
1070
1071 Ok(QuantizedWeightOutput {
1072 qdq: crate::onnx_utils::graph_builder::QdqWeightInput {
1073 original_name: weight.name.clone(),
1074 quantized_values: quantized.data(),
1075 scales,
1076 zero_points,
1077 bits: bits_used,
1078 axis: if is_per_channel { Some(0) } else { None },
1079 },
1080 quantized_size_bytes,
1081 mse,
1082 })
1083 }
1084}
1085
1086#[derive(Debug, Clone)]
1096#[non_exhaustive]
1097pub struct QuantizedWeightOutput {
1098 pub qdq: crate::onnx_utils::graph_builder::QdqWeightInput,
1100 pub quantized_size_bytes: usize,
1103 pub mse: f32,
1105}
1106
1107fn finite_min_max(data: &[f32], name: &str) -> Result<(f32, f32)> {
1113 let min = data
1114 .iter()
1115 .copied()
1116 .filter(|v| v.is_finite())
1117 .fold(f32::INFINITY, f32::min);
1118 let max = data
1119 .iter()
1120 .copied()
1121 .filter(|v| v.is_finite())
1122 .fold(f32::NEG_INFINITY, f32::max);
1123 if !min.is_finite() || !max.is_finite() {
1124 return Err(QuantizeError::InvalidTensor {
1125 reason: format!(
1126 "Tensor '{}' contains only non-finite values (NaN/Inf)",
1127 name
1128 ),
1129 });
1130 }
1131 Ok((min, max))
1132}
1133
1134#[cfg(test)]
1135mod tests {
1136 use super::*;
1137
1138 #[test]
1143 fn test_should_quantize_no_restrictions() {
1144 let config = QuantConfig::default();
1145 assert!(config.should_quantize("any.layer", 1));
1146 assert!(config.should_quantize("any.layer", 1_000_000));
1147 }
1148
1149 #[test]
1150 fn test_should_quantize_excluded_layer() {
1151 let config = QuantConfig {
1152 excluded_layers: vec!["head.weight".to_string()],
1153 ..Default::default()
1154 };
1155 assert!(!config.should_quantize("head.weight", 1024));
1156 assert!(config.should_quantize("body.weight", 1024));
1157 }
1158
1159 #[test]
1160 fn test_should_quantize_min_elements() {
1161 let config = QuantConfig {
1162 min_elements: 512,
1163 ..Default::default()
1164 };
1165 assert!(!config.should_quantize("small.bias", 4));
1166 assert!(!config.should_quantize("small.bias", 511));
1167 assert!(config.should_quantize("large.weight", 512));
1168 assert!(config.should_quantize("large.weight", 1024));
1169 }
1170
1171 #[test]
1172 fn test_should_quantize_excluded_takes_priority_over_min_elements() {
1173 let config = QuantConfig {
1174 excluded_layers: vec!["head.weight".to_string()],
1175 min_elements: 1,
1176 ..Default::default()
1177 };
1178 assert!(!config.should_quantize("head.weight", 1_000_000));
1180 }
1181
1182 #[test]
1183 fn test_bits_for_layer_default() {
1184 let config = QuantConfig {
1185 bits: 8,
1186 ..Default::default()
1187 };
1188 assert_eq!(config.bits_for_layer("any.weight"), 8);
1189 }
1190
1191 #[test]
1192 fn test_bits_for_layer_override() {
1193 let mut layer_bits = std::collections::HashMap::new();
1194 layer_bits.insert("head.weight".to_string(), 4u8);
1195 let config = QuantConfig {
1196 bits: 8,
1197 layer_bits,
1198 ..Default::default()
1199 };
1200 assert_eq!(config.bits_for_layer("head.weight"), 4);
1201 assert_eq!(config.bits_for_layer("body.weight"), 8);
1202 }
1203
1204 #[test]
1209 fn test_quant_params() {
1210 let params = QuantParams::from_range(-1.0, 1.0);
1211
1212 assert_eq!(params.quantize(0.0), params.zero_point);
1213
1214 let original = 0.5;
1215 let quantized = params.quantize(original);
1216 let dequantized = params.dequantize(quantized);
1217
1218 assert!((original - dequantized).abs() < 0.01);
1219 }
1220
1221 #[test]
1222 fn test_quantize_tensor() {
1223 let data = vec![0.0, 0.5, 1.0, -0.5, -1.0];
1224 let shape = vec![5];
1225
1226 let quantized = QuantizedTensor::from_f32(&data, shape).unwrap();
1227
1228 assert_eq!(quantized.data.len(), 5);
1229 assert_eq!(quantized.size_bytes(), 5);
1230 }
1231
1232 #[test]
1233 fn test_per_channel_quantization() {
1234 let mut data: Vec<f32> = Vec::with_capacity(200);
1235 data.extend(std::iter::repeat_n(0.5_f32, 100)); data.extend(std::iter::repeat_n(5.0_f32, 100)); let shape = vec![2, 100];
1239
1240 let quantized = QuantizedTensor::from_f32_per_channel(&data, shape).unwrap();
1241
1242 assert!(quantized.per_channel);
1243 assert!(quantized.channel_params.is_some());
1244 assert_eq!(quantized.channel_params.as_ref().unwrap().len(), 2);
1245
1246 let dequantized = quantized.to_f32();
1247 let error: f32 = data
1248 .iter()
1249 .zip(dequantized.iter())
1250 .map(|(a, b)| (a - b).powi(2))
1251 .sum::<f32>()
1252 / data.len() as f32;
1253
1254 println!("Per-channel MSE: {}", error);
1255 assert!(error < 0.1);
1256 }
1257
1258 #[test]
1259 fn test_per_channel_vs_per_tensor() {
1260 let mut data: Vec<f32> = Vec::with_capacity(2000);
1261 data.extend(std::iter::repeat_n(0.01_f32, 1000));
1262 data.extend(std::iter::repeat_n(10.0_f32, 1000));
1263
1264 let shape = vec![2, 1000];
1265
1266 let per_tensor = QuantizedTensor::from_f32(&data, shape.clone()).unwrap();
1268 let per_tensor_error = per_tensor.quantization_error(&data);
1269
1270 let per_channel = QuantizedTensor::from_f32_per_channel(&data, shape).unwrap();
1272 let per_channel_error = per_channel.quantization_error(&data);
1273
1274 println!("Per-tensor error: {:.8}", per_tensor_error);
1275 println!("Per-channel error: {:.8}", per_channel_error);
1276
1277 assert!(per_channel_error < per_tensor_error);
1279 assert!(per_channel_error < per_tensor_error * 0.5);
1280 }
1281
1282 #[test]
1283 fn test_per_channel_benefit() {
1284 let mut data = vec![];
1285
1286 for i in 0..1000 {
1287 data.push(-0.1 + (i as f32 / 1000.0) * 0.2);
1288 }
1289
1290 for i in 0..1000 {
1291 data.push(-10.0 + (i as f32 / 1000.0) * 20.0);
1292 }
1293
1294 let shape = vec![2, 1000];
1295
1296 let per_tensor = QuantizedTensor::from_f32(&data, shape.clone()).unwrap();
1297 let per_tensor_error = per_tensor.quantization_error(&data);
1298
1299 let per_channel = QuantizedTensor::from_f32_per_channel(&data, shape).unwrap();
1300 let per_channel_error = per_channel.quantization_error(&data);
1301
1302 println!("Per-tensor MSE: {:.8}", per_tensor_error);
1303 println!("Per-channel MSE: {:.8}", per_channel_error);
1304
1305 assert!(
1306 per_channel_error < per_tensor_error,
1307 "Per-channel ({:.8}) should be better than per-tensor ({:.8})",
1308 per_channel_error,
1309 per_tensor_error
1310 );
1311 }
1312
1313 #[test]
1314 fn test_int4_quant_params() {
1315 let params = QuantParamsInt4::from_range(-1.0, 1.0);
1316
1317 assert!(params.quantize(-10.0) >= -8);
1318 assert!(params.quantize(-10.0) <= 7);
1319 assert!(params.quantize(10.0) >= -8);
1320 assert!(params.quantize(10.0) <= 7);
1321
1322 let zero_quant = params.quantize(0.0);
1323 assert!((-8..=7).contains(&zero_quant));
1324
1325 for &original in &[-1.0, -0.5, 0.0, 0.5, 1.0] {
1326 let quantized = params.quantize(original);
1327 let dequantized = params.dequantize(quantized);
1328
1329 println!(
1330 "Original: {:.2}, Quantized: {}, Dequantized: {:.2}, Error: {:.4}",
1331 original,
1332 quantized,
1333 dequantized,
1334 (original - dequantized).abs()
1335 );
1336
1337 assert!((original - dequantized).abs() < params.scale * 2.0);
1338 }
1339 }
1340
1341 #[test]
1342 fn test_int4_extreme_values() {
1343 let params = QuantParamsInt4::from_range(-100.0, 100.0);
1345
1346 let q_neg = params.quantize(-100.0);
1347 let q_pos = params.quantize(100.0);
1348
1349 assert_eq!(q_neg, -8);
1350 assert_eq!(q_pos, 7);
1351 }
1352
1353 #[test]
1354 fn test_int4_vs_int8_error() {
1355 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
1356
1357 let params_int8 = QuantParams::from_range(-1.0, 1.0);
1358 let error_int8: f32 = data
1359 .iter()
1360 .map(|&v| {
1361 let q = params_int8.quantize(v);
1362 let dq = params_int8.dequantize(q);
1363 (v - dq).powi(2)
1364 })
1365 .sum::<f32>()
1366 / data.len() as f32;
1367
1368 let params_int4 = QuantParamsInt4::from_range(-1.0, 1.0);
1369 let error_int4: f32 = data
1370 .iter()
1371 .map(|&v| {
1372 let q = params_int4.quantize(v);
1373 let dq = params_int4.dequantize(q);
1374 (v - dq).powi(2)
1375 })
1376 .sum::<f32>()
1377 / data.len() as f32;
1378
1379 println!("INT8 MSE: {:.8}", error_int8);
1380 println!("INT4 MSE: {:.8}", error_int4);
1381
1382 assert!(error_int4 > error_int8);
1383
1384 assert!(
1385 error_int4 < error_int8 * 500.0,
1386 "INT4 error ({:.8}) is too high compared to INT8 ({:.8})",
1387 error_int4,
1388 error_int8
1389 );
1390
1391 assert!(error_int4.is_finite());
1392 assert!(error_int4 < 0.01);
1393 }
1394
1395 #[test]
1396 fn test_int4_range() {
1397 let params = QuantParamsInt4::from_range(-1.0, 1.0);
1398
1399 assert!(params.quantize(-10.0) == -8);
1400 assert!(params.quantize(10.0) == 7);
1401
1402 for i in -8..=7 {
1404 let value = i as f32 * params.scale;
1405 let quantized = params.quantize(value);
1406 assert!((-8..=7).contains(&quantized));
1407 }
1408 }
1409
1410 #[test]
1411 fn test_int4_optimal_precision() {
1412 let params = QuantParamsInt4::from_range(-1.0, 1.0);
1413
1414 let mut unique_values = std::collections::HashSet::new();
1415
1416 for i in 0..1000 {
1418 let value = -1.0 + (i as f32 / 1000.0) * 2.0;
1419 unique_values.insert(params.quantize(value));
1420 }
1421
1422 println!("Unique quantized values: {}", unique_values.len());
1423 assert!(unique_values.len() >= 14);
1424 }
1425
1426 #[test]
1427 fn test_int4_tensor_quantization() {
1428 let data = vec![0.0, 0.5, 1.0, -0.5, -1.0];
1429 let shape = vec![5];
1430
1431 let quantized = QuantizedTensorInt4::from_f32(&data, shape).unwrap();
1432
1433 assert_eq!(quantized.data.len(), 5);
1434 assert_eq!(quantized.size_bytes(), 5);
1435 assert_eq!(quantized.packed_size_bytes(), 3);
1436
1437 for &val in &quantized.data {
1438 assert!((-8..=7).contains(&val), "Value {} out of INT4 range", val);
1439 }
1440 }
1441
1442 #[test]
1443 fn test_int4_round_trip() {
1444 let original = vec![-1.0, -0.5, 0.0, 0.5, 1.0];
1445 let shape = vec![5];
1446
1447 let quantized = QuantizedTensorInt4::from_f32(&original, shape).unwrap();
1448 let dequantized = quantized.to_f32();
1449
1450 println!("Original: {:?}", original);
1451 println!("Quantized: {:?}", quantized.data);
1452 println!("Dequantized: {:?}", dequantized);
1453
1454 for (orig, deq) in original.iter().zip(dequantized.iter()) {
1455 let error = (orig - deq).abs();
1456 println!(" {:.2} -> {:.2}, error: {:.4}", orig, deq, error);
1457 assert!(error < 0.15, "Error too large: {}", error);
1458 }
1459 }
1460
1461 #[test]
1462 fn test_int4_per_channel() {
1463 let mut data = vec![];
1464
1465 for i in 0..100 {
1467 data.push(-0.1 + (i as f32 / 100.0) * 0.2);
1468 }
1469
1470 for i in 0..100 {
1472 data.push(-10.0 + (i as f32 / 100.0) * 20.0);
1473 }
1474
1475 let shape = vec![2, 100];
1476
1477 let quantized = QuantizedTensorInt4::from_f32_per_channel(&data, shape).unwrap();
1478
1479 assert!(quantized.per_channel);
1480 assert!(quantized.channel_params.is_some());
1481 assert_eq!(quantized.channel_params.as_ref().unwrap().len(), 2);
1482
1483 let error = quantized.quantization_error(&data);
1484 println!("INT4 per-channel MSE: {:.8}", error);
1485
1486 assert!(error < 1.0, "Error too high: {}", error);
1487 }
1488
1489 #[test]
1490 fn test_int4_vs_int8_compression() {
1491 let data: Vec<f32> = (0..1000).map(|i| (i as f32 / 1000.0) * 2.0 - 1.0).collect();
1492 let shape = vec![1000];
1493
1494 let int8_quantized = QuantizedTensor::from_f32(&data, shape.clone()).unwrap();
1495 let int8_size = int8_quantized.size_bytes();
1496 let int8_error = int8_quantized.quantization_error(&data);
1497
1498 let int4_quantized = QuantizedTensorInt4::from_f32(&data, shape).unwrap();
1499 let int4_size = int4_quantized.size_bytes();
1500 let int4_packed_size = int4_quantized.packed_size_bytes();
1501 let int4_error = int4_quantized.quantization_error(&data);
1502
1503 println!("INT8: {} bytes, MSE: {:.8}", int8_size, int8_error);
1504 println!(
1505 "INT4 (unpacked): {} bytes, MSE: {:.8}",
1506 int4_size, int4_error
1507 );
1508 println!(
1509 "INT4 (packed): {} bytes, MSE: {:.8}",
1510 int4_packed_size, int4_error
1511 );
1512
1513 assert_eq!(int4_size, int8_size);
1514
1515 assert!(int4_packed_size <= int8_size / 2 + 1);
1516
1517 assert!(int4_error > int8_error);
1518
1519 assert!(int4_error < 0.01, "INT4 error too high: {}", int4_error);
1520 }
1521
1522 #[test]
1523 fn test_int4_large_tensor() {
1524 let size = 64 * 3 * 3 * 3; let data: Vec<f32> = (0..size)
1526 .map(|i| ((i as f32 / size as f32) * 2.0 - 1.0) * 0.5)
1527 .collect();
1528
1529 let shape = vec![64, 3, 3, 3];
1530
1531 let quantized = QuantizedTensorInt4::from_f32_per_channel(&data, shape).unwrap();
1532
1533 assert_eq!(quantized.data.len(), size);
1534 assert_eq!(quantized.channel_params.as_ref().unwrap().len(), 64);
1535
1536 let error = quantized.quantization_error(&data);
1537 println!("Large tensor INT4 error: {:.8}", error);
1538
1539 assert!(error < 0.01, "Error too high for large tensor: {}", error);
1540 }
1541
1542 #[test]
1543 fn test_int4_extreme_ranges() {
1544 let test_cases = vec![
1545 (vec![-0.001, 0.0, 0.001], "tiny range"),
1546 (vec![-100.0, 0.0, 100.0], "large range"),
1547 (vec![0.0, 0.0, 0.0], "all zeros"),
1548 (vec![1.0, 1.0, 1.0], "all same"),
1549 ];
1550
1551 for (data, desc) in test_cases {
1552 println!("\nTesting: {}", desc);
1553 let shape = vec![data.len()];
1554
1555 let result = QuantizedTensorInt4::from_f32(&data, shape);
1556 assert!(result.is_ok(), "Failed on {}", desc);
1557
1558 let quantized = result.unwrap();
1559 let dequantized = quantized.to_f32();
1560
1561 println!(" Original: {:?}", data);
1562 println!(" Dequantized: {:?}", dequantized);
1563
1564 for &val in &quantized.data {
1565 assert!(
1566 (-8..=7).contains(&val),
1567 "Value {} out of range for {}",
1568 val,
1569 desc
1570 );
1571 }
1572 }
1573 }
1574
1575 #[test]
1576 fn test_int4_pack_unpack_pair() {
1577 let test_cases = vec![
1578 (-8, 7),
1579 (-8, -8),
1580 (7, 7),
1581 (0, 0),
1582 (-1, 0),
1583 (0, -1),
1584 (-5, 3),
1585 (6, -4),
1586 ];
1587
1588 for (val1, val2) in test_cases {
1589 println!("\nTesting: ({}, {})", val1, val2);
1590
1591 let packed = pack_int4_pair(val1, val2);
1592 let (unpacked1, unpacked2) = unpack_int4_pair(packed);
1593
1594 println!(" Packed: 0x{:02X} (binary: {:08b})", packed, packed);
1595 println!(" Unpacked: ({}, {})", unpacked1, unpacked2);
1596
1597 assert_eq!(val1, unpacked1, "First value mismatch");
1598 assert_eq!(val2, unpacked2, "Second value mismatch");
1599 }
1600 }
1601
1602 #[test]
1603 fn test_int4_pack_unpack_vector() {
1604 let values = vec![-8, -7, -1, 0, 1, 7];
1605 let packed = pack_int4(&values);
1606 let unpacked = unpack_int4(&packed, values.len());
1607
1608 println!("\nEven length:");
1609 println!(" Original: {:?}", values);
1610 println!(" Packed: {:?} ({} bytes)", packed, packed.len());
1611 println!(" Unpacked: {:?}", unpacked);
1612
1613 assert_eq!(values, unpacked);
1614 assert_eq!(packed.len(), values.len().div_ceil(2));
1615 }
1616
1617 #[test]
1618 fn test_int4_pack_unpack_odd_length() {
1619 let values = vec![-8, -5, 0, 5, 7];
1620 let packed = pack_int4(&values);
1621 let unpacked = unpack_int4(&packed, values.len());
1622
1623 println!("\nOdd length:");
1624 println!(" Original: {:?}", values);
1625 println!(" Packed: {:?} ({} bytes)", packed, packed.len());
1626 println!(" Unpacked: {:?}", unpacked);
1627
1628 assert_eq!(values, unpacked);
1629 assert_eq!(packed.len(), values.len().div_ceil(2));
1630 }
1631
1632 #[test]
1633 fn test_int4_pack_all_values() {
1634 let values: Vec<i8> = (-8..=7).collect();
1635 let packed = pack_int4(&values);
1636 let unpacked = unpack_int4(&packed, values.len());
1637
1638 println!("\nAll INT4 values:");
1639 println!(" Original: {:?}", values);
1640 println!(" Packed: {} bytes", packed.len());
1641 println!(" Unpacked: {:?}", unpacked);
1642
1643 assert_eq!(values, unpacked);
1644 assert_eq!(packed.len(), 8);
1645 }
1646
1647 #[test]
1648 fn test_int4_pack_large_vector() {
1649 let values: Vec<i8> = (0..1000).map(|i| ((i % 16) - 8) as i8).collect();
1650 let packed = pack_int4(&values);
1651 let unpacked = unpack_int4(&packed, values.len());
1652
1653 assert_eq!(values, unpacked);
1654 assert_eq!(packed.len(), 500);
1655
1656 println!("\nLarge vector:");
1657 println!(" Original: {} values", values.len());
1658 println!(
1659 " Packed: {} bytes ({}x compression)",
1660 packed.len(),
1661 values.len() / packed.len()
1662 );
1663 println!(" Unpacked: {} values", unpacked.len());
1664 }
1665
1666 #[test]
1667 fn test_int4_compression_ratio() {
1668 let size = 10000;
1669 let values: Vec<i8> = (0..size).map(|i| ((i % 16) - 8) as i8).collect();
1670
1671 let unpacked_size = values.len() * std::mem::size_of::<i8>();
1672
1673 let packed = pack_int4(&values);
1674 let packed_size = packed.len();
1675
1676 let compression_ratio = unpacked_size as f32 / packed_size as f32;
1677
1678 println!("\nCompression test:");
1679 println!(" Values: {}", size);
1680 println!(" Unpacked: {} bytes", unpacked_size);
1681 println!(" Packed: {} bytes", packed_size);
1682 println!(" Compression: {:.2}x", compression_ratio);
1683
1684 assert!(
1685 (compression_ratio - 2.0).abs() < 0.01,
1686 "Expected ~2x compression, got {:.2}x",
1687 compression_ratio
1688 );
1689 }
1690
1691 #[test]
1692 fn test_int4_tensor_packing() {
1693 let data: Vec<f32> = (0..1000).map(|i| (i as f32 / 1000.0) * 2.0 - 1.0).collect();
1694 let shape = vec![1000];
1695
1696 let mut quantized = QuantizedTensorInt4::from_f32(&data, shape).unwrap();
1697
1698 println!("Before packing:");
1699 println!(" Unpacked size: {} bytes", quantized.unpacked_size_bytes());
1700 println!(" Is packed: {}", quantized.is_packed());
1701
1702 assert!(!quantized.is_packed());
1703 assert_eq!(quantized.size_bytes(), 1000);
1704
1705 quantized.pack();
1706
1707 println!("\nAfter packing:");
1708 println!(" Packed size: {} bytes", quantized.size_bytes());
1709 println!(" Is packed: {}", quantized.is_packed());
1710 println!(
1711 " Compression: {}x",
1712 quantized.unpacked_size_bytes() / quantized.size_bytes()
1713 );
1714
1715 assert!(quantized.is_packed());
1716 assert_eq!(quantized.size_bytes(), 500);
1717
1718 let dequantized = quantized.to_f32();
1719 assert_eq!(dequantized.len(), 1000);
1720
1721 let error = quantized.quantization_error(&data);
1722 println!(" MSE after packing: {:.8}", error);
1723 assert!(error < 0.01);
1724 }
1725
1726 #[test]
1727 fn test_int4_packed_vs_unpacked_error() {
1728 let data: Vec<f32> = (0..100).map(|i| (i as f32 / 100.0) * 2.0 - 1.0).collect();
1729 let shape = vec![100];
1730
1731 let unpacked = QuantizedTensorInt4::from_f32(&data, shape.clone()).unwrap();
1732 let error_unpacked = unpacked.quantization_error(&data);
1733
1734 let mut packed = QuantizedTensorInt4::from_f32(&data, shape).unwrap();
1735 packed.pack();
1736 let error_packed = packed.quantization_error(&data);
1737
1738 println!("Unpacked error: {:.8}", error_unpacked);
1739 println!("Packed error: {:.8}", error_packed);
1740
1741 assert!((error_unpacked - error_packed).abs() < 1e-6);
1742 }
1743
1744 #[test]
1745 fn test_int4_per_channel_packing() {
1746 let mut data = vec![];
1747 for i in 0..500 {
1748 data.push((i as f32 / 500.0) * 0.2 - 0.1); }
1750 for i in 0..500 {
1751 data.push((i as f32 / 500.0) * 20.0 - 10.0); }
1753
1754 let shape = vec![2, 500];
1755
1756 let mut quantized = QuantizedTensorInt4::from_f32_per_channel(&data, shape).unwrap();
1757
1758 let error_before = quantized.quantization_error(&data);
1759 println!("Error before packing: {:.8}", error_before);
1760
1761 quantized.pack();
1762
1763 let error_after = quantized.quantization_error(&data);
1764 println!("Error after packing: {:.8}", error_after);
1765 println!(
1766 "Size: {} bytes (packed from {} bytes)",
1767 quantized.size_bytes(),
1768 quantized.unpacked_size_bytes()
1769 );
1770
1771 assert!((error_before - error_after).abs() < 1e-6);
1772
1773 assert_eq!(quantized.size_bytes(), 500);
1774 }
1775
1776 #[test]
1777 fn test_int4_compression_comparison() {
1778 let size = 10000;
1779 let data: Vec<f32> = (0..size)
1780 .map(|i| ((i as f32 / size as f32) * 2.0 - 1.0) * 0.5)
1781 .collect();
1782 let shape = vec![size];
1783
1784 let fp32_size = size * std::mem::size_of::<f32>();
1785
1786 let int8 = QuantizedTensor::from_f32(&data, shape.clone()).unwrap();
1787 let int8_size = int8.size_bytes();
1788
1789 let int4_unpacked = QuantizedTensorInt4::from_f32(&data, shape.clone()).unwrap();
1790 let int4_unpacked_size = int4_unpacked.size_bytes();
1791
1792 let mut int4_packed = QuantizedTensorInt4::from_f32(&data, shape).unwrap();
1793 int4_packed.pack();
1794 let int4_packed_size = int4_packed.size_bytes();
1795
1796 println!("\nCompression Comparison:");
1797 println!(" FP32: {} bytes", fp32_size);
1798 println!(
1799 " INT8: {} bytes ({:.1}x)",
1800 int8_size,
1801 fp32_size as f32 / int8_size as f32
1802 );
1803 println!(
1804 " INT4 unpacked: {} bytes ({:.1}x)",
1805 int4_unpacked_size,
1806 fp32_size as f32 / int4_unpacked_size as f32
1807 );
1808 println!(
1809 " INT4 packed: {} bytes ({:.1}x)",
1810 int4_packed_size,
1811 fp32_size as f32 / int4_packed_size as f32
1812 );
1813
1814 assert_eq!(fp32_size / int8_size, 4); assert_eq!(fp32_size / int4_packed_size, 8); }
1817
1818 #[test]
1819 #[ignore] fn test_int4_real_model() {
1821 use crate::onnx_utils::OnnxModel;
1822
1823 println!("\n{}", "=".repeat(60));
1824 println!("INT4 Real Model Test");
1825 println!("\n{}", "=".repeat(60));
1826
1827 let model_paths = vec![
1828 "test_models/mnist.onnx",
1829 "mnist.onnx",
1830 "test_models/resnet18-v1-7.onnx",
1831 "resnet18-v1-7.onnx",
1832 ];
1833
1834 let mut model = None;
1835 for path in &model_paths {
1836 if std::path::Path::new(path).exists() {
1837 println!("Loading model: {}", path);
1838 match OnnxModel::load(path) {
1839 Ok(m) => {
1840 model = Some(m);
1841 break;
1842 }
1843 Err(e) => println!(" Failed: {}", e),
1844 }
1845 }
1846 }
1847
1848 let model = match model {
1849 Some(m) => m,
1850 None => {
1851 println!("No test models found. Skipping test.");
1852 println!("Place mnist.onnx or resnet18-v1-7.onnx in current directory.");
1853 return;
1854 }
1855 };
1856
1857 let info = model.info();
1858 println!("✓ Model loaded: {}", info.name);
1859 println!(" Nodes: {}", info.num_nodes);
1860 println!();
1861
1862 println!("Extracting weights...");
1863 let weights = model.extract_weights();
1864 println!("✓ Found {} weight tensors", weights.len());
1865
1866 if weights.is_empty() {
1867 println!("No weights to quantize!");
1868 return;
1869 }
1870
1871 println!();
1872 println!("\n{}", "=".repeat(60));
1873 println!("Testing Per-Tensor Quantization");
1874 println!("\n{}", "=".repeat(60));
1875
1876 let test_weights: Vec<_> = weights
1877 .iter()
1878 .filter(|w| w.data.len() > 1000)
1879 .take(5)
1880 .collect();
1881
1882 println!("Testing {} large layers:\n", test_weights.len());
1883
1884 for (idx, weight) in test_weights.iter().enumerate() {
1885 let name = if weight.name.len() > 40 {
1886 format!("{}...", &weight.name[..37])
1887 } else {
1888 weight.name.clone()
1889 };
1890
1891 println!("[{}] {}", idx + 1, name);
1892 println!(
1893 " Shape: {:?}, Elements: {}",
1894 weight.shape,
1895 weight.data.len()
1896 );
1897
1898 let fp32_size = weight.data.len() * 4;
1899
1900 let int8_result = QuantizedTensor::from_f32(&weight.data, weight.shape.clone());
1901 let (int8_size, int8_error) = if let Ok(q) = int8_result {
1902 (q.size_bytes(), q.quantization_error(&weight.data))
1903 } else {
1904 println!(" INT8 failed!");
1905 continue;
1906 };
1907
1908 let int4_result = QuantizedTensorInt4::from_f32(&weight.data, weight.shape.clone());
1909 let (int4_unpacked_size, int4_error) = if let Ok(q) = int4_result {
1910 (q.size_bytes(), q.quantization_error(&weight.data))
1911 } else {
1912 println!(" INT4 failed!");
1913 continue;
1914 };
1915
1916 let mut int4_packed =
1917 QuantizedTensorInt4::from_f32(&weight.data, weight.shape.clone()).unwrap();
1918 int4_packed.pack();
1919 let int4_packed_size = int4_packed.size_bytes();
1920 let int4_packed_error = int4_packed.quantization_error(&weight.data);
1921
1922 println!(" FP32: {:7} bytes", fp32_size);
1923 println!(
1924 " INT8: {:7} bytes ({:.1}x) MSE: {:.8}",
1925 int8_size,
1926 fp32_size as f32 / int8_size as f32,
1927 int8_error
1928 );
1929 println!(
1930 " INT4 unpacked: {:7} bytes ({:.1}x) MSE: {:.8}",
1931 int4_unpacked_size,
1932 fp32_size as f32 / int4_unpacked_size as f32,
1933 int4_error
1934 );
1935 println!(
1936 " INT4 packed: {:7} bytes ({:.1}x) MSE: {:.8}",
1937 int4_packed_size,
1938 fp32_size as f32 / int4_packed_size as f32,
1939 int4_packed_error
1940 );
1941
1942 assert_eq!(int4_error, int4_packed_error, "Packing changed error!");
1943
1944 let int8_ratio = fp32_size as f32 / int8_size as f32;
1945 let int4_ratio = fp32_size as f32 / int4_packed_size as f32;
1946
1947 assert!(
1948 (int8_ratio - 4.0).abs() < 0.1,
1949 "INT8 compression should be ~4x"
1950 );
1951 assert!(
1952 (int4_ratio - 8.0).abs() < 0.1,
1953 "INT4 compression should be ~8x"
1954 );
1955
1956 println!();
1957 }
1958
1959 println!("\n{}", "=".repeat(60));
1960 println!("Testing Per-Channel Quantization");
1961 println!("\n{}", "=".repeat(60));
1962
1963 let conv_weights: Vec<_> = weights
1965 .iter()
1966 .filter(|w| w.shape.len() >= 2 && w.shape[0] > 1)
1967 .take(3)
1968 .collect();
1969
1970 if conv_weights.is_empty() {
1971 println!("No multi-channel layers found for per-channel test.");
1972 } else {
1973 println!("Testing {} conv layers:\n", conv_weights.len());
1974
1975 for (idx, weight) in conv_weights.iter().enumerate() {
1976 let name = if weight.name.len() > 40 {
1977 format!("{}...", &weight.name[..37])
1978 } else {
1979 weight.name.clone()
1980 };
1981
1982 println!("[{}] {}", idx + 1, name);
1983 println!(
1984 " Shape: {:?}, Channels: {}",
1985 weight.shape, weight.shape[0]
1986 );
1987
1988 let per_tensor =
1989 QuantizedTensorInt4::from_f32(&weight.data, weight.shape.clone()).unwrap();
1990 let per_tensor_error = per_tensor.quantization_error(&weight.data);
1991
1992 let per_channel_result =
1993 QuantizedTensorInt4::from_f32_per_channel(&weight.data, weight.shape.clone());
1994
1995 if let Ok(per_channel) = per_channel_result {
1996 let per_channel_error = per_channel.quantization_error(&weight.data);
1997
1998 let improvement =
1999 ((per_tensor_error - per_channel_error) / per_tensor_error) * 100.0;
2000
2001 println!(" Per-tensor: MSE: {:.8}", per_tensor_error);
2002 println!(
2003 " Per-channel: MSE: {:.8} ({:.1}% better)",
2004 per_channel_error, improvement
2005 );
2006
2007 assert!(
2008 per_channel_error <= per_tensor_error * 1.1,
2009 "Per-channel should not be significantly worse"
2010 );
2011 } else {
2012 println!(" Per-channel failed!");
2013 }
2014
2015 println!();
2016 }
2017 }
2018
2019 println!("\n{}", "=".repeat(60));
2020 println!("Summary");
2021 println!("\n{}", "=".repeat(60));
2022
2023 println!("✓ INT4 quantization works on real model weights");
2024 println!("✓ Compression ratios correct (4x INT8, 8x INT4)");
2025 println!("✓ Bit packing is lossless");
2026 println!("✓ Per-channel quantization works");
2027 println!("\nINT4 implementation is ready for CLI integration!");
2028 }
2029
2030 #[test]
2035 fn test_all_nan_returns_error() {
2036 let data = vec![f32::NAN, f32::NAN, f32::NAN];
2037 let result = QuantizedTensor::from_f32(&data, vec![3]);
2038 assert!(result.is_err());
2039 let err = result.unwrap_err().to_string();
2040 assert!(
2041 err.contains("non-finite"),
2042 "error should mention non-finite: {}",
2043 err
2044 );
2045 }
2046
2047 #[test]
2048 fn test_all_inf_returns_error() {
2049 let data = vec![f32::INFINITY, f32::NEG_INFINITY];
2050 let result = QuantizedTensor::from_f32(&data, vec![2]);
2051 assert!(result.is_err());
2052 }
2053
2054 #[test]
2055 fn test_all_nan_int4_returns_error() {
2056 let data = vec![f32::NAN; 4];
2057 let result = QuantizedTensorInt4::from_f32(&data, vec![4]);
2058 assert!(result.is_err());
2059 }
2060
2061 #[test]
2062 fn test_all_nan_per_channel_returns_error() {
2063 let data = vec![f32::NAN; 6];
2064 let result = QuantizedTensor::from_f32_per_channel(&data, vec![2, 3]);
2065 assert!(result.is_err());
2066 let err = result.unwrap_err().to_string();
2067 assert!(
2068 err.contains("Channel 0"),
2069 "error should mention channel: {}",
2070 err
2071 );
2072 }
2073
2074 #[test]
2075 fn test_mixed_nan_finite_succeeds() {
2076 let data = vec![f32::NAN, 1.0, -1.0, f32::NAN];
2078 let result = QuantizedTensor::from_f32(&data, vec![4]);
2079 assert!(result.is_ok());
2080 }
2081
2082 #[test]
2087 fn test_int8_symmetric_params_zero_point_is_zero() {
2088 let params = QuantParams::from_range_symmetric(-0.5, 2.0);
2089 assert_eq!(params.zero_point(), 0, "symmetric must have zp=0");
2090 let expected_scale = 2.0_f32 / 127.0;
2092 assert!(
2093 (params.scale() - expected_scale).abs() < 1e-6,
2094 "scale {} vs expected {}",
2095 params.scale(),
2096 expected_scale
2097 );
2098 }
2099
2100 #[test]
2101 fn test_int4_symmetric_params_zero_point_is_zero() {
2102 let params = QuantParamsInt4::from_range_symmetric(-3.0, 1.0);
2103 assert_eq!(params.zero_point(), 0);
2104 let expected_scale = 3.0_f32 / 7.0;
2106 assert!((params.scale() - expected_scale).abs() < 1e-6);
2107 }
2108
2109 #[test]
2110 fn test_symmetric_zero_dequantizes_to_zero() {
2111 let params = QuantParams::from_range_symmetric(-10.0, 10.0);
2113 let q = params.quantize(0.0);
2114 assert_eq!(q, 0);
2115 let dq = params.dequantize(q);
2116 assert_eq!(dq, 0.0);
2117 }
2118
2119 #[test]
2120 fn test_symmetric_asymmetric_produce_different_scales() {
2121 let asym = QuantParams::from_range(0.0, 10.0);
2123 let sym = QuantParams::from_range_symmetric(0.0, 10.0);
2124 assert_ne!(asym.zero_point(), sym.zero_point());
2125 assert!(
2128 sym.scale() > asym.scale(),
2129 "symmetric scale {} should exceed asymmetric {}",
2130 sym.scale(),
2131 asym.scale()
2132 );
2133 }
2134
2135 #[test]
2136 fn test_symmetric_constant_tensor_handled() {
2137 let params = QuantParams::from_range_symmetric(0.0, 0.0);
2139 assert!(params.scale() > 0.0);
2140 assert_eq!(params.zero_point(), 0);
2141 }
2142
2143 #[test]
2144 fn test_from_f32_symmetric_tensor_has_zero_zp() {
2145 let data: Vec<f32> = (0..100).map(|i| (i as f32 - 50.0) * 0.1).collect();
2146 let tensor = QuantizedTensor::from_f32_symmetric(&data, vec![100]).unwrap();
2147 assert_eq!(tensor.params().zero_point(), 0);
2148 }
2149
2150 #[test]
2151 fn test_from_f32_per_channel_symmetric_every_channel_zp_zero() {
2152 let mut data = Vec::new();
2154 for ch in 0..4 {
2155 let scale = (ch + 1) as f32;
2156 for i in 0..16 {
2157 data.push((i as f32 - 8.0) * 0.1 * scale);
2158 }
2159 }
2160 let tensor = QuantizedTensor::from_f32_per_channel_symmetric(&data, vec![4, 16]).unwrap();
2161
2162 let channel_params = tensor
2163 .channel_params
2164 .as_ref()
2165 .expect("per-channel expected");
2166 assert_eq!(channel_params.len(), 4);
2167 for (i, p) in channel_params.iter().enumerate() {
2168 assert_eq!(p.zero_point(), 0, "channel {} zp should be 0", i);
2169 assert!(p.scale() > 0.0, "channel {} scale must be positive", i);
2170 }
2171 }
2172
2173 #[test]
2174 fn test_symmetric_round_trip_error_bounded() {
2175 let data: Vec<f32> = (0..500).map(|i| (i as f32 - 250.0) / 250.0).collect();
2176 let tensor = QuantizedTensor::from_f32_symmetric(&data, vec![500]).unwrap();
2177 let mse = tensor.quantization_error(&data);
2178 assert!(mse < 1e-3, "symmetric MSE unexpectedly high: {}", mse);
2180 }
2181
2182 #[test]
2183 fn test_int4_symmetric_round_trip_error_bounded() {
2184 let data: Vec<f32> = (0..500).map(|i| (i as f32 - 250.0) / 250.0).collect();
2185 let tensor = QuantizedTensorInt4::from_f32_symmetric(&data, vec![500]).unwrap();
2186 let mse = tensor.quantization_error(&data);
2187 assert!(mse < 0.01, "INT4 symmetric MSE too high: {}", mse);
2189 }
2190
2191 #[test]
2192 fn test_quantizer_symmetric_config_routes_correctly() {
2193 let data: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.1).collect();
2194 let config = QuantConfig {
2195 bits: 8,
2196 per_channel: true,
2197 symmetric: true,
2198 ..Default::default()
2199 };
2200 let q = Quantizer::new(config)
2201 .quantize_tensor(&data, vec![4, 16])
2202 .unwrap();
2203 let (_, zero_points) = q.get_all_scales_zero_points();
2204 assert!(
2205 zero_points.iter().all(|&z| z == 0),
2206 "all zero_points must be 0 under symmetric config, got {:?}",
2207 zero_points
2208 );
2209 }
2210}