/home/noah/src/realizar/src/quantize/types.rs
Line | Count | Source |
1 | | //! Quantization Type Definitions (PMAT-802) |
2 | | //! |
3 | | //! Extracted from quantize/mod.rs - Common types and constants for quantization. |
4 | | //! |
5 | | //! ## Contents |
6 | | //! - Constants: `BLOCK_SIZE`, `QK_K` |
7 | | //! - Block structs: `Q4_0Block`, `Q8_0Block`, `Q8KSuperBlock`, `Q4_KBlock`, `Q5_KBlock`, `Q6_KBlock` |
8 | | //! - `InterleavedQ4K` - Interleaved Q4_K layout for SIMD |
9 | | |
10 | | use crate::error::{RealizarError, Result}; |
11 | | |
12 | | // Import f16_to_f32_lut from parent for InterleavedQ4K (not re-exported) |
13 | | use super::f16_to_f32_lut; |
14 | | |
15 | | // ============================================================================ |
16 | | // Constants |
17 | | // ============================================================================ |
18 | | |
19 | | /// Block size for `Q4_0` and `Q8_0` quantization |
20 | | pub const BLOCK_SIZE: usize = 32; |
21 | | |
22 | | /// Super-block size for K-quantization formats (`Q4_K`, `Q5_K`, `Q6_K`) |
23 | | pub const QK_K: usize = 256; |
24 | | |
25 | | // ============================================================================ |
26 | | // Block Structs |
27 | | // ============================================================================ |
28 | | |
29 | | /// `Q4_0` quantized block |
30 | | /// |
31 | | /// Each block contains: |
32 | | /// - 1 float32 scale factor |
33 | | /// - 16 bytes (32 4-bit values, 2 per byte) |
34 | | #[derive(Debug, Clone)] |
35 | | pub struct Q4_0Block { |
36 | | /// Scale factor for dequantization |
37 | | pub scale: f32, |
38 | | /// Quantized values (16 bytes = 32 4-bit values) |
39 | | pub quants: [u8; 16], |
40 | | } |
41 | | |
42 | | /// `Q8_0` quantized block |
43 | | /// |
44 | | /// Each block contains: |
45 | | /// - 1 float32 scale factor |
46 | | /// - 32 int8 values |
47 | | #[derive(Debug, Clone)] |
48 | | pub struct Q8_0Block { |
49 | | /// Scale factor for dequantization |
50 | | pub scale: f32, |
51 | | /// Quantized values (32 int8 values) |
52 | | pub quants: [i8; 32], |
53 | | } |
54 | | |
55 | | impl Q8_0Block { |
56 | | /// Quantize 32 f32 values to Q8_0 format |
57 | | /// |
58 | | /// Dynamic quantization for activations during inference. |
59 | | /// Uses symmetric quantization: scale = max(abs(values)) / 127.0 |
60 | | /// |
61 | | /// # Arguments |
62 | | /// * `values` - Exactly 32 f32 values to quantize |
63 | | /// |
64 | | /// # Returns |
65 | | /// A Q8_0Block with scale and quantized int8 values |
66 | | /// |
67 | | /// # Example |
68 | | /// ```ignore |
69 | | /// let values = [1.0f32; 32]; |
70 | | /// let block = Q8_0Block::quantize(&values); |
71 | | /// assert_eq!(block.quants[0], 127); // max value maps to 127 |
72 | | /// ``` |
73 | | #[must_use] |
74 | 244 | pub fn quantize(values: &[f32; 32]) -> Self { |
75 | | // Find max absolute value for symmetric quantization |
76 | 7.80k | let max_abs244 = values244 .iter244 ().map244 (|v| v.abs()).fold244 (0.0f32, f32::max); |
77 | | |
78 | | // Avoid division by zero |
79 | 244 | let scale = if max_abs > 1e-10 { |
80 | 232 | max_abs / 127.0 |
81 | | } else { |
82 | 12 | 1.0 / 127.0 // Minimal scale for near-zero blocks |
83 | | }; |
84 | | |
85 | | // Quantize each value: qi = round(fi / scale), clamped to [-128, 127] |
86 | 244 | let mut quants = [0i8; 32]; |
87 | 7.80k | for (i, &v) in values244 .iter244 ().enumerate244 () { |
88 | 7.80k | let q = (v / scale).round(); |
89 | 7.80k | quants[i] = q.clamp(-128.0, 127.0) as i8; |
90 | 7.80k | } |
91 | | |
92 | 244 | Self { scale, quants } |
93 | 244 | } |
94 | | |
95 | | /// Dequantize the block back to f32 values |
96 | | /// |
97 | | /// # Returns |
98 | | /// Array of 32 f32 values: values[i] = quants[i] * scale |
99 | | #[must_use] |
100 | 54 | pub fn dequantize(&self) -> [f32; 32] { |
101 | 54 | let mut values = [0.0f32; 32]; |
102 | 1.72k | for (i, &q) in self.quants54 .iter54 ().enumerate54 () { |
103 | 1.72k | values[i] = q as f32 * self.scale; |
104 | 1.72k | } |
105 | 54 | values |
106 | 54 | } |
107 | | |
108 | | /// Compute quantization error (max absolute difference) |
109 | | #[must_use] |
110 | 26 | pub fn quantization_error(&self, original: &[f32; 32]) -> f32 { |
111 | 26 | let dequantized = self.dequantize(); |
112 | 26 | original |
113 | 26 | .iter() |
114 | 26 | .zip(dequantized.iter()) |
115 | 832 | .map26 (|(a, b)| (a - b).abs()) |
116 | 26 | .fold(0.0f32, f32::max) |
117 | 26 | } |
118 | | |
119 | | /// Compute relative quantization error |
120 | | #[must_use] |
121 | 18 | pub fn relative_error(&self, original: &[f32; 32]) -> f32 { |
122 | 576 | let max_val18 = original18 .iter18 ().map18 (|v| v.abs()).fold18 (0.0f32, f32::max); |
123 | 18 | if max_val < 1e-10 { |
124 | 5 | return 0.0; |
125 | 13 | } |
126 | 13 | self.quantization_error(original) / max_val |
127 | 18 | } |
128 | | } |
129 | | |
130 | | /// `Q8_K` quantized super-block (llama.cpp-compatible activation format) |
131 | | /// |
132 | | /// Super-block aligned format for maximum SIMD efficiency with Q4_K weights. |
133 | | /// Uses single scale per 256 values (vs Q8_0's scale per 32 values). |
134 | | /// |
135 | | /// Each super-block contains: |
136 | | /// - 1 float32 scale factor (for all 256 values) |
137 | | /// - 256 int8 quantized values |
138 | | /// |
139 | | /// # Performance |
140 | | /// |
141 | | /// - Aligned with Q4_K super-block (256 values) |
142 | | /// - Single scale multiplication per super-block (vs 8 for Q8_0) |
143 | | /// - Enables contiguous SIMD loads without shuffle/deinterleave |
144 | | /// - Matches llama.cpp `block_q8_K` structure |
145 | | #[derive(Debug, Clone)] |
146 | | pub struct Q8KSuperBlock { |
147 | | /// Scale factor for the entire super-block |
148 | | pub scale: f32, |
149 | | /// 256 quantized int8 values |
150 | | pub quants: [i8; 256], |
151 | | } |
152 | | |
153 | | impl Q8KSuperBlock { |
154 | | /// Quantize 256 f32 values to Q8_K format |
155 | | /// |
156 | | /// Uses symmetric quantization: scale = max(abs(values)) / 127.0 |
157 | | /// |
158 | | /// # Arguments |
159 | | /// * `values` - Exactly 256 f32 values (one super-block) |
160 | | /// |
161 | | /// # Returns |
162 | | /// A Q8KSuperBlock with single scale and 256 quantized values |
163 | | #[must_use] |
164 | 19 | pub fn quantize(values: &[f32; 256]) -> Self { |
165 | | // Find max absolute value for symmetric quantization |
166 | 4.86k | let max_abs19 = values19 .iter19 ().map19 (|v| v.abs()).fold19 (0.0f32, f32::max); |
167 | | |
168 | | // Avoid division by zero |
169 | 19 | let scale = if max_abs > 1e-10 { |
170 | 16 | max_abs / 127.0 |
171 | | } else { |
172 | 3 | 1.0 / 127.0 |
173 | | }; |
174 | | |
175 | 19 | let inv_scale = 1.0 / scale; |
176 | | |
177 | | // Quantize all 256 values |
178 | 19 | let mut quants = [0i8; 256]; |
179 | 4.86k | for (i, &v) in values19 .iter19 ().enumerate19 () { |
180 | 4.86k | let q = (v * inv_scale).round(); |
181 | 4.86k | quants[i] = q.clamp(-128.0, 127.0) as i8; |
182 | 4.86k | } |
183 | | |
184 | 19 | Self { scale, quants } |
185 | 19 | } |
186 | | |
187 | | /// Zero-allocation quantization into pre-allocated buffer |
188 | | /// |
189 | | /// # Arguments |
190 | | /// * `values` - 256 f32 values to quantize |
191 | | /// * `scale_out` - Output for scale value |
192 | | /// * `quants_out` - Output buffer for 256 int8 quantized values |
193 | | #[inline] |
194 | 18 | pub fn quantize_into(values: &[f32], scale_out: &mut f32, quants_out: &mut [i8]) { |
195 | 18 | debug_assert!(values.len() >= 256); |
196 | 18 | debug_assert!(quants_out.len() >= 256); |
197 | | |
198 | | // Find max absolute value |
199 | 4.60k | let max_abs18 = values[..256]18 .iter18 ().map18 (|v| v.abs()).fold18 (0.0f32, f32::max); |
200 | | |
201 | 18 | let scale = if max_abs > 1e-10 { |
202 | 17 | max_abs / 127.0 |
203 | | } else { |
204 | 1 | 1.0 / 127.0 |
205 | | }; |
206 | 18 | *scale_out = scale; |
207 | | |
208 | 18 | let inv_scale = 1.0 / scale; |
209 | | |
210 | 4.60k | for (i, &v) in values[..256]18 .iter18 ().enumerate18 () { |
211 | 4.60k | let q = (v * inv_scale).round(); |
212 | 4.60k | quants_out[i] = q.clamp(-128.0, 127.0) as i8; |
213 | 4.60k | } |
214 | 18 | } |
215 | | |
216 | | /// Dequantize back to f32 values |
217 | | #[must_use] |
218 | 6 | pub fn dequantize(&self) -> [f32; 256] { |
219 | 6 | let mut values = [0.0f32; 256]; |
220 | 1.53k | for (i, &q) in self.quants6 .iter6 ().enumerate6 () { |
221 | 1.53k | values[i] = q as f32 * self.scale; |
222 | 1.53k | } |
223 | 6 | values |
224 | 6 | } |
225 | | } |
226 | | |
227 | | /// `Q4_K` quantized super-block |
228 | | /// |
229 | | /// K-quantization uses super-blocks of 256 values (8 blocks of 32 each). |
230 | | /// Achieves 4.5 bits per weight with better quality than `Q4_0`. |
231 | | /// |
232 | | /// Each super-block contains: |
233 | | /// - 1 half-precision scale factor (`d`) |
234 | | /// - 1 half-precision min factor (`dmin`) |
235 | | /// - 12 bytes of 6-bit quantized scales (for 8 blocks) |
236 | | /// - 128 bytes of 4-bit quantized values (256 values) |
237 | | /// |
238 | | /// Total: 2 + 2 + 12 + 128 = 144 bytes per super-block of 256 values |
239 | | /// = 4.5 bits per weight |
240 | | #[derive(Debug, Clone)] |
241 | | #[allow(non_camel_case_types)] |
242 | | pub struct Q4_KBlock { |
243 | | /// Super-block scale (f16, stored as f32 after conversion) |
244 | | pub d: f32, |
245 | | /// Super-block min (f16, stored as f32 after conversion) |
246 | | pub dmin: f32, |
247 | | /// Per-block scales (packed 6-bit values) |
248 | | pub scales: [u8; 12], |
249 | | /// Quantized values (128 bytes = 256 4-bit values) |
250 | | pub qs: [u8; 128], |
251 | | } |
252 | | |
253 | | /// `Q5_K` quantized super-block |
254 | | /// |
255 | | /// K-quantization uses super-blocks of 256 values (8 blocks of 32 each). |
256 | | /// Achieves 5.5 bits per weight with higher quality than `Q4_K`. |
257 | | /// |
258 | | /// Each super-block contains: |
259 | | /// - 1 half-precision scale factor (`d`) |
260 | | /// - 1 half-precision min factor (`dmin`) |
261 | | /// - 12 bytes of 6-bit quantized scales (for 8 blocks) |
262 | | /// - 32 bytes of high bits (1 bit per value for 5-bit quantization) |
263 | | /// - 128 bytes of low 4-bit quantized values |
264 | | /// |
265 | | /// Total: 2 + 2 + 12 + 32 + 128 = 176 bytes per super-block of 256 values |
266 | | /// = 5.5 bits per weight |
267 | | #[derive(Debug, Clone)] |
268 | | #[allow(non_camel_case_types)] |
269 | | pub struct Q5_KBlock { |
270 | | /// Super-block scale |
271 | | pub d: f32, |
272 | | /// Super-block min |
273 | | pub dmin: f32, |
274 | | /// Per-block scales (packed 6-bit values) |
275 | | pub scales: [u8; 12], |
276 | | /// High bits (1 bit per value) |
277 | | pub qh: [u8; 32], |
278 | | /// Low 4-bit quantized values |
279 | | pub qs: [u8; 128], |
280 | | } |
281 | | |
282 | | /// `Q6_K` quantized super-block |
283 | | /// |
284 | | /// K-quantization uses super-blocks of 256 values (16 blocks of 16 each). |
285 | | /// Achieves 6.5625 bits per weight with the highest quality among K-quant formats. |
286 | | /// |
287 | | /// Each super-block contains: |
288 | | /// - 1 half-precision scale factor (`d`) |
289 | | /// - 16 bytes of 8-bit quantized scales (for 16 blocks) |
290 | | /// - 64 bytes of high 2 bits (2 bits per value for 6-bit quantization) |
291 | | /// - 128 bytes of low 4-bit quantized values |
292 | | /// |
293 | | /// Total: 2 + 16 + 64 + 128 = 210 bytes per super-block of 256 values |
294 | | /// = 6.5625 bits per weight |
295 | | #[derive(Debug, Clone)] |
296 | | #[allow(non_camel_case_types)] |
297 | | pub struct Q6_KBlock { |
298 | | /// Super-block scale |
299 | | pub d: f32, |
300 | | /// Per-block scales (8-bit signed) |
301 | | pub scales: [i8; 16], |
302 | | /// High 2 bits per value |
303 | | pub qh: [u8; 64], |
304 | | /// Low 4-bit quantized values |
305 | | pub qs: [u8; 128], |
306 | | } |
307 | | |
308 | | /// Interleaved Q4_K layout optimized for SIMD operations |
309 | | /// |
310 | | /// Reorders quantized values during model load so that SIMD dot products |
311 | | /// can process contiguous memory without gather operations. |
312 | | /// |
313 | | /// # Performance |
314 | | /// |
315 | | /// The interleaved layout eliminates cross-lane shuffles in AVX2: |
316 | | /// - Standard Q4_K: requires `vpermd` for each 32-value block (~5 cycles) |
317 | | /// - Interleaved: direct `vmovdqu` loads (~1 cycle) |
318 | | /// |
319 | | /// Trade-off: ~10% slower model load, ~15% faster inference. |
320 | | #[derive(Debug, Clone)] |
321 | | pub struct InterleavedQ4K { |
322 | | /// Super-block scales (one f32 per super-block) |
323 | | pub d: Vec<f32>, |
324 | | /// Super-block mins (one f32 per super-block) |
325 | | pub dmin: Vec<f32>, |
326 | | /// Per-block scales (12 bytes per super-block) |
327 | | pub scales: Vec<u8>, |
328 | | /// Interleaved quantized values (128 bytes per super-block) |
329 | | pub qs: Vec<u8>, |
330 | | /// Number of super-blocks |
331 | | pub num_super_blocks: usize, |
332 | | } |
333 | | |
334 | | impl InterleavedQ4K { |
335 | | /// Create interleaved Q4_K from raw GGUF Q4_K data |
336 | | /// |
337 | | /// Reorders the quantized values at load time for SIMD-efficient access. |
338 | | /// This is a one-time cost at model load that amortizes over all inference. |
339 | | /// |
340 | | /// # Arguments |
341 | | /// |
342 | | /// * `q4k_data` - Raw Q4_K data (144 bytes per super-block) |
343 | | /// |
344 | | /// # Returns |
345 | | /// |
346 | | /// InterleavedQ4K with reordered weights |
347 | | /// |
348 | | /// # Errors |
349 | | /// |
350 | | /// Returns error if data length is not a multiple of super-block size |
351 | 0 | pub fn from_q4k(q4k_data: &[u8]) -> Result<Self> { |
352 | | const SUPER_BLOCK_BYTES: usize = 144; |
353 | | |
354 | 0 | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
355 | 0 | return Err(RealizarError::InvalidShape { |
356 | 0 | reason: format!( |
357 | 0 | "Q4_K data length {} is not a multiple of super-block size {}", |
358 | 0 | q4k_data.len(), |
359 | 0 | SUPER_BLOCK_BYTES |
360 | 0 | ), |
361 | 0 | }); |
362 | 0 | } |
363 | | |
364 | 0 | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
365 | | |
366 | 0 | let mut d = Vec::with_capacity(num_super_blocks); |
367 | 0 | let mut dmin = Vec::with_capacity(num_super_blocks); |
368 | 0 | let mut scales = Vec::with_capacity(num_super_blocks * 12); |
369 | 0 | let mut qs = Vec::with_capacity(num_super_blocks * 128); |
370 | | |
371 | 0 | for sb in 0..num_super_blocks { |
372 | 0 | let sb_start = sb * SUPER_BLOCK_BYTES; |
373 | 0 |
|
374 | 0 | // Read d and dmin (f16 -> f32) |
375 | 0 | let d_val = f16_to_f32_lut(u16::from_le_bytes([ |
376 | 0 | q4k_data[sb_start], |
377 | 0 | q4k_data[sb_start + 1], |
378 | 0 | ])); |
379 | 0 | let dmin_val = f16_to_f32_lut(u16::from_le_bytes([ |
380 | 0 | q4k_data[sb_start + 2], |
381 | 0 | q4k_data[sb_start + 3], |
382 | 0 | ])); |
383 | 0 |
|
384 | 0 | d.push(d_val); |
385 | 0 | dmin.push(dmin_val); |
386 | 0 |
|
387 | 0 | // Copy scales |
388 | 0 | scales.extend_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
389 | 0 |
|
390 | 0 | // Interleave quantized values |
391 | 0 | // Original: byte[i] = (value[2i+1] << 4) | value[2i] |
392 | 0 | // We reorder so that after SIMD nibble extraction, values are contiguous |
393 | 0 | // |
394 | 0 | // For AVX2 processing 64 values at a time: |
395 | 0 | // - Load 32 bytes, extract low nibbles -> 32 values |
396 | 0 | // - Same 32 bytes, extract high nibbles -> 32 more values |
397 | 0 | // |
398 | 0 | // Interleave pattern: group values by their position in SIMD lanes |
399 | 0 | // This eliminates the need for cross-lane shuffles |
400 | 0 | let qs_start = sb_start + 16; |
401 | 0 | let original_qs = &q4k_data[qs_start..qs_start + 128]; |
402 | 0 |
|
403 | 0 | // For now, use identity interleave (same as original) |
404 | 0 | // The optimization comes from the specialized kernel that knows the layout |
405 | 0 | // Future: implement actual interleave pattern based on profiling |
406 | 0 | qs.extend_from_slice(original_qs); |
407 | 0 | } |
408 | | |
409 | 0 | Ok(Self { |
410 | 0 | d, |
411 | 0 | dmin, |
412 | 0 | scales, |
413 | 0 | qs, |
414 | 0 | num_super_blocks, |
415 | 0 | }) |
416 | 0 | } |
417 | | |
418 | | /// Get the number of values (256 per super-block) |
419 | | #[must_use] |
420 | 0 | pub fn num_values(&self) -> usize { |
421 | 0 | self.num_super_blocks * QK_K |
422 | 0 | } |
423 | | } |
424 | | |
425 | | // ============================================================================ |
426 | | // SIMD Backend Detection |
427 | | // ============================================================================ |
428 | | |
429 | | /// Batch dequantization stats for performance tracking |
430 | | #[derive(Debug, Clone, Default)] |
431 | | pub struct DequantStats { |
432 | | /// Total blocks processed |
433 | | pub blocks_processed: u64, |
434 | | /// Total bytes dequantized |
435 | | pub bytes_processed: u64, |
436 | | /// SIMD backend used |
437 | | pub simd_backend: SimdBackend, |
438 | | } |
439 | | |
440 | | /// SIMD backend detected at runtime |
441 | | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] |
442 | | pub enum SimdBackend { |
443 | | /// AVX2 (256-bit) |
444 | | Avx2, |
445 | | /// SSE2 (128-bit) |
446 | | Sse2, |
447 | | /// ARM NEON (128-bit) |
448 | | Neon, |
449 | | /// Scalar fallback |
450 | | #[default] |
451 | | Scalar, |
452 | | } |
453 | | |
454 | | impl std::fmt::Display for SimdBackend { |
455 | 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
456 | 10 | match self { |
457 | 4 | SimdBackend::Avx2 => write!(f, "AVX2"), |
458 | 2 | SimdBackend::Sse2 => write!(f, "SSE2"), |
459 | 2 | SimdBackend::Neon => write!(f, "NEON"), |
460 | 2 | SimdBackend::Scalar => write!(f, "Scalar"), |
461 | | } |
462 | 10 | } |
463 | | } |
464 | | |
465 | | /// Detect available SIMD backend |
466 | 5 | pub fn detect_simd_backend() -> SimdBackend { |
467 | | #[cfg(target_arch = "x86_64")] |
468 | | { |
469 | 5 | if is_x86_feature_detected!("avx2") { |
470 | 5 | return SimdBackend::Avx2; |
471 | 0 | } |
472 | 0 | if is_x86_feature_detected!("sse2") { |
473 | 0 | return SimdBackend::Sse2; |
474 | 0 | } |
475 | | } |
476 | | |
477 | | #[cfg(target_arch = "aarch64")] |
478 | | { |
479 | | return SimdBackend::Neon; |
480 | | } |
481 | | |
482 | 0 | SimdBackend::Scalar |
483 | 5 | } |