use crate::{QScheme, TorshResult};
use scirs2_core::parallel_ops::*;
use torsh_core::{DType, TorshError};
use torsh_tensor::Tensor;
#[inline]
fn calculate_strides(shape: &[usize]) -> Vec<usize> {
let mut strides = vec![1; shape.len()];
for i in (0..shape.len().saturating_sub(1)).rev() {
strides[i] = strides[i + 1] * shape[i + 1];
}
strides
}
fn dtype_range(dtype: DType) -> TorshResult<(i32, i32)> {
match dtype {
DType::I8 => Ok((-128, 127)),
DType::U8 => Ok((0, 255)),
DType::I16 => Ok((-32768, 32767)),
_ => Err(TorshError::InvalidArgument(format!(
"Unsupported quantization dtype: {dtype:?}"
))),
}
}
#[inline]
fn quantize_scalar_f32(
data: &[f32],
scale: f32,
zero_point: i32,
qmin: i32,
qmax: i32,
output: &mut [f32],
) {
let inv_scale = 1.0 / scale;
let zero_point_f32 = zero_point as f32;
let (lo, hi) = (qmin as f32, qmax as f32);
for (out, &val) in output.iter_mut().zip(data.iter()) {
*out = ((val * inv_scale).round() + zero_point_f32).clamp(lo, hi);
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2", enable = "fma")]
unsafe fn quantize_avx2_f32(
data: &[f32],
scale: f32,
zero_point: i32,
qmin: i32,
qmax: i32,
output: &mut [f32],
) {
use std::arch::x86_64::*;
let inv_scale_vec = _mm256_set1_ps(1.0 / scale);
let zero_point_vec = _mm256_set1_ps(zero_point as f32);
let min_vec = _mm256_set1_ps(qmin as f32);
let max_vec = _mm256_set1_ps(qmax as f32);
let n_chunks = data.len() / 8;
for i in 0..n_chunks {
let base = i * 8;
let input = _mm256_loadu_ps(data.as_ptr().add(base));
let scaled = _mm256_fmadd_ps(input, inv_scale_vec, zero_point_vec);
let rounded = _mm256_round_ps::<{ _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC }>(scaled);
let clamped = _mm256_max_ps(_mm256_min_ps(rounded, max_vec), min_vec);
_mm256_storeu_ps(output.as_mut_ptr().add(base), clamped);
}
let processed = n_chunks * 8;
if processed < data.len() {
quantize_scalar_f32(
&data[processed..],
scale,
zero_point,
qmin,
qmax,
&mut output[processed..],
);
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f")]
unsafe fn quantize_avx512_f32(
data: &[f32],
scale: f32,
zero_point: i32,
qmin: i32,
qmax: i32,
output: &mut [f32],
) {
use std::arch::x86_64::*;
let inv_scale_vec = _mm512_set1_ps(1.0 / scale);
let zero_point_vec = _mm512_set1_ps(zero_point as f32);
let min_vec = _mm512_set1_ps(qmin as f32);
let max_vec = _mm512_set1_ps(qmax as f32);
let n_chunks = data.len() / 16;
for i in 0..n_chunks {
let base = i * 16;
let input = _mm512_loadu_ps(data.as_ptr().add(base));
let scaled = _mm512_fmadd_ps(input, inv_scale_vec, zero_point_vec);
let rounded = _mm512_roundscale_ps::<0x00>(scaled);
let clamped = _mm512_max_ps(_mm512_min_ps(rounded, max_vec), min_vec);
_mm512_storeu_ps(output.as_mut_ptr().add(base), clamped);
}
let processed = n_chunks * 16;
if processed < data.len() {
quantize_scalar_f32(
&data[processed..],
scale,
zero_point,
qmin,
qmax,
&mut output[processed..],
);
}
}
#[inline]
fn quantize_optimized(data: &[f32], scale: f32, zero_point: i32, qmin: i32, qmax: i32) -> Vec<f32> {
let mut output = vec![0.0f32; data.len()];
#[cfg(target_arch = "x86_64")]
{
if data.len() >= 16 && std::arch::is_x86_feature_detected!("avx512f") {
unsafe { quantize_avx512_f32(data, scale, zero_point, qmin, qmax, &mut output) };
return output;
}
if data.len() >= 8
&& std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("fma")
{
unsafe { quantize_avx2_f32(data, scale, zero_point, qmin, qmax, &mut output) };
return output;
}
}
quantize_scalar_f32(data, scale, zero_point, qmin, qmax, &mut output);
output
}
pub fn quantize_per_tensor_affine(
tensor: &Tensor,
scale: f32,
zero_point: i32,
) -> TorshResult<(Tensor, f32, i32)> {
quantize_per_tensor_affine_dtype(tensor, scale, zero_point, DType::I8)
}
pub fn quantize_per_tensor_affine_dtype(
tensor: &Tensor,
scale: f32,
zero_point: i32,
dtype: DType,
) -> TorshResult<(Tensor, f32, i32)> {
let data = tensor.data()?;
if scale <= 0.0 {
return Err(TorshError::InvalidArgument(
"Quantization scale must be positive".to_string(),
));
}
let (qmin, qmax) = dtype_range(dtype)?;
if !(qmin..=qmax).contains(&zero_point) {
return Err(TorshError::InvalidArgument(format!(
"Zero point {zero_point} must be in range [{qmin}, {qmax}] for {dtype:?}"
)));
}
let quantized_f32: Vec<f32> = if data.len() > 1000 {
data.par_chunks(4096) .flat_map(|chunk| quantize_optimized(chunk, scale, zero_point, qmin, qmax))
.collect()
} else {
quantize_optimized(&data, scale, zero_point, qmin, qmax)
};
let quantized_tensor = Tensor::from_data(
quantized_f32,
tensor.shape().dims().to_vec(),
tensor.device(),
);
Ok((quantized_tensor?, scale, zero_point))
}
pub fn quantize_per_tensor_symmetric(tensor: &Tensor, scale: f32) -> TorshResult<(Tensor, f32)> {
let (quantized_tensor, computed_scale, _) = quantize_per_tensor_affine(tensor, scale, 0)?;
Ok((quantized_tensor, computed_scale))
}
pub fn quantize_per_tensor_symmetric_dtype(
tensor: &Tensor,
scale: f32,
zero_point: i32,
dtype: DType,
) -> TorshResult<(Tensor, f32, i32)> {
quantize_per_tensor_affine_dtype(tensor, scale, zero_point, dtype)
}
pub fn calculate_symmetric_qparams(
tensor: &Tensor,
qmin: i32,
qmax: i32,
) -> TorshResult<(f32, i32)> {
let data = tensor.data()?;
if data.is_empty() {
return Err(TorshError::InvalidArgument(
"Cannot calculate quantization parameters for empty tensor".to_string(),
));
}
if qmin >= qmax {
return Err(TorshError::InvalidArgument(
"qmin must be less than qmax".to_string(),
));
}
if data.iter().any(|val| !val.is_finite()) {
return Err(TorshError::InvalidArgument(
"Tensor contains non-finite values (NaN or infinity)".to_string(),
));
}
let max_abs = data.iter().fold(0.0f32, |acc, &val| acc.max(val.abs()));
let zero_point = if qmin >= 0 {
(((qmin as i64) + (qmax as i64) + 1) / 2) as i32
} else {
0
};
let headroom = (qmax - zero_point).max(1) as f32;
let scale = if max_abs < 1e-7 {
1e-7 / headroom
} else {
max_abs / headroom
};
Ok((scale, zero_point))
}
pub fn calculate_qparams(
tensor: &Tensor,
qmin: i32,
qmax: i32,
_dtype: DType,
) -> TorshResult<(f32, i32)> {
let data = tensor.data()?;
if data.is_empty() {
return Err(TorshError::InvalidArgument(
"Cannot calculate quantization parameters for empty tensor".to_string(),
));
}
if qmin >= qmax {
return Err(TorshError::InvalidArgument(
"qmin must be less than qmax".to_string(),
));
}
let (min_val, max_val) = if data.len() > 10000 {
data.par_iter().map(|&val| (val, val)).reduce(
|| (f32::INFINITY, f32::NEG_INFINITY),
|(min1, max1), (min2, max2)| (min1.min(min2), max1.max(max2)),
)
} else {
data.iter()
.fold((f32::INFINITY, f32::NEG_INFINITY), |(min, max), &val| {
(min.min(val), max.max(val))
})
};
if !min_val.is_finite() || !max_val.is_finite() {
return Err(TorshError::InvalidArgument(
"Tensor contains non-finite values (NaN or infinity)".to_string(),
));
}
let min_val = min_val.min(0.0);
let max_val = max_val.max(0.0);
let range = max_val - min_val;
let adjusted_range = if range < 1e-7 {
1e-7 } else {
range
};
let scale = adjusted_range / (qmax - qmin) as f32;
let zero_point_exact = qmin as f64 - (min_val as f64) / (scale as f64);
let zero_point = zero_point_exact.round().max(qmin as f64).min(qmax as f64) as i32;
Ok((scale, zero_point))
}
pub fn quantize_per_channel_affine(
tensor: &Tensor,
scales: &[f32],
zero_points: &[i32],
axis: usize,
) -> TorshResult<(Tensor, Vec<f32>, Vec<i32>)> {
quantize_per_channel_affine_dtype(tensor, scales, zero_points, axis, DType::I8)
}
pub fn quantize_per_channel_affine_dtype(
tensor: &Tensor,
scales: &[f32],
zero_points: &[i32],
axis: usize,
dtype: DType,
) -> TorshResult<(Tensor, Vec<f32>, Vec<i32>)> {
let data = tensor.data()?;
let binding = tensor.shape();
let shape = binding.dims();
if axis >= shape.len() {
return Err(TorshError::InvalidArgument(
"Axis out of bounds".to_string(),
));
}
let channel_size = shape[axis];
if scales.len() != channel_size || zero_points.len() != channel_size {
return Err(TorshError::InvalidArgument(
"Scales and zero_points length must match channel size".to_string(),
));
}
let (qmin, qmax) = dtype_range(dtype)?;
let (qmin_f, qmax_f) = (qmin as f32, qmax as f32);
for (channel, (&scale, &zero_point)) in scales.iter().zip(zero_points.iter()).enumerate() {
if scale <= 0.0 {
return Err(TorshError::InvalidArgument(format!(
"Scale for channel {channel} must be positive"
)));
}
if !(qmin..=qmax).contains(&zero_point) {
return Err(TorshError::InvalidArgument(format!(
"Zero point {zero_point} for channel {channel} must be in range \
[{qmin}, {qmax}] for {dtype:?}"
)));
}
}
let strides = calculate_strides(shape);
let quantized_f32: Vec<f32> = data
.iter()
.enumerate()
.map(|(idx, &x)| {
let channel_idx = (idx / strides[axis]) % shape[axis];
let scale = scales[channel_idx];
let zero_point = zero_points[channel_idx];
((x / scale).round() + zero_point as f32).clamp(qmin_f, qmax_f)
})
.collect();
let quantized_tensor = Tensor::from_data(quantized_f32, shape.to_vec(), tensor.device());
Ok((quantized_tensor?, scales.to_vec(), zero_points.to_vec()))
}
pub fn calculate_per_channel_symmetric_qparams(
tensor: &Tensor,
axis: usize,
dtype: DType,
) -> TorshResult<(Vec<f32>, Vec<i32>)> {
let data = tensor.data()?;
let binding = tensor.shape();
let shape = binding.dims();
if axis >= shape.len() {
return Err(TorshError::InvalidArgument(
"Axis out of bounds".to_string(),
));
}
let (qmin, qmax) = dtype_range(dtype)?;
let zero_point = if qmin >= 0 {
(((qmin as i64) + (qmax as i64) + 1) / 2) as i32
} else {
0
};
let headroom = (qmax - zero_point).max(1) as f32;
if data.iter().any(|val| !val.is_finite()) {
return Err(TorshError::InvalidArgument(
"Tensor contains non-finite values (NaN or infinity)".to_string(),
));
}
let channel_size = shape[axis];
let strides = calculate_strides(shape);
let mut channel_abs_max = vec![0.0f32; channel_size];
for (idx, &val) in data.iter().enumerate() {
let channel_idx = (idx / strides[axis]) % shape[axis];
channel_abs_max[channel_idx] = channel_abs_max[channel_idx].max(val.abs());
}
let scales = channel_abs_max
.iter()
.map(|&max_abs| {
if max_abs < 1e-7 {
1e-7 / headroom
} else {
max_abs / headroom
}
})
.collect();
Ok((scales, vec![zero_point; channel_size]))
}
pub fn calculate_per_channel_qparams(
tensor: &Tensor,
axis: usize,
dtype: DType,
) -> TorshResult<(Vec<f32>, Vec<i32>)> {
let data = tensor.data()?;
let binding = tensor.shape();
let shape = binding.dims();
if axis >= shape.len() {
return Err(TorshError::InvalidArgument(
"Axis out of bounds".to_string(),
));
}
let (qmin, qmax) = dtype_range(dtype)?;
let channel_size = shape[axis];
let mut channel_mins = vec![f32::INFINITY; channel_size];
let mut channel_maxs = vec![f32::NEG_INFINITY; channel_size];
let mut strides = vec![1; shape.len()];
for i in (0..shape.len() - 1).rev() {
strides[i] = strides[i + 1] * shape[i + 1];
}
for (idx, &val) in data.iter().enumerate() {
let channel_idx = (idx / strides[axis]) % shape[axis];
channel_mins[channel_idx] = channel_mins[channel_idx].min(val);
channel_maxs[channel_idx] = channel_maxs[channel_idx].max(val);
}
let mut scales = Vec::with_capacity(channel_size);
let mut zero_points = Vec::with_capacity(channel_size);
for ch in 0..channel_size {
let min_val = channel_mins[ch].min(0.0);
let max_val = channel_maxs[ch].max(0.0);
let scale = (max_val - min_val) / (qmax - qmin) as f32;
let scale = if scale == 0.0 { 1.0 } else { scale };
let zero_point = (qmin as f32 - min_val / scale)
.round()
.max(qmin as f32)
.min(qmax as f32) as i32;
scales.push(scale);
zero_points.push(zero_point);
}
Ok((scales, zero_points))
}
pub fn quantize_tensor_auto(
tensor: &Tensor,
dtype: DType,
scheme: QScheme,
) -> TorshResult<(Tensor, f32, i32)> {
let (qmin, qmax) = dtype_range(dtype)?;
match scheme {
QScheme::PerTensorAffine => {
let (scale, zero_point) = calculate_qparams(tensor, qmin, qmax, dtype)?;
quantize_per_tensor_affine_dtype(tensor, scale, zero_point, dtype)
}
QScheme::PerTensorSymmetric => {
let (scale, zero_point) = calculate_symmetric_qparams(tensor, qmin, qmax)?;
quantize_per_tensor_symmetric_dtype(tensor, scale, zero_point, dtype)
}
QScheme::PerChannelAffine | QScheme::PerChannelSymmetric => {
Err(TorshError::InvalidArgument(format!(
"{scheme:?} produces one scale/zero-point pair per channel which \
cannot be returned through this scalar API; call \
quantize_per_channel_auto instead"
)))
}
QScheme::Int4PerTensor => {
crate::quantize_int4_per_tensor(tensor, &crate::QuantConfig::int4())
}
QScheme::Int4PerChannel => {
let axis = 0;
crate::quantize_int4_per_channel(tensor, axis, &crate::QuantConfig::int4())
}
QScheme::Binary => {
crate::quantize_binary(tensor)
}
QScheme::Ternary => {
crate::quantize_ternary(tensor)
}
QScheme::GroupWise => {
let axis = 0;
let group_size = 32;
crate::quantize_group_wise(
tensor,
axis,
group_size,
&crate::QuantConfig::group_wise(axis, group_size),
)
}
QScheme::MixedPrecision => {
Err(TorshError::InvalidArgument(
"Mixed precision quantization requires specialized API".to_string(),
))
}
}
}
pub fn quantize_per_channel_auto(
tensor: &Tensor,
axis: usize,
dtype: DType,
scheme: QScheme,
) -> TorshResult<(Tensor, Vec<f32>, Vec<i32>)> {
match scheme {
QScheme::PerChannelAffine => {
let (scales, zero_points) = calculate_per_channel_qparams(tensor, axis, dtype)?;
quantize_per_channel_affine_dtype(tensor, &scales, &zero_points, axis, dtype)
}
QScheme::PerChannelSymmetric => {
let (scales, zero_points) =
calculate_per_channel_symmetric_qparams(tensor, axis, dtype)?;
quantize_per_channel_affine_dtype(tensor, &scales, &zero_points, axis, dtype)
}
_ => Err(TorshError::InvalidArgument(
"Scheme not supported for per-channel quantization".to_string(),
)),
}
}
pub fn quantize_auto(
tensor: &Tensor,
config: &crate::QuantConfig,
) -> TorshResult<(Tensor, f32, i32)> {
quantize_tensor_auto(tensor, config.dtype, config.scheme)
}
#[allow(dead_code)]
pub fn quantize_dynamic(module: &mut dyn crate::qat::Module) -> TorshResult<()> {
let mut quantized_params = Vec::new();
let mut_params = module.parameters_mut();
for param in mut_params {
let config = crate::QuantConfig::int8();
let (quantized, _scale, _zero_point) = quantize_auto(param, &config)?;
quantized_params.push(quantized);
}
Ok(())
}
#[allow(dead_code)]
pub fn prepare_qat(module: &mut dyn crate::qat::Module) -> TorshResult<()> {
module.train(true);
let named_params = module.named_parameters();
for (name, param) in named_params {
if param.numel() == 0 {
return Err(TorshError::InvalidArgument(format!(
"Parameter {} is empty and cannot be quantized",
name
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use torsh_tensor::creation::{tensor_1d, tensor_2d};
#[cfg(target_arch = "x86_64")]
#[test]
fn test_simd_dispatch_matches_scalar_kernel() {
let data: Vec<f32> = (0..133).map(|i| (i as f32 - 66.0) * 0.37).collect();
let scale = 0.05;
for (qmin, qmax, zero_point) in [(-128, 127, -3), (0, 255, 170)] {
let mut expected = vec![0.0f32; data.len()];
quantize_scalar_f32(&data, scale, zero_point, qmin, qmax, &mut expected);
let actual = quantize_optimized(&data, scale, zero_point, qmin, qmax);
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
assert!(
(a - e).abs() <= 1.0,
"lane {i}: simd {a} vs scalar {e} (range [{qmin}, {qmax}])"
);
assert!((qmin as f32..=qmax as f32).contains(a));
}
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_avx2_kernel_matches_scalar_kernel() {
if !std::arch::is_x86_feature_detected!("avx2")
|| !std::arch::is_x86_feature_detected!("fma")
{
return;
}
let data: Vec<f32> = (0..37).map(|i| (i as f32 - 18.0) * 0.31).collect();
let (scale, zero_point, qmin, qmax) = (0.05f32, 12, -128, 127);
let mut expected = vec![0.0f32; data.len()];
quantize_scalar_f32(&data, scale, zero_point, qmin, qmax, &mut expected);
let mut actual = vec![0.0f32; data.len()];
unsafe {
quantize_avx2_f32(&data, scale, zero_point, qmin, qmax, &mut actual);
}
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
assert!((a - e).abs() <= 1.0, "lane {i}: avx2 {a} vs scalar {e}");
}
}
#[test]
fn test_calculate_qparams() {
let data = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
let tensor = tensor_1d(&data).unwrap();
let (scale, zero_point) = calculate_qparams(&tensor, -128, 127, DType::I8).unwrap();
assert!(scale > 0.0);
assert!((-128..=127).contains(&zero_point));
}
#[test]
fn test_quantize_per_tensor_affine() {
let data = vec![0.0, 1.0, 2.0, 3.0];
let tensor = tensor_1d(&data).unwrap();
let scale = 0.1;
let zero_point = 0;
let (quantized, ret_scale, ret_zero_point) =
quantize_per_tensor_affine(&tensor, scale, zero_point).unwrap();
assert_eq!(ret_scale, scale);
assert_eq!(ret_zero_point, zero_point);
assert_eq!(quantized.shape().dims(), tensor.shape().dims());
}
#[test]
fn test_quantize_tensor_auto() {
let data = vec![-1.0, 0.0, 1.0, 2.0];
let tensor = tensor_1d(&data).unwrap();
let (quantized, scale, zero_point) =
quantize_tensor_auto(&tensor, DType::I8, QScheme::PerTensorAffine).unwrap();
assert!(scale > 0.0);
assert!((-128..=127).contains(&zero_point));
assert_eq!(quantized.shape().dims(), tensor.shape().dims());
}
#[test]
fn test_per_channel_quantization() {
let tensor = tensor_2d(&[
&[0.0, 1.0, 2.0], &[0.0, 5.0, 10.0], ])
.unwrap();
let axis = 0; let (scales, zero_points) =
calculate_per_channel_qparams(&tensor, axis, DType::I8).unwrap();
assert_eq!(scales.len(), 2);
assert_eq!(zero_points.len(), 2);
assert!(scales[1] > scales[0]);
let (quantized, ret_scales, ret_zero_points) =
quantize_per_channel_affine(&tensor, &scales, &zero_points, axis).unwrap();
assert_eq!(ret_scales, scales);
assert_eq!(ret_zero_points, zero_points);
assert_eq!(quantized.shape().dims(), tensor.shape().dims());
}
#[test]
fn test_per_channel_auto() {
let tensor = tensor_2d(&[&[-2.0, 0.0, 2.0], &[-10.0, 0.0, 10.0]]).unwrap();
let (quantized, scales, zero_points) =
quantize_per_channel_auto(&tensor, 0, DType::I8, QScheme::PerChannelAffine).unwrap();
assert_eq!(scales.len(), 2);
assert_eq!(zero_points.len(), 2);
assert!(scales[1] > scales[0]); assert_eq!(quantized.shape().dims(), tensor.shape().dims());
}
#[test]
fn test_per_channel_symmetric() {
let tensor = tensor_2d(&[&[-1.0, 0.0, 1.0], &[-5.0, 0.0, 5.0]]).unwrap();
let (_quantized, scales, zero_points) =
quantize_per_channel_auto(&tensor, 0, DType::I8, QScheme::PerChannelSymmetric).unwrap();
assert_eq!(scales.len(), 2);
assert_eq!(zero_points.len(), 2);
for &zp in &zero_points {
assert_eq!(zp, 0);
}
assert!(scales[1] > scales[0]); }
}