use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq)]
pub enum QuantizerError {
#[error("Input tensor is empty")]
EmptyInput,
#[error("Dimension mismatch: values.len()={values_len} != product(dims)={dims_product}")]
DimensionMismatch {
values_len: usize,
dims_product: usize,
},
#[error("Invalid percentile {0}: must be in [0, 100]")]
InvalidPercentile(f64),
#[error("Dims must be non-empty (scalar tensors are not supported)")]
InvalidDims,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QuantizationMode {
Int8Symmetric,
Int8Asymmetric,
Int4,
Fp16,
Bf16,
}
impl QuantizationMode {
pub fn name(&self) -> &'static str {
match self {
Self::Int8Symmetric => "Int8Symmetric",
Self::Int8Asymmetric => "Int8Asymmetric",
Self::Int4 => "Int4",
Self::Fp16 => "Fp16",
Self::Bf16 => "Bf16",
}
}
pub fn bits_per_element(&self) -> f64 {
match self {
Self::Int8Symmetric | Self::Int8Asymmetric => 8.0,
Self::Int4 => 4.0,
Self::Fp16 | Self::Bf16 => 16.0,
}
}
}
#[derive(Debug, Clone)]
pub struct QuantizerConfig {
pub mode: QuantizationMode,
pub per_channel: bool,
pub channel_dim: usize,
pub calibration_percentile: f64,
}
impl Default for QuantizerConfig {
fn default() -> Self {
Self {
mode: QuantizationMode::Int8Symmetric,
per_channel: false,
channel_dim: 0,
calibration_percentile: 99.9,
}
}
}
#[derive(Debug, Clone)]
pub struct QuantizedTensor {
pub mode: QuantizationMode,
pub data: Vec<i32>,
pub scale: f64,
pub zero_point: i32,
pub original_dims: Vec<usize>,
pub original_min: f64,
pub original_max: f64,
pub(crate) channel_scales: Vec<f64>,
pub(crate) channel_zero_points: Vec<i32>,
}
#[derive(Debug, Clone)]
pub struct DequantizedTensor {
pub values: Vec<f64>,
pub dims: Vec<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct QuantizerStats {
pub elements_quantized: usize,
pub avg_compression_ratio: f64,
pub avg_quantization_error: f64,
pub modes_used: Vec<String>,
total_cr_weight: f64,
total_cr_sum: f64,
total_err_weight: f64,
total_err_sum: f64,
}
impl QuantizerStats {
fn record(&mut self, n: usize, cr: f64, err: f64, mode_name: &str) {
self.elements_quantized += n;
let w = n as f64;
self.total_cr_sum += cr * w;
self.total_cr_weight += w;
self.avg_compression_ratio = if self.total_cr_weight > 0.0 {
self.total_cr_sum / self.total_cr_weight
} else {
0.0
};
self.total_err_sum += err * w;
self.total_err_weight += w;
self.avg_quantization_error = if self.total_err_weight > 0.0 {
self.total_err_sum / self.total_err_weight
} else {
0.0
};
let mode_str = mode_name.to_string();
if !self.modes_used.contains(&mode_str) {
self.modes_used.push(mode_str);
}
}
}
pub fn percentile(values: &[f64], p: f64) -> Result<f64, QuantizerError> {
if !(0.0..=100.0).contains(&p) {
return Err(QuantizerError::InvalidPercentile(p));
}
if values.is_empty() {
return Err(QuantizerError::EmptyInput);
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = sorted.len();
if n == 1 {
return Ok(sorted[0]);
}
let idx = if p == 0.0 {
0
} else {
let raw = (p / 100.0 * n as f64).ceil() as usize;
raw.saturating_sub(1).min(n - 1)
};
Ok(sorted[idx])
}
struct ScaleZp {
scale: f64,
zero_point: i32,
}
fn compute_scale_zp(
abs_values: &[f64],
mode: QuantizationMode,
calib_pct: f64,
) -> Result<ScaleZp, QuantizerError> {
let p = percentile(abs_values, calib_pct)?;
match mode {
QuantizationMode::Int8Symmetric => {
let scale = if p == 0.0 { 1.0 } else { p / 127.0 };
Ok(ScaleZp {
scale,
zero_point: 0,
})
}
QuantizationMode::Int8Asymmetric => {
let max_val = p;
let min_val = -p;
let range = max_val - min_val;
let scale = if range == 0.0 { 1.0 } else { range / 255.0 };
let zero_point = (-min_val / scale).round().clamp(0.0, 255.0) as i32;
Ok(ScaleZp { scale, zero_point })
}
QuantizationMode::Int4 => {
let scale = if p == 0.0 { 1.0 } else { p / 7.0 };
Ok(ScaleZp {
scale,
zero_point: 0,
})
}
QuantizationMode::Fp16 | QuantizationMode::Bf16 => Ok(ScaleZp {
scale: 1.0,
zero_point: 0,
}),
}
}
fn quantize_element(x: f64, mode: QuantizationMode, scale: f64, zero_point: i32) -> i32 {
match mode {
QuantizationMode::Int8Symmetric => (x / scale).round().clamp(-127.0, 127.0) as i32,
QuantizationMode::Int8Asymmetric => {
((x / scale).round() + zero_point as f64).clamp(0.0, 255.0) as i32
}
QuantizationMode::Int4 => (x / scale).round().clamp(-7.0, 7.0) as i32,
QuantizationMode::Fp16 => {
let clamped = x.clamp(-65504.0, 65504.0);
let quantized = (clamped * 1024.0).round();
quantized as i32
}
QuantizationMode::Bf16 => {
let bits = (x as f32).to_bits();
let bf16_bits = (bits >> 16) as u16;
bf16_bits as i32
}
}
}
fn dequantize_element(q: i32, mode: QuantizationMode, scale: f64, zero_point: i32) -> f64 {
match mode {
QuantizationMode::Int8Symmetric => q as f64 * scale,
QuantizationMode::Int8Asymmetric => (q - zero_point) as f64 * scale,
QuantizationMode::Int4 => q as f64 * scale,
QuantizationMode::Fp16 => {
q as f64 / 1024.0
}
QuantizationMode::Bf16 => {
let bf16_bits = q as u16;
let f32_bits = (bf16_bits as u32) << 16;
f32::from_bits(f32_bits) as f64
}
}
}
pub struct TensorQuantizer {
config: QuantizerConfig,
stats: QuantizerStats,
}
impl TensorQuantizer {
pub fn new(config: QuantizerConfig) -> Self {
Self {
config,
stats: QuantizerStats::default(),
}
}
pub fn stats(&self) -> &QuantizerStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = QuantizerStats::default();
}
pub fn quantize(
&mut self,
values: &[f64],
dims: &[usize],
) -> Result<QuantizedTensor, QuantizerError> {
if dims.is_empty() {
return Err(QuantizerError::InvalidDims);
}
if values.is_empty() {
return Err(QuantizerError::EmptyInput);
}
let expected: usize = dims.iter().product();
if values.len() != expected {
return Err(QuantizerError::DimensionMismatch {
values_len: values.len(),
dims_product: expected,
});
}
let original_min = values.iter().cloned().fold(f64::INFINITY, f64::min);
let original_max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let qt = if self.config.per_channel {
self.quantize_per_channel(values, dims, original_min, original_max)?
} else {
self.quantize_per_tensor(values, dims, original_min, original_max)?
};
let n = values.len();
let cr = Self::compression_ratio(n, &self.config.mode);
let err = self.quantization_error_internal(values, &qt).unwrap_or(0.0);
self.stats.record(n, cr, err, self.config.mode.name());
Ok(qt)
}
fn quantize_per_tensor(
&self,
values: &[f64],
dims: &[usize],
original_min: f64,
original_max: f64,
) -> Result<QuantizedTensor, QuantizerError> {
let abs_values: Vec<f64> = values.iter().map(|x| x.abs()).collect();
let szp = compute_scale_zp(
&abs_values,
self.config.mode,
self.config.calibration_percentile,
)?;
let data: Vec<i32> = values
.iter()
.map(|&x| quantize_element(x, self.config.mode, szp.scale, szp.zero_point))
.collect();
Ok(QuantizedTensor {
mode: self.config.mode,
data,
scale: szp.scale,
zero_point: szp.zero_point,
original_dims: dims.to_vec(),
original_min,
original_max,
channel_scales: Vec::new(),
channel_zero_points: Vec::new(),
})
}
fn quantize_per_channel(
&self,
values: &[f64],
dims: &[usize],
original_min: f64,
original_max: f64,
) -> Result<QuantizedTensor, QuantizerError> {
let channel_dim = self.config.channel_dim;
if channel_dim >= dims.len() {
return self.quantize_per_tensor(values, dims, original_min, original_max);
}
let num_channels = dims[channel_dim];
let total = values.len();
let per_channel = total / num_channels;
let inner: usize = dims[channel_dim + 1..].iter().product();
let mut channel_scales = vec![1.0f64; num_channels];
let mut channel_zero_points = vec![0i32; num_channels];
let mut data = vec![0i32; total];
for c in 0..num_channels {
let channel_vals: Vec<f64> = (0..total)
.filter(|&idx| {
let stride: usize = if channel_dim + 1 < dims.len() {
inner
} else {
1
};
(idx / stride) % num_channels == c
})
.map(|idx| values[idx])
.collect();
if channel_vals.is_empty() {
continue;
}
let abs_vals: Vec<f64> = channel_vals.iter().map(|x| x.abs()).collect();
let szp = compute_scale_zp(
&abs_vals,
self.config.mode,
self.config.calibration_percentile,
)?;
channel_scales[c] = szp.scale;
channel_zero_points[c] = szp.zero_point;
let stride = inner;
let mut local_idx = 0usize;
for (idx, slot) in data.iter_mut().enumerate() {
let ch_idx = (idx / stride) % num_channels;
if ch_idx == c {
*slot = quantize_element(
channel_vals[local_idx],
self.config.mode,
szp.scale,
szp.zero_point,
);
local_idx += 1;
}
}
}
let global_scale = if num_channels > 0 {
channel_scales.iter().sum::<f64>() / num_channels as f64
} else {
1.0
};
let global_zp = if num_channels > 0 {
(channel_zero_points.iter().map(|&z| z as i64).sum::<i64>() / num_channels as i64)
as i32
} else {
0
};
let _ = per_channel;
Ok(QuantizedTensor {
mode: self.config.mode,
data,
scale: global_scale,
zero_point: global_zp,
original_dims: dims.to_vec(),
original_min,
original_max,
channel_scales,
channel_zero_points,
})
}
pub fn dequantize(&self, qt: &QuantizedTensor) -> Result<DequantizedTensor, QuantizerError> {
if qt.data.is_empty() {
return Err(QuantizerError::EmptyInput);
}
let values = if !qt.channel_scales.is_empty() {
let num_channels = qt.channel_scales.len();
let inner: usize = if qt.original_dims.len() > 1 {
let channel_dim = self.config.channel_dim.min(qt.original_dims.len() - 1);
qt.original_dims[channel_dim + 1..].iter().product()
} else {
1
};
let stride = inner;
qt.data
.iter()
.enumerate()
.map(|(idx, &q)| {
let ch_idx = (idx / stride) % num_channels;
let s = qt.channel_scales.get(ch_idx).copied().unwrap_or(qt.scale);
let zp = qt
.channel_zero_points
.get(ch_idx)
.copied()
.unwrap_or(qt.zero_point);
dequantize_element(q, qt.mode, s, zp)
})
.collect()
} else {
qt.data
.iter()
.map(|&q| dequantize_element(q, qt.mode, qt.scale, qt.zero_point))
.collect()
};
Ok(DequantizedTensor {
values,
dims: qt.original_dims.clone(),
})
}
pub fn quantization_error(
&self,
original: &[f64],
qt: &QuantizedTensor,
) -> Result<f64, QuantizerError> {
self.quantization_error_internal(original, qt)
}
fn quantization_error_internal(
&self,
original: &[f64],
qt: &QuantizedTensor,
) -> Result<f64, QuantizerError> {
if original.is_empty() {
return Err(QuantizerError::EmptyInput);
}
if original.len() != qt.data.len() {
return Err(QuantizerError::DimensionMismatch {
values_len: original.len(),
dims_product: qt.data.len(),
});
}
let dq = self.dequantize(qt)?;
let mse = original
.iter()
.zip(dq.values.iter())
.map(|(&a, &b)| (a - b).powi(2))
.sum::<f64>()
/ original.len() as f64;
Ok(mse)
}
pub fn compression_ratio(original_len: usize, mode: &QuantizationMode) -> f64 {
let _ = original_len; 64.0 / mode.bits_per_element()
}
pub fn clamp_to_range(x: f64, mode: &QuantizationMode) -> f64 {
match mode {
QuantizationMode::Int8Symmetric => x.clamp(-127.0, 127.0),
QuantizationMode::Int8Asymmetric => x.clamp(0.0, 255.0),
QuantizationMode::Int4 => x.clamp(-7.0, 7.0),
QuantizationMode::Fp16 => x.clamp(-65504.0, 65504.0),
QuantizationMode::Bf16 => x.clamp(f32::MIN as f64, f32::MAX as f64),
}
}
}
#[cfg(test)]
mod tests {
use super::{percentile, QuantizationMode, QuantizerConfig, QuantizerError, TensorQuantizer};
fn default_quantizer(mode: QuantizationMode) -> TensorQuantizer {
TensorQuantizer::new(QuantizerConfig {
mode,
per_channel: false,
channel_dim: 0,
calibration_percentile: 99.9,
})
}
fn mse(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(a.len(), b.len());
a.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y).powi(2))
.sum::<f64>()
/ a.len() as f64
}
#[test]
fn test_percentile_single_element() {
let v = vec![42.0];
assert_eq!(percentile(&v, 50.0).expect("test: should succeed"), 42.0);
assert_eq!(percentile(&v, 0.0).expect("test: should succeed"), 42.0);
assert_eq!(percentile(&v, 100.0).expect("test: should succeed"), 42.0);
}
#[test]
fn test_percentile_sorted_five() {
let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
assert_eq!(percentile(&v, 0.0).expect("test: should succeed"), 1.0);
assert_eq!(percentile(&v, 100.0).expect("test: should succeed"), 5.0);
assert_eq!(percentile(&v, 50.0).expect("test: should succeed"), 3.0);
}
#[test]
fn test_percentile_unsorted() {
let v = vec![5.0, 1.0, 3.0, 2.0, 4.0];
assert_eq!(percentile(&v, 100.0).expect("test: should succeed"), 5.0);
assert_eq!(percentile(&v, 0.0).expect("test: should succeed"), 1.0);
}
#[test]
fn test_percentile_invalid() {
let v = vec![1.0, 2.0];
assert_eq!(
percentile(&v, -1.0).unwrap_err(),
QuantizerError::InvalidPercentile(-1.0)
);
assert_eq!(
percentile(&v, 101.0).unwrap_err(),
QuantizerError::InvalidPercentile(101.0)
);
}
#[test]
fn test_percentile_empty() {
assert_eq!(
percentile(&[], 50.0).unwrap_err(),
QuantizerError::EmptyInput
);
}
#[test]
fn test_empty_input_error() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
assert_eq!(
q.quantize(&[], &[0]).unwrap_err(),
QuantizerError::EmptyInput
);
}
#[test]
fn test_empty_dims_error() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
assert_eq!(
q.quantize(&[1.0], &[]).unwrap_err(),
QuantizerError::InvalidDims
);
}
#[test]
fn test_dimension_mismatch_error() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let err = q.quantize(&[1.0, 2.0, 3.0], &[2]).unwrap_err();
assert!(matches!(err, QuantizerError::DimensionMismatch { .. }));
}
#[test]
fn test_int8sym_quantize_roundtrip() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values = vec![1.0_f64, -1.0, 0.5, -0.5, 0.0];
let qt = q.quantize(&values, &[5]).expect("test: should succeed");
assert_eq!(qt.mode, QuantizationMode::Int8Symmetric);
assert_eq!(qt.data.len(), 5);
let dq = q.dequantize(&qt).expect("test: should succeed");
assert_eq!(dq.values.len(), 5);
assert!((dq.values[4] - 0.0).abs() < 0.02);
}
#[test]
fn test_int8sym_scale_calculation() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values: Vec<f64> = (1..=127).map(|x| x as f64).collect();
let qt = q.quantize(&values, &[127]).expect("test: should succeed");
assert!((qt.scale - 1.0).abs() < 0.01, "scale={}", qt.scale);
}
#[test]
fn test_int8sym_clamp() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values = vec![100.0, -100.0, 1000.0, -1000.0];
let qt = q.quantize(&values, &[4]).expect("test: should succeed");
for &v in &qt.data {
assert!((-127..=127).contains(&v), "out of range: {v}");
}
}
#[test]
fn test_int8sym_zero_point_is_zero() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values = vec![0.1, 0.5, -0.3];
let qt = q.quantize(&values, &[3]).expect("test: should succeed");
assert_eq!(qt.zero_point, 0);
}
#[test]
fn test_int8sym_mse_low() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values: Vec<f64> = (0..256).map(|i| (i as f64 / 128.0) - 1.0).collect();
let qt = q.quantize(&values, &[256]).expect("test: should succeed");
let err = q
.quantization_error(&values, &qt)
.expect("test: should succeed");
assert!(err < 1e-4, "MSE too high: {err}");
}
#[test]
fn test_int8asym_roundtrip() {
let mut q = default_quantizer(QuantizationMode::Int8Asymmetric);
let values = vec![0.2_f64, 0.5, -0.5, 0.0, 0.8];
let qt = q.quantize(&values, &[5]).expect("test: should succeed");
assert_eq!(qt.mode, QuantizationMode::Int8Asymmetric);
let dq = q.dequantize(&qt).expect("test: should succeed");
let err = mse(&values, &dq.values);
assert!(err < 1e-4, "MSE too high: {err}");
}
#[test]
fn test_int8asym_data_range() {
let mut q = default_quantizer(QuantizationMode::Int8Asymmetric);
let values = vec![-1.0, 0.0, 0.5, 1.0];
let qt = q.quantize(&values, &[4]).expect("test: should succeed");
for &v in &qt.data {
assert!((0..=255).contains(&v), "out of [0,255]: {v}");
}
}
#[test]
fn test_int8asym_zero_point_nonzero() {
let mut q = default_quantizer(QuantizationMode::Int8Asymmetric);
let values = vec![-1.0, 1.0];
let qt = q.quantize(&values, &[2]).expect("test: should succeed");
assert!(qt.zero_point > 0, "zero_point={}", qt.zero_point);
}
#[test]
fn test_int4_data_range() {
let mut q = default_quantizer(QuantizationMode::Int4);
let values = vec![-1.0, 0.0, 0.5, -0.5, 1.0, 0.25];
let qt = q.quantize(&values, &[6]).expect("test: should succeed");
for &v in &qt.data {
assert!((-7..=7).contains(&v), "out of [-7,7]: {v}");
}
}
#[test]
fn test_int4_roundtrip() {
let mut q = default_quantizer(QuantizationMode::Int4);
let values: Vec<f64> = (-7..=7).map(|x| x as f64 * 0.1).collect();
let qt = q.quantize(&values, &[15]).expect("test: should succeed");
let dq = q.dequantize(&qt).expect("test: should succeed");
let err = mse(&values, &dq.values);
assert!(err < 1e-3, "MSE={err}");
}
#[test]
fn test_int4_scale() {
let mut q = default_quantizer(QuantizationMode::Int4);
let values = vec![7.0, -7.0, 3.5];
let qt = q.quantize(&values, &[3]).expect("test: should succeed");
assert!((qt.scale - 1.0).abs() < 0.01, "scale={}", qt.scale);
}
#[test]
fn test_fp16_roundtrip_small() {
let mut q = default_quantizer(QuantizationMode::Fp16);
let values = vec![1.0_f64, 0.5, -0.5, 0.25, -0.25];
let qt = q.quantize(&values, &[5]).expect("test: should succeed");
assert_eq!(qt.mode, QuantizationMode::Fp16);
let dq = q.dequantize(&qt).expect("test: should succeed");
for (&orig, &rec) in values.iter().zip(dq.values.iter()) {
assert!((orig - rec).abs() < 0.002, "orig={orig} rec={rec}");
}
}
#[test]
fn test_fp16_clamp_large() {
let mut q = default_quantizer(QuantizationMode::Fp16);
let values = vec![1e6_f64, -1e6];
let qt = q.quantize(&values, &[2]).expect("test: should succeed");
let dq = q.dequantize(&qt).expect("test: should succeed");
assert!(dq.values[0] <= 65504.1);
assert!(dq.values[1] >= -65504.1);
}
#[test]
fn test_fp16_zero() {
let mut q = default_quantizer(QuantizationMode::Fp16);
let values = vec![0.0_f64];
let qt = q.quantize(&values, &[1]).expect("test: should succeed");
let dq = q.dequantize(&qt).expect("test: should succeed");
assert_eq!(dq.values[0], 0.0);
}
#[test]
fn test_fp16_data_stored_as_scaled_int() {
let mut q = default_quantizer(QuantizationMode::Fp16);
let values = vec![1.0_f64];
let qt = q.quantize(&values, &[1]).expect("test: should succeed");
assert_eq!(qt.data[0], 1024);
}
#[test]
fn test_bf16_roundtrip() {
let mut q = default_quantizer(QuantizationMode::Bf16);
let values = vec![1.0_f64, 0.5, -0.5, std::f64::consts::PI, -2.71];
let qt = q.quantize(&values, &[5]).expect("test: should succeed");
assert_eq!(qt.mode, QuantizationMode::Bf16);
let dq = q.dequantize(&qt).expect("test: should succeed");
for (&orig, &rec) in values.iter().zip(dq.values.iter()) {
let rel_err = if orig.abs() > 1e-9 {
(orig - rec).abs() / orig.abs()
} else {
(orig - rec).abs()
};
assert!(rel_err < 0.02, "orig={orig} rec={rec} rel_err={rel_err}");
}
}
#[test]
fn test_bf16_stores_u16_bits() {
let mut q = default_quantizer(QuantizationMode::Bf16);
let values = vec![1.0_f64];
let qt = q.quantize(&values, &[1]).expect("test: should succeed");
assert_eq!(qt.data[0], 0x3F80i32, "bf16 bits={}", qt.data[0]);
}
#[test]
fn test_bf16_zero() {
let mut q = default_quantizer(QuantizationMode::Bf16);
let values = vec![0.0_f64];
let qt = q.quantize(&values, &[1]).expect("test: should succeed");
let dq = q.dequantize(&qt).expect("test: should succeed");
assert_eq!(dq.values[0], 0.0);
}
#[test]
fn test_compression_ratio_int8() {
let cr = TensorQuantizer::compression_ratio(100, &QuantizationMode::Int8Symmetric);
assert!((cr - 8.0).abs() < 1e-10, "cr={cr}");
}
#[test]
fn test_compression_ratio_int4() {
let cr = TensorQuantizer::compression_ratio(100, &QuantizationMode::Int4);
assert!((cr - 16.0).abs() < 1e-10, "cr={cr}");
}
#[test]
fn test_compression_ratio_fp16() {
let cr = TensorQuantizer::compression_ratio(100, &QuantizationMode::Fp16);
assert!((cr - 4.0).abs() < 1e-10, "cr={cr}");
}
#[test]
fn test_compression_ratio_bf16() {
let cr = TensorQuantizer::compression_ratio(100, &QuantizationMode::Bf16);
assert!((cr - 4.0).abs() < 1e-10, "cr={cr}");
}
#[test]
fn test_clamp_int8sym() {
assert_eq!(
TensorQuantizer::clamp_to_range(200.0, &QuantizationMode::Int8Symmetric),
127.0
);
assert_eq!(
TensorQuantizer::clamp_to_range(-200.0, &QuantizationMode::Int8Symmetric),
-127.0
);
}
#[test]
fn test_clamp_int8asym() {
assert_eq!(
TensorQuantizer::clamp_to_range(-1.0, &QuantizationMode::Int8Asymmetric),
0.0
);
assert_eq!(
TensorQuantizer::clamp_to_range(300.0, &QuantizationMode::Int8Asymmetric),
255.0
);
}
#[test]
fn test_clamp_int4() {
assert_eq!(
TensorQuantizer::clamp_to_range(10.0, &QuantizationMode::Int4),
7.0
);
assert_eq!(
TensorQuantizer::clamp_to_range(-10.0, &QuantizationMode::Int4),
-7.0
);
}
#[test]
fn test_clamp_fp16() {
assert_eq!(
TensorQuantizer::clamp_to_range(1e10, &QuantizationMode::Fp16),
65504.0
);
assert_eq!(
TensorQuantizer::clamp_to_range(-1e10, &QuantizationMode::Fp16),
-65504.0
);
}
#[test]
fn test_quantization_error_zero_tensor() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values = vec![0.0_f64; 16];
let qt = q.quantize(&values, &[16]).expect("test: should succeed");
let err = q
.quantization_error(&values, &qt)
.expect("test: should succeed");
assert_eq!(err, 0.0);
}
#[test]
fn test_quantization_error_length_mismatch() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values = vec![1.0, 2.0, 3.0];
let qt = q.quantize(&values, &[3]).expect("test: should succeed");
let err = q.quantization_error(&[1.0, 2.0], &qt);
assert!(matches!(err, Err(QuantizerError::DimensionMismatch { .. })));
}
#[test]
fn test_quantization_error_bf16_low() {
let mut q = default_quantizer(QuantizationMode::Bf16);
let values: Vec<f64> = (0..64).map(|i| i as f64 * 0.01).collect();
let qt = q.quantize(&values, &[64]).expect("test: should succeed");
let err = q
.quantization_error(&values, &qt)
.expect("test: should succeed");
assert!(err < 1e-4, "MSE={err}");
}
#[test]
fn test_per_channel_produces_channel_scales() {
let config = QuantizerConfig {
mode: QuantizationMode::Int8Symmetric,
per_channel: true,
channel_dim: 0,
calibration_percentile: 99.9,
};
let mut q = TensorQuantizer::new(config);
let values = vec![1.0, 2.0, 3.0, 4.0, 0.1, 0.2, 0.3, 0.4];
let qt = q.quantize(&values, &[2, 4]).expect("test: should succeed");
assert_eq!(qt.channel_scales.len(), 2);
assert!(
(qt.channel_scales[0] - qt.channel_scales[1]).abs() > 0.01,
"scales equal: {:?}",
qt.channel_scales
);
}
#[test]
fn test_per_channel_dequantize() {
let config = QuantizerConfig {
mode: QuantizationMode::Int8Symmetric,
per_channel: true,
channel_dim: 0,
calibration_percentile: 100.0,
};
let mut q = TensorQuantizer::new(config);
let values = vec![10.0, 20.0, 30.0, 40.0, 1.0, 2.0, 3.0, 4.0];
let qt = q.quantize(&values, &[2, 4]).expect("test: should succeed");
let dq = q.dequantize(&qt).expect("test: should succeed");
assert_eq!(dq.values.len(), 8);
let err = mse(&values, &dq.values);
assert!(err < 1.0, "MSE={err}");
}
#[test]
fn test_stats_accumulate() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let v1 = vec![1.0, 2.0, 3.0];
let v2 = vec![4.0, 5.0, 6.0, 7.0, 8.0];
q.quantize(&v1, &[3]).expect("test: should succeed");
q.quantize(&v2, &[5]).expect("test: should succeed");
let stats = q.stats();
assert_eq!(stats.elements_quantized, 8);
assert_eq!(stats.modes_used, vec!["Int8Symmetric"]);
assert!(stats.avg_compression_ratio > 0.0);
}
#[test]
fn test_stats_reset() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
q.quantize(&[1.0, 2.0], &[2]).expect("test: should succeed");
q.reset_stats();
let stats = q.stats();
assert_eq!(stats.elements_quantized, 0);
assert!(stats.modes_used.is_empty());
}
#[test]
fn test_stats_multiple_modes_if_changed() {
let mut q = default_quantizer(QuantizationMode::Int4);
q.quantize(&[1.0, 2.0], &[2]).expect("test: should succeed");
q.quantize(&[3.0, 4.0], &[2]).expect("test: should succeed");
assert_eq!(q.stats().modes_used.len(), 1);
assert_eq!(q.stats().modes_used[0], "Int4");
}
#[test]
fn test_2d_tensor_int8sym() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values: Vec<f64> = (0..12).map(|i| i as f64 * 0.1).collect();
let qt = q.quantize(&values, &[3, 4]).expect("test: should succeed");
assert_eq!(qt.original_dims, vec![3, 4]);
assert_eq!(qt.data.len(), 12);
let dq = q.dequantize(&qt).expect("test: should succeed");
assert_eq!(dq.dims, vec![3, 4]);
}
#[test]
fn test_all_zeros_int8sym() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values = vec![0.0_f64; 8];
let qt = q.quantize(&values, &[8]).expect("test: should succeed");
assert_eq!(qt.scale, 1.0);
let dq = q.dequantize(&qt).expect("test: should succeed");
for v in &dq.values {
assert_eq!(*v, 0.0);
}
}
#[test]
fn test_all_zeros_bf16() {
let mut q = default_quantizer(QuantizationMode::Bf16);
let values = vec![0.0_f64; 4];
let qt = q.quantize(&values, &[4]).expect("test: should succeed");
let dq = q.dequantize(&qt).expect("test: should succeed");
for v in &dq.values {
assert_eq!(*v, 0.0);
}
}
#[test]
fn test_original_min_max_preserved() {
let mut q = default_quantizer(QuantizationMode::Int8Symmetric);
let values = vec![-3.5_f64, 0.0, 7.2];
let qt = q.quantize(&values, &[3]).expect("test: should succeed");
assert!((qt.original_min - (-3.5)).abs() < 1e-10);
assert!((qt.original_max - 7.2).abs() < 1e-10);
}
#[test]
fn test_calibration_percentile_effect() {
let values: Vec<f64> = (1..=100).map(|x| x as f64).collect();
let mut q99 = TensorQuantizer::new(QuantizerConfig {
mode: QuantizationMode::Int8Symmetric,
calibration_percentile: 99.9,
..QuantizerConfig::default()
});
let mut q50 = TensorQuantizer::new(QuantizerConfig {
mode: QuantizationMode::Int8Symmetric,
calibration_percentile: 50.0,
..QuantizerConfig::default()
});
let qt99 = q99.quantize(&values, &[100]).expect("test: should succeed");
let qt50 = q50.quantize(&values, &[100]).expect("test: should succeed");
assert!(
qt50.scale < qt99.scale,
"scale_50={} scale_99={}",
qt50.scale,
qt99.scale
);
}
}