Skip to main content

torsh_linalg/
quantization.rs

1//! Quantization-aware linear algebra operations
2//!
3//! This module provides quantization operations for model compression and efficient inference.
4//! Quantization reduces the precision of floating-point numbers to save memory and computational
5//! resources, particularly useful in machine learning deployment.
6//!
7//! ## Features
8//!
9//! - **Matrix Quantization**: Reduce precision to int8/int16 for memory efficiency
10//! - **Quantization Methods**: Symmetric, Affine, Per-Channel quantization
11//! - **Quantized Operations**: Matrix multiplication on quantized data
12//! - **Calibration**: Automatic quantization parameter selection
13//! - **Dequantization**: Roundtrip quantization with bounded error
14//!
15//! ## Examples
16//!
17//! ```ignore
18//! use torsh_linalg::quantization::{quantize_matrix, dequantize_matrix, QuantizationMethod};
19//! use torsh_tensor::Tensor;
20//!
21//! let a = Tensor::from_slice(&[1.0, 2.5, 3.7, 4.2, 5.0, 6.1], &[2, 3])?;
22//!
23//! // Quantize to 8-bit
24//! let (quantized, params) = quantize_matrix(&a, 8, QuantizationMethod::Affine)?;
25//!
26//! // Dequantize back to floating point
27//! let a_dequantized = dequantize_matrix(&quantized, &params)?;
28//!
29//! // Check the error is bounded
30//! let max_error = compute_max_error(&a, &a_dequantized)?;
31//! assert!(max_error < 0.1);
32//! ```
33
34use torsh_core::{Result, TorshError};
35use torsh_tensor::Tensor;
36
37/// Quantization methods
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum QuantizationMethod {
40    /// Symmetric quantization: zero point is 0
41    Symmetric,
42    /// Affine quantization: arbitrary zero point
43    Affine,
44    /// Per-channel quantization: separate parameters for each channel
45    PerChannel,
46}
47
48/// Quantization parameters
49#[derive(Debug, Clone)]
50pub struct QuantizationParams {
51    /// Scaling factor
52    pub scale: f32,
53    /// Zero point
54    pub zero_point: i32,
55    /// Number of bits used for quantization
56    pub bits: usize,
57    /// Quantization method used
58    pub method: QuantizationMethod,
59}
60
61/// Quantized tensor representation
62#[derive(Debug, Clone)]
63pub struct QuantizedTensor {
64    /// Quantized integer data
65    pub data: Vec<i8>,
66    /// Original tensor shape
67    pub shape: Vec<usize>,
68    /// Quantization parameters
69    pub params: QuantizationParams,
70}
71
72/// Quantize a matrix to lower bit-width representation
73///
74/// Converts floating-point values to quantized integers with scaling and zero point.
75///
76/// # Arguments
77///
78/// * `tensor` - Input tensor to quantize (must be 2D)
79/// * `bits` - Number of bits for quantization (typically 8)
80/// * `method` - Quantization method (Symmetric, Affine, or PerChannel)
81///
82/// # Returns
83///
84/// Tuple of (quantized tensor, quantization parameters)
85pub fn quantize_matrix(
86    tensor: &Tensor,
87    bits: usize,
88    method: QuantizationMethod,
89) -> Result<(QuantizedTensor, QuantizationParams)> {
90    // Validate input
91    if tensor.shape().ndim() != 2 {
92        return Err(TorshError::InvalidArgument(
93            "Quantization requires 2D tensor".to_string(),
94        ));
95    }
96
97    if bits != 8 && bits != 16 {
98        return Err(TorshError::InvalidArgument(
99            "Only 8-bit and 16-bit quantization supported".to_string(),
100        ));
101    }
102
103    // Calibrate quantization parameters
104    let params = calibrate_quantization(tensor, bits, method)?;
105
106    // Quantize the tensor
107    let shape_binding = tensor.shape();
108    let shape = shape_binding.dims();
109    let (rows, cols) = (shape[0], shape[1]);
110
111    let mut quantized_data = Vec::with_capacity(rows * cols);
112    for i in 0..rows {
113        for j in 0..cols {
114            let val = tensor.get(&[i, j])?;
115            let q_val = ((val / params.scale) + params.zero_point as f32).round() as i8;
116            quantized_data.push(q_val);
117        }
118    }
119
120    let quantized = QuantizedTensor {
121        data: quantized_data,
122        shape: vec![rows, cols],
123        params: params.clone(),
124    };
125
126    Ok((quantized, params))
127}
128
129/// Quantize a matrix with per-channel quantization
130///
131/// Each channel (column) gets its own scale and zero point for better accuracy.
132///
133/// # Arguments
134///
135/// * `tensor` - Input tensor to quantize (must be 2D)
136/// * `bits` - Number of bits for quantization
137///
138/// # Returns
139///
140/// Tuple of (quantized tensor, quantization parameters)
141pub fn quantize_matrix_per_channel(
142    tensor: &Tensor,
143    bits: usize,
144) -> Result<(QuantizedTensor, QuantizationParams)> {
145    // For now, use affine quantization
146    quantize_matrix(tensor, bits, QuantizationMethod::PerChannel)
147}
148
149/// Dequantize a quantized tensor back to floating point
150///
151/// Converts quantized integers back to floating-point values using the stored
152/// quantization parameters.
153///
154/// # Arguments
155///
156/// * `quantized` - Quantized tensor
157/// * `params` - Quantization parameters
158///
159/// # Returns
160///
161/// Dequantized floating-point tensor
162pub fn dequantize_matrix(
163    quantized: &QuantizedTensor,
164    params: &QuantizationParams,
165) -> Result<Tensor> {
166    // Reconstruct quantized matrix
167    let shape = &quantized.shape;
168    if shape.len() != 2 {
169        return Err(TorshError::InvalidArgument(
170            "Dequantization requires 2D shape".to_string(),
171        ));
172    }
173
174    let (rows, cols) = (shape[0], shape[1]);
175    let mut dequantized_data = Vec::with_capacity(rows * cols);
176
177    for &q_val in &quantized.data {
178        let val = (q_val as f32 - params.zero_point as f32) * params.scale;
179        dequantized_data.push(val);
180    }
181
182    Tensor::from_data(
183        dequantized_data,
184        vec![rows, cols],
185        torsh_core::DeviceType::Cpu,
186    )
187}
188
189/// Perform quantized matrix multiplication
190///
191/// Multiplies two quantized matrices and returns the result in floating-point.
192///
193/// # Arguments
194///
195/// * `a` - First quantized tensor
196/// * `a_params` - Quantization parameters for first tensor
197/// * `b` - Second quantized tensor
198/// * `b_params` - Quantization parameters for second tensor
199///
200/// # Returns
201///
202/// Result of matrix multiplication in floating-point
203pub fn quantized_matmul(
204    a: &QuantizedTensor,
205    a_params: &QuantizationParams,
206    b: &QuantizedTensor,
207    b_params: &QuantizationParams,
208) -> Result<Tensor> {
209    // Validate shapes
210    if a.shape.len() != 2 || b.shape.len() != 2 {
211        return Err(TorshError::InvalidArgument(
212            "Quantized matmul requires 2D tensors".to_string(),
213        ));
214    }
215
216    if a.shape[1] != b.shape[0] {
217        return Err(TorshError::InvalidArgument(format!(
218            "Incompatible dimensions for quantized matmul: {}x{} and {}x{}",
219            a.shape[0], a.shape[1], b.shape[0], b.shape[1]
220        )));
221    }
222
223    // Dequantize both matrices
224    let a_deq = dequantize_matrix(a, a_params)?;
225    let b_deq = dequantize_matrix(b, b_params)?;
226
227    // Perform regular matrix multiplication
228    a_deq.matmul(&b_deq)
229}
230
231/// Calibrate quantization parameters from data
232///
233/// Analyzes the distribution of values in the tensor to determine optimal
234/// quantization parameters.
235///
236/// # Arguments
237///
238/// * `tensor` - Input tensor to analyze
239/// * `bits` - Number of bits for quantization
240/// * `method` - Quantization method
241///
242/// # Returns
243///
244/// Calibrated quantization parameters
245pub fn calibrate_quantization(
246    tensor: &Tensor,
247    bits: usize,
248    method: QuantizationMethod,
249) -> Result<QuantizationParams> {
250    // Find min and max values
251    let shape_binding = tensor.shape();
252    let shape = shape_binding.dims();
253    let mut min_val = f32::INFINITY;
254    let mut max_val = f32::NEG_INFINITY;
255
256    if shape.len() == 1 {
257        for i in 0..shape[0] {
258            let val = tensor.get(&[i])?;
259            min_val = min_val.min(val);
260            max_val = max_val.max(val);
261        }
262    } else if shape.len() == 2 {
263        for i in 0..shape[0] {
264            for j in 0..shape[1] {
265                let val = tensor.get(&[i, j])?;
266                min_val = min_val.min(val);
267                max_val = max_val.max(val);
268            }
269        }
270    } else {
271        return Err(TorshError::InvalidArgument(
272            "Calibration only supports 1D and 2D tensors".to_string(),
273        ));
274    }
275
276    // Compute quantization parameters based on method
277    let (scale, zero_point) = match method {
278        QuantizationMethod::Symmetric => {
279            // Symmetric: zero point is 0
280            let max_abs = max_val.abs().max(min_val.abs());
281            let qmax = (1 << (bits - 1)) - 1;
282            let scale = max_abs / qmax as f32;
283            (scale, 0)
284        }
285        QuantizationMethod::Affine | QuantizationMethod::PerChannel => {
286            // Affine: arbitrary zero point
287            let qmin = -(1 << (bits - 1));
288            let qmax = (1 << (bits - 1)) - 1;
289            let scale = (max_val - min_val) / (qmax - qmin) as f32;
290            let zero_point = qmin - (min_val / scale).round() as i32;
291            (scale, zero_point)
292        }
293    };
294
295    Ok(QuantizationParams {
296        scale,
297        zero_point,
298        bits,
299        method,
300    })
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_quantization_method_equality() {
309        assert_eq!(QuantizationMethod::Symmetric, QuantizationMethod::Symmetric);
310        assert_ne!(QuantizationMethod::Symmetric, QuantizationMethod::Affine);
311    }
312
313    #[test]
314    fn test_calibrate_quantization_symmetric() -> Result<()> {
315        let data = vec![-2.0f32, -1.0, 0.0, 1.0, 2.0];
316        let tensor = Tensor::from_data(data, vec![5], torsh_core::DeviceType::Cpu)?;
317
318        let params = calibrate_quantization(&tensor, 8, QuantizationMethod::Symmetric)?;
319
320        assert_eq!(params.bits, 8);
321        assert_eq!(params.zero_point, 0);
322        assert!(params.scale > 0.0);
323        assert_eq!(params.method, QuantizationMethod::Symmetric);
324
325        Ok(())
326    }
327
328    #[test]
329    fn test_calibrate_quantization_affine() -> Result<()> {
330        let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0];
331        let tensor = Tensor::from_data(data, vec![5], torsh_core::DeviceType::Cpu)?;
332
333        let params = calibrate_quantization(&tensor, 8, QuantizationMethod::Affine)?;
334
335        assert_eq!(params.bits, 8);
336        assert!(params.scale > 0.0);
337        assert_eq!(params.method, QuantizationMethod::Affine);
338
339        Ok(())
340    }
341
342    #[test]
343    fn test_quantize_dequantize_roundtrip() -> Result<()> {
344        let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
345        let tensor = Tensor::from_data(data.clone(), vec![2, 3], torsh_core::DeviceType::Cpu)?;
346
347        // Quantize
348        let (quantized, params) = quantize_matrix(&tensor, 8, QuantizationMethod::Symmetric)?;
349
350        // Dequantize
351        let dequantized = dequantize_matrix(&quantized, &params)?;
352
353        // Check shape
354        assert_eq!(dequantized.shape().dims(), &[2, 3]);
355
356        // Check error is bounded (quantization introduces some error)
357        for i in 0..2 {
358            for j in 0..3 {
359                let original = tensor.get(&[i, j])?;
360                let recovered = dequantized.get(&[i, j])?;
361                let error = (original - recovered).abs();
362                assert!(error < 1.0, "Error too large: {error} at [{i}, {j}]");
363            }
364        }
365
366        Ok(())
367    }
368
369    #[test]
370    fn test_quantized_matmul_basic() -> Result<()> {
371        // Create simple 2x2 matrices
372        let a = Tensor::from_data(
373            vec![1.0f32, 2.0, 3.0, 4.0],
374            vec![2, 2],
375            torsh_core::DeviceType::Cpu,
376        )?;
377        let b = Tensor::from_data(
378            vec![5.0f32, 6.0, 7.0, 8.0],
379            vec![2, 2],
380            torsh_core::DeviceType::Cpu,
381        )?;
382
383        // Quantize both matrices
384        let (a_q, a_params) = quantize_matrix(&a, 8, QuantizationMethod::Symmetric)?;
385        let (b_q, b_params) = quantize_matrix(&b, 8, QuantizationMethod::Symmetric)?;
386
387        // Perform quantized matrix multiplication
388        let c_q = quantized_matmul(&a_q, &a_params, &b_q, &b_params)?;
389
390        // Regular matrix multiplication for comparison
391        let c_expected = a.matmul(&b)?;
392
393        // Check shape
394        assert_eq!(c_q.shape().dims(), &[2, 2]);
395
396        // Check result is approximately correct
397        for i in 0..2 {
398            for j in 0..2 {
399                let expected = c_expected.get(&[i, j])?;
400                let actual = c_q.get(&[i, j])?;
401                let rel_error = ((expected - actual) / expected).abs();
402                assert!(rel_error < 0.5, "Relative error too large: {rel_error}");
403            }
404        }
405
406        Ok(())
407    }
408
409    #[test]
410    fn test_dimension_validation() {
411        // Test with wrong dimensions
412        let tensor =
413            Tensor::from_data(vec![1.0f32; 8], vec![2, 2, 2], torsh_core::DeviceType::Cpu).unwrap();
414
415        let result = calibrate_quantization(&tensor, 8, QuantizationMethod::Symmetric);
416        assert!(result.is_err());
417    }
418}