use crate::error::{check_bits, check_block, Result};
use crate::kernels::quantize_sym_packed;
use crate::packed::Packed;
use crate::scale::Scale;
use crate::tensor::Quantized;
pub fn quantize<S: Scale, const BITS: u32, const BLOCK: usize>(
values: &[f32],
) -> Result<Quantized<S>> {
quantize_with::<S>(values, BITS, BLOCK)
}
pub fn quantize_with<S: Scale>(values: &[f32], bits: u32, block: usize) -> Result<Quantized<S>> {
check_bits(bits)?;
check_block(block)?;
if values.is_empty() {
return Ok(Quantized::Symmetric {
scales: Vec::new(),
codes: Packed::from_raw(Vec::new(), bits, 0),
block,
len: 0,
});
}
let (scales_f, codes) = quantize_sym_packed(values, bits, block);
Ok(Quantized::Symmetric {
scales: scales_f.into_iter().map(S::from_f32).collect(),
codes,
block,
len: values.len(),
})
}
pub fn quantize_tensor<S: Scale, const BITS: u32>(values: &[f32]) -> Result<Quantized<S>> {
quantize_with::<S>(values, BITS, values.len().max(1))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eight_bit_roundtrip_stays_within_half_step() {
let w = [0.42_f32, -0.10, 0.70, -0.50];
let q = quantize::<f32, 8, 4>(&w).unwrap();
let back = q.dequantize();
for (a, b) in w.iter().zip(&back) {
assert!((a - b).abs() < 0.01, "{a} vs {b}");
}
}
#[test]
fn packed_four_bit_uses_half_byte_per_code() {
let w = [0.1_f32; 32];
let q = quantize::<f32, 4, 32>(&w).unwrap();
assert_eq!(q.codes().len(), 16);
}
#[test]
fn remainder_block_roundtrips() {
let w: Vec<f32> = (0..40).map(|i| (i as f32) * 0.01 - 0.2).collect();
let q = quantize::<f32, 8, 32>(&w).unwrap();
assert_eq!(q.len(), 40);
let back = q.dequantize();
for (a, b) in w.iter().zip(&back) {
assert!((a - b).abs() < 0.01, "{a} vs {b}");
}
}
#[test]
fn dequantize_into_rejects_wrong_length() {
let w = [0.1_f32; 8];
let q = quantize::<f32, 8, 8>(&w).unwrap();
let mut out = [0.0f32; 3];
assert!(matches!(
q.dequantize_into(&mut out),
Err(crate::Error::LengthMismatch {
expected: 8,
got: 3
})
));
}
#[test]
fn four_bit_remainder_roundtrips() {
let w: Vec<f32> = (0..40).map(|i| (i as f32) * 0.02 - 0.4).collect();
let q = quantize::<f32, 4, 32>(&w).unwrap();
let back = q.dequantize();
for (a, b) in w.iter().zip(&back) {
assert!((a - b).abs() < 0.08, "{a} vs {b}");
}
}
#[test]
fn fused_dot_matches_dequant_then_dot() {
let w: Vec<f32> = (0..64).map(|i| (i as f32) * 0.01 - 0.3).collect();
let q = quantize::<f32, 8, 32>(&w).unwrap();
let recon = q.dequantize();
let naive: f32 = recon.iter().zip(&w).map(|(a, b)| a * b).sum();
let fused = q.dot(&w).unwrap();
assert!((naive - fused).abs() < 1e-4, "{naive} vs {fused}");
}
}