use half::f16;
use serde::{Deserialize, Serialize};
#[must_use]
pub fn quantize_to_f16(embedding: &[f32]) -> Vec<f16> {
embedding.iter().map(|&x| f16::from_f32(x)).collect()
}
#[must_use]
pub fn dequantize_f16(quantized: &[f16]) -> Vec<f32> {
quantized.iter().map(|&x| x.to_f32()).collect()
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct QuantizationParams {
pub scale: f32,
pub zero_point: f32,
pub min_val: f32,
pub max_val: f32,
}
impl QuantizationParams {
#[must_use]
pub fn from_data(data: &[f32]) -> Self {
if data.is_empty() {
return Self {
scale: 1.0,
zero_point: 0.0,
min_val: 0.0,
max_val: 0.0,
};
}
let min_val = data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let abs_max = min_val.abs().max(max_val.abs());
let scale = if abs_max > 1e-12 {
abs_max / 127.0
} else {
1.0
};
Self {
scale,
zero_point: 0.0, min_val,
max_val,
}
}
#[must_use]
pub fn from_data_asymmetric(data: &[f32]) -> Self {
if data.is_empty() {
return Self {
scale: 1.0,
zero_point: 0.0,
min_val: 0.0,
max_val: 0.0,
};
}
let min_val = data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let range = max_val - min_val;
let scale = if range > 1e-12 {
range / 255.0
} else {
1.0
};
let zero_point = -min_val / scale;
Self {
scale,
zero_point,
min_val,
max_val,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuantizedEmbedding {
pub values: Vec<i8>,
pub params: QuantizationParams,
}
impl QuantizedEmbedding {
#[must_use]
pub fn size_bytes(&self) -> usize {
self.values.len() + std::mem::size_of::<QuantizationParams>()
}
#[must_use]
pub fn dequantize(&self) -> Vec<f32> {
dequantize_i8(&self.values, self.params.scale, self.params.zero_point)
}
}
#[must_use]
pub fn quantize_to_i8(embedding: &[f32]) -> (Vec<i8>, f32, f32) {
let params = QuantizationParams::from_data(embedding);
let quantized: Vec<i8> = embedding
.iter()
.map(|&x| {
let q = (x / params.scale).round();
q.clamp(-128.0, 127.0) as i8
})
.collect();
(quantized, params.scale, params.zero_point)
}
#[must_use]
pub fn quantize_to_i8_full(embedding: &[f32]) -> QuantizedEmbedding {
let params = QuantizationParams::from_data(embedding);
let values: Vec<i8> = embedding
.iter()
.map(|&x| {
let q = (x / params.scale).round();
q.clamp(-128.0, 127.0) as i8
})
.collect();
QuantizedEmbedding { values, params }
}
#[must_use]
pub fn dequantize_i8(quantized: &[i8], scale: f32, zero_point: f32) -> Vec<f32> {
quantized
.iter()
.map(|&q| (q as f32 - zero_point) * scale)
.collect()
}
#[must_use]
pub fn quantize_to_u8(embedding: &[f32]) -> (Vec<u8>, f32, f32) {
let params = QuantizationParams::from_data_asymmetric(embedding);
let quantized: Vec<u8> = embedding
.iter()
.map(|&x| {
let q = (x / params.scale + params.zero_point).round();
q.clamp(0.0, 255.0) as u8
})
.collect();
(quantized, params.scale, params.zero_point)
}
#[must_use]
pub fn dequantize_u8(quantized: &[u8], scale: f32, zero_point: f32) -> Vec<f32> {
quantized
.iter()
.map(|&q| (q as f32 - zero_point) * scale)
.collect()
}
#[must_use]
pub fn compute_quantization_error(original: &[f32], dequantized: &[f32]) -> f32 {
if original.len() != dequantized.len() || original.is_empty() {
return f32::NAN;
}
let mse: f32 = original
.iter()
.zip(dequantized.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f32>()
/ original.len() as f32;
mse
}
#[must_use]
pub fn compute_cosine_preservation(
original_a: &[f32],
original_b: &[f32],
dequant_a: &[f32],
dequant_b: &[f32],
) -> f32 {
let original_sim = cosine_similarity(original_a, original_b);
let quant_sim = cosine_similarity(dequant_a, dequant_b);
if original_sim.abs() < 1e-12 {
return 1.0;
}
quant_sim / original_sim
}
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() {
return 0.0;
}
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a * norm_b < 1e-12 {
return 0.0;
}
dot / (norm_a * norm_b)
}
#[derive(Debug, Clone)]
pub struct QuantizationStats {
pub mse: f32,
pub rmse: f32,
pub max_error: f32,
pub mean_error: f32,
pub compression_ratio: f32,
}
impl QuantizationStats {
#[must_use]
pub fn compute(original: &[f32], dequantized: &[f32], quantized_bytes: usize) -> Self {
let mse = compute_quantization_error(original, dequantized);
let rmse = mse.sqrt();
let errors: Vec<f32> = original
.iter()
.zip(dequantized.iter())
.map(|(a, b)| (a - b).abs())
.collect();
let max_error = errors.iter().fold(0.0f32, |a, &b| a.max(b));
let mean_error = errors.iter().sum::<f32>() / errors.len().max(1) as f32;
let original_bytes = original.len() * std::mem::size_of::<f32>();
let compression_ratio = original_bytes as f32 / quantized_bytes.max(1) as f32;
Self {
mse,
rmse,
max_error,
mean_error,
compression_ratio,
}
}
}
pub struct BatchQuantizer {
pub symmetric: bool,
pub precision: QuantizationPrecision,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantizationPrecision {
F16,
Int8,
UInt8,
}
impl BatchQuantizer {
#[must_use]
pub fn new(precision: QuantizationPrecision) -> Self {
Self {
symmetric: matches!(precision, QuantizationPrecision::Int8),
precision,
}
}
pub fn quantize_batch(&self, embeddings: &[Vec<f32>]) -> Vec<QuantizedEmbedding> {
embeddings
.iter()
.map(|emb| match self.precision {
QuantizationPrecision::F16 => {
let f16_vals = quantize_to_f16(emb);
let bytes: Vec<i8> = f16_vals
.iter()
.flat_map(|v| {
let bits = v.to_bits();
[(bits & 0xFF) as i8, ((bits >> 8) & 0xFF) as i8]
})
.collect();
QuantizedEmbedding {
values: bytes,
params: QuantizationParams {
scale: 1.0,
zero_point: 0.0,
min_val: 0.0,
max_val: 0.0,
},
}
}
QuantizationPrecision::Int8 | QuantizationPrecision::UInt8 => {
quantize_to_i8_full(emb)
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_f16_roundtrip() {
let original = vec![0.5, -0.3, 0.8, -0.1, 0.0, 1.0, -1.0];
let quantized = quantize_to_f16(&original);
let restored = dequantize_f16(&quantized);
for (orig, rest) in original.iter().zip(restored.iter()) {
assert!((orig - rest).abs() < 0.01, "F16 roundtrip error too large");
}
}
#[test]
fn test_i8_roundtrip() {
let original = vec![0.5, -0.3, 0.8, -0.1, 0.0, 0.9, -0.9];
let (quantized, scale, zero_point) = quantize_to_i8(&original);
let restored = dequantize_i8(&quantized, scale, zero_point);
for (orig, rest) in original.iter().zip(restored.iter()) {
assert!((orig - rest).abs() < 0.02, "I8 roundtrip error too large");
}
}
#[test]
fn test_u8_roundtrip() {
let original = vec![0.1, 0.3, 0.5, 0.7, 0.9];
let (quantized, scale, zero_point) = quantize_to_u8(&original);
let restored = dequantize_u8(&quantized, scale, zero_point);
for (orig, rest) in original.iter().zip(restored.iter()) {
assert!((orig - rest).abs() < 0.02, "U8 roundtrip error too large");
}
}
#[test]
fn test_quantization_params() {
let data = vec![-0.5, 0.0, 0.5, 1.0];
let params = QuantizationParams::from_data(&data);
assert!(params.scale > 0.0);
assert_eq!(params.min_val, -0.5);
assert_eq!(params.max_val, 1.0);
}
#[test]
fn test_quantization_error() {
let original = vec![0.5, -0.3, 0.8];
let modified = vec![0.51, -0.29, 0.79];
let error = compute_quantization_error(&original, &modified);
assert!(error < 0.001);
}
#[test]
fn test_cosine_preservation() {
let a = vec![0.6, 0.8, 0.0];
let b = vec![0.0, 0.6, 0.8];
let a_quant = vec![0.61, 0.79, 0.01];
let b_quant = vec![0.01, 0.59, 0.81];
let preservation = compute_cosine_preservation(&a, &b, &a_quant, &b_quant);
assert!(preservation > 0.95 && preservation < 1.05);
}
#[test]
fn test_quantization_stats() {
let original = vec![0.5, -0.3, 0.8, -0.1];
let (quantized, scale, zero_point) = quantize_to_i8(&original);
let restored = dequantize_i8(&quantized, scale, zero_point);
let stats = QuantizationStats::compute(&original, &restored, quantized.len());
assert!(stats.mse >= 0.0);
assert!(stats.rmse >= 0.0);
assert!(stats.compression_ratio > 1.0); }
#[test]
fn test_batch_quantizer() {
let embeddings = vec![
vec![0.5, -0.3, 0.8],
vec![-0.1, 0.2, 0.9],
];
let quantizer = BatchQuantizer::new(QuantizationPrecision::Int8);
let quantized = quantizer.quantize_batch(&embeddings);
assert_eq!(quantized.len(), 2);
assert_eq!(quantized[0].values.len(), 3);
}
#[test]
fn test_quantized_embedding_dequantize() {
let original = vec![0.5, -0.3, 0.8, -0.1];
let quantized = quantize_to_i8_full(&original);
let restored = quantized.dequantize();
assert_eq!(restored.len(), original.len());
for (orig, rest) in original.iter().zip(restored.iter()) {
assert!((orig - rest).abs() < 0.02);
}
}
#[test]
fn test_empty_input() {
let empty: Vec<f32> = vec![];
let f16_result = quantize_to_f16(&empty);
assert!(f16_result.is_empty());
let (i8_result, _, _) = quantize_to_i8(&empty);
assert!(i8_result.is_empty());
let params = QuantizationParams::from_data(&empty);
assert_eq!(params.scale, 1.0);
}
}