/home/noah/src/realizar/src/quantize/fused_q5k_q6k.rs
Line | Count | Source |
1 | | //! Fused Q5K and Q6K dot product operations (PMAT-802) |
2 | | //! |
3 | | //! Implements fused dequant+dot operations for Q5_K and Q6_K formats: |
4 | | //! - `fused_q6k_dot`, `fused_q6k_dot_simd` - Q6_K dot products |
5 | | //! - `fused_q5k_dot`, `fused_q5k_dot_simd` - Q5_K dot products |
6 | | |
7 | | use crate::error::{RealizarError, Result}; |
8 | | use super::dequant::read_f16; |
9 | | use super::simd::extract_scale_min; |
10 | | use super::types::{QK_K, Q8_0Block}; |
11 | | |
12 | | /// Fused Q6_K dequantize + dot product |
13 | | /// |
14 | | /// Computes the dot product of Q6_K quantized weights with f32 activations. |
15 | 8 | pub fn fused_q6k_dot(q6k_data: &[u8], activations: &[f32]) -> Result<f32> { |
16 | | const SUPER_BLOCK_BYTES: usize = 210; |
17 | | |
18 | | // Validate Q6_K data length |
19 | 8 | if !q6k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
20 | 1 | return Err(RealizarError::InvalidShape { |
21 | 1 | reason: format!( |
22 | 1 | "Q6_K data length {} is not a multiple of super-block size {}", |
23 | 1 | q6k_data.len(), |
24 | 1 | SUPER_BLOCK_BYTES |
25 | 1 | ), |
26 | 1 | }); |
27 | 7 | } |
28 | | |
29 | 7 | let num_super_blocks = q6k_data.len() / SUPER_BLOCK_BYTES; |
30 | 7 | let expected_values = num_super_blocks * QK_K; |
31 | | |
32 | | // Validate activation length matches |
33 | 7 | if activations.len() != expected_values { |
34 | 3 | return Err(RealizarError::InvalidShape { |
35 | 3 | reason: format!( |
36 | 3 | "Activation length {} doesn't match Q6_K values count {}", |
37 | 3 | activations.len(), |
38 | 3 | expected_values |
39 | 3 | ), |
40 | 3 | }); |
41 | 4 | } |
42 | | |
43 | | // Accumulator for dot product result |
44 | 4 | let mut acc = 0.0f32; |
45 | | |
46 | 7 | for sb_idx in 0..num_super_blocks4 { |
47 | 7 | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
48 | 7 | let act_start = sb_idx * QK_K; |
49 | | |
50 | | // Q6_K layout: ql (128) + qh (64) + scales (16) + d (2) |
51 | 7 | let ql = &q6k_data[sb_start..sb_start + 128]; |
52 | 7 | let qh = &q6k_data[sb_start + 128..sb_start + 192]; |
53 | | |
54 | | // Read scales (16 bytes, i8) |
55 | 7 | let mut scales = [0i8; 16]; |
56 | 112 | for (i, scale) in scales7 .iter_mut7 ().enumerate7 () { |
57 | | #[allow(clippy::cast_possible_wrap)] |
58 | 112 | { |
59 | 112 | *scale = q6k_data[sb_start + 192 + i] as i8; |
60 | 112 | } |
61 | | } |
62 | | |
63 | | // Read d (f16 -> f32) at offset 208 |
64 | 7 | let d = read_f16(&q6k_data[sb_start + 208..sb_start + 210]); |
65 | | |
66 | | // Fused dequant+dot following candle's exact layout |
67 | | // Process 128 values at a time (n=0, n=128) |
68 | 14 | for n in (0..QK_K)7 .step_by7 (128) { |
69 | 14 | let idx = n / 128; |
70 | 14 | let sc = &scales[8 * idx..]; |
71 | 14 | let ql_slice = &ql[64 * idx..]; |
72 | 14 | let qh_slice = &qh[32 * idx..]; |
73 | | |
74 | 462 | for l448 in 0..32 { |
75 | 448 | let is = l / 16; // Scale index selector |
76 | 448 | |
77 | 448 | // Extract 4 values per iteration (at positions l, l+32, l+64, l+96) |
78 | 448 | let q1 = ((ql_slice[l] & 0xF) | ((qh_slice[l] & 3) << 4)) as i32 - 32; |
79 | 448 | let q2 = ((ql_slice[l + 32] & 0xF) | (((qh_slice[l] >> 2) & 3) << 4)) as i32 - 32; |
80 | 448 | let q3 = ((ql_slice[l] >> 4) | (((qh_slice[l] >> 4) & 3) << 4)) as i32 - 32; |
81 | 448 | let q4 = ((ql_slice[l + 32] >> 4) | (((qh_slice[l] >> 6) & 3) << 4)) as i32 - 32; |
82 | 448 | |
83 | 448 | // Dequantize and accumulate dot product |
84 | 448 | let v1 = d * (sc[is] as f32) * (q1 as f32); |
85 | 448 | let v2 = d * (sc[is + 2] as f32) * (q2 as f32); |
86 | 448 | let v3 = d * (sc[is + 4] as f32) * (q3 as f32); |
87 | 448 | let v4 = d * (sc[is + 6] as f32) * (q4 as f32); |
88 | 448 | |
89 | 448 | acc += v1 * activations[act_start + n + l]; |
90 | 448 | acc += v2 * activations[act_start + n + l + 32]; |
91 | 448 | acc += v3 * activations[act_start + n + l + 64]; |
92 | 448 | acc += v4 * activations[act_start + n + l + 96]; |
93 | 448 | } |
94 | | } |
95 | | } |
96 | | |
97 | 4 | Ok(acc) |
98 | 8 | } |
99 | | |
100 | | /// SIMD-accelerated fused Q6_K dequant+dot (with scalar fallback) |
101 | | /// |
102 | | /// Per Williams et al. (2009) roofline model, memory bandwidth is the bottleneck. |
103 | | /// This function provides a unified interface with runtime feature detection. |
104 | | /// Currently uses scalar implementation; SIMD Q6_K optimization can be added later. |
105 | | /// |
106 | | /// # Arguments |
107 | | /// |
108 | | /// * `q6k_data` - Raw Q6_K quantized data (210 bytes per super-block) |
109 | | /// * `activations` - Input activations (256 values per super-block) |
110 | | /// |
111 | | /// # Returns |
112 | | /// |
113 | | /// Dot product result as f32 |
114 | | /// |
115 | | /// # Errors |
116 | | /// |
117 | | /// Returns error if data sizes don't match or are malformed |
118 | 2.18k | pub fn fused_q6k_dot_simd(q6k_data: &[u8], activations: &[f32]) -> Result<f32> { |
119 | | // PAR-126: AVX2 SIMD implementation for Q6_K |
120 | | // Critical optimization: Q6_K scalar was 9x slower than Q4_K SIMD |
121 | | #[cfg(target_arch = "x86_64")] |
122 | | { |
123 | 2.18k | if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") { |
124 | | // SAFETY: We've verified AVX2 and FMA are available at runtime |
125 | 2.18k | return unsafe { fused_q6k_dot_avx2(q6k_data, activations) }; |
126 | 0 | } |
127 | | } |
128 | | // Fallback to scalar implementation |
129 | 0 | fused_q6k_dot(q6k_data, activations) |
130 | 2.18k | } |
131 | | |
132 | | /// PAR-126: AVX2 SIMD implementation for Q6_K dot product |
133 | | /// |
134 | | /// Uses AVX2 + FMA to achieve ~8x speedup over scalar. |
135 | | /// Q6_K layout: ql (128) + qh (64) + scales (16) + d (2) = 210 bytes |
136 | | /// |
137 | | /// # Safety |
138 | | /// Requires AVX2 and FMA instruction sets |
139 | | #[cfg(target_arch = "x86_64")] |
140 | | #[target_feature(enable = "avx2", enable = "fma")] |
141 | | #[allow(unsafe_op_in_unsafe_fn)] |
142 | 2.18k | unsafe fn fused_q6k_dot_avx2(q6k_data: &[u8], activations: &[f32]) -> Result<f32> { |
143 | | #[allow(clippy::wildcard_imports)] |
144 | | use std::arch::x86_64::*; |
145 | | |
146 | | const SUPER_BLOCK_BYTES: usize = 210; |
147 | | |
148 | 2.18k | if !q6k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
149 | 1 | return Err(RealizarError::InvalidShape { |
150 | 1 | reason: format!( |
151 | 1 | "Q6_K data length {} is not a multiple of super-block size {}", |
152 | 1 | q6k_data.len(), |
153 | 1 | SUPER_BLOCK_BYTES |
154 | 1 | ), |
155 | 1 | }); |
156 | 2.18k | } |
157 | | |
158 | 2.18k | let num_super_blocks = q6k_data.len() / SUPER_BLOCK_BYTES; |
159 | 2.18k | let expected_values = num_super_blocks * QK_K; |
160 | | |
161 | 2.18k | if activations.len() != expected_values { |
162 | 1 | return Err(RealizarError::InvalidShape { |
163 | 1 | reason: format!( |
164 | 1 | "Activation length {} doesn't match Q6_K values count {}", |
165 | 1 | activations.len(), |
166 | 1 | expected_values |
167 | 1 | ), |
168 | 1 | }); |
169 | 2.18k | } |
170 | | |
171 | | // 4 independent accumulators to hide FMA latency |
172 | 2.18k | let mut acc0 = _mm256_setzero_ps(); |
173 | 2.18k | let mut acc1 = _mm256_setzero_ps(); |
174 | 2.18k | let mut acc2 = _mm256_setzero_ps(); |
175 | 2.18k | let mut acc3 = _mm256_setzero_ps(); |
176 | | |
177 | | // Masks for 6-bit extraction (reserved for optimized path) |
178 | 2.18k | let _mask_0f = _mm256_set1_epi8(0x0F_i8); |
179 | 2.18k | let _mask_03 = _mm256_set1_epi8(0x03_i8); |
180 | 2.18k | let offset_32 = _mm256_set1_epi32(32); |
181 | | |
182 | 2.18k | for sb_idx in 0..num_super_blocks { |
183 | 2.18k | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
184 | 2.18k | let act_start = sb_idx * QK_K; |
185 | | |
186 | | // Prefetch next super-block |
187 | 2.18k | if sb_idx + 1 < num_super_blocks { |
188 | 0 | let next_sb = (sb_idx + 1) * SUPER_BLOCK_BYTES; |
189 | 0 | _mm_prefetch(q6k_data.as_ptr().add(next_sb).cast::<i8>(), _MM_HINT_T0); |
190 | 2.18k | } |
191 | | |
192 | | // Q6_K layout: ql (128) + qh (64) + scales (16) + d (2) |
193 | 2.18k | let ql_ptr = q6k_data.as_ptr().add(sb_start); |
194 | 2.18k | let qh_ptr = q6k_data.as_ptr().add(sb_start + 128); |
195 | 2.18k | let scales_ptr = q6k_data.as_ptr().add(sb_start + 192); |
196 | | |
197 | | // Read d (f16 -> f32) |
198 | 2.18k | let d = read_f16(&q6k_data[sb_start + 208..sb_start + 210]); |
199 | 2.18k | let d_vec = _mm256_set1_ps(d); |
200 | | |
201 | | // Read all 16 scales (as i8, will use in inner loop) |
202 | 2.18k | let mut scales = [0i8; 16]; |
203 | 2.18k | std::ptr::copy_nonoverlapping(scales_ptr, scales.as_mut_ptr().cast::<u8>(), 16); |
204 | | |
205 | | // Process 128 values at a time (n=0, n=128) |
206 | 4.36k | for n in (0..QK_K)2.18k .step_by2.18k (128) { |
207 | 4.36k | let idx = n / 128; |
208 | 4.36k | let sc = &scales[8 * idx..]; |
209 | 4.36k | let ql_slice = ql_ptr.add(64 * idx); |
210 | 4.36k | let qh_slice = qh_ptr.add(32 * idx); |
211 | 4.36k | let act_base = activations.as_ptr().add(act_start + n); |
212 | | |
213 | | // Process 32 values at a time using AVX2 |
214 | | // Each iteration handles l=0..8, extracting 4 values each (32 total) |
215 | 17.4k | for l_base in (0..32)4.36k .step_by4.36k (8) { |
216 | 17.4k | // Load 8 bytes of ql[l], ql[l+32], qh[l] |
217 | 17.4k | let ql_lo_64 = std::ptr::read_unaligned(ql_slice.add(l_base).cast::<u64>()); |
218 | 17.4k | let ql_hi_64 = std::ptr::read_unaligned(ql_slice.add(l_base + 32).cast::<u64>()); |
219 | 17.4k | let qh_64 = std::ptr::read_unaligned(qh_slice.add(l_base).cast::<u64>()); |
220 | 17.4k | |
221 | 17.4k | // Convert to SIMD vectors (expand u8 to i32 for arithmetic) |
222 | 17.4k | let ql_lo = _mm256_cvtepu8_epi32(_mm_set_epi64x(0, ql_lo_64 as i64)); |
223 | 17.4k | let ql_hi = _mm256_cvtepu8_epi32(_mm_set_epi64x(0, ql_hi_64 as i64)); |
224 | 17.4k | let qh = _mm256_cvtepu8_epi32(_mm_set_epi64x(0, qh_64 as i64)); |
225 | 17.4k | |
226 | 17.4k | // Extract 6-bit values (4 values per input byte) |
227 | 17.4k | // q1 = (ql[l] & 0xF) | ((qh[l] & 3) << 4) - 32 |
228 | 17.4k | let q1_lo = _mm256_and_si256(ql_lo, _mm256_set1_epi32(0x0F)); |
229 | 17.4k | let q1_hi = _mm256_slli_epi32(_mm256_and_si256(qh, _mm256_set1_epi32(0x03)), 4); |
230 | 17.4k | let q1 = _mm256_sub_epi32(_mm256_or_si256(q1_lo, q1_hi), offset_32); |
231 | 17.4k | |
232 | 17.4k | // q2 = (ql[l+32] & 0xF) | (((qh[l] >> 2) & 3) << 4) - 32 |
233 | 17.4k | let q2_lo = _mm256_and_si256(ql_hi, _mm256_set1_epi32(0x0F)); |
234 | 17.4k | let q2_hi = _mm256_slli_epi32( |
235 | 17.4k | _mm256_and_si256(_mm256_srli_epi32(qh, 2), _mm256_set1_epi32(0x03)), |
236 | 17.4k | 4, |
237 | 17.4k | ); |
238 | 17.4k | let q2 = _mm256_sub_epi32(_mm256_or_si256(q2_lo, q2_hi), offset_32); |
239 | 17.4k | |
240 | 17.4k | // q3 = (ql[l] >> 4) | (((qh[l] >> 4) & 3) << 4) - 32 |
241 | 17.4k | let q3_lo = _mm256_srli_epi32(ql_lo, 4); |
242 | 17.4k | let q3_hi = _mm256_slli_epi32( |
243 | 17.4k | _mm256_and_si256(_mm256_srli_epi32(qh, 4), _mm256_set1_epi32(0x03)), |
244 | 17.4k | 4, |
245 | 17.4k | ); |
246 | 17.4k | let q3 = _mm256_sub_epi32(_mm256_or_si256(q3_lo, q3_hi), offset_32); |
247 | 17.4k | |
248 | 17.4k | // q4 = (ql[l+32] >> 4) | (((qh[l] >> 6) & 3) << 4) - 32 |
249 | 17.4k | let q4_lo = _mm256_srli_epi32(ql_hi, 4); |
250 | 17.4k | let q4_hi = _mm256_slli_epi32(_mm256_srli_epi32(qh, 6), 4); |
251 | 17.4k | let q4 = _mm256_sub_epi32(_mm256_or_si256(q4_lo, q4_hi), offset_32); |
252 | 17.4k | |
253 | 17.4k | // Determine scale index: is = l / 16 (0 for l<16, 1 for l>=16) |
254 | 17.4k | let is = l_base / 16; |
255 | 17.4k | |
256 | 17.4k | // Get scales for each of the 4 output values |
257 | 17.4k | let sc1 = sc[is] as f32; |
258 | 17.4k | let sc2 = sc[is + 2] as f32; |
259 | 17.4k | let sc3 = sc[is + 4] as f32; |
260 | 17.4k | let sc4 = sc[is + 6] as f32; |
261 | 17.4k | |
262 | 17.4k | // Convert quantized values to f32 and multiply by d*scale |
263 | 17.4k | let q1_f32 = _mm256_cvtepi32_ps(q1); |
264 | 17.4k | let q2_f32 = _mm256_cvtepi32_ps(q2); |
265 | 17.4k | let q3_f32 = _mm256_cvtepi32_ps(q3); |
266 | 17.4k | let q4_f32 = _mm256_cvtepi32_ps(q4); |
267 | 17.4k | |
268 | 17.4k | let dequant1 = _mm256_mul_ps(_mm256_mul_ps(d_vec, _mm256_set1_ps(sc1)), q1_f32); |
269 | 17.4k | let dequant2 = _mm256_mul_ps(_mm256_mul_ps(d_vec, _mm256_set1_ps(sc2)), q2_f32); |
270 | 17.4k | let dequant3 = _mm256_mul_ps(_mm256_mul_ps(d_vec, _mm256_set1_ps(sc3)), q3_f32); |
271 | 17.4k | let dequant4 = _mm256_mul_ps(_mm256_mul_ps(d_vec, _mm256_set1_ps(sc4)), q4_f32); |
272 | 17.4k | |
273 | 17.4k | // Load activations |
274 | 17.4k | let act1 = _mm256_loadu_ps(act_base.add(l_base)); |
275 | 17.4k | let act2 = _mm256_loadu_ps(act_base.add(l_base + 32)); |
276 | 17.4k | let act3 = _mm256_loadu_ps(act_base.add(l_base + 64)); |
277 | 17.4k | let act4 = _mm256_loadu_ps(act_base.add(l_base + 96)); |
278 | 17.4k | |
279 | 17.4k | // FMA: acc += dequant * act |
280 | 17.4k | acc0 = _mm256_fmadd_ps(dequant1, act1, acc0); |
281 | 17.4k | acc1 = _mm256_fmadd_ps(dequant2, act2, acc1); |
282 | 17.4k | acc2 = _mm256_fmadd_ps(dequant3, act3, acc2); |
283 | 17.4k | acc3 = _mm256_fmadd_ps(dequant4, act4, acc3); |
284 | 17.4k | } |
285 | | } |
286 | | } |
287 | | |
288 | | // Combine 4 accumulators |
289 | 2.18k | let acc_01 = _mm256_add_ps(acc0, acc1); |
290 | 2.18k | let acc_23 = _mm256_add_ps(acc2, acc3); |
291 | 2.18k | let acc = _mm256_add_ps(acc_01, acc_23); |
292 | | |
293 | | // Horizontal sum |
294 | 2.18k | let sum_halves = _mm_add_ps(_mm256_castps256_ps128(acc), _mm256_extractf128_ps(acc, 1)); |
295 | 2.18k | let temp = _mm_add_ps(sum_halves, _mm_movehl_ps(sum_halves, sum_halves)); |
296 | 2.18k | let temp = _mm_add_ss(temp, _mm_shuffle_ps(temp, temp, 1)); |
297 | 2.18k | let result = _mm_cvtss_f32(temp); |
298 | | |
299 | 2.18k | Ok(result) |
300 | 2.18k | } |
301 | | |
302 | | /// Fused Q5_K dequantize + dot product |
303 | | /// |
304 | | /// Computes the dot product of Q5_K quantized weights with f32 activations |
305 | | /// WITHOUT allocating an intermediate f32 buffer. Dequantization happens |
306 | | /// inline, accumulating directly into a register. |
307 | | /// |
308 | | /// # Arguments |
309 | | /// |
310 | | /// * `q5k_data` - Raw Q5_K quantized data (super-blocks of 176 bytes) |
311 | | /// * `activations` - f32 activation values (must match dequantized length) |
312 | | /// |
313 | | /// # Returns |
314 | | /// |
315 | | /// The dot product as f32 |
316 | | /// |
317 | | /// # Errors |
318 | | /// |
319 | | /// Returns error if: |
320 | | /// - `q5k_data` length is not a multiple of 176 bytes (super-block size) |
321 | | /// - `activations` length doesn't match the number of quantized values |
322 | | /// |
323 | | /// # Examples |
324 | | /// |
325 | | /// ```rust,ignore |
326 | | /// let weights_q5k = load_q5k_weights(); |
327 | | /// let activations = get_layer_activations(); |
328 | | /// let result = fused_q5k_dot(&weights_q5k, &activations)?; |
329 | | /// ``` |
330 | | #[allow(clippy::similar_names)] |
331 | 2.15k | pub fn fused_q5k_dot(q5k_data: &[u8], activations: &[f32]) -> Result<f32> { |
332 | | const SUPER_BLOCK_BYTES: usize = 176; |
333 | | |
334 | | // Validate Q5_K data length |
335 | 2.15k | if !q5k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
336 | 3 | return Err(RealizarError::InvalidShape { |
337 | 3 | reason: format!( |
338 | 3 | "Q5_K data length {} is not a multiple of super-block size {}", |
339 | 3 | q5k_data.len(), |
340 | 3 | SUPER_BLOCK_BYTES |
341 | 3 | ), |
342 | 3 | }); |
343 | 2.15k | } |
344 | | |
345 | 2.15k | let num_super_blocks = q5k_data.len() / SUPER_BLOCK_BYTES; |
346 | 2.15k | let expected_values = num_super_blocks * QK_K; |
347 | | |
348 | | // Validate activation length matches |
349 | 2.15k | if activations.len() != expected_values { |
350 | 1 | return Err(RealizarError::InvalidShape { |
351 | 1 | reason: format!( |
352 | 1 | "Activation length {} doesn't match Q5_K values count {}", |
353 | 1 | activations.len(), |
354 | 1 | expected_values |
355 | 1 | ), |
356 | 1 | }); |
357 | 2.15k | } |
358 | | |
359 | | // Accumulator for dot product result |
360 | 2.15k | let mut acc = 0.0f32; |
361 | 2.15k | let mut activation_idx = 0; |
362 | | |
363 | 2.15k | for sb_idx in 0..num_super_blocks { |
364 | 2.15k | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
365 | | |
366 | | // Read d (f16 -> f32) |
367 | 2.15k | let d = read_f16(&q5k_data[sb_start..sb_start + 2]); |
368 | | |
369 | | // Read dmin (f16 -> f32) |
370 | 2.15k | let dmin = read_f16(&q5k_data[sb_start + 2..sb_start + 4]); |
371 | | |
372 | | // Read scales (12 bytes) |
373 | 2.15k | let mut scales = [0u8; 12]; |
374 | 2.15k | scales.copy_from_slice(&q5k_data[sb_start + 4..sb_start + 16]); |
375 | | |
376 | | // Read qh - high bits (32 bytes) |
377 | 2.15k | let qh_start = sb_start + 16; |
378 | 2.15k | let qh = &q5k_data[qh_start..qh_start + 32]; |
379 | | |
380 | | // Read qs - low 4 bits (128 bytes) |
381 | 2.15k | let qs_start = sb_start + 48; |
382 | 2.15k | let qs = &q5k_data[qs_start..qs_start + 128]; |
383 | | |
384 | | // Fused dequant+dot for 8 blocks of 32 values each |
385 | 19.3k | for block_idx17.2k in 0..8 { |
386 | | // Extract 6-bit scale and min for this block |
387 | 17.2k | let (scale, min) = extract_scale_min(&scales, block_idx); |
388 | | |
389 | | // Process 32 values |
390 | 17.2k | let block_start = block_idx * 16; |
391 | 17.2k | let qh_block_start = block_idx * 4; |
392 | | |
393 | 292k | for byte_idx275k in 0..16 { |
394 | 275k | let qs_byte = qs[block_start + byte_idx]; |
395 | 275k | let high_bits_byte = qh[qh_block_start + byte_idx / 4]; |
396 | 275k | let bit_offset = (byte_idx % 4) * 2; |
397 | 275k | |
398 | 275k | // Low value: dequantize and accumulate |
399 | 275k | let q_low_4bit = qs_byte & 0x0F; |
400 | 275k | let q_low_high_bit = (high_bits_byte >> bit_offset) & 0x01; |
401 | 275k | #[allow(clippy::cast_possible_wrap)] |
402 | 275k | let q_low = ((q_low_high_bit << 4) | q_low_4bit) as i8; |
403 | 275k | let value_low = d * scale * f32::from(q_low) - dmin * min; |
404 | 275k | acc += value_low * activations[activation_idx]; |
405 | 275k | activation_idx += 1; |
406 | 275k | |
407 | 275k | // High value: dequantize and accumulate |
408 | 275k | let q_high_4bit = (qs_byte >> 4) & 0x0F; |
409 | 275k | let q_high_high_bit = (high_bits_byte >> (bit_offset + 1)) & 0x01; |
410 | 275k | #[allow(clippy::cast_possible_wrap)] |
411 | 275k | let q_high = ((q_high_high_bit << 4) | q_high_4bit) as i8; |
412 | 275k | let value_high = d * scale * f32::from(q_high) - dmin * min; |
413 | 275k | acc += value_high * activations[activation_idx]; |
414 | 275k | activation_idx += 1; |
415 | 275k | } |
416 | | } |
417 | | } |
418 | | |
419 | 2.15k | Ok(acc) |
420 | 2.15k | } |
421 | | |
422 | | /// SIMD-accelerated fused Q5_K dequant+dot (with scalar fallback) |
423 | | /// |
424 | | /// Provides unified interface with runtime feature detection. |
425 | | /// Currently uses scalar implementation; SIMD Q5_K can be added later. |
426 | | /// |
427 | | /// # Errors |
428 | | /// |
429 | | /// Returns error if data sizes don't match or are malformed. |
430 | | /// See [`fused_q5k_dot`] for details. |
431 | 2.15k | pub fn fused_q5k_dot_simd(q5k_data: &[u8], activations: &[f32]) -> Result<f32> { |
432 | | // Q5_K SIMD optimization deferred to Phase 2 |
433 | 2.15k | fused_q5k_dot(q5k_data, activations) |
434 | 2.15k | } |
435 | | |
436 | | /// Fused Q4_K with Q8 blocks dot product |
437 | | /// |
438 | | /// Computes the dot product of Q4_K quantized weights with Q8_0 quantized activations. |
439 | 1.02k | pub fn fused_q4k_q8_dot(q4k_data: &[u8], q8_blocks: &[Q8_0Block]) -> Result<f32> { |
440 | | const SUPER_BLOCK_BYTES: usize = 144; |
441 | | |
442 | | // Validate Q4_K data length |
443 | 1.02k | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
444 | 4 | return Err(RealizarError::InvalidShape { |
445 | 4 | reason: format!( |
446 | 4 | "Q4_K data length {} is not a multiple of super-block size {}", |
447 | 4 | q4k_data.len(), |
448 | 4 | SUPER_BLOCK_BYTES |
449 | 4 | ), |
450 | 4 | }); |
451 | 1.01k | } |
452 | | |
453 | 1.01k | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
454 | 1.01k | let expected_values = num_super_blocks * QK_K; // 256 values per super-block |
455 | 1.01k | let expected_q8_blocks = expected_values / 32; |
456 | | |
457 | | // Validate Q8 block count matches |
458 | 1.01k | if q8_blocks.len() != expected_q8_blocks { |
459 | 3 | return Err(RealizarError::InvalidShape { |
460 | 3 | reason: format!( |
461 | 3 | "Q8_0 block count {} doesn't match expected {} (for {} Q4_K values)", |
462 | 3 | q8_blocks.len(), |
463 | 3 | expected_q8_blocks, |
464 | 3 | expected_values |
465 | 3 | ), |
466 | 3 | }); |
467 | 1.01k | } |
468 | | |
469 | | // Accumulator for dot product result |
470 | 1.01k | let mut acc = 0.0f32; |
471 | 1.01k | let mut q8_block_idx = 0; |
472 | | |
473 | 16.1k | for sb_idx in 0..num_super_blocks1.01k { |
474 | 16.1k | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
475 | | |
476 | | // Read d (f16 -> f32) - super-block scale |
477 | 16.1k | let d = read_f16(&q4k_data[sb_start..sb_start + 2]); |
478 | | |
479 | | // Read dmin (f16 -> f32) - super-block min |
480 | 16.1k | let dmin = read_f16(&q4k_data[sb_start + 2..sb_start + 4]); |
481 | | |
482 | | // Read scales (12 bytes) - packed 6-bit scales for 8 blocks |
483 | 16.1k | let mut scales = [0u8; 12]; |
484 | 16.1k | scales.copy_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
485 | | |
486 | | // Read qs (128 bytes) - 256 4-bit quantized values |
487 | 16.1k | let qs_start = sb_start + 16; |
488 | 16.1k | let qs = &q4k_data[qs_start..qs_start + 128]; |
489 | | |
490 | | // Process 8 blocks of 32 values each |
491 | 145k | for block_idx129k in 0..8 { |
492 | | // Extract 6-bit scale and min for this block |
493 | 129k | let (scale, min) = extract_scale_min(&scales, block_idx); |
494 | | |
495 | | // Get the Q8 block for this 32-value chunk |
496 | 129k | let q8_block = &q8_blocks[q8_block_idx]; |
497 | 129k | let q8_scale = q8_block.scale; |
498 | 129k | q8_block_idx += 1; |
499 | | |
500 | | // Process 32 values (16 bytes, 2 4-bit values per byte) |
501 | 129k | let block_start = block_idx * 16; |
502 | 2.19M | for byte_idx2.06M in 0..16 { |
503 | 2.06M | let byte = qs[block_start + byte_idx]; |
504 | 2.06M | let q8_idx = byte_idx * 2; |
505 | 2.06M | |
506 | 2.06M | // Low 4 bits: fused dequant and accumulate |
507 | 2.06M | #[allow(clippy::cast_possible_wrap)] |
508 | 2.06M | let q4_low = (byte & 0x0F) as i8; |
509 | 2.06M | let w_low = d * scale * f32::from(q4_low) - dmin * min; |
510 | 2.06M | let a_low = q8_scale * f32::from(q8_block.quants[q8_idx]); |
511 | 2.06M | acc += w_low * a_low; |
512 | 2.06M | |
513 | 2.06M | // High 4 bits: fused dequant and accumulate |
514 | 2.06M | #[allow(clippy::cast_possible_wrap)] |
515 | 2.06M | let q4_high = ((byte >> 4) & 0x0F) as i8; |
516 | 2.06M | let w_high = d * scale * f32::from(q4_high) - dmin * min; |
517 | 2.06M | let a_high = q8_scale * f32::from(q8_block.quants[q8_idx + 1]); |
518 | 2.06M | acc += w_high * a_high; |
519 | 2.06M | } |
520 | | } |
521 | | } |
522 | | |
523 | 1.01k | Ok(acc) |
524 | 1.02k | } |
525 | | |