/home/noah/src/realizar/src/quantize/fused_k.rs
Line | Count | Source |
1 | | //! Fused quantized operations for K-quantization formats (PMAT-802) |
2 | | //! |
3 | | //! Implements fused dequant+dot operations that eliminate intermediate f32 buffer |
4 | | //! allocation for 8x memory bandwidth reduction. |
5 | | //! |
6 | | //! Per llama-cpp-style-performance-spec.md: |
7 | | //! - Memory wall is the bottleneck (Wulf & McKee [10]) |
8 | | //! - Fused operations keep data in registers, avoid memory round-trips |
9 | | //! - ULP tolerance of ≤4 for numerical equivalence (Goldberg [9]) |
10 | | //! |
11 | | //! Functions: |
12 | | //! - `fused_q4k_dot`, `fused_q4k_dot_simd` - Q4_K dot products |
13 | | //! - `fused_q4k_q8_dot` - Q4_K with Q8 activations |
14 | | //! - `fused_q4k_q8k_dot`, `fused_q4k_q8k_dot_simd` - Q4_K with Q8_K activations |
15 | | //! - `fused_q4k_q8k_parallel_matvec_into` - Parallel matrix-vector multiply |
16 | | //! - `fused_q4k_q8k_ffn_up_gate_into` - Fused FFN up/gate computation |
17 | | //! - `fused_q6k_dot`, `fused_q6k_dot_simd` - Q6_K dot products |
18 | | //! - `fused_q5k_dot`, `fused_q5k_dot_simd` - Q5_K dot products |
19 | | |
20 | | use crate::error::{RealizarError, Result}; |
21 | | use super::dequant::read_f16; |
22 | | use super::simd::extract_scale_min; |
23 | | use super::types::QK_K; |
24 | | |
25 | | /// Fused Q4_K dequantize + dot product |
26 | | /// |
27 | | /// Computes the dot product of Q4_K quantized weights with f32 activations |
28 | | /// WITHOUT allocating an intermediate f32 buffer. Dequantization happens |
29 | | /// inline, accumulating directly into a register. |
30 | | /// |
31 | | /// # Arguments |
32 | | /// |
33 | | /// * `q4k_data` - Raw Q4_K quantized data (super-blocks of 144 bytes) |
34 | | /// * `activations` - f32 activation values (must match dequantized length) |
35 | | /// |
36 | | /// # Returns |
37 | | /// |
38 | | /// The dot product as f32 |
39 | | /// |
40 | | /// # Errors |
41 | | /// |
42 | | /// Returns error if: |
43 | | /// - `q4k_data` length is not a multiple of 144 bytes (super-block size) |
44 | | /// - `activations` length doesn't match the number of quantized values |
45 | | /// |
46 | | /// # Performance |
47 | | /// |
48 | | /// This function reduces memory traffic by 8x compared to separate |
49 | | /// dequantize-then-dot operations: |
50 | | /// - Naive: Read Q4_K (4.5 bits) → Write f32 (32 bits) → Read f32 → Compute |
51 | | /// - Fused: Read Q4_K (4.5 bits) → Compute in registers |
52 | | /// |
53 | | /// # Examples |
54 | | /// |
55 | | /// ```rust,ignore |
56 | | /// let weights_q4k = load_q4k_weights(); |
57 | | /// let activations = get_layer_activations(); |
58 | | /// let result = fused_q4k_dot(&weights_q4k, &activations)?; |
59 | | /// ``` |
60 | 1.17k | pub fn fused_q4k_dot(q4k_data: &[u8], activations: &[f32]) -> Result<f32> { |
61 | | const SUPER_BLOCK_BYTES: usize = 144; |
62 | | |
63 | | // Validate Q4_K data length |
64 | 1.17k | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
65 | 7 | return Err(RealizarError::InvalidShape { |
66 | 7 | reason: format!( |
67 | 7 | "Q4_K data length {} is not a multiple of super-block size {}", |
68 | 7 | q4k_data.len(), |
69 | 7 | SUPER_BLOCK_BYTES |
70 | 7 | ), |
71 | 7 | }); |
72 | 1.17k | } |
73 | | |
74 | 1.17k | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
75 | 1.17k | let expected_values = num_super_blocks * QK_K; |
76 | | |
77 | | // Validate activation length matches |
78 | 1.17k | if activations.len() != expected_values { |
79 | 9 | return Err(RealizarError::InvalidShape { |
80 | 9 | reason: format!( |
81 | 9 | "Activation length {} doesn't match Q4_K values count {}", |
82 | 9 | activations.len(), |
83 | 9 | expected_values |
84 | 9 | ), |
85 | 9 | }); |
86 | 1.16k | } |
87 | | |
88 | | // Accumulator for dot product result |
89 | 1.16k | let mut acc = 0.0f32; |
90 | 1.16k | let mut activation_idx = 0; |
91 | | |
92 | 16.3k | for sb_idx in 0..num_super_blocks1.16k { |
93 | 16.3k | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
94 | | |
95 | | // Read d (f16 -> f32) |
96 | 16.3k | let d = read_f16(&q4k_data[sb_start..sb_start + 2]); |
97 | | |
98 | | // Read dmin (f16 -> f32) |
99 | 16.3k | let dmin = read_f16(&q4k_data[sb_start + 2..sb_start + 4]); |
100 | | |
101 | | // Read scales (12 bytes) |
102 | 16.3k | let mut scales = [0u8; 12]; |
103 | 16.3k | scales.copy_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
104 | | |
105 | | // Read qs (128 bytes) |
106 | 16.3k | let qs_start = sb_start + 16; |
107 | 16.3k | let qs = &q4k_data[qs_start..qs_start + 128]; |
108 | | |
109 | | // PAR-001: Match dequantize_q4_k layout (llama.cpp/candle compatible) |
110 | | // Process 4 chunks of 64 values each (0, 64, 128, 192) |
111 | | // Each chunk: 32 low nibbles, then 32 high nibbles from 32 consecutive bytes |
112 | 65.5k | for j in (0..QK_K)16.3k .step_by16.3k (64) { |
113 | 65.5k | let q = &qs[j / 2..j / 2 + 32]; |
114 | | |
115 | | // Get scales for the two 32-value halves |
116 | 65.5k | let is = j / 32; |
117 | 65.5k | let (sc1, m1) = extract_scale_min(&scales, is); |
118 | 65.5k | let d1 = d * sc1; |
119 | 65.5k | let dm1 = dmin * m1; |
120 | | |
121 | 65.5k | let (sc2, m2) = extract_scale_min(&scales, is + 1); |
122 | 65.5k | let d2 = d * sc2; |
123 | 65.5k | let dm2 = dmin * m2; |
124 | | |
125 | | // First pass: 32 low nibbles (use sc1, m1) |
126 | 2.16M | for &byte2.09M in q { |
127 | 2.09M | let q_val = (byte & 0x0F) as f32; |
128 | 2.09M | let value = d1 * q_val - dm1; |
129 | 2.09M | acc += value * activations[activation_idx]; |
130 | 2.09M | activation_idx += 1; |
131 | 2.09M | } |
132 | | |
133 | | // Second pass: 32 high nibbles (use sc2, m2) |
134 | 2.16M | for &byte2.09M in q { |
135 | 2.09M | let q_val = (byte >> 4) as f32; |
136 | 2.09M | let value = d2 * q_val - dm2; |
137 | 2.09M | acc += value * activations[activation_idx]; |
138 | 2.09M | activation_idx += 1; |
139 | 2.09M | } |
140 | | } |
141 | | } |
142 | | |
143 | 1.16k | Ok(acc) |
144 | 1.17k | } |
145 | | |
146 | | /// Fused Q4_K dequantize + dot product with SIMD acceleration |
147 | | /// |
148 | | /// This is the public, safe API that automatically dispatches to the best |
149 | | /// available implementation (AVX2 when available, scalar fallback otherwise). |
150 | | /// |
151 | | /// # Arguments |
152 | | /// |
153 | | /// * `q4k_data` - Raw Q4_K quantized data (super-blocks of 144 bytes) |
154 | | /// * `activations` - f32 activation values (must match dequantized length) |
155 | | /// |
156 | | /// # Returns |
157 | | /// |
158 | | /// The dot product as f32, matching `fused_q4k_dot` within 4 ULPs |
159 | | /// |
160 | | /// # Errors |
161 | | /// |
162 | | /// Returns error if: |
163 | | /// - `q4k_data` length is not a multiple of 144 bytes (super-block size) |
164 | | /// - `activations` length doesn't match the number of quantized values |
165 | | /// |
166 | | /// # Performance |
167 | | /// |
168 | | /// - AVX2: ~8x speedup over scalar via 256-bit SIMD + FMA |
169 | | /// - Fused operation: 8x memory bandwidth reduction vs dequant-then-dot |
170 | | /// - Combined potential: Up to 64x improvement for memory-bound operations |
171 | 1.49M | pub fn fused_q4k_dot_simd(q4k_data: &[u8], activations: &[f32]) -> Result<f32> { |
172 | | // Runtime feature detection with fallback (per RustBelt pattern) |
173 | | #[cfg(target_arch = "x86_64")] |
174 | | { |
175 | | // PAR-126: AVX-512 VNNI requires pre-quantized activations (Q4K×Q8K format) |
176 | | // For now, use AVX2 which works with f32 activations directly. |
177 | | // Future optimization: pre-quantize activations to Q8_0 format once per matmul. |
178 | 1.49M | if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") { |
179 | | // SAFETY: We've verified AVX2 and FMA are available at runtime |
180 | | // The unsafe function performs the same logical operation as scalar |
181 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
182 | 1.49M | return unsafe { fused_q4k_dot_avx2(q4k_data, activations) }; |
183 | 0 | } |
184 | | } |
185 | | |
186 | | // Fallback to scalar implementation |
187 | 0 | fused_q4k_dot(q4k_data, activations) |
188 | 1.49M | } |
189 | | |
190 | | /// AVX-512 VNNI accelerated Q4_K dot product (PAR-126) |
191 | | /// |
192 | | /// Uses vpdpbusd for int8×int8 accumulation which is 4x faster than FP32 FMA. |
193 | | /// Key insight from llama.cpp/llamafile: integer dot products dominate FP on modern CPUs. |
194 | | /// |
195 | | /// # Algorithm |
196 | | /// 1. Quantize f32 activations to int8 (on the fly, per super-block) |
197 | | /// 2. Expand 4-bit weights to int8 |
198 | | /// 3. Use vpdpbusd for packed u8×i8 dot product |
199 | | /// 4. Scale and accumulate at the end |
200 | | /// |
201 | | /// # Safety |
202 | | /// Requires AVX-512F, AVX-512BW, and AVX-512VNNI |
203 | | #[cfg(target_arch = "x86_64")] |
204 | | #[target_feature(enable = "avx512f", enable = "avx512bw", enable = "avx512vnni")] |
205 | | #[allow(unsafe_op_in_unsafe_fn)] |
206 | 0 | unsafe fn fused_q4k_dot_avx512_vnni(q4k_data: &[u8], activations: &[f32]) -> Result<f32> { |
207 | | #[allow(clippy::wildcard_imports)] |
208 | | use std::arch::x86_64::*; |
209 | | |
210 | | const SUPER_BLOCK_BYTES: usize = 144; |
211 | | |
212 | 0 | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
213 | 0 | return Err(RealizarError::InvalidShape { |
214 | 0 | reason: format!( |
215 | 0 | "Q4_K data length {} is not a multiple of super-block size {}", |
216 | 0 | q4k_data.len(), |
217 | 0 | SUPER_BLOCK_BYTES |
218 | 0 | ), |
219 | 0 | }); |
220 | 0 | } |
221 | | |
222 | 0 | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
223 | 0 | let expected_values = num_super_blocks * QK_K; |
224 | | |
225 | 0 | if activations.len() != expected_values { |
226 | 0 | return Err(RealizarError::InvalidShape { |
227 | 0 | reason: format!( |
228 | 0 | "Activation length {} doesn't match Q4_K values count {}", |
229 | 0 | activations.len(), |
230 | 0 | expected_values |
231 | 0 | ), |
232 | 0 | }); |
233 | 0 | } |
234 | | |
235 | | // Final accumulator (FP32) |
236 | 0 | let mut total_sum = 0.0f32; |
237 | 0 | let mut activation_idx = 0; |
238 | | |
239 | | // Nibble mask for 4-bit extraction |
240 | 0 | let nibble_mask = _mm512_set1_epi8(0x0F_i8); |
241 | | |
242 | 0 | for sb_idx in 0..num_super_blocks { |
243 | 0 | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
244 | | |
245 | | // Prefetch next super-block |
246 | 0 | if sb_idx + 1 < num_super_blocks { |
247 | 0 | let next_sb = (sb_idx + 1) * SUPER_BLOCK_BYTES; |
248 | 0 | _mm_prefetch(q4k_data.as_ptr().add(next_sb).cast::<i8>(), _MM_HINT_T0); |
249 | 0 | } |
250 | | |
251 | | // Read d and dmin (f16 → f32) |
252 | 0 | let d = read_f16(&q4k_data[sb_start..sb_start + 2]); |
253 | 0 | let dmin = read_f16(&q4k_data[sb_start + 2..sb_start + 4]); |
254 | | |
255 | | // Read scales (12 bytes) |
256 | 0 | let mut scales = [0u8; 12]; |
257 | 0 | scales.copy_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
258 | | |
259 | | // Pointer to quantized data (128 bytes) |
260 | 0 | let qs_ptr = q4k_data.as_ptr().add(sb_start + 16); |
261 | | |
262 | | // Process 64 values at a time (matches AVX-512 width of 64 bytes) |
263 | 0 | for j in (0..QK_K).step_by(64) { |
264 | 0 | let q_start = j / 2; |
265 | | |
266 | | // Get scales for the two 32-value halves |
267 | 0 | let is = j / 32; |
268 | 0 | let (sc1, m1) = extract_scale_min(&scales, is); |
269 | 0 | let (sc2, m2) = extract_scale_min(&scales, is + 1); |
270 | | |
271 | | // Load 32 bytes of quantized data (64 nibbles) |
272 | 0 | let q_bytes_256 = _mm256_loadu_si256(qs_ptr.add(q_start).cast::<__m256i>()); |
273 | | |
274 | | // Expand to 512-bit for AVX-512 operations |
275 | 0 | let q_bytes = _mm512_castsi256_si512(q_bytes_256); |
276 | 0 | let q_bytes = _mm512_inserti64x4(q_bytes, q_bytes_256, 1); |
277 | | |
278 | | // Extract low and high nibbles |
279 | 0 | let q_lo = _mm512_and_si512(q_bytes, nibble_mask); |
280 | 0 | let q_hi = _mm512_and_si512(_mm512_srli_epi16(q_bytes, 4), nibble_mask); |
281 | | |
282 | | // Process with integer dot products |
283 | | // We need to quantize activations and use vpdpbusd |
284 | | |
285 | | // Load 32 f32 activations for low nibbles, convert to int8 |
286 | 0 | let act_slice = &activations[activation_idx..activation_idx + 32]; |
287 | | |
288 | | // Find min/max for quantization |
289 | 0 | let act_max = act_slice.iter().copied().fold(f32::NEG_INFINITY, f32::max); |
290 | 0 | let act_min = act_slice.iter().copied().fold(f32::INFINITY, f32::min); |
291 | 0 | let act_scale = if act_max > act_min { |
292 | 0 | 127.0 / (act_max - act_min) |
293 | | } else { |
294 | 0 | 1.0 |
295 | | }; |
296 | | |
297 | | // Quantize activations to int8 |
298 | 0 | let mut act_i8 = [0i8; 32]; |
299 | 0 | for (i, &a) in act_slice.iter().enumerate() { |
300 | 0 | act_i8[i] = ((a - act_min) * act_scale).round() as i8; |
301 | 0 | } |
302 | | |
303 | | // Load quantized activations |
304 | 0 | let act_vec = _mm256_loadu_si256(act_i8.as_ptr().cast::<__m256i>()); |
305 | | |
306 | | // Expand q_lo from u8 to match |
307 | 0 | let q_lo_256 = _mm512_castsi512_si256(q_lo); |
308 | | |
309 | | // Use vpdpbusd: u8 × i8 → i32 accumulation |
310 | 0 | let _zero = _mm512_setzero_si512(); |
311 | 0 | let q_lo_512 = _mm512_cvtepu8_epi16(q_lo_256); |
312 | 0 | let act_512 = _mm512_cvtepi8_epi16(act_vec); |
313 | 0 | let prod = _mm512_mullo_epi16(q_lo_512, act_512); |
314 | | |
315 | | // Horizontal sum |
316 | 0 | let sum_256 = _mm256_add_epi16( |
317 | 0 | _mm512_castsi512_si256(prod), |
318 | 0 | _mm512_extracti64x4_epi64(prod, 1), |
319 | | ); |
320 | 0 | let sum_128 = _mm_add_epi16( |
321 | 0 | _mm256_castsi256_si128(sum_256), |
322 | 0 | _mm256_extracti128_si256(sum_256, 1), |
323 | | ); |
324 | | |
325 | | // Expand to i32 and sum |
326 | 0 | let sum_32 = _mm256_cvtepi16_epi32(sum_128); |
327 | 0 | let sum_arr: [i32; 8] = std::mem::transmute(sum_32); |
328 | 0 | let int_sum: i32 = sum_arr.iter().sum(); |
329 | | |
330 | | // Scale back to float |
331 | 0 | let float_sum = int_sum as f32 * d * sc1 / act_scale - (32.0 * dmin * m1); |
332 | 0 | total_sum += float_sum; |
333 | 0 | activation_idx += 32; |
334 | | |
335 | | // Process high nibbles similarly |
336 | 0 | let act_slice2 = &activations[activation_idx..activation_idx + 32]; |
337 | 0 | let act_max2 = act_slice2.iter().copied().fold(f32::NEG_INFINITY, f32::max); |
338 | 0 | let act_min2 = act_slice2.iter().copied().fold(f32::INFINITY, f32::min); |
339 | 0 | let act_scale2 = if act_max2 > act_min2 { |
340 | 0 | 127.0 / (act_max2 - act_min2) |
341 | | } else { |
342 | 0 | 1.0 |
343 | | }; |
344 | | |
345 | 0 | let mut act_i8_2 = [0i8; 32]; |
346 | 0 | for (i, &a) in act_slice2.iter().enumerate() { |
347 | 0 | act_i8_2[i] = ((a - act_min2) * act_scale2).round() as i8; |
348 | 0 | } |
349 | | |
350 | 0 | let act_vec2 = _mm256_loadu_si256(act_i8_2.as_ptr().cast::<__m256i>()); |
351 | 0 | let q_hi_256 = _mm512_castsi512_si256(q_hi); |
352 | 0 | let q_hi_512 = _mm512_cvtepu8_epi16(q_hi_256); |
353 | 0 | let act_512_2 = _mm512_cvtepi8_epi16(act_vec2); |
354 | 0 | let prod2 = _mm512_mullo_epi16(q_hi_512, act_512_2); |
355 | | |
356 | 0 | let sum_256_2 = _mm256_add_epi16( |
357 | 0 | _mm512_castsi512_si256(prod2), |
358 | 0 | _mm512_extracti64x4_epi64(prod2, 1), |
359 | | ); |
360 | 0 | let sum_128_2 = _mm_add_epi16( |
361 | 0 | _mm256_castsi256_si128(sum_256_2), |
362 | 0 | _mm256_extracti128_si256(sum_256_2, 1), |
363 | | ); |
364 | 0 | let sum_32_2 = _mm256_cvtepi16_epi32(sum_128_2); |
365 | 0 | let sum_arr2: [i32; 8] = std::mem::transmute(sum_32_2); |
366 | 0 | let int_sum2: i32 = sum_arr2.iter().sum(); |
367 | | |
368 | 0 | let float_sum2 = int_sum2 as f32 * d * sc2 / act_scale2 - (32.0 * dmin * m2); |
369 | 0 | total_sum += float_sum2; |
370 | 0 | activation_idx += 32; |
371 | | } |
372 | | } |
373 | | |
374 | 0 | Ok(total_sum) |
375 | 0 | } |
376 | | |
377 | | /// AVX2-accelerated fused Q4_K dequant+dot kernel (PARITY-003: llama.cpp-style SIMD) |
378 | | /// |
379 | | /// # Safety |
380 | | /// |
381 | | /// Caller must ensure: |
382 | | /// 1. AVX2 and FMA CPU features are available (use `is_x86_feature_detected!`) |
383 | | /// 2. Input slices are valid (handled by Rust's slice guarantees) |
384 | | /// |
385 | | /// This function is marked unsafe due to SIMD intrinsics, but is logically |
386 | | /// equivalent to the scalar `fused_q4k_dot` (within ULP tolerance). |
387 | | /// |
388 | | /// # Optimizations (PARITY-003) |
389 | | /// - SIMD loads: `_mm256_loadu_si256` for 32-byte bulk loads (vs scalar byte loads) |
390 | | /// - SIMD nibble extraction: `_mm256_and_si256` with 0x0F mask (vs scalar & 0x0F) |
391 | | /// - 4 independent accumulators to hide FMA latency |
392 | | /// - Software prefetching for next super-block |
393 | | /// - Matches llama.cpp ggml_vec_dot_q4_K_q8_K pattern |
394 | | #[cfg(target_arch = "x86_64")] |
395 | | #[target_feature(enable = "avx2", enable = "fma")] |
396 | | #[allow(unsafe_op_in_unsafe_fn)] |
397 | 1.49M | unsafe fn fused_q4k_dot_avx2(q4k_data: &[u8], activations: &[f32]) -> Result<f32> { |
398 | | // Allow wildcard import for SIMD intrinsics (standard pattern for arch-specific code) |
399 | | #[allow(clippy::wildcard_imports)] |
400 | | use std::arch::x86_64::*; |
401 | | |
402 | | const SUPER_BLOCK_BYTES: usize = 144; |
403 | | |
404 | | // Validate inputs (same as scalar) |
405 | 1.49M | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
406 | 3 | return Err(RealizarError::InvalidShape { |
407 | 3 | reason: format!( |
408 | 3 | "Q4_K data length {} is not a multiple of super-block size {}", |
409 | 3 | q4k_data.len(), |
410 | 3 | SUPER_BLOCK_BYTES |
411 | 3 | ), |
412 | 3 | }); |
413 | 1.49M | } |
414 | | |
415 | 1.49M | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
416 | 1.49M | let expected_values = num_super_blocks * QK_K; |
417 | | |
418 | 1.49M | if activations.len() != expected_values { |
419 | 317k | return Err(RealizarError::InvalidShape { |
420 | 317k | reason: format!( |
421 | 317k | "Activation length {} doesn't match Q4_K values count {}", |
422 | 317k | activations.len(), |
423 | 317k | expected_values |
424 | 317k | ), |
425 | 317k | }); |
426 | 1.18M | } |
427 | | |
428 | | // Nibble mask for extracting 4-bit values (llama.cpp pattern) |
429 | 1.18M | let nibble_mask = _mm256_set1_epi8(0x0F_i8); |
430 | | |
431 | | // PARITY-003: 4 independent accumulators to hide FMA latency |
432 | | // FMA latency = 4 cycles, throughput = 2/cycle |
433 | | // With 4 independent chains, we saturate the FMA throughput |
434 | 1.18M | let mut acc0 = _mm256_setzero_ps(); |
435 | 1.18M | let mut acc1 = _mm256_setzero_ps(); |
436 | 1.18M | let mut acc2 = _mm256_setzero_ps(); |
437 | 1.18M | let mut acc3 = _mm256_setzero_ps(); |
438 | 1.18M | let mut activation_idx = 0; |
439 | | |
440 | 1.20M | for sb_idx in 0..num_super_blocks1.18M { |
441 | 1.20M | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
442 | | |
443 | | // Prefetch next super-block while processing current |
444 | 1.20M | if sb_idx + 1 < num_super_blocks { |
445 | 27.4k | let next_sb = (sb_idx + 1) * SUPER_BLOCK_BYTES; |
446 | 27.4k | // SAFETY: Prefetch is a hint, pointer arithmetic is in bounds (checked above) |
447 | 27.4k | _mm_prefetch(q4k_data.as_ptr().add(next_sb).cast::<i8>(), _MM_HINT_T0); |
448 | 1.18M | } |
449 | | |
450 | | // Read d and dmin (f16 -> f32) |
451 | 1.20M | let d = read_f16(&q4k_data[sb_start..sb_start + 2]); |
452 | 1.20M | let dmin = read_f16(&q4k_data[sb_start + 2..sb_start + 4]); |
453 | | |
454 | | // Read scales (12 bytes) |
455 | 1.20M | let mut scales = [0u8; 12]; |
456 | 1.20M | scales.copy_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
457 | | |
458 | | // Pointer to quantized data (128 bytes = 256 nibbles = 256 values) |
459 | 1.20M | let qs_ptr = q4k_data.as_ptr().add(sb_start + 16); |
460 | | |
461 | | // PAR-001: Match dequantize_q4_k layout (llama.cpp/candle compatible) |
462 | | // Process 4 chunks of 64 values each (j=0, 64, 128, 192) |
463 | | // Each chunk: 32 low nibbles (sc1), then 32 high nibbles (sc2) |
464 | 4.83M | for j in (0..QK_K)1.20M .step_by1.20M (64) { |
465 | 4.83M | let q_start = j / 2; // 32 bytes per 64-value chunk |
466 | 4.83M | |
467 | 4.83M | // Get scales for the two 32-value halves |
468 | 4.83M | let is = j / 32; |
469 | 4.83M | let (sc1, m1) = extract_scale_min(&scales, is); |
470 | 4.83M | let (sc2, m2) = extract_scale_min(&scales, is + 1); |
471 | 4.83M | |
472 | 4.83M | // Precompute d*scale and dmin*min for both halves |
473 | 4.83M | let d_scale1 = d * sc1; |
474 | 4.83M | let dm1 = dmin * m1; |
475 | 4.83M | let d_scale2 = d * sc2; |
476 | 4.83M | let dm2 = dmin * m2; |
477 | 4.83M | |
478 | 4.83M | // SIMD OPTIMIZATION: Load 32 bytes at once (64 nibbles = 64 values) |
479 | 4.83M | // llama.cpp pattern: _mm256_loadu_si256 + AND/SHIFT for nibble extraction |
480 | 4.83M | let q_bytes = _mm256_loadu_si256(qs_ptr.add(q_start).cast::<__m256i>()); |
481 | 4.83M | |
482 | 4.83M | // Extract low nibbles (first 32 values) and high nibbles (second 32 values) |
483 | 4.83M | let q_lo = _mm256_and_si256(q_bytes, nibble_mask); |
484 | 4.83M | let q_hi = _mm256_and_si256(_mm256_srli_epi16(q_bytes, 4), nibble_mask); |
485 | 4.83M | |
486 | 4.83M | // Process low nibbles (32 values with scale sc1/m1) |
487 | 4.83M | // Split into 4 groups of 8 for f32 conversion (AVX2 can convert 8 i32→f32) |
488 | 4.83M | let d_scale1_vec = _mm256_set1_ps(d_scale1); |
489 | 4.83M | let dm1_vec = _mm256_set1_ps(dm1); |
490 | 4.83M | |
491 | 4.83M | // Extract bytes 0-7 (low nibbles) → convert to f32 |
492 | 4.83M | let q_lo_128_0 = _mm256_castsi256_si128(q_lo); |
493 | 4.83M | let q_lo_i32_0 = _mm256_cvtepu8_epi32(q_lo_128_0); |
494 | 4.83M | let q_lo_f32_0 = _mm256_cvtepi32_ps(q_lo_i32_0); |
495 | 4.83M | let dequant0 = _mm256_fmsub_ps(d_scale1_vec, q_lo_f32_0, dm1_vec); |
496 | 4.83M | let act0 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx)); |
497 | 4.83M | acc0 = _mm256_fmadd_ps(dequant0, act0, acc0); |
498 | 4.83M | activation_idx += 8; |
499 | 4.83M | |
500 | 4.83M | // Extract bytes 8-15 (low nibbles) |
501 | 4.83M | let q_lo_shifted = _mm_srli_si128(q_lo_128_0, 8); |
502 | 4.83M | let q_lo_i32_1 = _mm256_cvtepu8_epi32(q_lo_shifted); |
503 | 4.83M | let q_lo_f32_1 = _mm256_cvtepi32_ps(q_lo_i32_1); |
504 | 4.83M | let dequant1 = _mm256_fmsub_ps(d_scale1_vec, q_lo_f32_1, dm1_vec); |
505 | 4.83M | let act1 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx)); |
506 | 4.83M | acc1 = _mm256_fmadd_ps(dequant1, act1, acc1); |
507 | 4.83M | activation_idx += 8; |
508 | 4.83M | |
509 | 4.83M | // Extract bytes 16-23 (low nibbles from high 128 bits) |
510 | 4.83M | let q_lo_128_1 = _mm256_extracti128_si256(q_lo, 1); |
511 | 4.83M | let q_lo_i32_2 = _mm256_cvtepu8_epi32(q_lo_128_1); |
512 | 4.83M | let q_lo_f32_2 = _mm256_cvtepi32_ps(q_lo_i32_2); |
513 | 4.83M | let dequant2 = _mm256_fmsub_ps(d_scale1_vec, q_lo_f32_2, dm1_vec); |
514 | 4.83M | let act2 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx)); |
515 | 4.83M | acc2 = _mm256_fmadd_ps(dequant2, act2, acc2); |
516 | 4.83M | activation_idx += 8; |
517 | 4.83M | |
518 | 4.83M | // Extract bytes 24-31 (low nibbles) |
519 | 4.83M | let q_lo_shifted2 = _mm_srli_si128(q_lo_128_1, 8); |
520 | 4.83M | let q_lo_i32_3 = _mm256_cvtepu8_epi32(q_lo_shifted2); |
521 | 4.83M | let q_lo_f32_3 = _mm256_cvtepi32_ps(q_lo_i32_3); |
522 | 4.83M | let dequant3 = _mm256_fmsub_ps(d_scale1_vec, q_lo_f32_3, dm1_vec); |
523 | 4.83M | let act3 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx)); |
524 | 4.83M | acc3 = _mm256_fmadd_ps(dequant3, act3, acc3); |
525 | 4.83M | activation_idx += 8; |
526 | 4.83M | |
527 | 4.83M | // Process high nibbles (32 values with scale sc2/m2) |
528 | 4.83M | let d_scale2_vec = _mm256_set1_ps(d_scale2); |
529 | 4.83M | let dm2_vec = _mm256_set1_ps(dm2); |
530 | 4.83M | |
531 | 4.83M | // Extract bytes 0-7 (high nibbles) |
532 | 4.83M | let q_hi_128_0 = _mm256_castsi256_si128(q_hi); |
533 | 4.83M | let q_hi_i32_0 = _mm256_cvtepu8_epi32(q_hi_128_0); |
534 | 4.83M | let q_hi_f32_0 = _mm256_cvtepi32_ps(q_hi_i32_0); |
535 | 4.83M | let dequant4 = _mm256_fmsub_ps(d_scale2_vec, q_hi_f32_0, dm2_vec); |
536 | 4.83M | let act4 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx)); |
537 | 4.83M | acc0 = _mm256_fmadd_ps(dequant4, act4, acc0); |
538 | 4.83M | activation_idx += 8; |
539 | 4.83M | |
540 | 4.83M | // Extract bytes 8-15 (high nibbles) |
541 | 4.83M | let q_hi_shifted = _mm_srli_si128(q_hi_128_0, 8); |
542 | 4.83M | let q_hi_i32_1 = _mm256_cvtepu8_epi32(q_hi_shifted); |
543 | 4.83M | let q_hi_f32_1 = _mm256_cvtepi32_ps(q_hi_i32_1); |
544 | 4.83M | let dequant5 = _mm256_fmsub_ps(d_scale2_vec, q_hi_f32_1, dm2_vec); |
545 | 4.83M | let act5 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx)); |
546 | 4.83M | acc1 = _mm256_fmadd_ps(dequant5, act5, acc1); |
547 | 4.83M | activation_idx += 8; |
548 | 4.83M | |
549 | 4.83M | // Extract bytes 16-23 (high nibbles from high 128 bits) |
550 | 4.83M | let q_hi_128_1 = _mm256_extracti128_si256(q_hi, 1); |
551 | 4.83M | let q_hi_i32_2 = _mm256_cvtepu8_epi32(q_hi_128_1); |
552 | 4.83M | let q_hi_f32_2 = _mm256_cvtepi32_ps(q_hi_i32_2); |
553 | 4.83M | let dequant6 = _mm256_fmsub_ps(d_scale2_vec, q_hi_f32_2, dm2_vec); |
554 | 4.83M | let act6 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx)); |
555 | 4.83M | acc2 = _mm256_fmadd_ps(dequant6, act6, acc2); |
556 | 4.83M | activation_idx += 8; |
557 | 4.83M | |
558 | 4.83M | // Extract bytes 24-31 (high nibbles) |
559 | 4.83M | let q_hi_shifted2 = _mm_srli_si128(q_hi_128_1, 8); |
560 | 4.83M | let q_hi_i32_3 = _mm256_cvtepu8_epi32(q_hi_shifted2); |
561 | 4.83M | let q_hi_f32_3 = _mm256_cvtepi32_ps(q_hi_i32_3); |
562 | 4.83M | let dequant7 = _mm256_fmsub_ps(d_scale2_vec, q_hi_f32_3, dm2_vec); |
563 | 4.83M | let act7 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx)); |
564 | 4.83M | acc3 = _mm256_fmadd_ps(dequant7, act7, acc3); |
565 | 4.83M | activation_idx += 8; |
566 | 4.83M | } |
567 | | } |
568 | | |
569 | | // Combine 4 accumulators → single accumulator |
570 | 1.18M | let acc_01 = _mm256_add_ps(acc0, acc1); |
571 | 1.18M | let acc_23 = _mm256_add_ps(acc2, acc3); |
572 | 1.18M | let acc = _mm256_add_ps(acc_01, acc_23); |
573 | | |
574 | | // Horizontal sum: reduce 8 lanes to single value |
575 | 1.18M | let sum_halves = _mm_add_ps(_mm256_castps256_ps128(acc), _mm256_extractf128_ps(acc, 1)); |
576 | 1.18M | let temp = _mm_add_ps(sum_halves, _mm_movehl_ps(sum_halves, sum_halves)); |
577 | 1.18M | let temp = _mm_add_ss(temp, _mm_shuffle_ps(temp, temp, 1)); |
578 | 1.18M | let result = _mm_cvtss_f32(temp); |
579 | | |
580 | 1.18M | Ok(result) |
581 | 1.49M | } |
582 | | |
583 | | // ============================================================================ |
584 | | // Q4_K × Q8_K KERNELS (Super-block aligned integer-only arithmetic) |
585 | | // ============================================================================ |
586 | | |
587 | | /// Fused Q4_K × Q8_K dot product (super-block aligned, llama.cpp-style) |
588 | | /// |
589 | | /// Uses Q8_K format (256 values per super-block, single scale) for maximum |
590 | | /// SIMD efficiency. This matches llama.cpp's `ggml_vec_dot_q4_K_q8_K`. |
591 | | /// |
592 | | /// # Arguments |
593 | | /// * `q4k_data` - Raw Q4_K quantized data (144 bytes per super-block) |
594 | | /// * `q8k_scales` - Q8_K scales (one per super-block) |
595 | | /// * `q8k_quants` - Q8_K quantized int8 values (256 per super-block) |
596 | | /// |
597 | | /// # Performance |
598 | | /// |
599 | | /// Compared to Q4_K × f32: |
600 | | /// - 8x fewer memory reads for activations |
601 | | /// - Integer-only inner loop (no f32 conversion until end) |
602 | | /// - Single scale multiplication per super-block (vs 8 for Q8_0) |
603 | 40 | pub fn fused_q4k_q8k_dot(q4k_data: &[u8], q8k_scales: &[f32], q8k_quants: &[i8]) -> Result<f32> { |
604 | | const SUPER_BLOCK_BYTES: usize = 144; |
605 | | |
606 | 40 | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
607 | 7 | return Err(RealizarError::InvalidShape { |
608 | 7 | reason: format!( |
609 | 7 | "Q4_K data length {} is not a multiple of {}", |
610 | 7 | q4k_data.len(), |
611 | 7 | SUPER_BLOCK_BYTES |
612 | 7 | ), |
613 | 7 | }); |
614 | 33 | } |
615 | | |
616 | 33 | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
617 | 33 | let expected_values = num_super_blocks * QK_K; |
618 | | |
619 | 33 | if q8k_scales.len() < num_super_blocks { |
620 | 3 | return Err(RealizarError::InvalidShape { |
621 | 3 | reason: format!( |
622 | 3 | "Q8_K scales count {} < expected {}", |
623 | 3 | q8k_scales.len(), |
624 | 3 | num_super_blocks |
625 | 3 | ), |
626 | 3 | }); |
627 | 30 | } |
628 | | |
629 | 30 | if q8k_quants.len() < expected_values { |
630 | 4 | return Err(RealizarError::InvalidShape { |
631 | 4 | reason: format!( |
632 | 4 | "Q8_K quants count {} < expected {}", |
633 | 4 | q8k_quants.len(), |
634 | 4 | expected_values |
635 | 4 | ), |
636 | 4 | }); |
637 | 26 | } |
638 | | |
639 | 26 | let mut total_acc = 0.0f32; |
640 | | |
641 | 45 | for sb_idx in 0..num_super_blocks26 { |
642 | 45 | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
643 | 45 | let q8_start = sb_idx * QK_K; |
644 | | |
645 | | // Read Q4_K super-block header |
646 | 45 | let d = read_f16(&q4k_data[sb_start..sb_start + 2]); |
647 | 45 | let dmin = read_f16(&q4k_data[sb_start + 2..sb_start + 4]); |
648 | | |
649 | | // Read scales (12 bytes for 8 blocks) |
650 | 45 | let mut scales = [0u8; 12]; |
651 | 45 | scales.copy_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
652 | | |
653 | | // Q8_K scale for this super-block |
654 | 45 | let q8_scale = q8k_scales[sb_idx]; |
655 | | |
656 | | // Process 4 chunks of 64 values (matching dequantize_q4_k layout) |
657 | | // The dequantized output order is: 32 low nibbles, then 32 high nibbles |
658 | | // So activations[j..j+32] correspond to low nibbles, activations[j+32..j+64] to high |
659 | 180 | for j in (0..QK_K)45 .step_by45 (64) { |
660 | 180 | let q_offset = sb_start + 16 + j / 2; // 32 bytes per 64-value chunk |
661 | 180 | let q8_offset = q8_start + j; |
662 | | |
663 | | // Get scales for low and high nibbles |
664 | 180 | let is = j / 32; |
665 | 180 | let (sc1, m1) = extract_scale_min(&scales, is); |
666 | 180 | let (sc2, m2) = extract_scale_min(&scales, is + 1); |
667 | | |
668 | | // Combined scale factors |
669 | 180 | let d_sc1_q8 = d * sc1 * q8_scale; |
670 | 180 | let dm1_q8 = dmin * m1 * q8_scale; |
671 | 180 | let d_sc2_q8 = d * sc2 * q8_scale; |
672 | 180 | let dm2_q8 = dmin * m2 * q8_scale; |
673 | | |
674 | | // Accumulators for low and high nibbles |
675 | 180 | let mut sum_lo: i32 = 0; // q4_lo × q8 (for activations[j..j+32]) |
676 | 180 | let mut sum_hi: i32 = 0; // q4_hi × q8 (for activations[j+32..j+64]) |
677 | 180 | let mut q8_sum_lo: i32 = 0; |
678 | 180 | let mut q8_sum_hi: i32 = 0; |
679 | | |
680 | 5.94k | for b5.76k in 0..32 { |
681 | 5.76k | let q4_byte = q4k_data[q_offset + b]; |
682 | 5.76k | |
683 | 5.76k | // Low nibble × activation[j + b] (first 32 positions in dequant order) |
684 | 5.76k | let q4_lo = (q4_byte & 0x0F) as i32; |
685 | 5.76k | let q8_lo = q8k_quants[q8_offset + b] as i32; |
686 | 5.76k | sum_lo += q4_lo * q8_lo; |
687 | 5.76k | q8_sum_lo += q8_lo; |
688 | 5.76k | |
689 | 5.76k | // High nibble × activation[j + 32 + b] (second 32 positions in dequant order) |
690 | 5.76k | let q4_hi = ((q4_byte >> 4) & 0x0F) as i32; |
691 | 5.76k | let q8_hi = q8k_quants[q8_offset + 32 + b] as i32; |
692 | 5.76k | sum_hi += q4_hi * q8_hi; |
693 | 5.76k | q8_sum_hi += q8_hi; |
694 | 5.76k | } |
695 | | |
696 | | // Apply formula: (d * scale * sum_q4_q8 - dmin * min * sum_q8) * q8_scale |
697 | 180 | total_acc += d_sc1_q8 * (sum_lo as f32) - dm1_q8 * (q8_sum_lo as f32); |
698 | 180 | total_acc += d_sc2_q8 * (sum_hi as f32) - dm2_q8 * (q8_sum_hi as f32); |
699 | | } |
700 | | } |
701 | | |
702 | 26 | Ok(total_acc) |
703 | 40 | } |
704 | | |
705 | | /// SIMD-accelerated Q4_K × Q8_K dot product |
706 | | /// |
707 | | /// Uses AVX-512 VNNI (vpdpbusd) for maximum throughput, falls back to AVX2 or scalar. |
708 | | /// Single scale per super-block eliminates per-block overhead. |
709 | 536 | pub fn fused_q4k_q8k_dot_simd( |
710 | 536 | q4k_data: &[u8], |
711 | 536 | q8k_scales: &[f32], |
712 | 536 | q8k_quants: &[i8], |
713 | 536 | ) -> Result<f32> { |
714 | | #[cfg(target_arch = "x86_64")] |
715 | | { |
716 | | // PAR-126: Use V2 optimized AVX-512 VNNI kernel (deferred horizontal sums) |
717 | 536 | if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("avx512vnni") { |
718 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
719 | 536 | return unsafe { fused_q4k_q8k_dot_avx512vnni_v2(q4k_data, q8k_scales, q8k_quants) }; |
720 | 0 | } |
721 | | // Fallback to AVX2 (layout issue resolved) |
722 | 0 | if is_x86_feature_detected!("avx2") { |
723 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
724 | 0 | return unsafe { fused_q4k_q8k_dot_avx2(q4k_data, q8k_scales, q8k_quants) }; |
725 | 0 | } |
726 | | } |
727 | | |
728 | 0 | fused_q4k_q8k_dot(q4k_data, q8k_scales, q8k_quants) |
729 | 536 | } |
730 | | |
731 | | /// PAR-126 V2: AVX-512 VNNI kernel with deferred horizontal sums |
732 | | /// |
733 | | /// Key optimization: Instead of extracting block sums to scalars in the inner loop, |
734 | | /// keeps all 8 block accumulators in __m256i vectors and only reduces at the end. |
735 | | /// This eliminates 24 horizontal sums per row (from 8 to 1). |
736 | | #[cfg(target_arch = "x86_64")] |
737 | | #[target_feature(enable = "avx512f", enable = "avx512vnni", enable = "avx512bw")] |
738 | | #[allow(unsafe_op_in_unsafe_fn)] |
739 | | #[allow(clippy::similar_names)] |
740 | | #[allow(clippy::too_many_lines)] |
741 | 536 | unsafe fn fused_q4k_q8k_dot_avx512vnni_v2( |
742 | 536 | q4k_data: &[u8], |
743 | 536 | q8k_scales: &[f32], |
744 | 536 | q8k_quants: &[i8], |
745 | 536 | ) -> Result<f32> { |
746 | | #[allow(clippy::wildcard_imports)] |
747 | | use std::arch::x86_64::*; |
748 | | |
749 | | const SUPER_BLOCK_BYTES: usize = 144; |
750 | | |
751 | 536 | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
752 | 3 | return Err(RealizarError::InvalidShape { |
753 | 3 | reason: format!( |
754 | 3 | "Q4_K data length {} is not a multiple of {}", |
755 | 3 | q4k_data.len(), |
756 | 3 | SUPER_BLOCK_BYTES |
757 | 3 | ), |
758 | 3 | }); |
759 | 533 | } |
760 | | |
761 | 533 | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
762 | 533 | let expected_values = num_super_blocks * QK_K; |
763 | | |
764 | 533 | if q8k_scales.len() < num_super_blocks || q8k_quants.len() < expected_values { |
765 | 1 | return Err(RealizarError::InvalidShape { |
766 | 1 | reason: "Q8_K buffer too small".to_string(), |
767 | 1 | }); |
768 | 532 | } |
769 | | |
770 | 532 | let nibble_mask = _mm256_set1_epi8(0x0F_i8); |
771 | 532 | let ones_16 = _mm256_set1_epi16(1); |
772 | | |
773 | | // Global float accumulator |
774 | 532 | let mut total_acc = _mm256_setzero_ps(); |
775 | | |
776 | 553 | for sb_idx in 0..num_super_blocks532 { |
777 | 553 | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
778 | 553 | let q8_start = sb_idx * QK_K; |
779 | | |
780 | | // Prefetch next super-block |
781 | 553 | if sb_idx + 1 < num_super_blocks { |
782 | 22 | _mm_prefetch( |
783 | 22 | q4k_data |
784 | 22 | .as_ptr() |
785 | 22 | .add((sb_idx + 1) * SUPER_BLOCK_BYTES) |
786 | 22 | .cast::<i8>(), |
787 | 22 | _MM_HINT_T0, |
788 | 22 | ); |
789 | 22 | _mm_prefetch( |
790 | 22 | q8k_quants.as_ptr().add((sb_idx + 1) * QK_K).cast::<i8>(), |
791 | 22 | _MM_HINT_T0, |
792 | 22 | ); |
793 | 531 | } |
794 | | |
795 | | // Read Q4_K header |
796 | 553 | let d = read_f16(&q4k_data[sb_start..sb_start + 2]); |
797 | 553 | let dmin = read_f16(&q4k_data[sb_start + 2..sb_start + 4]); |
798 | | |
799 | 553 | let mut scales_raw = [0u8; 12]; |
800 | 553 | scales_raw.copy_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
801 | | |
802 | 553 | let q8_scale = q8k_scales[sb_idx]; |
803 | 553 | let d_q8 = d * q8_scale; |
804 | 553 | let dmin_q8 = dmin * q8_scale; |
805 | | |
806 | 553 | let qs_ptr = q4k_data.as_ptr().add(sb_start + 16); |
807 | 553 | let q8_ptr = q8k_quants.as_ptr().add(q8_start); |
808 | | |
809 | | // PAR-126 V2: Keep all 8 block results in vectors |
810 | | // block_dots_vec[i] = sum of (Q4[block_i] * Q8[block_i]) |
811 | | // block_q8sums_vec[i] = sum of Q8[block_i] |
812 | 553 | let mut block_dots_vec = _mm256_setzero_si256(); |
813 | 553 | let mut block_q8sums_vec = _mm256_setzero_si256(); |
814 | | |
815 | | // Process 64 values (2 blocks) per iteration, 4 iterations = 8 blocks |
816 | | // Each iteration produces 2 block sums that go into specific lanes |
817 | 2.76k | for chunk2.21k in 0..4 { |
818 | 2.21k | let j = chunk * 64; |
819 | 2.21k | let q_offset = j / 2; |
820 | | |
821 | | // Load 32 bytes Q4 (64 nibbles packed) |
822 | 2.21k | let q4_bytes = _mm256_loadu_si256(qs_ptr.add(q_offset).cast::<__m256i>()); |
823 | | |
824 | | // Extract nibbles: lo = first 32 values, hi = next 32 values |
825 | 2.21k | let q4_lo = _mm256_and_si256(q4_bytes, nibble_mask); |
826 | 2.21k | let q4_hi = _mm256_and_si256(_mm256_srli_epi16(q4_bytes, 4), nibble_mask); |
827 | | |
828 | | // Load 64 bytes Q8 |
829 | 2.21k | let q8_lo = _mm256_loadu_si256(q8_ptr.add(j).cast::<__m256i>()); |
830 | 2.21k | let q8_hi = _mm256_loadu_si256(q8_ptr.add(j + 32).cast::<__m256i>()); |
831 | | |
832 | | // Q4 × Q8 products -> i16 via maddubs -> i32 via madd |
833 | 2.21k | let prod_lo_i16 = _mm256_maddubs_epi16(q4_lo, q8_lo); |
834 | 2.21k | let prod_hi_i16 = _mm256_maddubs_epi16(q4_hi, q8_hi); |
835 | 2.21k | let prod_lo_i32 = _mm256_madd_epi16(prod_lo_i16, ones_16); |
836 | 2.21k | let prod_hi_i32 = _mm256_madd_epi16(prod_hi_i16, ones_16); |
837 | | |
838 | | // Reduce each 256-bit register to ONE sum using hadd (within-lane reduction) |
839 | | // prod_lo_i32 has 8 i32 values, we need their sum for block chunk*2 |
840 | | // prod_hi_i32 has 8 i32 values, we need their sum for block chunk*2+1 |
841 | | |
842 | | // Use hadd twice to get 8 -> 4 -> 2 values per register |
843 | 2.21k | let prod_lo_h1 = _mm256_hadd_epi32(prod_lo_i32, prod_hi_i32); // interleaves lo and hi |
844 | 2.21k | let prod_h2 = _mm256_hadd_epi32(prod_lo_h1, prod_lo_h1); // further reduce |
845 | | |
846 | | // Now prod_h2 has: [sum_lo_lane0, sum_hi_lane0, sum_lo_lane0, sum_hi_lane0, |
847 | | // sum_lo_lane1, sum_hi_lane1, sum_lo_lane1, sum_hi_lane1] |
848 | | // We need to extract the two unique sums and place them in block_dots_vec |
849 | | |
850 | | // Extract two sums: add lane0 and lane1 of each block |
851 | 2.21k | let lane0 = _mm256_castsi256_si128(prod_h2); |
852 | 2.21k | let lane1 = _mm256_extracti128_si256(prod_h2, 1); |
853 | 2.21k | let sums_128 = _mm_add_epi32(lane0, lane1); // [dot_lo, dot_hi, dot_lo, dot_hi] |
854 | | |
855 | | // Insert into correct position based on chunk |
856 | | // block_dots_vec lanes: [0,1,2,3,4,5,6,7] for blocks [0,1,2,3,4,5,6,7] |
857 | 2.21k | match chunk { |
858 | 553 | 0 => { |
859 | 553 | // Put in lanes 0,1 |
860 | 553 | block_dots_vec = |
861 | 553 | _mm256_insert_epi32(block_dots_vec, _mm_extract_epi32(sums_128, 0), 0); |
862 | 553 | block_dots_vec = |
863 | 553 | _mm256_insert_epi32(block_dots_vec, _mm_extract_epi32(sums_128, 1), 1); |
864 | 553 | }, |
865 | 553 | 1 => { |
866 | 553 | block_dots_vec = |
867 | 553 | _mm256_insert_epi32(block_dots_vec, _mm_extract_epi32(sums_128, 0), 2); |
868 | 553 | block_dots_vec = |
869 | 553 | _mm256_insert_epi32(block_dots_vec, _mm_extract_epi32(sums_128, 1), 3); |
870 | 553 | }, |
871 | 553 | 2 => { |
872 | 553 | block_dots_vec = |
873 | 553 | _mm256_insert_epi32(block_dots_vec, _mm_extract_epi32(sums_128, 0), 4); |
874 | 553 | block_dots_vec = |
875 | 553 | _mm256_insert_epi32(block_dots_vec, _mm_extract_epi32(sums_128, 1), 5); |
876 | 553 | }, |
877 | 553 | 3 => { |
878 | 553 | block_dots_vec = |
879 | 553 | _mm256_insert_epi32(block_dots_vec, _mm_extract_epi32(sums_128, 0), 6); |
880 | 553 | block_dots_vec = |
881 | 553 | _mm256_insert_epi32(block_dots_vec, _mm_extract_epi32(sums_128, 1), 7); |
882 | 553 | }, |
883 | 0 | _ => unreachable!(), |
884 | | } |
885 | | |
886 | | // Q8 sums for dmin correction using sad_epu8 (sum of absolute differences) |
887 | | // Convert signed i8 to unsigned by adding 128, compute SAD, then adjust |
888 | 2.21k | let bias = _mm256_set1_epi8(-128_i8); |
889 | 2.21k | let _q8_lo_u = _mm256_sub_epi8(q8_lo, bias); // signed [-128,127] -> unsigned [0,255] |
890 | 2.21k | let _q8_hi_u = _mm256_sub_epi8(q8_hi, bias); |
891 | | |
892 | | // Use mpsadbw or manual approach for sum |
893 | | // Simpler: sign-extend and sum using madd |
894 | 2.21k | let q8_lo_i16_a = _mm256_cvtepi8_epi16(_mm256_castsi256_si128(q8_lo)); |
895 | 2.21k | let q8_lo_i16_b = _mm256_cvtepi8_epi16(_mm256_extracti128_si256(q8_lo, 1)); |
896 | 2.21k | let q8_hi_i16_a = _mm256_cvtepi8_epi16(_mm256_castsi256_si128(q8_hi)); |
897 | 2.21k | let q8_hi_i16_b = _mm256_cvtepi8_epi16(_mm256_extracti128_si256(q8_hi, 1)); |
898 | | |
899 | 2.21k | let q8_lo_i32_a = _mm256_madd_epi16(q8_lo_i16_a, ones_16); |
900 | 2.21k | let q8_lo_i32_b = _mm256_madd_epi16(q8_lo_i16_b, ones_16); |
901 | 2.21k | let q8_hi_i32_a = _mm256_madd_epi16(q8_hi_i16_a, ones_16); |
902 | 2.21k | let q8_hi_i32_b = _mm256_madd_epi16(q8_hi_i16_b, ones_16); |
903 | | |
904 | 2.21k | let q8_lo_sum = _mm256_add_epi32(q8_lo_i32_a, q8_lo_i32_b); |
905 | 2.21k | let q8_hi_sum = _mm256_add_epi32(q8_hi_i32_a, q8_hi_i32_b); |
906 | | |
907 | | // Reduce to two sums using hadd |
908 | 2.21k | let q8_h1 = _mm256_hadd_epi32(q8_lo_sum, q8_hi_sum); |
909 | 2.21k | let q8_h2 = _mm256_hadd_epi32(q8_h1, q8_h1); |
910 | | |
911 | 2.21k | let q8_lane0 = _mm256_castsi256_si128(q8_h2); |
912 | 2.21k | let q8_lane1 = _mm256_extracti128_si256(q8_h2, 1); |
913 | 2.21k | let q8_sums_128 = _mm_add_epi32(q8_lane0, q8_lane1); |
914 | | |
915 | 2.21k | match chunk { |
916 | 553 | 0 => { |
917 | 553 | block_q8sums_vec = |
918 | 553 | _mm256_insert_epi32(block_q8sums_vec, _mm_extract_epi32(q8_sums_128, 0), 0); |
919 | 553 | block_q8sums_vec = |
920 | 553 | _mm256_insert_epi32(block_q8sums_vec, _mm_extract_epi32(q8_sums_128, 1), 1); |
921 | 553 | }, |
922 | 553 | 1 => { |
923 | 553 | block_q8sums_vec = |
924 | 553 | _mm256_insert_epi32(block_q8sums_vec, _mm_extract_epi32(q8_sums_128, 0), 2); |
925 | 553 | block_q8sums_vec = |
926 | 553 | _mm256_insert_epi32(block_q8sums_vec, _mm_extract_epi32(q8_sums_128, 1), 3); |
927 | 553 | }, |
928 | 553 | 2 => { |
929 | 553 | block_q8sums_vec = |
930 | 553 | _mm256_insert_epi32(block_q8sums_vec, _mm_extract_epi32(q8_sums_128, 0), 4); |
931 | 553 | block_q8sums_vec = |
932 | 553 | _mm256_insert_epi32(block_q8sums_vec, _mm_extract_epi32(q8_sums_128, 1), 5); |
933 | 553 | }, |
934 | 553 | 3 => { |
935 | 553 | block_q8sums_vec = |
936 | 553 | _mm256_insert_epi32(block_q8sums_vec, _mm_extract_epi32(q8_sums_128, 0), 6); |
937 | 553 | block_q8sums_vec = |
938 | 553 | _mm256_insert_epi32(block_q8sums_vec, _mm_extract_epi32(q8_sums_128, 1), 7); |
939 | 553 | }, |
940 | 0 | _ => unreachable!(), |
941 | | } |
942 | | } |
943 | | |
944 | | // Extract all 8 scales and mins into vectors |
945 | 553 | let mut scales = [0.0f32; 8]; |
946 | 553 | let mut mins = [0.0f32; 8]; |
947 | 4.97k | for i4.42k in 0..8 { |
948 | 4.42k | let (sc, m) = extract_scale_min(&scales_raw, i); |
949 | 4.42k | scales[i] = sc; |
950 | 4.42k | mins[i] = m; |
951 | 4.42k | } |
952 | | |
953 | 553 | let scales_vec = _mm256_loadu_ps(scales.as_ptr()); |
954 | 553 | let mins_vec = _mm256_loadu_ps(mins.as_ptr()); |
955 | | |
956 | | // Convert block results to f32 |
957 | 553 | let dots_f32 = _mm256_cvtepi32_ps(block_dots_vec); |
958 | 553 | let q8sums_f32 = _mm256_cvtepi32_ps(block_q8sums_vec); |
959 | | |
960 | | // Compute: d_q8 * scales * dots - dmin_q8 * mins * q8sums |
961 | 553 | let d_q8_vec = _mm256_set1_ps(d_q8); |
962 | 553 | let dmin_q8_vec = _mm256_set1_ps(dmin_q8); |
963 | | |
964 | 553 | let term1 = _mm256_mul_ps(d_q8_vec, _mm256_mul_ps(scales_vec, dots_f32)); |
965 | 553 | let term2 = _mm256_mul_ps(dmin_q8_vec, _mm256_mul_ps(mins_vec, q8sums_f32)); |
966 | 553 | let result = _mm256_sub_ps(term1, term2); |
967 | | |
968 | | // Accumulate into total (defer final horizontal sum) |
969 | 553 | total_acc = _mm256_add_ps(total_acc, result); |
970 | | } |
971 | | |
972 | | // ONE horizontal sum at the very end |
973 | 532 | let sum128 = _mm_add_ps( |
974 | 532 | _mm256_castps256_ps128(total_acc), |
975 | 532 | _mm256_extractf128_ps(total_acc, 1), |
976 | | ); |
977 | 532 | let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); |
978 | 532 | let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1)); |
979 | | |
980 | 532 | Ok(_mm_cvtss_f32(sum32)) |
981 | 536 | } |
982 | | |
983 | | /// AVX-512 VNNI optimized Q4_K × Q8_K dot product (llama.cpp style) |
984 | | /// |
985 | | /// Uses `vpdpbusd` instruction for 16-way uint8×int8→int32 multiply-accumulate. |
986 | | /// This is the fastest CPU path for quantized matmul. |
987 | | /// |
988 | | /// # Safety |
989 | | /// Requires AVX-512F and AVX-512 VNNI CPU features. |
990 | | #[cfg(target_arch = "x86_64")] |
991 | | #[target_feature(enable = "avx512f", enable = "avx512vnni", enable = "avx512bw")] |
992 | | #[allow(unsafe_op_in_unsafe_fn)] |
993 | 0 | unsafe fn fused_q4k_q8k_dot_avx512vnni( |
994 | 0 | q4k_data: &[u8], |
995 | 0 | q8k_scales: &[f32], |
996 | 0 | q8k_quants: &[i8], |
997 | 0 | ) -> Result<f32> { |
998 | | #[allow(clippy::wildcard_imports)] |
999 | | use std::arch::x86_64::*; |
1000 | | |
1001 | | const SUPER_BLOCK_BYTES: usize = 144; |
1002 | | |
1003 | 0 | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
1004 | 0 | return Err(RealizarError::InvalidShape { |
1005 | 0 | reason: format!( |
1006 | 0 | "Q4_K data length {} is not a multiple of {}", |
1007 | 0 | q4k_data.len(), |
1008 | 0 | SUPER_BLOCK_BYTES |
1009 | 0 | ), |
1010 | 0 | }); |
1011 | 0 | } |
1012 | | |
1013 | 0 | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
1014 | 0 | let expected_values = num_super_blocks * QK_K; |
1015 | | |
1016 | 0 | if q8k_scales.len() < num_super_blocks || q8k_quants.len() < expected_values { |
1017 | 0 | return Err(RealizarError::InvalidShape { |
1018 | 0 | reason: "Q8_K buffer too small".to_string(), |
1019 | 0 | }); |
1020 | 0 | } |
1021 | | |
1022 | 0 | let _nibble_mask = _mm512_set1_epi8(0x0F_i8); // Reserved for future optimization |
1023 | 0 | let mut total_acc = 0.0f32; |
1024 | | |
1025 | 0 | for sb_idx in 0..num_super_blocks { |
1026 | 0 | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
1027 | 0 | let q8_start = sb_idx * QK_K; |
1028 | | |
1029 | | // Prefetch next super-block |
1030 | 0 | if sb_idx + 1 < num_super_blocks { |
1031 | 0 | _mm_prefetch( |
1032 | 0 | q4k_data |
1033 | 0 | .as_ptr() |
1034 | 0 | .add((sb_idx + 1) * SUPER_BLOCK_BYTES) |
1035 | 0 | .cast::<i8>(), |
1036 | 0 | _MM_HINT_T0, |
1037 | 0 | ); |
1038 | 0 | } |
1039 | | |
1040 | | // Read Q4_K header |
1041 | 0 | let d = read_f16(&q4k_data[sb_start..sb_start + 2]); |
1042 | 0 | let dmin = read_f16(&q4k_data[sb_start + 2..sb_start + 4]); |
1043 | | |
1044 | 0 | let mut scales = [0u8; 12]; |
1045 | 0 | scales.copy_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
1046 | | |
1047 | 0 | let q8_scale = q8k_scales[sb_idx]; |
1048 | 0 | let d_q8 = d * q8_scale; |
1049 | 0 | let dmin_q8 = dmin * q8_scale; |
1050 | | |
1051 | 0 | let qs_ptr = q4k_data.as_ptr().add(sb_start + 16); |
1052 | 0 | let q8_ptr = q8k_quants.as_ptr().add(q8_start); |
1053 | | |
1054 | | // Process 4 chunks of 64 values (matching dequantize_q4_k layout) |
1055 | 0 | for j in (0..QK_K).step_by(64) { |
1056 | 0 | let q_offset = j / 2; // 32 bytes per 64-value chunk |
1057 | 0 |
|
1058 | 0 | let is = j / 32; |
1059 | 0 | let (sc1, m1) = extract_scale_min(&scales, is); |
1060 | 0 | let (sc2, m2) = extract_scale_min(&scales, is + 1); |
1061 | 0 |
|
1062 | 0 | // Load 32 bytes of Q4_K (as lower half of 512-bit register) |
1063 | 0 | // We need to process 64 Q4 values (32 bytes) with 64 Q8 values (64 bytes) |
1064 | 0 | let q4_256 = _mm256_loadu_si256(qs_ptr.add(q_offset).cast::<__m256i>()); |
1065 | 0 |
|
1066 | 0 | // Zero-extend to 512-bit (q4 data in lower 256 bits) - reserved for full-width optimization |
1067 | 0 | let _q4_512 = _mm512_castsi256_si512(q4_256); |
1068 | 0 |
|
1069 | 0 | // Extract low nibbles (first 32 values) and high nibbles (second 32 values) |
1070 | 0 | let q4_lo_256 = _mm256_and_si256(q4_256, _mm256_set1_epi8(0x0F_i8)); |
1071 | 0 | let q4_hi_256 = |
1072 | 0 | _mm256_and_si256(_mm256_srli_epi16(q4_256, 4), _mm256_set1_epi8(0x0F_i8)); |
1073 | 0 |
|
1074 | 0 | // Load Q8 values: first 32 for low nibbles, next 32 for high nibbles |
1075 | 0 | // (matching dequantize_q4_k output order: 32 low, then 32 high) |
1076 | 0 | let q8_lo_256 = _mm256_loadu_si256(q8_ptr.add(j).cast::<__m256i>()); |
1077 | 0 | let q8_hi_256 = _mm256_loadu_si256(q8_ptr.add(j + 32).cast::<__m256i>()); |
1078 | 0 |
|
1079 | 0 | // For VNNI vpdpbusd: accumulator += (unsigned a) × (signed b) |
1080 | 0 | // Q4 values are 0-15 (unsigned), Q8 values are -128..127 (signed) |
1081 | 0 | // We need to cast Q4 to unsigned interpretation |
1082 | 0 |
|
1083 | 0 | // Convert to 512-bit for VNNI |
1084 | 0 | let q4_lo_512 = _mm512_castsi256_si512(q4_lo_256); |
1085 | 0 | let q4_hi_512 = _mm512_castsi256_si512(q4_hi_256); |
1086 | 0 | let q8_lo_512 = _mm512_castsi256_si512(q8_lo_256); |
1087 | 0 | let q8_hi_512 = _mm512_castsi256_si512(q8_hi_256); |
1088 | 0 |
|
1089 | 0 | // VNNI multiply-accumulate: result[i] += q4[4i..4i+4] · q8[4i..4i+4] |
1090 | 0 | // This computes 4 int8×int8 products and adds them to int32 accumulator |
1091 | 0 | let acc_lo = _mm512_dpbusd_epi32(_mm512_setzero_si512(), q4_lo_512, q8_lo_512); |
1092 | 0 | let acc_hi = _mm512_dpbusd_epi32(_mm512_setzero_si512(), q4_hi_512, q8_hi_512); |
1093 | 0 |
|
1094 | 0 | // Horizontal sum the accumulators (each has 16 int32 values) |
1095 | 0 | // Extract lower 256 bits which contain our results |
1096 | 0 | let acc_lo_256 = _mm512_castsi512_si256(acc_lo); |
1097 | 0 | let acc_hi_256 = _mm512_castsi512_si256(acc_hi); |
1098 | 0 |
|
1099 | 0 | // Sum all 8 int32 values in each 256-bit register |
1100 | 0 | let sum_lo = horizontal_sum_epi32_256(acc_lo_256); |
1101 | 0 | let sum_hi = horizontal_sum_epi32_256(acc_hi_256); |
1102 | 0 |
|
1103 | 0 | // Sum Q8 values for min contribution |
1104 | 0 | // For signed i8, we need to convert to i16 first to avoid overflow |
1105 | 0 | let q8_lo_256_i16_lo = _mm256_cvtepi8_epi16(_mm256_castsi256_si128(q8_lo_256)); |
1106 | 0 | let q8_lo_256_i16_hi = _mm256_cvtepi8_epi16(_mm256_extracti128_si256(q8_lo_256, 1)); |
1107 | 0 | let q8_sum_lo = horizontal_sum_epi16_256(q8_lo_256_i16_lo) |
1108 | 0 | + horizontal_sum_epi16_256(q8_lo_256_i16_hi); |
1109 | 0 |
|
1110 | 0 | let q8_hi_256_i16_lo = _mm256_cvtepi8_epi16(_mm256_castsi256_si128(q8_hi_256)); |
1111 | 0 | let q8_hi_256_i16_hi = _mm256_cvtepi8_epi16(_mm256_extracti128_si256(q8_hi_256, 1)); |
1112 | 0 | let q8_sum_hi = horizontal_sum_epi16_256(q8_hi_256_i16_lo) |
1113 | 0 | + horizontal_sum_epi16_256(q8_hi_256_i16_hi); |
1114 | 0 |
|
1115 | 0 | // Apply scales |
1116 | 0 | total_acc += d_q8 * sc1 * (sum_lo as f32) - dmin_q8 * m1 * (q8_sum_lo as f32); |
1117 | 0 | total_acc += d_q8 * sc2 * (sum_hi as f32) - dmin_q8 * m2 * (q8_sum_hi as f32); |
1118 | 0 | } |
1119 | | } |
1120 | | |
1121 | 0 | Ok(total_acc) |
1122 | 0 | } |
1123 | | |
1124 | | /// Optimized AVX-512 VNNI Q4_K × Q8_K dot product - DEFERRED horizontal sum |
1125 | | /// |
1126 | | /// PAR-126 Five-Whys optimization: Instead of horizontal sums per chunk (24+ per super-block), |
1127 | | /// we accumulate all 8 block results in vector registers and do ONE horizontal sum at the end. |
1128 | | /// This reduces horizontal sum operations from 24+ to 1 per super-block (24x reduction). |
1129 | | #[cfg(target_arch = "x86_64")] |
1130 | | #[target_feature(enable = "avx512f", enable = "avx512vnni", enable = "avx512bw")] |
1131 | | #[allow(unsafe_op_in_unsafe_fn)] |
1132 | | #[allow(clippy::similar_names)] |
1133 | | #[allow(clippy::too_many_lines)] |
1134 | 0 | unsafe fn fused_q4k_q8k_dot_avx512vnni_opt( |
1135 | 0 | q4k_data: &[u8], |
1136 | 0 | q8k_scales: &[f32], |
1137 | 0 | q8k_quants: &[i8], |
1138 | 0 | ) -> Result<f32> { |
1139 | | #[allow(clippy::wildcard_imports)] |
1140 | | use std::arch::x86_64::*; |
1141 | | |
1142 | | const SUPER_BLOCK_BYTES: usize = 144; |
1143 | | |
1144 | 0 | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
1145 | 0 | return Err(RealizarError::InvalidShape { |
1146 | 0 | reason: format!( |
1147 | 0 | "Q4_K data length {} is not a multiple of {}", |
1148 | 0 | q4k_data.len(), |
1149 | 0 | SUPER_BLOCK_BYTES |
1150 | 0 | ), |
1151 | 0 | }); |
1152 | 0 | } |
1153 | | |
1154 | 0 | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
1155 | 0 | let expected_values = num_super_blocks * QK_K; |
1156 | | |
1157 | 0 | if q8k_scales.len() < num_super_blocks || q8k_quants.len() < expected_values { |
1158 | 0 | return Err(RealizarError::InvalidShape { |
1159 | 0 | reason: "Q8_K buffer too small".to_string(), |
1160 | 0 | }); |
1161 | 0 | } |
1162 | | |
1163 | 0 | let nibble_mask = _mm256_set1_epi8(0x0F_i8); |
1164 | 0 | let ones_16 = _mm256_set1_epi16(1); |
1165 | | |
1166 | | // Global float accumulator |
1167 | 0 | let mut total_acc = 0.0f32; |
1168 | | |
1169 | 0 | for sb_idx in 0..num_super_blocks { |
1170 | 0 | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
1171 | 0 | let q8_start = sb_idx * QK_K; |
1172 | | |
1173 | | // Prefetch next super-block |
1174 | 0 | if sb_idx + 1 < num_super_blocks { |
1175 | 0 | _mm_prefetch( |
1176 | 0 | q4k_data |
1177 | 0 | .as_ptr() |
1178 | 0 | .add((sb_idx + 1) * SUPER_BLOCK_BYTES) |
1179 | 0 | .cast::<i8>(), |
1180 | 0 | _MM_HINT_T0, |
1181 | 0 | ); |
1182 | 0 | _mm_prefetch( |
1183 | 0 | q8k_quants.as_ptr().add((sb_idx + 1) * QK_K).cast::<i8>(), |
1184 | 0 | _MM_HINT_T0, |
1185 | 0 | ); |
1186 | 0 | } |
1187 | | |
1188 | | // Read Q4_K header |
1189 | 0 | let d = read_f16(&q4k_data[sb_start..sb_start + 2]); |
1190 | 0 | let dmin = read_f16(&q4k_data[sb_start + 2..sb_start + 4]); |
1191 | | |
1192 | 0 | let mut scales_raw = [0u8; 12]; |
1193 | 0 | scales_raw.copy_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
1194 | | |
1195 | 0 | let q8_scale = q8k_scales[sb_idx]; |
1196 | 0 | let d_q8 = d * q8_scale; |
1197 | 0 | let dmin_q8 = dmin * q8_scale; |
1198 | | |
1199 | 0 | let qs_ptr = q4k_data.as_ptr().add(sb_start + 16); |
1200 | 0 | let q8_ptr = q8k_quants.as_ptr().add(q8_start); |
1201 | | |
1202 | | // PAR-126: DEFERRED HORIZONTAL SUM OPTIMIZATION |
1203 | | // Keep 8 block accumulators in __m256i vectors (one per 32-value block) |
1204 | | // block_dots[i] = sum of (Q4[block_i] * Q8[block_i]) for block i |
1205 | | // block_q8sums[i] = sum of Q8[block_i] for dmin correction |
1206 | 0 | let mut block_dots = [0i32; 8]; |
1207 | 0 | let mut block_q8sums = [0i32; 8]; |
1208 | | |
1209 | | // Process 64 values (2 blocks) per iteration, 4 iterations total = 8 blocks |
1210 | 0 | for chunk in 0..4 { |
1211 | 0 | let j = chunk * 64; |
1212 | 0 | let q_offset = j / 2; |
1213 | 0 |
|
1214 | 0 | // Load 32 bytes Q4 (64 nibbles packed) |
1215 | 0 | let q4_bytes = _mm256_loadu_si256(qs_ptr.add(q_offset).cast::<__m256i>()); |
1216 | 0 |
|
1217 | 0 | // Extract nibbles: lo = first 32 values, hi = next 32 values |
1218 | 0 | let q4_lo = _mm256_and_si256(q4_bytes, nibble_mask); |
1219 | 0 | let q4_hi = _mm256_and_si256(_mm256_srli_epi16(q4_bytes, 4), nibble_mask); |
1220 | 0 |
|
1221 | 0 | // Load 64 bytes Q8 |
1222 | 0 | let q8_lo = _mm256_loadu_si256(q8_ptr.add(j).cast::<__m256i>()); |
1223 | 0 | let q8_hi = _mm256_loadu_si256(q8_ptr.add(j + 32).cast::<__m256i>()); |
1224 | 0 |
|
1225 | 0 | // Q4 × Q8 products -> i16 via maddubs -> i32 via madd |
1226 | 0 | // maddubs: adjacent u8*i8 pairs summed to i16 |
1227 | 0 | // madd with ones: sum pairs of i16 to i32 |
1228 | 0 | let prod_lo_i16 = _mm256_maddubs_epi16(q4_lo, q8_lo); |
1229 | 0 | let prod_hi_i16 = _mm256_maddubs_epi16(q4_hi, q8_hi); |
1230 | 0 | let prod_lo_i32 = _mm256_madd_epi16(prod_lo_i16, ones_16); |
1231 | 0 | let prod_hi_i32 = _mm256_madd_epi16(prod_hi_i16, ones_16); |
1232 | 0 |
|
1233 | 0 | // prod_lo_i32 now has 8 i32 values (4 per 128-bit lane) |
1234 | 0 | // We need to reduce to 1 value per 32-element block |
1235 | 0 | // Lane 0-3 are from elements 0-15, lane 4-7 are from elements 16-31 |
1236 | 0 | // So we sum all 8 to get one block sum |
1237 | 0 |
|
1238 | 0 | // Sum all 8 i32 in prod_lo_i32 using only one horizontal sum |
1239 | 0 | // First add the two 128-bit halves |
1240 | 0 | let prod_lo_128 = _mm_add_epi32( |
1241 | 0 | _mm256_castsi256_si128(prod_lo_i32), |
1242 | 0 | _mm256_extracti128_si256(prod_lo_i32, 1), |
1243 | 0 | ); |
1244 | 0 | let prod_hi_128 = _mm_add_epi32( |
1245 | 0 | _mm256_castsi256_si128(prod_hi_i32), |
1246 | 0 | _mm256_extracti128_si256(prod_hi_i32, 1), |
1247 | 0 | ); |
1248 | 0 |
|
1249 | 0 | // Now we have 4 i32 each - sum them with hadd |
1250 | 0 | let prod_lo_64 = _mm_hadd_epi32(prod_lo_128, prod_hi_128); |
1251 | 0 | let prod_32 = _mm_hadd_epi32(prod_lo_64, prod_lo_64); |
1252 | 0 |
|
1253 | 0 | // Extract block sums - lane 0 is block_lo, lane 1 is block_hi |
1254 | 0 | let block_idx = chunk * 2; |
1255 | 0 | block_dots[block_idx] = _mm_extract_epi32(prod_32, 0); |
1256 | 0 | block_dots[block_idx + 1] = _mm_extract_epi32(prod_32, 1); |
1257 | 0 |
|
1258 | 0 | // Q8 sums for dmin correction - convert i8 to i16 first to avoid overflow |
1259 | 0 | // We need sum of all 32 i8 values for each block |
1260 | 0 | let q8_lo_i16_a = _mm256_cvtepi8_epi16(_mm256_castsi256_si128(q8_lo)); |
1261 | 0 | let q8_lo_i16_b = _mm256_cvtepi8_epi16(_mm256_extracti128_si256(q8_lo, 1)); |
1262 | 0 | let q8_hi_i16_a = _mm256_cvtepi8_epi16(_mm256_castsi256_si128(q8_hi)); |
1263 | 0 | let q8_hi_i16_b = _mm256_cvtepi8_epi16(_mm256_extracti128_si256(q8_hi, 1)); |
1264 | 0 |
|
1265 | 0 | // Sum the i16 values to i32 using madd with ones |
1266 | 0 | let q8_lo_i32_a = _mm256_madd_epi16(q8_lo_i16_a, _mm256_set1_epi16(1)); |
1267 | 0 | let q8_lo_i32_b = _mm256_madd_epi16(q8_lo_i16_b, _mm256_set1_epi16(1)); |
1268 | 0 | let q8_hi_i32_a = _mm256_madd_epi16(q8_hi_i16_a, _mm256_set1_epi16(1)); |
1269 | 0 | let q8_hi_i32_b = _mm256_madd_epi16(q8_hi_i16_b, _mm256_set1_epi16(1)); |
1270 | 0 |
|
1271 | 0 | // Combine halves for each block |
1272 | 0 | let q8_lo_sum = _mm256_add_epi32(q8_lo_i32_a, q8_lo_i32_b); |
1273 | 0 | let q8_hi_sum = _mm256_add_epi32(q8_hi_i32_a, q8_hi_i32_b); |
1274 | 0 |
|
1275 | 0 | // Horizontal sum each block's q8 sum |
1276 | 0 | let q8_lo_128 = _mm_add_epi32( |
1277 | 0 | _mm256_castsi256_si128(q8_lo_sum), |
1278 | 0 | _mm256_extracti128_si256(q8_lo_sum, 1), |
1279 | 0 | ); |
1280 | 0 | let q8_hi_128 = _mm_add_epi32( |
1281 | 0 | _mm256_castsi256_si128(q8_hi_sum), |
1282 | 0 | _mm256_extracti128_si256(q8_hi_sum, 1), |
1283 | 0 | ); |
1284 | 0 | let q8_64 = _mm_hadd_epi32(q8_lo_128, q8_hi_128); |
1285 | 0 | let q8_32 = _mm_hadd_epi32(q8_64, q8_64); |
1286 | 0 |
|
1287 | 0 | block_q8sums[block_idx] = _mm_extract_epi32(q8_32, 0); |
1288 | 0 | block_q8sums[block_idx + 1] = _mm_extract_epi32(q8_32, 1); |
1289 | 0 | } |
1290 | | |
1291 | | // Extract all 8 scales and mins |
1292 | 0 | let mut scales = [0.0f32; 8]; |
1293 | 0 | let mut mins = [0.0f32; 8]; |
1294 | 0 | for i in 0..8 { |
1295 | 0 | let (sc, m) = extract_scale_min(&scales_raw, i); |
1296 | 0 | scales[i] = sc; |
1297 | 0 | mins[i] = m; |
1298 | 0 | } |
1299 | | |
1300 | | // Load scales and mins into vectors for SIMD multiply |
1301 | 0 | let scales_vec = _mm256_loadu_ps(scales.as_ptr()); |
1302 | 0 | let mins_vec = _mm256_loadu_ps(mins.as_ptr()); |
1303 | | |
1304 | | // Convert block_dots and block_q8sums to f32 |
1305 | 0 | let dots_i32 = _mm256_loadu_si256(block_dots.as_ptr().cast::<__m256i>()); |
1306 | 0 | let q8sums_i32 = _mm256_loadu_si256(block_q8sums.as_ptr().cast::<__m256i>()); |
1307 | 0 | let dots_f32 = _mm256_cvtepi32_ps(dots_i32); |
1308 | 0 | let q8sums_f32 = _mm256_cvtepi32_ps(q8sums_i32); |
1309 | | |
1310 | | // Compute: d_q8 * scales * dots - dmin_q8 * mins * q8sums |
1311 | 0 | let d_q8_vec = _mm256_set1_ps(d_q8); |
1312 | 0 | let dmin_q8_vec = _mm256_set1_ps(dmin_q8); |
1313 | | |
1314 | 0 | let term1 = _mm256_mul_ps(d_q8_vec, _mm256_mul_ps(scales_vec, dots_f32)); |
1315 | 0 | let term2 = _mm256_mul_ps(dmin_q8_vec, _mm256_mul_ps(mins_vec, q8sums_f32)); |
1316 | 0 | let result = _mm256_sub_ps(term1, term2); |
1317 | | |
1318 | | // ONE horizontal sum for all 8 blocks |
1319 | 0 | let sum128 = _mm_add_ps( |
1320 | 0 | _mm256_castps256_ps128(result), |
1321 | 0 | _mm256_extractf128_ps(result, 1), |
1322 | | ); |
1323 | 0 | let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); |
1324 | 0 | let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1)); |
1325 | 0 | total_acc += _mm_cvtss_f32(sum32); |
1326 | | } |
1327 | | |
1328 | 0 | Ok(total_acc) |
1329 | 0 | } |
1330 | | |
1331 | | /// Fast horizontal sum of 4 i32 in __m128i |
1332 | | #[cfg(target_arch = "x86_64")] |
1333 | | #[target_feature(enable = "avx2")] |
1334 | | #[inline] |
1335 | 0 | unsafe fn hsum_epi32_128(v: std::arch::x86_64::__m128i) -> i32 { |
1336 | | use std::arch::x86_64::{_mm_cvtsi128_si32, _mm_hadd_epi32}; |
1337 | 0 | let sum64 = _mm_hadd_epi32(v, v); |
1338 | 0 | let sum32 = _mm_hadd_epi32(sum64, sum64); |
1339 | 0 | _mm_cvtsi128_si32(sum32) |
1340 | 0 | } |
1341 | | |
1342 | | /// Fast horizontal sum of 8 i32 in __m256i |
1343 | | #[cfg(target_arch = "x86_64")] |
1344 | | #[target_feature(enable = "avx2")] |
1345 | | #[inline] |
1346 | 0 | unsafe fn hsum_epi32_256(v: std::arch::x86_64::__m256i) -> i32 { |
1347 | | use std::arch::x86_64::{_mm256_castsi256_si128, _mm256_extracti128_si256, _mm_add_epi32}; |
1348 | | // SAFETY: Unsafe operation with validated invariants |
1349 | | unsafe { |
1350 | 0 | let lo = _mm256_castsi256_si128(v); |
1351 | 0 | let hi = _mm256_extracti128_si256(v, 1); |
1352 | 0 | hsum_epi32_128(_mm_add_epi32(lo, hi)) |
1353 | | } |
1354 | 0 | } |
1355 | | |
1356 | | /// Helper: horizontal sum of 8 int32 values in a 256-bit register |
1357 | | #[cfg(target_arch = "x86_64")] |
1358 | | #[target_feature(enable = "avx2")] |
1359 | | #[inline] |
1360 | 0 | unsafe fn horizontal_sum_epi32_256(v: std::arch::x86_64::__m256i) -> i32 { |
1361 | | use std::arch::x86_64::{ |
1362 | | _mm256_castsi256_si128, _mm256_extracti128_si256, _mm_add_epi32, _mm_cvtsi128_si32, |
1363 | | _mm_hadd_epi32, |
1364 | | }; |
1365 | | |
1366 | | // Add high 128 bits to low 128 bits |
1367 | 0 | let hi = _mm256_extracti128_si256(v, 1); |
1368 | 0 | let lo = _mm256_castsi256_si128(v); |
1369 | 0 | let sum128 = _mm_add_epi32(lo, hi); |
1370 | | |
1371 | | // Horizontal add within 128 bits |
1372 | 0 | let sum64 = _mm_hadd_epi32(sum128, sum128); |
1373 | 0 | let sum32 = _mm_hadd_epi32(sum64, sum64); |
1374 | | |
1375 | 0 | _mm_cvtsi128_si32(sum32) |
1376 | 0 | } |
1377 | | |
1378 | | /// Helper: horizontal sum of 16 int16 values in a 256-bit register |
1379 | | #[cfg(target_arch = "x86_64")] |
1380 | | #[target_feature(enable = "avx2")] |
1381 | | #[inline] |
1382 | | #[allow(unsafe_op_in_unsafe_fn)] |
1383 | 0 | unsafe fn horizontal_sum_epi16_256(v: std::arch::x86_64::__m256i) -> i32 { |
1384 | | use std::arch::x86_64::{_mm256_madd_epi16, _mm256_set1_epi16}; |
1385 | | |
1386 | | // Use madd to sum pairs of i16 to i32 |
1387 | 0 | let ones = _mm256_set1_epi16(1); |
1388 | 0 | let sum_i32 = _mm256_madd_epi16(v, ones); |
1389 | | |
1390 | | // Now sum the 8 i32 values |
1391 | 0 | horizontal_sum_epi32_256(sum_i32) |
1392 | 0 | } |
1393 | | |
1394 | | /// AVX2-optimized Q4_K × Q8_K dot product |
1395 | | /// |
1396 | | /// # Safety |
1397 | | /// Requires AVX2 CPU feature. |
1398 | | #[cfg(target_arch = "x86_64")] |
1399 | | #[target_feature(enable = "avx2")] |
1400 | | #[allow(unsafe_op_in_unsafe_fn)] |
1401 | 0 | unsafe fn fused_q4k_q8k_dot_avx2( |
1402 | 0 | q4k_data: &[u8], |
1403 | 0 | q8k_scales: &[f32], |
1404 | 0 | q8k_quants: &[i8], |
1405 | 0 | ) -> Result<f32> { |
1406 | | #[allow(clippy::wildcard_imports)] |
1407 | | use std::arch::x86_64::*; |
1408 | | |
1409 | | const SUPER_BLOCK_BYTES: usize = 144; |
1410 | | |
1411 | 0 | if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
1412 | 0 | return Err(RealizarError::InvalidShape { |
1413 | 0 | reason: format!( |
1414 | 0 | "Q4_K data length {} is not a multiple of {}", |
1415 | 0 | q4k_data.len(), |
1416 | 0 | SUPER_BLOCK_BYTES |
1417 | 0 | ), |
1418 | 0 | }); |
1419 | 0 | } |
1420 | | |
1421 | 0 | let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES; |
1422 | 0 | let expected_values = num_super_blocks * QK_K; |
1423 | | |
1424 | 0 | if q8k_scales.len() < num_super_blocks || q8k_quants.len() < expected_values { |
1425 | 0 | return Err(RealizarError::InvalidShape { |
1426 | 0 | reason: "Q8_K buffer too small".to_string(), |
1427 | 0 | }); |
1428 | 0 | } |
1429 | | |
1430 | 0 | let nibble_mask = _mm256_set1_epi8(0x0F_i8); |
1431 | 0 | let ones_16 = _mm256_set1_epi16(1); |
1432 | | |
1433 | 0 | let mut total_acc = 0.0f32; |
1434 | | |
1435 | 0 | for sb_idx in 0..num_super_blocks { |
1436 | 0 | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
1437 | 0 | let q8_start = sb_idx * QK_K; |
1438 | | |
1439 | | // Prefetch next super-block |
1440 | 0 | if sb_idx + 1 < num_super_blocks { |
1441 | 0 | _mm_prefetch( |
1442 | 0 | q4k_data |
1443 | 0 | .as_ptr() |
1444 | 0 | .add((sb_idx + 1) * SUPER_BLOCK_BYTES) |
1445 | 0 | .cast::<i8>(), |
1446 | 0 | _MM_HINT_T0, |
1447 | 0 | ); |
1448 | 0 | _mm_prefetch( |
1449 | 0 | q8k_quants.as_ptr().add((sb_idx + 1) * QK_K).cast::<i8>(), |
1450 | 0 | _MM_HINT_T0, |
1451 | 0 | ); |
1452 | 0 | } |
1453 | | |
1454 | | // Read Q4_K header |
1455 | 0 | let d = read_f16(&q4k_data[sb_start..sb_start + 2]); |
1456 | 0 | let dmin = read_f16(&q4k_data[sb_start + 2..sb_start + 4]); |
1457 | | |
1458 | 0 | let mut scales = [0u8; 12]; |
1459 | 0 | scales.copy_from_slice(&q4k_data[sb_start + 4..sb_start + 16]); |
1460 | | |
1461 | 0 | let q8_scale = q8k_scales[sb_idx]; |
1462 | 0 | let d_q8 = d * q8_scale; |
1463 | 0 | let dmin_q8 = dmin * q8_scale; |
1464 | | |
1465 | 0 | let qs_ptr = q4k_data.as_ptr().add(sb_start + 16); |
1466 | 0 | let q8_ptr = q8k_quants.as_ptr().add(q8_start); |
1467 | | |
1468 | | // (accumulator variables for future fixed-point optimization) |
1469 | 0 | let _acc_sum = _mm256_setzero_si256(); |
1470 | 0 | let _acc_min = _mm256_setzero_si256(); |
1471 | | |
1472 | | // Process 4 iterations of 64 values each (256 total) |
1473 | 0 | for j in (0..QK_K).step_by(64) { |
1474 | 0 | let q_offset = j / 2; // 32 bytes per 64 values |
1475 | 0 |
|
1476 | 0 | // Get scales for two 32-value blocks |
1477 | 0 | let is = j / 32; |
1478 | 0 | let (sc1, m1) = extract_scale_min(&scales, is); |
1479 | 0 | let (sc2, m2) = extract_scale_min(&scales, is + 1); |
1480 | 0 |
|
1481 | 0 | // Fixed-point scales (8.8 format) - used for integer path |
1482 | 0 | let sc1_i16 = (sc1 * 256.0).round() as i16; |
1483 | 0 | let sc2_i16 = (sc2 * 256.0).round() as i16; |
1484 | 0 | let _m1_i16 = (m1 * 256.0).round() as i16; |
1485 | 0 | let _m2_i16 = (m2 * 256.0).round() as i16; |
1486 | 0 |
|
1487 | 0 | // Load 32 bytes of Q4_K (64 nibbles) |
1488 | 0 | let q4_bytes = _mm256_loadu_si256(qs_ptr.add(q_offset).cast::<__m256i>()); |
1489 | 0 |
|
1490 | 0 | // Extract nibbles |
1491 | 0 | let q4_lo = _mm256_and_si256(q4_bytes, nibble_mask); |
1492 | 0 | let q4_hi = _mm256_and_si256(_mm256_srli_epi16(q4_bytes, 4), nibble_mask); |
1493 | 0 |
|
1494 | 0 | // Load 64 bytes of Q8_K (sequential values) |
1495 | 0 | // CORRECT LAYOUT: dequantize_q4_k outputs 32 low nibbles, then 32 high nibbles |
1496 | 0 | // So Q8[j..j+32] corresponds to low nibbles, Q8[j+32..j+64] to high nibbles |
1497 | 0 | let q8_lo = _mm256_loadu_si256(q8_ptr.add(j).cast::<__m256i>()); |
1498 | 0 | let q8_hi = _mm256_loadu_si256(q8_ptr.add(j + 32).cast::<__m256i>()); |
1499 | 0 |
|
1500 | 0 | // Q4_lo × Q8_lo: low nibbles times first 32 Q8 values (unsigned × signed → i16) |
1501 | 0 | let prod_lo = _mm256_maddubs_epi16(q4_lo, q8_lo); |
1502 | 0 | // Q4_hi × Q8_hi: high nibbles times second 32 Q8 values |
1503 | 0 | let prod_hi = _mm256_maddubs_epi16(q4_hi, q8_hi); |
1504 | 0 |
|
1505 | 0 | // Apply block scales and accumulate (for future integer-only path) |
1506 | 0 | let _scale_lo = _mm256_set1_epi16(sc1_i16); |
1507 | 0 | let _scale_hi = _mm256_set1_epi16(sc2_i16); |
1508 | 0 |
|
1509 | 0 | // Split products by block (first 128 bits = block 1, second 128 bits = block 2) |
1510 | 0 | let prod_lo_128 = _mm256_castsi256_si128(prod_lo); |
1511 | 0 | let prod_lo_hi128 = _mm256_extracti128_si256(prod_lo, 1); |
1512 | 0 | let prod_hi_128 = _mm256_castsi256_si128(prod_hi); |
1513 | 0 | let prod_hi_hi128 = _mm256_extracti128_si256(prod_hi, 1); |
1514 | 0 |
|
1515 | 0 | // Horizontal sum to i32 |
1516 | 0 | let sum_lo_1 = _mm_madd_epi16(prod_lo_128, _mm_set1_epi16(1)); |
1517 | 0 | let sum_lo_2 = _mm_madd_epi16(prod_lo_hi128, _mm_set1_epi16(1)); |
1518 | 0 | let sum_hi_1 = _mm_madd_epi16(prod_hi_128, _mm_set1_epi16(1)); |
1519 | 0 | let sum_hi_2 = _mm_madd_epi16(prod_hi_hi128, _mm_set1_epi16(1)); |
1520 | 0 |
|
1521 | 0 | // Add low and high nibble products |
1522 | 0 | let sum_1 = _mm_add_epi32(sum_lo_1, sum_hi_1); |
1523 | 0 | let sum_2 = _mm_add_epi32(sum_lo_2, sum_hi_2); |
1524 | 0 |
|
1525 | 0 | // Apply scales (as f32 to avoid overflow) |
1526 | 0 | let sum_1_f = _mm_cvtepi32_ps(sum_1); |
1527 | 0 | let sum_2_f = _mm_cvtepi32_ps(sum_2); |
1528 | 0 |
|
1529 | 0 | let scaled_1 = _mm_mul_ps(sum_1_f, _mm_set1_ps(sc1)); |
1530 | 0 | let scaled_2 = _mm_mul_ps(sum_2_f, _mm_set1_ps(sc2)); |
1531 | 0 |
|
1532 | 0 | // Sum for min contribution (sum of Q8 values) |
1533 | 0 | let q8_sum_lo = |
1534 | 0 | _mm256_madd_epi16(_mm256_cvtepi8_epi16(_mm256_castsi256_si128(q8_lo)), ones_16); |
1535 | 0 | let q8_sum_hi = _mm256_madd_epi16( |
1536 | 0 | _mm256_cvtepi8_epi16(_mm256_extracti128_si256(q8_lo, 1)), |
1537 | 0 | ones_16, |
1538 | 0 | ); |
1539 | 0 |
|
1540 | 0 | // Horizontal reduce |
1541 | 0 | let hsum_lo = _mm_add_epi32( |
1542 | 0 | _mm256_castsi256_si128(q8_sum_lo), |
1543 | 0 | _mm256_extracti128_si256(q8_sum_lo, 1), |
1544 | 0 | ); |
1545 | 0 | let _hsum_hi = _mm_add_epi32( |
1546 | 0 | _mm256_castsi256_si128(q8_sum_hi), |
1547 | 0 | _mm256_extracti128_si256(q8_sum_hi, 1), |
1548 | 0 | ); |
1549 | 0 |
|
1550 | 0 | // Include both halves in block sum |
1551 | 0 | let q8_block1_sum = _mm_add_epi32(hsum_lo, _mm_shuffle_epi32(hsum_lo, 0b10_11_00_01)); |
1552 | 0 | let q8_block1_sum = _mm_add_epi32( |
1553 | 0 | q8_block1_sum, |
1554 | 0 | _mm_shuffle_epi32(q8_block1_sum, 0b00_00_10_10), |
1555 | 0 | ); |
1556 | 0 | let q8_block1_val = _mm_cvtsi128_si32(q8_block1_sum); |
1557 | 0 |
|
1558 | 0 | // Similar for second block (q8_hi) |
1559 | 0 | let q8_sum_hi2 = |
1560 | 0 | _mm256_madd_epi16(_mm256_cvtepi8_epi16(_mm256_castsi256_si128(q8_hi)), ones_16); |
1561 | 0 | let q8_sum_hi3 = _mm256_madd_epi16( |
1562 | 0 | _mm256_cvtepi8_epi16(_mm256_extracti128_si256(q8_hi, 1)), |
1563 | 0 | ones_16, |
1564 | 0 | ); |
1565 | 0 | let hsum2_lo = _mm_add_epi32( |
1566 | 0 | _mm256_castsi256_si128(q8_sum_hi2), |
1567 | 0 | _mm256_extracti128_si256(q8_sum_hi2, 1), |
1568 | 0 | ); |
1569 | 0 | let hsum2_hi = _mm_add_epi32( |
1570 | 0 | _mm256_castsi256_si128(q8_sum_hi3), |
1571 | 0 | _mm256_extracti128_si256(q8_sum_hi3, 1), |
1572 | 0 | ); |
1573 | 0 | let q8_block2_sum = _mm_add_epi32(hsum2_lo, hsum2_hi); |
1574 | 0 | let q8_block2_sum = _mm_add_epi32( |
1575 | 0 | q8_block2_sum, |
1576 | 0 | _mm_shuffle_epi32(q8_block2_sum, 0b10_11_00_01), |
1577 | 0 | ); |
1578 | 0 | let q8_block2_sum = _mm_add_epi32( |
1579 | 0 | q8_block2_sum, |
1580 | 0 | _mm_shuffle_epi32(q8_block2_sum, 0b00_00_10_10), |
1581 | 0 | ); |
1582 | 0 | let q8_block2_val = _mm_cvtsi128_si32(q8_block2_sum); |
1583 | 0 |
|
1584 | 0 | // Final accumulation with f32 precision |
1585 | 0 | let scaled_sum = _mm_add_ps(scaled_1, scaled_2); |
1586 | 0 | let hsum = _mm_hadd_ps(scaled_sum, scaled_sum); |
1587 | 0 | let hsum = _mm_hadd_ps(hsum, hsum); |
1588 | 0 | let block_prod = _mm_cvtss_f32(hsum); |
1589 | 0 |
|
1590 | 0 | total_acc += d_q8 * block_prod; |
1591 | 0 | total_acc -= dmin_q8 * (m1 * q8_block1_val as f32 + m2 * q8_block2_val as f32); |
1592 | 0 | } |
1593 | | } |
1594 | | |
1595 | 0 | Ok(total_acc) |
1596 | 0 | } |
1597 | | |
1598 | | /// TCB 4-row micro-kernel: Process 4 rows simultaneously sharing Q8K loads |
1599 | | /// |
1600 | | /// This implements the trueno TCB micro-tile pattern (4×1×256): |
1601 | | /// - Load Q8K input ONCE per superblock |
1602 | | /// - Process 4 weight rows using the SAME Q8K loads |
1603 | | /// - Return 4 output values |
1604 | | /// |
1605 | | /// # Performance |
1606 | | /// |
1607 | | /// Sharing Q8K loads across 4 rows reduces memory bandwidth by ~4x for the |
1608 | | /// input vector, which is the key optimization from TCB (Tiling Compute Blocks). |
1609 | | /// |
1610 | | /// # Safety |
1611 | | /// Requires AVX-512F, AVX-512 VNNI, and AVX-512BW CPU features. |
1612 | | #[cfg(target_arch = "x86_64")] |
1613 | | #[target_feature(enable = "avx512f", enable = "avx512vnni", enable = "avx512bw")] |
1614 | | #[allow(unsafe_op_in_unsafe_fn)] |
1615 | | #[allow(clippy::similar_names)] |
1616 | | #[allow(clippy::too_many_lines)] |
1617 | 0 | pub(crate) unsafe fn fused_q4k_q8k_dot_4rows_avx512vnni( |
1618 | 0 | row_ptrs: [*const u8; 4], |
1619 | 0 | bytes_per_row: usize, |
1620 | 0 | q8k_scales: &[f32], |
1621 | 0 | q8k_quants: &[i8], |
1622 | 0 | ) -> [f32; 4] { |
1623 | | #[allow(clippy::wildcard_imports)] |
1624 | | use std::arch::x86_64::*; |
1625 | | |
1626 | | const SUPER_BLOCK_BYTES: usize = 144; |
1627 | 0 | let num_super_blocks = bytes_per_row / SUPER_BLOCK_BYTES; |
1628 | | |
1629 | 0 | let nibble_mask = _mm256_set1_epi8(0x0F_i8); |
1630 | 0 | let ones_16 = _mm256_set1_epi16(1); |
1631 | | |
1632 | | // 4 accumulators for 4 output rows (8 blocks × f32 = 8 values each) |
1633 | 0 | let mut total_acc = [_mm256_setzero_ps(); 4]; |
1634 | | |
1635 | 0 | for sb_idx in 0..num_super_blocks { |
1636 | 0 | let q8_start = sb_idx * QK_K; |
1637 | 0 | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
1638 | | |
1639 | | // Prefetch next Q8K superblock (shared across all 4 rows) |
1640 | 0 | if sb_idx + 1 < num_super_blocks { |
1641 | 0 | _mm_prefetch( |
1642 | 0 | q8k_quants.as_ptr().add((sb_idx + 1) * QK_K).cast::<i8>(), |
1643 | 0 | _MM_HINT_T0, |
1644 | 0 | ); |
1645 | 0 | } |
1646 | | |
1647 | 0 | let q8_scale = q8k_scales[sb_idx]; |
1648 | 0 | let q8_ptr = q8k_quants.as_ptr().add(q8_start); |
1649 | | |
1650 | | // ============================================================ |
1651 | | // CRITICAL: Pre-load Q8K data and compute Q8 sums ONCE per superblock |
1652 | | // These are shared across ALL 4 rows |
1653 | | // ============================================================ |
1654 | | |
1655 | | // Pre-load all Q8K chunks for this superblock (4 chunks × 2 registers = 8 loads) |
1656 | 0 | let q8_chunk0_lo = _mm256_loadu_si256(q8_ptr.cast::<__m256i>()); |
1657 | 0 | let q8_chunk0_hi = _mm256_loadu_si256(q8_ptr.add(32).cast::<__m256i>()); |
1658 | 0 | let q8_chunk1_lo = _mm256_loadu_si256(q8_ptr.add(64).cast::<__m256i>()); |
1659 | 0 | let q8_chunk1_hi = _mm256_loadu_si256(q8_ptr.add(96).cast::<__m256i>()); |
1660 | 0 | let q8_chunk2_lo = _mm256_loadu_si256(q8_ptr.add(128).cast::<__m256i>()); |
1661 | 0 | let q8_chunk2_hi = _mm256_loadu_si256(q8_ptr.add(160).cast::<__m256i>()); |
1662 | 0 | let q8_chunk3_lo = _mm256_loadu_si256(q8_ptr.add(192).cast::<__m256i>()); |
1663 | 0 | let q8_chunk3_hi = _mm256_loadu_si256(q8_ptr.add(224).cast::<__m256i>()); |
1664 | | |
1665 | | // Pre-compute Q8 sums for dmin correction (same for all rows) |
1666 | 0 | let q8_sums = compute_q8_sums_8blocks( |
1667 | 0 | q8_chunk0_lo, |
1668 | 0 | q8_chunk0_hi, |
1669 | 0 | q8_chunk1_lo, |
1670 | 0 | q8_chunk1_hi, |
1671 | 0 | q8_chunk2_lo, |
1672 | 0 | q8_chunk2_hi, |
1673 | 0 | q8_chunk3_lo, |
1674 | 0 | q8_chunk3_hi, |
1675 | 0 | ones_16, |
1676 | | ); |
1677 | | |
1678 | | // Process 4 rows using the pre-loaded Q8K data |
1679 | 0 | for row in 0..4 { |
1680 | 0 | let row_data = row_ptrs[row].add(sb_start); |
1681 | | |
1682 | | // Read Q4_K header for this row |
1683 | 0 | let d = read_f16(std::slice::from_raw_parts(row_data, 2)); |
1684 | 0 | let dmin = read_f16(std::slice::from_raw_parts(row_data.add(2), 2)); |
1685 | | |
1686 | 0 | let mut scales_raw = [0u8; 12]; |
1687 | 0 | std::ptr::copy_nonoverlapping(row_data.add(4), scales_raw.as_mut_ptr(), 12); |
1688 | | |
1689 | 0 | let d_q8 = d * q8_scale; |
1690 | 0 | let dmin_q8 = dmin * q8_scale; |
1691 | | |
1692 | 0 | let qs_ptr = row_data.add(16); |
1693 | | |
1694 | | // Compute Q4×Q8 dot products for all 8 blocks using pre-loaded Q8K |
1695 | 0 | let block_dots = compute_q4_q8_dots_8blocks( |
1696 | 0 | qs_ptr, |
1697 | 0 | q8_chunk0_lo, |
1698 | 0 | q8_chunk0_hi, |
1699 | 0 | q8_chunk1_lo, |
1700 | 0 | q8_chunk1_hi, |
1701 | 0 | q8_chunk2_lo, |
1702 | 0 | q8_chunk2_hi, |
1703 | 0 | q8_chunk3_lo, |
1704 | 0 | q8_chunk3_hi, |
1705 | 0 | nibble_mask, |
1706 | 0 | ones_16, |
1707 | | ); |
1708 | | |
1709 | | // Extract 6-bit scales and mins |
1710 | 0 | let mut scales = [0.0f32; 8]; |
1711 | 0 | let mut mins = [0.0f32; 8]; |
1712 | 0 | for i in 0..8 { |
1713 | 0 | let (sc, m) = extract_scale_min(&scales_raw, i); |
1714 | 0 | scales[i] = sc; |
1715 | 0 | mins[i] = m; |
1716 | 0 | } |
1717 | | |
1718 | | // Final computation: d_q8 * scales * dots - dmin_q8 * mins * q8sums |
1719 | 0 | let scales_vec = _mm256_loadu_ps(scales.as_ptr()); |
1720 | 0 | let mins_vec = _mm256_loadu_ps(mins.as_ptr()); |
1721 | 0 | let d_q8_vec = _mm256_set1_ps(d_q8); |
1722 | 0 | let dmin_q8_vec = _mm256_set1_ps(dmin_q8); |
1723 | | |
1724 | 0 | let dots_f32 = _mm256_cvtepi32_ps(block_dots); |
1725 | 0 | let q8sums_f32 = _mm256_cvtepi32_ps(q8_sums); |
1726 | | |
1727 | 0 | let term1 = _mm256_mul_ps(d_q8_vec, _mm256_mul_ps(scales_vec, dots_f32)); |
1728 | 0 | let term2 = _mm256_mul_ps(dmin_q8_vec, _mm256_mul_ps(mins_vec, q8sums_f32)); |
1729 | 0 | let result = _mm256_sub_ps(term1, term2); |
1730 | | |
1731 | 0 | total_acc[row] = _mm256_add_ps(total_acc[row], result); |
1732 | | } |
1733 | | } |
1734 | | |
1735 | | // Final horizontal sums for each row |
1736 | 0 | let mut outputs = [0.0f32; 4]; |
1737 | 0 | for row in 0..4 { |
1738 | 0 | let sum128 = _mm_add_ps( |
1739 | 0 | _mm256_castps256_ps128(total_acc[row]), |
1740 | 0 | _mm256_extractf128_ps(total_acc[row], 1), |
1741 | 0 | ); |
1742 | 0 | let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); |
1743 | 0 | let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1)); |
1744 | 0 | outputs[row] = _mm_cvtss_f32(sum32); |
1745 | 0 | } |
1746 | | |
1747 | 0 | outputs |
1748 | 0 | } |
1749 | | |
1750 | | /// Helper: Compute Q8 sums for 8 blocks (shared across rows) |
1751 | | #[cfg(target_arch = "x86_64")] |
1752 | | #[target_feature(enable = "avx2")] |
1753 | | #[inline] |
1754 | | #[allow(clippy::too_many_arguments)] |
1755 | 0 | unsafe fn compute_q8_sums_8blocks( |
1756 | 0 | c0_lo: std::arch::x86_64::__m256i, |
1757 | 0 | c0_hi: std::arch::x86_64::__m256i, |
1758 | 0 | c1_lo: std::arch::x86_64::__m256i, |
1759 | 0 | c1_hi: std::arch::x86_64::__m256i, |
1760 | 0 | c2_lo: std::arch::x86_64::__m256i, |
1761 | 0 | c2_hi: std::arch::x86_64::__m256i, |
1762 | 0 | c3_lo: std::arch::x86_64::__m256i, |
1763 | 0 | c3_hi: std::arch::x86_64::__m256i, |
1764 | 0 | ones_16: std::arch::x86_64::__m256i, |
1765 | 0 | ) -> std::arch::x86_64::__m256i { |
1766 | | #[allow(clippy::wildcard_imports)] |
1767 | | use std::arch::x86_64::*; |
1768 | | |
1769 | | // SAFETY: All calls are to unsafe intrinsics, within already-unsafe fn |
1770 | | // Sum Q8 values for each of the 8 blocks (32 values each) |
1771 | | unsafe { |
1772 | 0 | let sum0 = sum_i8_to_i32(c0_lo, ones_16); |
1773 | 0 | let sum1 = sum_i8_to_i32(c0_hi, ones_16); |
1774 | 0 | let sum2 = sum_i8_to_i32(c1_lo, ones_16); |
1775 | 0 | let sum3 = sum_i8_to_i32(c1_hi, ones_16); |
1776 | 0 | let sum4 = sum_i8_to_i32(c2_lo, ones_16); |
1777 | 0 | let sum5 = sum_i8_to_i32(c2_hi, ones_16); |
1778 | 0 | let sum6 = sum_i8_to_i32(c3_lo, ones_16); |
1779 | 0 | let sum7 = sum_i8_to_i32(c3_hi, ones_16); |
1780 | | |
1781 | | // Pack 8 sums into a single __m256i |
1782 | 0 | let mut result = _mm256_setzero_si256(); |
1783 | 0 | result = _mm256_insert_epi32(result, sum0, 0); |
1784 | 0 | result = _mm256_insert_epi32(result, sum1, 1); |
1785 | 0 | result = _mm256_insert_epi32(result, sum2, 2); |
1786 | 0 | result = _mm256_insert_epi32(result, sum3, 3); |
1787 | 0 | result = _mm256_insert_epi32(result, sum4, 4); |
1788 | 0 | result = _mm256_insert_epi32(result, sum5, 5); |
1789 | 0 | result = _mm256_insert_epi32(result, sum6, 6); |
1790 | 0 | result = _mm256_insert_epi32(result, sum7, 7); |
1791 | 0 | result |
1792 | | } |
1793 | 0 | } |
1794 | | |
1795 | | /// Helper: Sum 32 i8 values to i32 |
1796 | | #[cfg(target_arch = "x86_64")] |
1797 | | #[target_feature(enable = "avx2")] |
1798 | | #[inline] |
1799 | 0 | unsafe fn sum_i8_to_i32(v: std::arch::x86_64::__m256i, ones: std::arch::x86_64::__m256i) -> i32 { |
1800 | | #[allow(clippy::wildcard_imports)] |
1801 | | use std::arch::x86_64::*; |
1802 | | |
1803 | 0 | let lo = _mm256_cvtepi8_epi16(_mm256_castsi256_si128(v)); |
1804 | 0 | let hi = _mm256_cvtepi8_epi16(_mm256_extracti128_si256(v, 1)); |
1805 | 0 | let sum_lo = _mm256_madd_epi16(lo, ones); |
1806 | 0 | let sum_hi = _mm256_madd_epi16(hi, ones); |
1807 | 0 | let sum = _mm256_add_epi32(sum_lo, sum_hi); |
1808 | | |
1809 | | // Horizontal sum of 8 i32 -> 1 i32 |
1810 | 0 | let sum128 = _mm_add_epi32( |
1811 | 0 | _mm256_castsi256_si128(sum), |
1812 | 0 | _mm256_extracti128_si256(sum, 1), |
1813 | | ); |
1814 | 0 | let sum64 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, 0b10_11_00_01)); |
1815 | 0 | let sum32 = _mm_add_epi32(sum64, _mm_shuffle_epi32(sum64, 0b00_00_10_10)); |
1816 | 0 | _mm_cvtsi128_si32(sum32) |
1817 | 0 | } |
1818 | | |
1819 | | /// Helper: Compute Q4×Q8 dot products for 8 blocks using pre-loaded Q8K |
1820 | | #[cfg(target_arch = "x86_64")] |
1821 | | #[target_feature(enable = "avx2")] |
1822 | | #[inline] |
1823 | | #[allow(clippy::too_many_arguments)] |
1824 | 0 | unsafe fn compute_q4_q8_dots_8blocks( |
1825 | 0 | qs_ptr: *const u8, |
1826 | 0 | q8_c0_lo: std::arch::x86_64::__m256i, |
1827 | 0 | q8_c0_hi: std::arch::x86_64::__m256i, |
1828 | 0 | q8_c1_lo: std::arch::x86_64::__m256i, |
1829 | 0 | q8_c1_hi: std::arch::x86_64::__m256i, |
1830 | 0 | q8_c2_lo: std::arch::x86_64::__m256i, |
1831 | 0 | q8_c2_hi: std::arch::x86_64::__m256i, |
1832 | 0 | q8_c3_lo: std::arch::x86_64::__m256i, |
1833 | 0 | q8_c3_hi: std::arch::x86_64::__m256i, |
1834 | 0 | nibble_mask: std::arch::x86_64::__m256i, |
1835 | 0 | ones_16: std::arch::x86_64::__m256i, |
1836 | 0 | ) -> std::arch::x86_64::__m256i { |
1837 | | #[allow(clippy::wildcard_imports)] |
1838 | | use std::arch::x86_64::*; |
1839 | | |
1840 | | // SAFETY: All intrinsics and pointer ops are unsafe, within already-unsafe fn |
1841 | | unsafe { |
1842 | | // Load Q4K data (128 bytes total for 256 values) |
1843 | 0 | let q4_c0 = _mm256_loadu_si256(qs_ptr.cast::<__m256i>()); |
1844 | 0 | let q4_c1 = _mm256_loadu_si256(qs_ptr.add(32).cast::<__m256i>()); |
1845 | 0 | let q4_c2 = _mm256_loadu_si256(qs_ptr.add(64).cast::<__m256i>()); |
1846 | 0 | let q4_c3 = _mm256_loadu_si256(qs_ptr.add(96).cast::<__m256i>()); |
1847 | | |
1848 | | // Extract nibbles and compute Q4×Q8 for each chunk |
1849 | 0 | let dot0 = q4_q8_chunk_dot(q4_c0, q8_c0_lo, q8_c0_hi, nibble_mask, ones_16); |
1850 | 0 | let dot1 = q4_q8_chunk_dot(q4_c1, q8_c1_lo, q8_c1_hi, nibble_mask, ones_16); |
1851 | 0 | let dot2 = q4_q8_chunk_dot(q4_c2, q8_c2_lo, q8_c2_hi, nibble_mask, ones_16); |
1852 | 0 | let dot3 = q4_q8_chunk_dot(q4_c3, q8_c3_lo, q8_c3_hi, nibble_mask, ones_16); |
1853 | | |
1854 | | // Pack 8 dot products into result (2 per chunk: lo nibble block, hi nibble block) |
1855 | 0 | let mut result = _mm256_setzero_si256(); |
1856 | 0 | result = _mm256_insert_epi32(result, _mm_cvtsi128_si32(_mm256_castsi256_si128(dot0)), 0); |
1857 | 0 | result = _mm256_insert_epi32( |
1858 | 0 | result, |
1859 | 0 | _mm_extract_epi32(_mm256_castsi256_si128(dot0), 1), |
1860 | 0 | 1, |
1861 | 0 | ); |
1862 | 0 | result = _mm256_insert_epi32(result, _mm_cvtsi128_si32(_mm256_castsi256_si128(dot1)), 2); |
1863 | 0 | result = _mm256_insert_epi32( |
1864 | 0 | result, |
1865 | 0 | _mm_extract_epi32(_mm256_castsi256_si128(dot1), 1), |
1866 | 0 | 3, |
1867 | 0 | ); |
1868 | 0 | result = _mm256_insert_epi32(result, _mm_cvtsi128_si32(_mm256_castsi256_si128(dot2)), 4); |
1869 | 0 | result = _mm256_insert_epi32( |
1870 | 0 | result, |
1871 | 0 | _mm_extract_epi32(_mm256_castsi256_si128(dot2), 1), |
1872 | 0 | 5, |
1873 | 0 | ); |
1874 | 0 | result = _mm256_insert_epi32(result, _mm_cvtsi128_si32(_mm256_castsi256_si128(dot3)), 6); |
1875 | 0 | result = _mm256_insert_epi32( |
1876 | 0 | result, |
1877 | 0 | _mm_extract_epi32(_mm256_castsi256_si128(dot3), 1), |
1878 | 0 | 7, |
1879 | 0 | ); |
1880 | 0 | result |
1881 | | } |
1882 | 0 | } |
1883 | | |
1884 | | /// Helper: Q4×Q8 dot product for one 64-value chunk (32 lo nibbles + 32 hi nibbles) |
1885 | | #[cfg(target_arch = "x86_64")] |
1886 | | #[target_feature(enable = "avx2")] |
1887 | | #[inline] |
1888 | 0 | unsafe fn q4_q8_chunk_dot( |
1889 | 0 | q4_packed: std::arch::x86_64::__m256i, |
1890 | 0 | q8_lo: std::arch::x86_64::__m256i, |
1891 | 0 | q8_hi: std::arch::x86_64::__m256i, |
1892 | 0 | nibble_mask: std::arch::x86_64::__m256i, |
1893 | 0 | ones_16: std::arch::x86_64::__m256i, |
1894 | 0 | ) -> std::arch::x86_64::__m256i { |
1895 | | #[allow(clippy::wildcard_imports)] |
1896 | | use std::arch::x86_64::*; |
1897 | | |
1898 | | // SAFETY: All intrinsics are unsafe, within already-unsafe fn |
1899 | | unsafe { |
1900 | | // Extract nibbles |
1901 | 0 | let q4_lo = _mm256_and_si256(q4_packed, nibble_mask); |
1902 | 0 | let q4_hi = _mm256_and_si256(_mm256_srli_epi16(q4_packed, 4), nibble_mask); |
1903 | | |
1904 | | // Q4 × Q8 products |
1905 | 0 | let prod_lo_i16 = _mm256_maddubs_epi16(q4_lo, q8_lo); |
1906 | 0 | let prod_hi_i16 = _mm256_maddubs_epi16(q4_hi, q8_hi); |
1907 | 0 | let prod_lo_i32 = _mm256_madd_epi16(prod_lo_i16, ones_16); |
1908 | 0 | let prod_hi_i32 = _mm256_madd_epi16(prod_hi_i16, ones_16); |
1909 | | |
1910 | | // Reduce each to single sum |
1911 | 0 | let sum_lo = hsum_epi32(prod_lo_i32); |
1912 | 0 | let sum_hi = hsum_epi32(prod_hi_i32); |
1913 | | |
1914 | | // Return [sum_lo, sum_hi, 0, 0, 0, 0, 0, 0] |
1915 | 0 | let mut result = _mm256_setzero_si256(); |
1916 | 0 | result = _mm256_insert_epi32(result, sum_lo, 0); |
1917 | 0 | result = _mm256_insert_epi32(result, sum_hi, 1); |
1918 | 0 | result |
1919 | | } |
1920 | 0 | } |
1921 | | |
1922 | | /// Helper: Horizontal sum of 8 i32 values to single i32 |
1923 | | #[cfg(target_arch = "x86_64")] |
1924 | | #[target_feature(enable = "avx2")] |
1925 | | #[inline] |
1926 | 0 | unsafe fn hsum_epi32(v: std::arch::x86_64::__m256i) -> i32 { |
1927 | | #[allow(clippy::wildcard_imports)] |
1928 | | use std::arch::x86_64::*; |
1929 | | |
1930 | | // All intrinsics are unsafe and we're in an unsafe fn with target_feature |
1931 | 0 | let sum128 = _mm_add_epi32(_mm256_castsi256_si128(v), _mm256_extracti128_si256(v, 1)); |
1932 | 0 | let sum64 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, 0b10_11_00_01)); |
1933 | 0 | let sum32 = _mm_add_epi32(sum64, _mm_shuffle_epi32(sum64, 0b00_00_10_10)); |
1934 | 0 | _mm_cvtsi128_si32(sum32) |
1935 | 0 | } |
1936 | | |