Skip to main content

singe_kernel/cpu/
quantization.rs

1//! Quantize, dequantize, int4 unpacking, fp8 helpers, and quantized matmul.
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct QuantizedMatmulConfig {
5    pub rows: usize,
6    pub columns: usize,
7    pub reduction: usize,
8    pub activation_row_stride: usize,
9    pub weight_row_stride: usize,
10    pub output_row_stride: usize,
11    pub activation_zero_point: i32,
12    pub weight_zero_point: i32,
13    pub output_scale: f32,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct DequantizedWeightMatmulConfig {
18    pub rows: usize,
19    pub columns: usize,
20    pub reduction: usize,
21    pub activation_row_stride: usize,
22    pub weight_row_stride: usize,
23    pub output_row_stride: usize,
24}
25
26pub fn dequantize_u8_grouped(
27    input: &[u8],
28    scales: &[f32],
29    zero_points: &[f32],
30    group_size: usize,
31) -> Vec<f32> {
32    input
33        .iter()
34        .enumerate()
35        .map(|(index, value)| {
36            let group = index / group_size;
37            (*value as f32 - zero_points[group]) * scales[group]
38        })
39        .collect()
40}
41
42pub fn dequantize_i8_grouped(
43    input: &[i8],
44    scales: &[f32],
45    zero_points: &[f32],
46    group_size: usize,
47) -> Vec<f32> {
48    input
49        .iter()
50        .enumerate()
51        .map(|(index, value)| {
52            let group = index / group_size;
53            (*value as f32 - zero_points[group]) * scales[group]
54        })
55        .collect()
56}
57
58pub fn matmul_i8_i8_f32(
59    activations: &[i8],
60    weights: &[i8],
61    config: QuantizedMatmulConfig,
62) -> Vec<f32> {
63    let mut out = vec![0.0; config.rows * config.columns];
64    for row in 0..config.rows {
65        for column in 0..config.columns {
66            let mut sum = 0i32;
67            for reduction in 0..config.reduction {
68                let activation = activations[row * config.activation_row_stride + reduction];
69                let weight = weights[column * config.weight_row_stride + reduction];
70                sum += (activation as i32 - config.activation_zero_point)
71                    * (weight as i32 - config.weight_zero_point);
72            }
73            out[row * config.columns + column] = sum as f32 * config.output_scale;
74        }
75    }
76    out
77}
78
79pub fn matmul_f32_i8_dequantize_f32(
80    activations: &[f32],
81    weights: &[i8],
82    scales: &[f32],
83    zero_points: &[f32],
84    config: DequantizedWeightMatmulConfig,
85) -> Vec<f32> {
86    let mut out = vec![0.0; config.rows * config.columns];
87    for row in 0..config.rows {
88        for column in 0..config.columns {
89            let mut sum = 0.0f32;
90            for reduction in 0..config.reduction {
91                let activation = activations[row * config.activation_row_stride + reduction];
92                let weight = weights[column * config.weight_row_stride + reduction];
93                let weight = (weight as f32 - zero_points[column]) * scales[column];
94                sum += activation * weight;
95            }
96            out[row * config.columns + column] = sum;
97        }
98    }
99    out
100}
101
102pub fn scatter_strided_i8(
103    values: &[i8],
104    rows: usize,
105    columns: usize,
106    row_stride: usize,
107    fill: i8,
108) -> Vec<i8> {
109    let mut out = vec![fill; (rows - 1) * row_stride + columns];
110    for row in 0..rows {
111        for column in 0..columns {
112            out[row * row_stride + column] = values[row * columns + column];
113        }
114    }
115    out
116}
117
118pub fn scatter_strided_f32(
119    values: &[f32],
120    rows: usize,
121    columns: usize,
122    row_stride: usize,
123    fill: f32,
124) -> Vec<f32> {
125    let mut out = vec![fill; (rows - 1) * row_stride + columns];
126    for row in 0..rows {
127        for column in 0..columns {
128            out[row * row_stride + column] = values[row * columns + column];
129        }
130    }
131    out
132}
133
134pub fn gather_strided_f32(
135    values: &[f32],
136    rows: usize,
137    columns: usize,
138    row_stride: usize,
139) -> Vec<f32> {
140    let mut out = Vec::with_capacity(rows * columns);
141    for row in 0..rows {
142        for column in 0..columns {
143            out.push(values[row * row_stride + column]);
144        }
145    }
146    out
147}
148
149#[cfg(all(feature = "dtype-f32", feature = "dtype-i8"))]
150/// Per-token group int8 quantization using round-to-nearest-even.
151///
152/// Each group uses `scale = max(max(abs(group)), eps) / 127`.
153/// Values are divided by that scale, clamped to `[-128, 127]`, and rounded to nearest even before conversion to `i8`.
154pub fn per_token_group_quant_i8_f32(
155    input: &[f32],
156    group_size: usize,
157    eps: f32,
158) -> (Vec<i8>, Vec<f32>) {
159    let mut out = vec![0i8; input.len()];
160    let mut scales = Vec::with_capacity(input.len() / group_size);
161    for (group_index, group) in input.chunks_exact(group_size).enumerate() {
162        let max_abs = group
163            .iter()
164            .copied()
165            .map(f32::abs)
166            .fold(0.0f32, f32::max)
167            .max(eps);
168        let scale = max_abs / 127.0;
169        scales.push(scale);
170        for (index, value) in group.iter().copied().enumerate() {
171            out[group_index * group_size + index] = quantize_i8_round_nearest_even(value / scale);
172        }
173    }
174    (out, scales)
175}
176
177#[cfg(all(feature = "dtype-f32", feature = "dtype-i8"))]
178fn quantize_i8_round_nearest_even(value: f32) -> i8 {
179    let clamped = value.clamp(-128.0, 127.0);
180    let floored = clamped.floor();
181    let fraction = clamped - floored;
182    let rounded = if fraction > 0.5 || (fraction == 0.5 && floored.rem_euclid(2.0) != 0.0) {
183        floored + 1.0
184    } else {
185        floored
186    };
187    rounded as i8
188}
189
190pub fn matmul_f8e4m3_block_scaled_f32(
191    activations: &[u8],
192    weights: &[u8],
193    activation_scales: &[f32],
194    weight_scales: &[f32],
195    rows: usize,
196    columns: usize,
197    reduction: usize,
198    group_n: usize,
199    group_k: usize,
200) -> Vec<f32> {
201    let k_groups = reduction.div_ceil(group_k);
202    let mut out = vec![0.0f32; rows * columns];
203    for row in 0..rows {
204        for column in 0..columns {
205            let mut sum = 0.0f32;
206            for k in 0..reduction {
207                let k_group = k / group_k;
208                let activation_scale = activation_scales[row * k_groups + k_group];
209                let weight_scale = weight_scales[(column / group_n) * k_groups + k_group];
210                sum += f8e4m3_value(activations[row * reduction + k])
211                    * f8e4m3_value(weights[column * reduction + k])
212                    * activation_scale
213                    * weight_scale;
214            }
215            out[row * columns + column] = sum;
216        }
217    }
218    out
219}
220
221pub fn dequantize_f8e4m3_block_f32(
222    input: &[u8],
223    scales: &[f32],
224    rows: usize,
225    columns: usize,
226    block_size: usize,
227) -> Vec<f32> {
228    let scale_columns = columns.div_ceil(block_size);
229    let mut out = vec![0.0f32; rows * columns];
230    for row in 0..rows {
231        for column in 0..columns {
232            let offset = row * columns + column;
233            let scale_offset = (row / block_size) * scale_columns + column / block_size;
234            out[offset] = f8e4m3_value(input[offset]) * scales[scale_offset];
235        }
236    }
237    out
238}
239
240pub fn f8e4m3_value(value: u8) -> f32 {
241    let sign = if value & 0x80 == 0 { 1.0 } else { -1.0 };
242    let exponent = (value >> 3) & 0x0f;
243    let mantissa = value & 0x07;
244    if exponent == 0x0f && mantissa == 0x07 {
245        f32::NAN
246    } else if exponent == 0 {
247        if mantissa == 0 {
248            sign * 0.0
249        } else {
250            sign * (mantissa as f32 / 8.0) * 2f32.powi(-6)
251        }
252    } else {
253        sign * (1.0 + mantissa as f32 / 8.0) * 2f32.powi(exponent as i32 - 7)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[cfg(all(feature = "dtype-f32", feature = "dtype-i8"))]
262    #[test]
263    fn per_token_group_quant_i8_rounds_ties_to_even() {
264        let input = vec![127.0f32, 1.5, 2.5, -1.5, -2.5];
265        let (actual, scales) = per_token_group_quant_i8_f32(&input, 5, 1e-10);
266
267        assert_eq!(actual, vec![127, 2, 2, -2, -2]);
268        assert_eq!(scales, vec![1.0]);
269    }
270
271    #[test]
272    fn f8e4m3_value_decodes_full_representative_range() {
273        let cases = [
274            (0x00, 0.0),
275            (0x01, 0.001953125),
276            (0x07, 0.013671875),
277            (0x08, 0.015625),
278            (0x30, 0.5),
279            (0x38, 1.0),
280            (0x40, 2.0),
281            (0x76, 224.0),
282            (0x7e, 448.0),
283            (0x81, -0.001953125),
284            (0xb8, -1.0),
285            (0xfe, -448.0),
286        ];
287        for (byte, expected) in cases {
288            assert_eq!(f8e4m3_value(byte), expected, "byte {byte:#04x}");
289        }
290        assert!(f8e4m3_value(0x7f).is_nan());
291        assert!(f8e4m3_value(0xff).is_nan());
292    }
293}