use ndarray::{Array1, Array2};
#[derive(Debug, Clone)]
pub struct QuantizedTensor {
pub weights: Vec<i8>,
pub scale: f32,
pub shape: (usize, usize), }
impl QuantizedTensor {
pub fn from_vec(weights: Vec<i8>, scale: f32, rows: usize, cols: usize) -> Self {
Self {
weights,
scale,
shape: (rows, cols),
}
}
pub fn to_array2(&self) -> Array2<f32> {
let (rows, cols) = self.shape;
let mut arr = Array2::zeros((rows, cols));
for i in 0..rows {
for j in 0..cols {
arr[[i, j]] = self.weights[i * cols + j] as f32 * self.scale;
}
}
arr
}
pub fn len(&self) -> usize {
self.weights.len()
}
pub fn size_bytes(&self) -> usize {
self.weights.len() * std::mem::size_of::<i8>()
}
}
pub fn quantize_array2(arr: &Array2<f32>) -> QuantizedTensor {
let (rows, cols) = (arr.nrows(), arr.ncols());
let max_abs = arr.iter().map(|&v| v.abs()).fold(0.0f32, f32::max);
if max_abs < 1e-8 {
return QuantizedTensor {
weights: vec![0i8; rows * cols],
scale: 1.0,
shape: (rows, cols),
};
}
let scale = max_abs / 127.0;
let mut weights = Vec::with_capacity(rows * cols);
for i in 0..rows {
for j in 0..cols {
let q = (arr[[i, j]] / scale).round().clamp(-127.0, 127.0) as i8;
weights.push(q);
}
}
QuantizedTensor {
weights,
scale,
shape: (rows, cols),
}
}
pub fn quantize_array1(arr: &Array1<f32>) -> (Vec<i8>, f32) {
let max_abs = arr.iter().map(|&v| v.abs()).fold(0.0f32, f32::max);
if max_abs < 1e-8 {
return (vec![0i8; arr.len()], 1.0);
}
let scale = max_abs / 127.0;
let weights: Vec<i8> = arr
.iter()
.map(|&v| (v / scale).round().clamp(-127.0, 127.0) as i8)
.collect();
(weights, scale)
}
pub fn dequantize_to_array2(weights: &[i8], scale: f32, rows: usize, cols: usize) -> Array2<f32> {
let mut arr = Array2::zeros((rows, cols));
for i in 0..rows {
for j in 0..cols {
arr[[i, j]] = weights[i * cols + j] as f32 * scale;
}
}
arr
}
pub fn dequantize_to_array1(weights: &[i8], scale: f32) -> Array1<f32> {
Array1::from_iter(weights.iter().map(|&w| w as f32 * scale))
}
pub fn quantized_matmul(input: &Array2<f32>, qw: &QuantizedTensor) -> Array2<f32> {
let (batch, in_features) = input.dim();
let (out_features, _) = qw.shape;
assert_eq!(in_features, qw.shape.1, "Input features must match weight cols");
let mut output = Array2::zeros((batch, out_features));
let scale = qw.scale;
for b in 0..batch {
for o in 0..out_features {
let mut sum = 0.0f32;
for i in 0..in_features {
let w = qw.weights[o * in_features + i] as f32 * scale;
sum += input[[b, i]] * w;
}
output[[b, o]] = sum;
}
}
output
}
pub fn compression_ratio(element_count: usize) -> f32 {
(element_count * std::mem::size_of::<f32>()) as f32
/ (element_count * std::mem::size_of::<i8>()) as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quantize_dequantize_roundtrip() {
let mut arr = Array2::zeros((3, 4));
arr[[0, 0]] = 1.0;
arr[[0, 1]] = -0.5;
arr[[1, 2]] = 2.0;
arr[[2, 3]] = -1.5;
let qt = quantize_array2(&arr);
let recovered = qt.to_array2();
let max_err = arr
.iter()
.zip(recovered.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0f32, f32::max);
assert!(max_err <= qt.scale + 1e-6, "Max error {} > scale {}", max_err, qt.scale);
}
#[test]
fn test_quantize_all_zeros() {
let arr = Array2::zeros((5, 5));
let qt = quantize_array2(&arr);
assert!(qt.weights.iter().all(|&w| w == 0));
assert_eq!(qt.scale, 1.0);
}
#[test]
fn test_compression_ratio() {
let ratio = compression_ratio(1000);
assert!((ratio - 4.0).abs() < 0.01, "Expected ~4x compression, got {}", ratio);
}
#[test]
fn test_quantized_matmul() {
let mut w = Array2::zeros((2, 3));
w[[0, 0]] = 1.0;
w[[0, 1]] = 2.0;
w[[0, 2]] = 3.0;
w[[1, 0]] = -1.0;
w[[1, 1]] = 0.0;
w[[1, 2]] = 1.0;
let qw = quantize_array2(&w);
let input = Array2::from_shape_vec((1, 3), vec![1.0, 1.0, 1.0]).unwrap();
let output = quantized_matmul(&input, &qw);
assert!((output[[0, 0]] - 6.0).abs() < 0.1, "Expected ~6.0, got {}", output[[0, 0]]);
assert!(output[[0, 1]].abs() < 0.1, "Expected ~0.0, got {}", output[[0, 1]]);
}
#[test]
fn test_quantized_tensor_size() {
let arr = Array2::zeros((100, 100));
let qt = quantize_array2(&arr);
assert_eq!(qt.size_bytes(), 10000);
assert_eq!(qt.len(), 10000);
}
#[test]
fn test_quantize_array1() {
let arr = Array1::from_vec(vec![-2.0, -1.0, 0.0, 1.0, 2.0]);
let (weights, scale) = quantize_array1(&arr);
assert_eq!(weights.len(), 5);
assert!(scale > 0.0);
assert_eq!(weights[4], 127);
assert_eq!(weights[0], -127);
}
#[test]
fn test_dequantize_to_array1() {
let weights = vec![-127i8, 0, 127];
let scale = 0.1;
let arr = dequantize_to_array1(&weights, scale);
assert!((arr[0] - (-12.7)).abs() < 1e-5);
assert!((arr[1] - 0.0).abs() < 1e-5);
assert!((arr[2] - 12.7).abs() < 1e-5);
}
}