Skip to main content

ruvllm_esp32/
quantized.rs

1//! Quantized tensor operations for memory-efficient inference
2//!
3//! Supports INT8, INT4, and binary quantization for extreme memory savings.
4
5use heapless::Vec as HVec;
6use serde::{Deserialize, Serialize};
7
8/// Maximum tensor size for stack allocation (16KB)
9pub const MAX_TENSOR_SIZE: usize = 16 * 1024;
10
11/// Quantization type
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum QuantizationType {
14    /// 8-bit signed integer (-128 to 127)
15    Int8,
16    /// 4-bit signed integer (-8 to 7), packed 2 per byte
17    Int4,
18    /// Binary weights (-1 or +1), packed 8 per byte
19    Binary,
20    /// 16-bit fixed point (8.8 format)
21    Fixed16,
22}
23
24impl QuantizationType {
25    /// Bits per weight
26    pub const fn bits(&self) -> usize {
27        match self {
28            Self::Int8 => 8,
29            Self::Int4 => 4,
30            Self::Binary => 1,
31            Self::Fixed16 => 16,
32        }
33    }
34
35    /// Compression ratio vs FP32
36    pub const fn compression_ratio(&self) -> usize {
37        32 / self.bits()
38    }
39}
40
41/// Quantization parameters for dequantization
42#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
43pub struct QuantParams {
44    /// Scale factor: real_value = quantized_value * scale + zero_point
45    pub scale: f32,
46    /// Zero point offset
47    pub zero_point: f32,
48    /// Min value in original tensor
49    pub min_val: f32,
50    /// Max value in original tensor
51    pub max_val: f32,
52}
53
54impl Default for QuantParams {
55    fn default() -> Self {
56        Self {
57            scale: 1.0 / 127.0,
58            zero_point: 0.0,
59            min_val: -1.0,
60            max_val: 1.0,
61        }
62    }
63}
64
65/// Quantized tensor stored in compact format
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct QuantizedTensor<const N: usize> {
68    /// Quantized data
69    pub data: HVec<u8, N>,
70    /// Shape (max 4 dimensions for embedded)
71    pub shape: [usize; 4],
72    /// Number of dimensions used
73    pub ndim: usize,
74    /// Quantization type
75    pub quant_type: QuantizationType,
76    /// Quantization parameters
77    pub params: QuantParams,
78}
79
80impl<const N: usize> QuantizedTensor<N> {
81    /// Create a new quantized tensor from f32 data
82    pub fn from_f32(data: &[f32], shape: &[usize], quant_type: QuantizationType) -> crate::Result<Self> {
83        if data.is_empty() {
84            return Err(crate::Error::QuantizationError("Empty data"));
85        }
86
87        // Calculate min/max
88        let mut min_val = f32::MAX;
89        let mut max_val = f32::MIN;
90        for &v in data {
91            if v < min_val { min_val = v; }
92            if v > max_val { max_val = v; }
93        }
94
95        let params = match quant_type {
96            QuantizationType::Int8 => {
97                let scale = (max_val - min_val) / 255.0;
98                let zero_point = -min_val / scale - 128.0;
99                QuantParams { scale, zero_point, min_val, max_val }
100            }
101            QuantizationType::Int4 => {
102                let scale = (max_val - min_val) / 15.0;
103                let zero_point = -min_val / scale - 8.0;
104                QuantParams { scale, zero_point, min_val, max_val }
105            }
106            QuantizationType::Binary => {
107                QuantParams {
108                    scale: 1.0,
109                    zero_point: 0.0,
110                    min_val: -1.0,
111                    max_val: 1.0,
112                }
113            }
114            QuantizationType::Fixed16 => {
115                let scale = (max_val - min_val) / 65535.0;
116                QuantParams { scale, zero_point: min_val, min_val, max_val }
117            }
118        };
119
120        let quantized_data = Self::quantize_data(data, quant_type, &params)?;
121
122        let mut shape_arr = [0usize; 4];
123        let ndim = shape.len().min(4);
124        for (i, &s) in shape.iter().take(4).enumerate() {
125            shape_arr[i] = s;
126        }
127
128        Ok(Self {
129            data: quantized_data,
130            shape: shape_arr,
131            ndim,
132            quant_type,
133            params,
134        })
135    }
136
137    fn quantize_data(data: &[f32], quant_type: QuantizationType, params: &QuantParams) -> crate::Result<HVec<u8, N>> {
138        let mut result = HVec::new();
139
140        match quant_type {
141            QuantizationType::Int8 => {
142                for &v in data {
143                    let q = ((v - params.min_val) / params.scale).round() as i16;
144                    let q = q.clamp(-128, 127) as i8;
145                    result.push(q as u8).map_err(|_| crate::Error::BufferOverflow)?;
146                }
147            }
148            QuantizationType::Int4 => {
149                // Pack 2 values per byte
150                for chunk in data.chunks(2) {
151                    let v0 = ((chunk[0] - params.min_val) / params.scale).round() as i8;
152                    let v1 = if chunk.len() > 1 {
153                        ((chunk[1] - params.min_val) / params.scale).round() as i8
154                    } else {
155                        0
156                    };
157                    let v0 = (v0.clamp(-8, 7) + 8) as u8;
158                    let v1 = (v1.clamp(-8, 7) + 8) as u8;
159                    let packed = (v0 & 0x0F) | ((v1 & 0x0F) << 4);
160                    result.push(packed).map_err(|_| crate::Error::BufferOverflow)?;
161                }
162            }
163            QuantizationType::Binary => {
164                // Pack 8 values per byte
165                for chunk in data.chunks(8) {
166                    let mut byte = 0u8;
167                    for (i, &v) in chunk.iter().enumerate() {
168                        if v >= 0.0 {
169                            byte |= 1 << i;
170                        }
171                    }
172                    result.push(byte).map_err(|_| crate::Error::BufferOverflow)?;
173                }
174            }
175            QuantizationType::Fixed16 => {
176                for &v in data {
177                    let q = ((v - params.min_val) / params.scale).round() as u16;
178                    result.push((q >> 8) as u8).map_err(|_| crate::Error::BufferOverflow)?;
179                    result.push((q & 0xFF) as u8).map_err(|_| crate::Error::BufferOverflow)?;
180                }
181            }
182        }
183
184        Ok(result)
185    }
186
187    /// Get total number of elements
188    pub fn numel(&self) -> usize {
189        self.shape[..self.ndim].iter().product()
190    }
191
192    /// Get compressed size in bytes
193    pub fn compressed_size(&self) -> usize {
194        self.data.len()
195    }
196
197    /// Memory savings compared to FP32
198    pub fn memory_savings(&self) -> f32 {
199        let fp32_size = self.numel() * 4;
200        1.0 - (self.compressed_size() as f32 / fp32_size as f32)
201    }
202}
203
204/// INT8 matrix-vector multiplication (optimized for ESP32)
205///
206/// Computes: output = weights @ input
207/// Where weights is [out_dim, in_dim] and input is [in_dim]
208#[inline(never)] // Prevent inlining for better cache behavior
209pub fn matmul_int8(
210    weights: &[i8],
211    _weight_params: &QuantParams,
212    input: &[i8],
213    _input_params: &QuantParams,
214    output: &mut [i32],
215    out_dim: usize,
216    in_dim: usize,
217) {
218    debug_assert_eq!(weights.len(), out_dim * in_dim);
219    debug_assert_eq!(input.len(), in_dim);
220    debug_assert_eq!(output.len(), out_dim);
221
222    for i in 0..out_dim {
223        let mut acc: i32 = 0;
224        let row_start = i * in_dim;
225
226        // Process 4 elements at a time for better performance
227        let chunks = in_dim / 4;
228        for j in 0..chunks {
229            let idx = j * 4;
230            acc += weights[row_start + idx] as i32 * input[idx] as i32;
231            acc += weights[row_start + idx + 1] as i32 * input[idx + 1] as i32;
232            acc += weights[row_start + idx + 2] as i32 * input[idx + 2] as i32;
233            acc += weights[row_start + idx + 3] as i32 * input[idx + 3] as i32;
234        }
235
236        // Handle remainder
237        for j in (chunks * 4)..in_dim {
238            acc += weights[row_start + j] as i32 * input[j] as i32;
239        }
240
241        output[i] = acc;
242    }
243}
244
245/// Dequantize INT32 accumulator to f32
246#[inline]
247pub fn dequantize_accumulator(
248    acc: i32,
249    weight_params: &QuantParams,
250    input_params: &QuantParams,
251) -> f32 {
252    acc as f32 * weight_params.scale * input_params.scale
253}
254
255/// Binary XNOR-popcount for extreme efficiency
256///
257/// For binary neural networks: computes hamming similarity
258#[inline]
259pub fn binary_xnor_popcount(a: &[u8], b: &[u8]) -> i32 {
260    debug_assert_eq!(a.len(), b.len());
261
262    let mut count: i32 = 0;
263    for (&x, &y) in a.iter().zip(b.iter()) {
264        // XNOR: same bits = 1, different = 0
265        let xnor = !(x ^ y);
266        count += xnor.count_ones() as i32;
267    }
268
269    // Convert popcount to -1/+1 dot product equivalent
270    // Each byte has 8 bits, so:
271    // dot = popcount * 2 - total_bits
272    let total_bits = (a.len() * 8) as i32;
273    count * 2 - total_bits
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn test_int8_quantization() {
282        let data = [-1.0f32, -0.5, 0.0, 0.5, 1.0];
283        let tensor: QuantizedTensor<64> = QuantizedTensor::from_f32(
284            &data,
285            &[5],
286            QuantizationType::Int8
287        ).unwrap();
288
289        assert_eq!(tensor.numel(), 5);
290        assert_eq!(tensor.compressed_size(), 5);
291        assert!(tensor.memory_savings() > 0.7); // 75% savings
292    }
293
294    #[test]
295    fn test_binary_xnor() {
296        let a = [0b11110000u8, 0b10101010];
297        let b = [0b11110000u8, 0b10101010];
298
299        // Perfect match: all 16 bits same
300        let result = binary_xnor_popcount(&a, &b);
301        assert_eq!(result, 16); // 16 * 2 - 16 = 16
302    }
303
304    #[test]
305    fn test_int4_packing() {
306        let data = [0.0f32, 0.5, -0.5, 1.0];
307        let tensor: QuantizedTensor<64> = QuantizedTensor::from_f32(
308            &data,
309            &[4],
310            QuantizationType::Int4
311        ).unwrap();
312
313        // 4 values packed into 2 bytes
314        assert_eq!(tensor.compressed_size(), 2);
315    }
316}