1#[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"))]
150pub 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}