quantize/methods/symmetric/
mod.rs1use crate::error::{check_bits, check_block, Result};
4use crate::kernels::quantize_sym_packed;
5use crate::packed::Packed;
6use crate::scale::Scale;
7use crate::tensor::Quantized;
8
9pub fn quantize<S: Scale, const BITS: u32, const BLOCK: usize>(
15 values: &[f32],
16) -> Result<Quantized<S>> {
17 quantize_with::<S>(values, BITS, BLOCK)
18}
19
20pub fn quantize_with<S: Scale>(values: &[f32], bits: u32, block: usize) -> Result<Quantized<S>> {
22 check_bits(bits)?;
23 check_block(block)?;
24 if values.is_empty() {
25 return Ok(Quantized::Symmetric {
26 scales: Vec::new(),
27 codes: Packed::from_raw(Vec::new(), bits, 0),
28 block,
29 len: 0,
30 });
31 }
32 let (scales_f, codes) = quantize_sym_packed(values, bits, block);
33 Ok(Quantized::Symmetric {
34 scales: scales_f.into_iter().map(S::from_f32).collect(),
35 codes,
36 block,
37 len: values.len(),
38 })
39}
40
41pub fn quantize_tensor<S: Scale, const BITS: u32>(values: &[f32]) -> Result<Quantized<S>> {
43 quantize_with::<S>(values, BITS, values.len().max(1))
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[test]
51 fn eight_bit_roundtrip_stays_within_half_step() {
52 let w = [0.42_f32, -0.10, 0.70, -0.50];
53 let q = quantize::<f32, 8, 4>(&w).unwrap();
54 let back = q.dequantize();
55 for (a, b) in w.iter().zip(&back) {
56 assert!((a - b).abs() < 0.01, "{a} vs {b}");
57 }
58 }
59
60 #[test]
61 fn packed_four_bit_uses_half_byte_per_code() {
62 let w = [0.1_f32; 32];
63 let q = quantize::<f32, 4, 32>(&w).unwrap();
64 assert_eq!(q.codes().len(), 16);
65 }
66
67 #[test]
68 fn remainder_block_roundtrips() {
69 let w: Vec<f32> = (0..40).map(|i| (i as f32) * 0.01 - 0.2).collect();
70 let q = quantize::<f32, 8, 32>(&w).unwrap();
71 assert_eq!(q.len(), 40);
72 let back = q.dequantize();
73 for (a, b) in w.iter().zip(&back) {
74 assert!((a - b).abs() < 0.01, "{a} vs {b}");
75 }
76 }
77
78 #[test]
79 fn dequantize_into_rejects_wrong_length() {
80 let w = [0.1_f32; 8];
81 let q = quantize::<f32, 8, 8>(&w).unwrap();
82 let mut out = [0.0f32; 3];
83 assert!(matches!(
84 q.dequantize_into(&mut out),
85 Err(crate::Error::LengthMismatch {
86 expected: 8,
87 got: 3
88 })
89 ));
90 }
91
92 #[test]
93 fn four_bit_remainder_roundtrips() {
94 let w: Vec<f32> = (0..40).map(|i| (i as f32) * 0.02 - 0.4).collect();
95 let q = quantize::<f32, 4, 32>(&w).unwrap();
96 let back = q.dequantize();
97 for (a, b) in w.iter().zip(&back) {
98 assert!((a - b).abs() < 0.08, "{a} vs {b}");
99 }
100 }
101
102 #[test]
103 fn fused_dot_matches_dequant_then_dot() {
104 let w: Vec<f32> = (0..64).map(|i| (i as f32) * 0.01 - 0.3).collect();
105 let q = quantize::<f32, 8, 32>(&w).unwrap();
106 let recon = q.dequantize();
107 let naive: f32 = recon.iter().zip(&w).map(|(a, b)| a * b).sum();
108 let fused = q.dot(&w).unwrap();
109 assert!((naive - fused).abs() < 1e-4, "{naive} vs {fused}");
110 }
111}