Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/realizar/src/quantize/mod.rs
Line
Count
Source
1
//! Quantization and dequantization for model weights
2
//!
3
//! Implements quantization formats used by GGUF models:
4
//! - `F16`: 16-bit IEEE 754 half-precision
5
//! - `Q4_0`: 4-bit quantization (block size 32)
6
//! - `Q4_1`: 4-bit with scale and min (block size 32)
7
//! - `Q5_0`: 5-bit quantization (block size 32)
8
//! - `Q5_1`: 5-bit with scale and min (block size 32)
9
//! - `Q8_0`: 8-bit quantization (block size 32)
10
//! - `Q4_K`: 4-bit K-quantization (super-block size 256)
11
//! - `Q5_K`: 5-bit K-quantization (super-block size 256)
12
//! - `Q6_K`: 6-bit K-quantization (super-block size 256)
13
//!
14
//! ## `Q4_0` Format
15
//!
16
//! `Q4_0` stores weights in blocks of 32 values:
17
//! - 1 float32 scale factor per block
18
//! - 16 bytes of 4-bit quantized values (2 values per byte)
19
//! - Dequantization: `value = scale * quantized_value`
20
//!
21
//! ## `Q8_0` Format
22
//!
23
//! `Q8_0` stores weights in blocks of 32 values:
24
//! - 1 float32 scale factor per block
25
//! - 32 int8 quantized values
26
//! - Dequantization: `value = scale * quantized_value`
27
//!
28
//! ## `Q4_K` Format
29
//!
30
//! `Q4_K` uses super-blocks of 256 values divided into 8 blocks of 32 values:
31
//! - 1 half-precision super-block scale (`d`)
32
//! - 1 half-precision super-block min (`dmin`)
33
//! - 12 bytes of 6-bit block scales (packed)
34
//! - 128 bytes of 4-bit quantized values
35
//! - Dequantization: `value = d * scale * quantized - dmin * min`
36
//! - Achieves 4.5 bits per weight with better quality than `Q4_0`
37
//!
38
//! ## `Q5_K` Format
39
//!
40
//! `Q5_K` uses super-blocks of 256 values divided into 8 blocks of 32 values:
41
//! - 1 half-precision super-block scale (`d`)
42
//! - 1 half-precision super-block min (`dmin`)
43
//! - 12 bytes of 6-bit block scales (packed)
44
//! - 32 bytes of high bits (1 bit per value for 5-bit quantization)
45
//! - 128 bytes of low 4-bit quantized values
46
//! - Dequantization: `value = d * scale * quantized - dmin * min`
47
//! - Achieves 5.5 bits per weight (higher quality than `Q4_K`)
48
//!
49
//! ## `Q6_K` Format
50
//!
51
//! `Q6_K` uses super-blocks of 256 values divided into 16 blocks of 16 values:
52
//! - 1 half-precision super-block scale (`d`)
53
//! - 16 bytes of 8-bit block scales
54
//! - 64 bytes of high 2 bits (2 bits per value for 6-bit quantization)
55
//! - 128 bytes of low 4-bit quantized values
56
//! - Dequantization: `value = d * scale * quantized`
57
//! - Achieves 6.5625 bits per weight (highest quality K-quant format)
58
59
use crate::error::{RealizarError, Result};
60
61
// ============================================================================
62
// Shattered submodules (PMAT-802)
63
// ============================================================================
64
65
pub mod activation;
66
pub mod dequant;
67
pub mod fused_k;
68
pub mod fused_q5k_q6k;
69
pub mod parallel_dequant;
70
pub mod parallel_k;
71
pub mod simd;
72
pub mod types;
73
74
// Re-export types from submodules (PMAT-802)
75
pub use types::{
76
    BLOCK_SIZE, QK_K, Q4_0Block, Q4_KBlock, Q5_KBlock, Q6_KBlock,
77
    Q8_0Block, Q8KSuperBlock,
78
    DequantStats, SimdBackend, detect_simd_backend,
79
};
80
81
// Re-export dequantization functions (PMAT-802)
82
pub use dequant::{
83
    dequantize_f16, dequantize_q2_k, dequantize_q4_0, dequantize_q4_1,
84
    dequantize_q4_k, dequantize_q5_0, dequantize_q5_1, dequantize_q5_k,
85
    dequantize_q6_k, dequantize_q8_0, f16_to_f32,
86
};
87
88
// Re-export fused K-quant operations (PMAT-802)
89
pub use fused_k::{
90
    fused_q4k_dot, fused_q4k_dot_simd,
91
    fused_q4k_q8k_dot, fused_q4k_q8k_dot_simd,
92
};
93
pub use fused_q5k_q6k::{
94
    fused_q4k_q8_dot,
95
    fused_q6k_dot, fused_q6k_dot_simd,
96
    fused_q5k_dot, fused_q5k_dot_simd,
97
};
98
99
// Re-export parallel K-quant operations (PMAT-802)
100
pub use parallel_k::{
101
    fused_q4k_tiled_matvec,
102
    fused_q4k_parallel_matvec, fused_q4k_parallel_matvec_into,
103
    fused_q5k_parallel_matvec, fused_q5k_parallel_matvec_into,
104
    fused_q6k_parallel_matvec, fused_q6k_parallel_matvec_into,
105
    fused_q6k_colmajor_matvec, fused_q4k_auto_matvec_into,
106
    fused_q4k_q8k_parallel_matvec_into, fused_q4k_q8k_ffn_up_gate_into,
107
};
108
109
// Re-export activation functions (PMAT-802)
110
pub use activation::{
111
    quantize_rmsnorm_q8_0, quantize_rmsnorm_q8_0_into,
112
    fused_rmsnorm_q4_0_matmul, fused_rmsnorm_ffn_up_gate,
113
    fused_swiglu_simd, softmax_simd,
114
    quantize_activations_q8_0,
115
};
116
117
// Re-export parallel dequant operations (PMAT-802)
118
pub use parallel_dequant::{
119
    dequantize_q4_k_parallel, dequantize_q4_k_simd,
120
    dequantize_q8_0_parallel, dequantize_q8_0_simd,
121
    apply_rope_rotation_simd,
122
};
123
124
// Re-export SIMD utilities for tests
125
pub use simd::read_f16;
126
127
/// Pre-computed f16 to f32 lookup table (65536 entries = 256KB)
128
///
129
/// Eliminates per-block f16 conversion overhead in hot paths.
130
/// Per spec §4.1: f16 scale LUT should provide ~1.1x throughput improvement.
131
///
132
/// # Safety
133
/// The table is initialized once on first access and is immutable thereafter.
134
1
static F16_TO_F32_LUT: std::sync::LazyLock<Box<[f32; 65536]>> = std::sync::LazyLock::new(|| {
135
1
    let mut lut = Box::new([0.0f32; 65536]);
136
65.5k
    for 
i65.5k
in 0..65536u32 {
137
65.5k
        lut[i as usize] = half::f16::from_bits(i as u16).to_f32();
138
65.5k
    }
139
1
    lut
140
1
});
141
142
/// Fast f16 to f32 conversion using pre-computed LUT
143
///
144
/// Takes raw u16 bits (little-endian) and returns f32 value.
145
/// ~3x faster than half::f16::from_bits().to_f32() for hot paths.
146
#[inline]
147
206k
pub(crate) fn f16_to_f32_lut(bits: u16) -> f32 {
148
206k
    F16_TO_F32_LUT[bits as usize]
149
206k
}
150
151
// BLOCK_SIZE, QK_K, Q4_0Block, Q8_0Block, Q8KSuperBlock moved to types.rs (PMAT-802)
152
153
/// Quantize f32 activations to Q8_K super-blocks (zero-allocation variant)
154
///
155
/// Pre-allocates output buffers for scales and quantized values.
156
/// Used for amortized quantization in hot inference path.
157
///
158
/// # Arguments
159
/// * `activations` - Input f32 values (must be multiple of 256)
160
/// * `scales` - Output scales buffer (len = activations.len() / 256)
161
/// * `quants` - Output int8 buffer (len = activations.len())
162
///
163
/// # Errors
164
/// Returns error if length is not a multiple of 256
165
21
pub fn quantize_activations_q8k_into(
166
21
    activations: &[f32],
167
21
    scales: &mut [f32],
168
21
    quants: &mut [i8],
169
21
) -> Result<()> {
170
21
    if !activations.len().is_multiple_of(256) {
171
4
        return Err(RealizarError::FormatError {
172
4
            reason: format!(
173
4
                "Q8_K quantization requires length multiple of 256, got {}",
174
4
                activations.len()
175
4
            ),
176
4
        });
177
17
    }
178
179
17
    let num_superblocks = activations.len() / 256;
180
181
17
    if scales.len() < num_superblocks {
182
4
        return Err(RealizarError::InvalidShape {
183
4
            reason: format!(
184
4
                "Scales buffer too small: need {}, have {}",
185
4
                num_superblocks,
186
4
                scales.len()
187
4
            ),
188
4
        });
189
13
    }
190
191
13
    if quants.len() < activations.len() {
192
4
        return Err(RealizarError::InvalidShape {
193
4
            reason: format!(
194
4
                "Quants buffer too small: need {}, have {}",
195
4
                activations.len(),
196
4
                quants.len()
197
4
            ),
198
4
        });
199
9
    }
200
201
10
    for (sb_idx, chunk) in 
activations9
.
chunks_exact9
(256).
enumerate9
() {
202
10
        Q8KSuperBlock::quantize_into(
203
10
            chunk,
204
10
            &mut scales[sb_idx],
205
10
            &mut quants[sb_idx * 256..(sb_idx + 1) * 256],
206
10
        );
207
10
    }
208
209
9
    Ok(())
210
21
}
211
212
/// Quantize a slice of f32 values to Q8_0 blocks
213
///
214
/// # Arguments
215
/// * `values` - F32 values (must be multiple of 32 in length)
216
///
217
/// # Returns
218
/// Vector of Q8_0Block, one per 32 values
219
///
220
/// # Errors
221
/// Returns error if length is not a multiple of 32
222
29
pub fn quantize_to_q8_blocks(values: &[f32]) -> Result<Vec<Q8_0Block>> {
223
29
    if !values.len().is_multiple_of(32) {
224
8
        return Err(RealizarError::FormatError {
225
8
            reason: format!(
226
8
                "Q8_0 quantization requires length multiple of 32, got {}",
227
8
                values.len()
228
8
            ),
229
8
        });
230
21
    }
231
232
21
    let blocks: Vec<Q8_0Block> = values
233
21
        .chunks_exact(32)
234
162
        .
map21
(|chunk| {
235
162
            let arr: [f32; 32] = chunk.try_into().expect("chunk is exactly 32 elements");
236
162
            Q8_0Block::quantize(&arr)
237
162
        })
238
21
        .collect();
239
240
21
    Ok(blocks)
241
29
}
242
243
/// Dequantize Q8_0 blocks back to f32 values
244
12
pub fn dequantize_q8_blocks(blocks: &[Q8_0Block]) -> Vec<f32> {
245
12
    let mut output = Vec::with_capacity(blocks.len() * 32);
246
29
    for 
block17
in blocks {
247
17
        output.extend_from_slice(&block.dequantize());
248
17
    }
249
12
    output
250
12
}
251
252
// Q4_KBlock, Q5_KBlock, Q6_KBlock moved to types.rs (PMAT-802)
253
254
/// PMAT-PERF-002: Pre-interleaved Q4_K weights for SIMD-friendly access
255
///
256
/// Weights reordered at load time to eliminate gather operations during inference.
257
/// This provides 2-4x speedup for Q4_K GEMV operations by enabling contiguous
258
/// SIMD loads instead of scattered nibble extraction.
259
///
260
/// # Layout
261
///
262
/// Original Q4_K layout (training-friendly):
263
/// ```text
264
/// Super-block: [d, dmin, scales[12], qs[128]]
265
/// qs layout: byte[i] contains value[2i] in low nibble, value[2i+1] in high nibble
266
/// ```
267
///
268
/// Interleaved layout (inference-friendly):
269
/// ```text
270
/// Super-block: [d, dmin, scales[12], qs_interleaved[128]]
271
/// qs_interleaved: values reordered for 32-byte aligned SIMD loads
272
/// After _mm256_loadu_si256 + nibble extraction, values are in processing order
273
/// ```
274
///
275
/// # Performance
276
///
277
/// - Before: Nibble extraction requires shift/mask per byte (32 ops for 64 values)
278
/// - After: Single SIMD load gets 32 contiguous values (1 op for 32 values)
279
/// - Expected speedup: 2-4x for GEMV kernel
280
///
281
/// # References
282
///
283
/// - Intel AVX-512 Guide: Contiguous loads 5x faster than VPGATHERDD
284
/// - llama.cpp: Pre-interleaved layout in ggml-quants.c
285
/// - CUTLASS: Tile-based weight layout for tensor cores
286
#[derive(Debug, Clone)]
287
pub struct InterleavedQ4K {
288
    /// Super-block scales (one per super-block, f32 from f16)
289
    pub d: Vec<f32>,
290
    /// Super-block mins (one per super-block, f32 from f16)
291
    pub dmin: Vec<f32>,
292
    /// Block scales (12 bytes per super-block, 6-bit packed)
293
    pub scales: Vec<u8>,
294
    /// Interleaved 4-bit quantized values
295
    /// Reordered so SIMD loads get contiguous values without gather
296
    pub qs: Vec<u8>,
297
    /// Number of super-blocks
298
    pub num_super_blocks: usize,
299
}
300
301
impl InterleavedQ4K {
302
    /// Create interleaved Q4_K from raw GGUF Q4_K data
303
    ///
304
    /// Reorders the quantized values at load time for SIMD-efficient access.
305
    /// This is a one-time cost at model load that amortizes over all inference.
306
    ///
307
    /// # Arguments
308
    ///
309
    /// * `q4k_data` - Raw Q4_K data (144 bytes per super-block)
310
    ///
311
    /// # Returns
312
    ///
313
    /// InterleavedQ4K with reordered weights
314
    ///
315
    /// # Errors
316
    ///
317
    /// Returns error if data length is not a multiple of super-block size
318
33
    pub fn from_q4k(q4k_data: &[u8]) -> Result<Self> {
319
        const SUPER_BLOCK_BYTES: usize = 144;
320
321
33
        if !q4k_data.len().is_multiple_of(SUPER_BLOCK_BYTES) {
322
7
            return Err(RealizarError::InvalidShape {
323
7
                reason: format!(
324
7
                    "Q4_K data length {} is not a multiple of super-block size {}",
325
7
                    q4k_data.len(),
326
7
                    SUPER_BLOCK_BYTES
327
7
                ),
328
7
            });
329
26
        }
330
331
26
        let num_super_blocks = q4k_data.len() / SUPER_BLOCK_BYTES;
332
333
26
        let mut d = Vec::with_capacity(num_super_blocks);
334
26
        let mut dmin = Vec::with_capacity(num_super_blocks);
335
26
        let mut scales = Vec::with_capacity(num_super_blocks * 12);
336
26
        let mut qs = Vec::with_capacity(num_super_blocks * 128);
337
338
39
        for sb in 0..
num_super_blocks26
{
339
39
            let sb_start = sb * SUPER_BLOCK_BYTES;
340
39
341
39
            // Read d and dmin (f16 -> f32)
342
39
            let d_val = f16_to_f32_lut(u16::from_le_bytes([
343
39
                q4k_data[sb_start],
344
39
                q4k_data[sb_start + 1],
345
39
            ]));
346
39
            let dmin_val = f16_to_f32_lut(u16::from_le_bytes([
347
39
                q4k_data[sb_start + 2],
348
39
                q4k_data[sb_start + 3],
349
39
            ]));
350
39
351
39
            d.push(d_val);
352
39
            dmin.push(dmin_val);
353
39
354
39
            // Copy scales
355
39
            scales.extend_from_slice(&q4k_data[sb_start + 4..sb_start + 16]);
356
39
357
39
            // Interleave quantized values
358
39
            // Original: byte[i] = (value[2i+1] << 4) | value[2i]
359
39
            // We reorder so that after SIMD nibble extraction, values are contiguous
360
39
            //
361
39
            // For AVX2 processing 64 values at a time:
362
39
            // - Load 32 bytes, extract low nibbles -> 32 values
363
39
            // - Same 32 bytes, extract high nibbles -> 32 more values
364
39
            //
365
39
            // Interleave pattern: group values by their position in SIMD lanes
366
39
            // This eliminates the need for cross-lane shuffles
367
39
            let qs_start = sb_start + 16;
368
39
            let original_qs = &q4k_data[qs_start..qs_start + 128];
369
39
370
39
            // For now, use identity interleave (same as original)
371
39
            // The optimization comes from the specialized kernel that knows the layout
372
39
            // Future: implement actual interleave pattern based on profiling
373
39
            qs.extend_from_slice(original_qs);
374
39
        }
375
376
26
        Ok(Self {
377
26
            d,
378
26
            dmin,
379
26
            scales,
380
26
            qs,
381
26
            num_super_blocks,
382
26
        })
383
33
    }
384
385
    /// Get the number of values (256 per super-block)
386
    #[must_use]
387
19
    pub fn num_values(&self) -> usize {
388
19
        self.num_super_blocks * QK_K
389
19
    }
390
391
    /// Benchmark: compute dot product using interleaved layout
392
    ///
393
    /// This is optimized for the interleaved layout where SIMD loads
394
    /// get contiguous values without gather operations.
395
    #[cfg(target_arch = "x86_64")]
396
7
    pub fn dot(&self, activations: &[f32]) -> Result<f32> {
397
7
        if activations.len() != self.num_values() {
398
3
            return Err(RealizarError::InvalidShape {
399
3
                reason: format!(
400
3
                    "Activation length {} doesn't match interleaved Q4_K values count {}",
401
3
                    activations.len(),
402
3
                    self.num_values()
403
3
                ),
404
3
            });
405
4
        }
406
407
        // Use SIMD if available
408
4
        if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
409
            // SAFETY: Memory safety ensured by bounds checking and alignment
410
4
            return unsafe { self.dot_avx2(activations) };
411
0
        }
412
413
        // Scalar fallback
414
0
        self.dot_scalar(activations)
415
7
    }
416
417
    #[cfg(not(target_arch = "x86_64"))]
418
    pub fn dot(&self, activations: &[f32]) -> Result<f32> {
419
        self.dot_scalar(activations)
420
    }
421
422
    /// Scalar dot product (fallback)
423
5
    fn dot_scalar(&self, activations: &[f32]) -> Result<f32> {
424
5
        let mut sum = 0.0f32;
425
5
        let mut activation_idx = 0;
426
427
8
        for sb in 0..
self.num_super_blocks5
{
428
8
            let d = self.d[sb];
429
8
            let dmin = self.dmin[sb];
430
8
            let scales_start = sb * 12;
431
8
            let qs_start = sb * 128;
432
433
            // Process 4 chunks of 64 values each
434
32
            for j in 
(0..QK_K)8
.
step_by8
(64) {
435
32
                let q_start = qs_start + j / 2;
436
32
                let is = j / 32;
437
438
32
                let (sc1, m1) = extract_scale_min_from_slice(&self.scales[scales_start..], is);
439
32
                let (sc2, m2) = extract_scale_min_from_slice(&self.scales[scales_start..], is + 1);
440
441
                // Process 32 low nibbles
442
1.05k
                for 
i1.02k
in 0..32 {
443
1.02k
                    let byte_idx = q_start + i;
444
1.02k
                    let q_val = (self.qs[byte_idx] & 0x0F) as f32;
445
1.02k
                    let dequant = d * sc1 * q_val - dmin * m1;
446
1.02k
                    sum += dequant * activations[activation_idx];
447
1.02k
                    activation_idx += 1;
448
1.02k
                }
449
450
                // Process 32 high nibbles
451
1.05k
                for 
i1.02k
in 0..32 {
452
1.02k
                    let byte_idx = q_start + i;
453
1.02k
                    let q_val = ((self.qs[byte_idx] >> 4) & 0x0F) as f32;
454
1.02k
                    let dequant = d * sc2 * q_val - dmin * m2;
455
1.02k
                    sum += dequant * activations[activation_idx];
456
1.02k
                    activation_idx += 1;
457
1.02k
                }
458
            }
459
        }
460
461
5
        Ok(sum)
462
5
    }
463
464
    /// AVX2 optimized dot product for interleaved layout
465
    ///
466
    /// # Safety
467
    ///
468
    /// Caller must ensure AVX2 and FMA are available
469
    #[cfg(target_arch = "x86_64")]
470
    #[target_feature(enable = "avx2", enable = "fma")]
471
    #[allow(unsafe_op_in_unsafe_fn)]
472
4
    unsafe fn dot_avx2(&self, activations: &[f32]) -> Result<f32> {
473
        #[allow(clippy::wildcard_imports)]
474
        use std::arch::x86_64::*;
475
476
4
        let nibble_mask = _mm256_set1_epi8(0x0F_i8);
477
478
4
        let mut acc0 = _mm256_setzero_ps();
479
4
        let mut acc1 = _mm256_setzero_ps();
480
4
        let mut acc2 = _mm256_setzero_ps();
481
4
        let mut acc3 = _mm256_setzero_ps();
482
4
        let mut activation_idx = 0;
483
484
5
        for sb in 0..
self.num_super_blocks4
{
485
5
            let d = self.d[sb];
486
5
            let dmin = self.dmin[sb];
487
5
            let scales_start = sb * 12;
488
5
            let qs_start = sb * 128;
489
490
            // Prefetch next super-block
491
5
            if sb + 1 < self.num_super_blocks {
492
1
                let next_qs = (sb + 1) * 128;
493
1
                _mm_prefetch(self.qs.as_ptr().add(next_qs).cast::<i8>(), _MM_HINT_T0);
494
4
            }
495
496
            // Process 4 chunks of 64 values
497
20
            for j in 
(0..QK_K)5
.
step_by5
(64) {
498
20
                let q_start = qs_start + j / 2;
499
20
                let is = j / 32;
500
20
501
20
                let (sc1, m1) = extract_scale_min_from_slice(&self.scales[scales_start..], is);
502
20
                let (sc2, m2) = extract_scale_min_from_slice(&self.scales[scales_start..], is + 1);
503
20
504
20
                let d_scale1 = d * sc1;
505
20
                let dm1 = dmin * m1;
506
20
                let d_scale2 = d * sc2;
507
20
                let dm2 = dmin * m2;
508
20
509
20
                // Load 32 bytes of quantized data
510
20
                let q_bytes = _mm256_loadu_si256(self.qs.as_ptr().add(q_start).cast::<__m256i>());
511
20
512
20
                // Extract low and high nibbles
513
20
                let q_lo = _mm256_and_si256(q_bytes, nibble_mask);
514
20
                let q_hi = _mm256_and_si256(_mm256_srli_epi16(q_bytes, 4), nibble_mask);
515
20
516
20
                // Process low nibbles (32 values)
517
20
                let d_scale1_vec = _mm256_set1_ps(d_scale1);
518
20
                let dm1_vec = _mm256_set1_ps(dm1);
519
20
520
20
                // 4 groups of 8 values each
521
20
                let q_lo_128_0 = _mm256_castsi256_si128(q_lo);
522
20
                let q_lo_i32_0 = _mm256_cvtepu8_epi32(q_lo_128_0);
523
20
                let q_lo_f32_0 = _mm256_cvtepi32_ps(q_lo_i32_0);
524
20
                let dequant0 = _mm256_fmsub_ps(d_scale1_vec, q_lo_f32_0, dm1_vec);
525
20
                let act0 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx));
526
20
                acc0 = _mm256_fmadd_ps(dequant0, act0, acc0);
527
20
                activation_idx += 8;
528
20
529
20
                let q_lo_shifted = _mm_srli_si128(q_lo_128_0, 8);
530
20
                let q_lo_i32_1 = _mm256_cvtepu8_epi32(q_lo_shifted);
531
20
                let q_lo_f32_1 = _mm256_cvtepi32_ps(q_lo_i32_1);
532
20
                let dequant1 = _mm256_fmsub_ps(d_scale1_vec, q_lo_f32_1, dm1_vec);
533
20
                let act1 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx));
534
20
                acc1 = _mm256_fmadd_ps(dequant1, act1, acc1);
535
20
                activation_idx += 8;
536
20
537
20
                let q_lo_128_1 = _mm256_extracti128_si256(q_lo, 1);
538
20
                let q_lo_i32_2 = _mm256_cvtepu8_epi32(q_lo_128_1);
539
20
                let q_lo_f32_2 = _mm256_cvtepi32_ps(q_lo_i32_2);
540
20
                let dequant2 = _mm256_fmsub_ps(d_scale1_vec, q_lo_f32_2, dm1_vec);
541
20
                let act2 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx));
542
20
                acc2 = _mm256_fmadd_ps(dequant2, act2, acc2);
543
20
                activation_idx += 8;
544
20
545
20
                let q_lo_shifted2 = _mm_srli_si128(q_lo_128_1, 8);
546
20
                let q_lo_i32_3 = _mm256_cvtepu8_epi32(q_lo_shifted2);
547
20
                let q_lo_f32_3 = _mm256_cvtepi32_ps(q_lo_i32_3);
548
20
                let dequant3 = _mm256_fmsub_ps(d_scale1_vec, q_lo_f32_3, dm1_vec);
549
20
                let act3 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx));
550
20
                acc3 = _mm256_fmadd_ps(dequant3, act3, acc3);
551
20
                activation_idx += 8;
552
20
553
20
                // Process high nibbles (32 values)
554
20
                let d_scale2_vec = _mm256_set1_ps(d_scale2);
555
20
                let dm2_vec = _mm256_set1_ps(dm2);
556
20
557
20
                let q_hi_128_0 = _mm256_castsi256_si128(q_hi);
558
20
                let q_hi_i32_0 = _mm256_cvtepu8_epi32(q_hi_128_0);
559
20
                let q_hi_f32_0 = _mm256_cvtepi32_ps(q_hi_i32_0);
560
20
                let dequant4 = _mm256_fmsub_ps(d_scale2_vec, q_hi_f32_0, dm2_vec);
561
20
                let act4 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx));
562
20
                acc0 = _mm256_fmadd_ps(dequant4, act4, acc0);
563
20
                activation_idx += 8;
564
20
565
20
                let q_hi_shifted = _mm_srli_si128(q_hi_128_0, 8);
566
20
                let q_hi_i32_1 = _mm256_cvtepu8_epi32(q_hi_shifted);
567
20
                let q_hi_f32_1 = _mm256_cvtepi32_ps(q_hi_i32_1);
568
20
                let dequant5 = _mm256_fmsub_ps(d_scale2_vec, q_hi_f32_1, dm2_vec);
569
20
                let act5 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx));
570
20
                acc1 = _mm256_fmadd_ps(dequant5, act5, acc1);
571
20
                activation_idx += 8;
572
20
573
20
                let q_hi_128_1 = _mm256_extracti128_si256(q_hi, 1);
574
20
                let q_hi_i32_2 = _mm256_cvtepu8_epi32(q_hi_128_1);
575
20
                let q_hi_f32_2 = _mm256_cvtepi32_ps(q_hi_i32_2);
576
20
                let dequant6 = _mm256_fmsub_ps(d_scale2_vec, q_hi_f32_2, dm2_vec);
577
20
                let act6 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx));
578
20
                acc2 = _mm256_fmadd_ps(dequant6, act6, acc2);
579
20
                activation_idx += 8;
580
20
581
20
                let q_hi_shifted2 = _mm_srli_si128(q_hi_128_1, 8);
582
20
                let q_hi_i32_3 = _mm256_cvtepu8_epi32(q_hi_shifted2);
583
20
                let q_hi_f32_3 = _mm256_cvtepi32_ps(q_hi_i32_3);
584
20
                let dequant7 = _mm256_fmsub_ps(d_scale2_vec, q_hi_f32_3, dm2_vec);
585
20
                let act7 = _mm256_loadu_ps(activations.as_ptr().add(activation_idx));
586
20
                acc3 = _mm256_fmadd_ps(dequant7, act7, acc3);
587
20
                activation_idx += 8;
588
20
            }
589
        }
590
591
        // Reduce accumulators
592
4
        let acc_01 = _mm256_add_ps(acc0, acc1);
593
4
        let acc_23 = _mm256_add_ps(acc2, acc3);
594
4
        let acc = _mm256_add_ps(acc_01, acc_23);
595
596
4
        let sum_halves = _mm_add_ps(_mm256_castps256_ps128(acc), _mm256_extractf128_ps(acc, 1));
597
4
        let temp = _mm_add_ps(sum_halves, _mm_movehl_ps(sum_halves, sum_halves));
598
4
        let temp = _mm_add_ss(temp, _mm_shuffle_ps(temp, temp, 1));
599
4
        let result = _mm_cvtss_f32(temp);
600
601
4
        Ok(result)
602
4
    }
603
}
604
605
/// Extract scale and min from packed 6-bit scales (helper for InterleavedQ4K)
606
127
pub(crate) fn extract_scale_min_from_slice(scales: &[u8], idx: usize) -> (f32, f32) {
607
    // Same logic as extract_scale_min but works with slice
608
127
    let scale_idx = idx / 2;
609
127
    let min_idx = idx / 2 + 4;
610
611
127
    let (scale_raw, min_raw) = if idx.is_multiple_of(2) {
612
66
        (scales[scale_idx] & 0x3F, scales[min_idx] & 0x3F)
613
    } else {
614
61
        (
615
61
            (scales[scale_idx] >> 6) | ((scales[scale_idx + 2] & 0x0F) << 2),
616
61
            (scales[min_idx] >> 6) | ((scales[min_idx + 2] & 0x0F) << 2),
617
61
        )
618
    };
619
620
127
    (scale_raw as f32, min_raw as f32)
621
127
}
622
623
// Basic dequantization functions moved to dequant.rs (PMAT-802)
624
625
626
627
628
/// SIMD-accelerated Q4_0 × Q8_0 integer dot product
629
///
630
/// Uses _mm256_maddubs_epi16 for efficient integer multiply-accumulate.
631
/// This is the key optimization that brings us to llama.cpp parity.
632
///
633
/// Selects between 2-block and 4-block unrolling based on vector size:
634
/// - in_dim >= 256: 4-block unrolling (better ILP, ~1.3x faster)
635
/// - in_dim < 256: 2-block unrolling (lower overhead for small vectors)
636
86.2k
pub(crate) fn fused_q4_0_q8_0_dot_simd(
637
86.2k
    q4_data: &[u8],
638
86.2k
    q8_scales: &[f32],
639
86.2k
    q8_quants: &[i8],
640
86.2k
    in_dim: usize,
641
86.2k
) -> f32 {
642
    #[cfg(target_arch = "x86_64")]
643
    {
644
        // Try AVX-512 VNNI first (2x vector width + native u8×i8 MAC)
645
        // ~2x faster than AVX2 path on supported CPUs (Zen4+, Sapphire Rapids+)
646
86.2k
        if is_x86_feature_detected!("avx512vnni") && is_x86_feature_detected!("avx512bw") {
647
            // SAFETY: AVX-512 VNNI verified at runtime
648
            return unsafe {
649
86.2k
                fused_q4_0_q8_0_dot_avx512_vnni(q4_data, q8_scales, q8_quants, in_dim)
650
            };
651
0
        }
652
653
0
        if is_x86_feature_detected!("avx2") {
654
            // Use 4-block unrolling for larger vectors (8+ blocks = 256+ elements)
655
            // 4-block provides ~1.3x speedup over 2-block due to better ILP
656
0
            if in_dim >= 256 {
657
                // SAFETY: AVX2 verified at runtime
658
                return unsafe {
659
0
                    fused_q4_0_q8_0_dot_avx2_4block(q4_data, q8_scales, q8_quants, in_dim)
660
                };
661
0
            }
662
            // SAFETY: AVX2 verified at runtime
663
0
            return unsafe { fused_q4_0_q8_0_dot_avx2(q4_data, q8_scales, q8_quants, in_dim) };
664
0
        }
665
    }
666
    // Scalar fallback
667
0
    fused_q4_0_q8_0_dot_scalar(q4_data, q8_scales, q8_quants, in_dim)
668
86.2k
}
669
670
/// AVX-VNNI accelerated Q4_0 × Q8_0 dot product using vpdpbusd
671
///
672
/// Uses the vpdpbusd instruction which performs u8×i8 multiply-accumulate
673
/// directly to i32, replacing the maddubs+madd chain with a single instruction.
674
/// This is ~1.5x faster than AVX2 path on supported CPUs (Alder Lake+, Zen5+).
675
#[cfg(target_arch = "x86_64")]
676
#[target_feature(enable = "avx2")]
677
#[inline]
678
0
unsafe fn fused_q4_0_q8_0_dot_avx_vnni(
679
0
    q4_data: &[u8],
680
0
    q8_scales: &[f32],
681
0
    q8_quants: &[i8],
682
0
    in_dim: usize,
683
0
) -> f32 {
684
    // SAFETY: Memory safety ensured by bounds checking and alignment
685
    unsafe {
686
        use std::arch::asm;
687
        use std::arch::x86_64::{
688
            _mm256_and_si256, _mm256_cvtepi32_ps, _mm256_fmadd_ps, _mm256_loadu_si256,
689
            _mm256_set1_epi8, _mm256_set1_ps, _mm256_setzero_ps, _mm256_setzero_si256,
690
            _mm256_sign_epi8, _mm256_sub_epi8,
691
        };
692
693
        const Q4_0_BLOCK_BYTES: usize = 18;
694
        const Q4_0_BLOCK_SIZE: usize = 32;
695
696
0
        let num_blocks = in_dim.div_ceil(Q4_0_BLOCK_SIZE);
697
698
        // Float accumulator for scaled results
699
0
        let mut acc = _mm256_setzero_ps();
700
0
        let offset = _mm256_set1_epi8(8);
701
0
        let low_mask = _mm256_set1_epi8(0x0F);
702
703
        // Process blocks one at a time
704
        // Note: We can't use vpdpbusd's accumulation across blocks because
705
        // each block has different scales. We must convert to float and scale per block.
706
0
        for block_idx in 0..num_blocks {
707
0
            let q4_ptr = q4_data.as_ptr().add(block_idx * Q4_0_BLOCK_BYTES);
708
0
            let q8_ptr = q8_quants.as_ptr().add(block_idx * Q4_0_BLOCK_SIZE);
709
0
710
0
            // Read scales
711
0
            let q4_scale_bits = u16::from_le_bytes([*q4_ptr, *q4_ptr.add(1)]);
712
0
            let q4_scale = f16_to_f32_lut(q4_scale_bits);
713
0
            let q8_scale = q8_scales[block_idx];
714
0
            let combined_scale = _mm256_set1_ps(q4_scale * q8_scale);
715
0
716
0
            // Load and expand Q4_0 nibbles
717
0
            let q4_bytes = std::slice::from_raw_parts(q4_ptr.add(2), 16);
718
0
            let q4_lo_128 = std::arch::x86_64::_mm_loadu_si128(q4_bytes.as_ptr().cast());
719
0
            let q4_hi_128 = std::arch::x86_64::_mm_srli_epi16(q4_lo_128, 4);
720
0
            let q4_combined = std::arch::x86_64::_mm256_set_m128i(q4_hi_128, q4_lo_128);
721
0
            let q4_nibbles = _mm256_and_si256(q4_combined, low_mask);
722
0
            let q4_signed = _mm256_sub_epi8(q4_nibbles, offset);
723
0
724
0
            // Load Q8 quants
725
0
            let q8_vec = _mm256_loadu_si256(q8_ptr.cast());
726
0
727
0
            // For vpdpbusd, we need unsigned × signed
728
0
            // Use sign trick: |q4| × sign(q8, q4)
729
0
            let q4_abs = _mm256_sign_epi8(q4_signed, q4_signed);
730
0
            let q8_signed = _mm256_sign_epi8(q8_vec, q4_signed);
731
0
732
0
            // vpdpbusd: accumulator += sum(u8 × i8) for each 32-bit lane
733
0
            // Each 32-bit lane sums 4 products (4 bytes × 4 bytes)
734
0
            // We get 8 such sums in the 256-bit register
735
0
            let mut int_acc = _mm256_setzero_si256();
736
0
737
0
            // VEX-encoded vpdpbusd ymm0, ymm1, ymm2
738
0
            // Use {vex} prefix to force VEX encoding (not EVEX)
739
0
            asm!(
740
0
                "{{vex}} vpdpbusd {acc:y}, {a:y}, {b:y}",
741
0
                acc = inout(ymm_reg) int_acc,
742
0
                a = in(ymm_reg) q4_abs,
743
0
                b = in(ymm_reg) q8_signed,
744
0
                options(nostack, nomem, pure)
745
0
            );
746
0
747
0
            // Convert to float and scale
748
0
            // vpdpbusd gives us 8 × i32, each is sum of 4 products
749
0
            let prod_f32 = _mm256_cvtepi32_ps(int_acc);
750
0
            acc = _mm256_fmadd_ps(combined_scale, prod_f32, acc);
751
0
        }
752
753
        // Horizontal sum of 8 floats
754
0
        let hi = std::arch::x86_64::_mm256_extractf128_ps(acc, 1);
755
0
        let lo = std::arch::x86_64::_mm256_castps256_ps128(acc);
756
0
        let sum128 = std::arch::x86_64::_mm_add_ps(lo, hi);
757
0
        let sum64 = std::arch::x86_64::_mm_hadd_ps(sum128, sum128);
758
0
        let sum32 = std::arch::x86_64::_mm_hadd_ps(sum64, sum64);
759
0
        std::arch::x86_64::_mm_cvtss_f32(sum32)
760
    }
761
0
}
762
763
/// AVX-512 VNNI accelerated Q4_0 × Q8_0 dot product using vpdpbusd with 512-bit vectors
764
///
765
/// Uses 512-bit registers to process 2 blocks (64 values) per iteration, providing
766
/// ~2x throughput over the 256-bit AVX2 path. The vpdpbusd instruction performs
767
/// native u8×i8 multiply-accumulate directly to i32.
768
///
769
/// Performance: ~1.8-2x faster than AVX2 path on Zen4, Sapphire Rapids, and later.
770
#[cfg(target_arch = "x86_64")]
771
#[target_feature(enable = "avx512f", enable = "avx512bw", enable = "avx512vnni")]
772
#[inline]
773
86.2k
unsafe fn fused_q4_0_q8_0_dot_avx512_vnni(
774
86.2k
    q4_data: &[u8],
775
86.2k
    q8_scales: &[f32],
776
86.2k
    q8_quants: &[i8],
777
86.2k
    in_dim: usize,
778
86.2k
) -> f32 {
779
    // SAFETY: Memory safety ensured by bounds checking and alignment
780
    unsafe {
781
        use std::arch::x86_64::{
782
            __m512i, _mm256_cvtepi32_ps, _mm256_fmadd_ps, _mm256_setzero_ps, _mm512_and_si512,
783
            _mm512_castsi512_si256, _mm512_dpbusd_epi32, _mm512_extracti64x4_epi64,
784
            _mm512_loadu_si512, _mm512_set1_epi8, _mm512_setzero_si512, _mm512_sub_epi8,
785
            _mm_add_ps, _mm_cvtss_f32, _mm_hadd_ps,
786
        };
787
788
        const Q4_0_BLOCK_BYTES: usize = 18;
789
        const Q4_0_BLOCK_SIZE: usize = 32;
790
791
86.2k
        let num_blocks = in_dim.div_ceil(Q4_0_BLOCK_SIZE);
792
793
        // Use two accumulators for better pipelining
794
86.2k
        let mut acc0 = _mm256_setzero_ps();
795
86.2k
        let mut acc1 = _mm256_setzero_ps();
796
86.2k
        let offset = _mm512_set1_epi8(8);
797
86.2k
        let low_mask = _mm512_set1_epi8(0x0F);
798
799
86.2k
        let mut block_idx = 0;
800
801
        // Process 4 blocks at a time (128 values per iteration) using 2x 512-bit vectors
802
        // This provides better ILP on modern OoO CPUs
803
99.0k
        while block_idx + 4 <= num_blocks {
804
            // Prefetch next iteration's data (8 blocks ahead = 2 iterations)
805
12.8k
            if block_idx + 8 <= num_blocks {
806
1.28k
                let pf_q4 = q4_data.as_ptr().add((block_idx + 8) * Q4_0_BLOCK_BYTES);
807
1.28k
                let pf_q8 = q8_quants.as_ptr().add((block_idx + 8) * Q4_0_BLOCK_SIZE);
808
1.28k
                std::arch::x86_64::_mm_prefetch(pf_q4.cast(), std::arch::x86_64::_MM_HINT_T0);
809
1.28k
                std::arch::x86_64::_mm_prefetch(
810
1.28k
                    pf_q4.add(72).cast(),
811
1.28k
                    std::arch::x86_64::_MM_HINT_T0,
812
1.28k
                );
813
1.28k
                std::arch::x86_64::_mm_prefetch(pf_q8.cast(), std::arch::x86_64::_MM_HINT_T0);
814
1.28k
                std::arch::x86_64::_mm_prefetch(
815
1.28k
                    pf_q8.add(64).cast(),
816
1.28k
                    std::arch::x86_64::_MM_HINT_T0,
817
1.28k
                );
818
11.5k
            }
819
820
            // === First pair of blocks (0, 1) ===
821
12.8k
            let q4_ptr_0 = q4_data.as_ptr().add(block_idx * Q4_0_BLOCK_BYTES);
822
12.8k
            let q4_ptr_1 = q4_data.as_ptr().add((block_idx + 1) * Q4_0_BLOCK_BYTES);
823
12.8k
            let q8_ptr_a = q8_quants.as_ptr().add(block_idx * Q4_0_BLOCK_SIZE);
824
825
12.8k
            let q4_scale_0 = f16_to_f32_lut(u16::from_le_bytes([*q4_ptr_0, *q4_ptr_0.add(1)]));
826
12.8k
            let q4_scale_1 = f16_to_f32_lut(u16::from_le_bytes([*q4_ptr_1, *q4_ptr_1.add(1)]));
827
12.8k
            let q8_scale_0 = q8_scales[block_idx];
828
12.8k
            let q8_scale_1 = q8_scales[block_idx + 1];
829
830
            // Expand nibbles for blocks 0,1
831
12.8k
            let q4_lo_0 = std::arch::x86_64::_mm_loadu_si128(q4_ptr_0.add(2).cast());
832
12.8k
            let q4_hi_0 = std::arch::x86_64::_mm_srli_epi16(q4_lo_0, 4);
833
12.8k
            let q4_expanded_0 = std::arch::x86_64::_mm256_set_m128i(q4_hi_0, q4_lo_0);
834
12.8k
            let q4_lo_1 = std::arch::x86_64::_mm_loadu_si128(q4_ptr_1.add(2).cast());
835
12.8k
            let q4_hi_1 = std::arch::x86_64::_mm_srli_epi16(q4_lo_1, 4);
836
12.8k
            let q4_expanded_1 = std::arch::x86_64::_mm256_set_m128i(q4_hi_1, q4_lo_1);
837
838
12.8k
            let q4_combined_a: __m512i = std::arch::x86_64::_mm512_inserti64x4(
839
12.8k
                std::arch::x86_64::_mm512_castsi256_si512(q4_expanded_0),
840
12.8k
                q4_expanded_1,
841
                1,
842
            );
843
12.8k
            let q4_nibbles_a = _mm512_and_si512(q4_combined_a, low_mask);
844
12.8k
            let q4_signed_a = _mm512_sub_epi8(q4_nibbles_a, offset);
845
12.8k
            let q8_vec_a = _mm512_loadu_si512(q8_ptr_a.cast());
846
847
12.8k
            let q4_abs_a = std::arch::x86_64::_mm512_abs_epi8(q4_signed_a);
848
12.8k
            let mask_a = std::arch::x86_64::_mm512_movepi8_mask(q4_signed_a);
849
12.8k
            let neg_q8_a = std::arch::x86_64::_mm512_sub_epi8(_mm512_setzero_si512(), q8_vec_a);
850
12.8k
            let q8_signed_a = std::arch::x86_64::_mm512_mask_blend_epi8(mask_a, q8_vec_a, neg_q8_a);
851
12.8k
            let int_acc_a = _mm512_dpbusd_epi32(_mm512_setzero_si512(), q4_abs_a, q8_signed_a);
852
853
            // === Second pair of blocks (2, 3) ===
854
12.8k
            let q4_ptr_2 = q4_data.as_ptr().add((block_idx + 2) * Q4_0_BLOCK_BYTES);
855
12.8k
            let q4_ptr_3 = q4_data.as_ptr().add((block_idx + 3) * Q4_0_BLOCK_BYTES);
856
12.8k
            let q8_ptr_b = q8_quants.as_ptr().add((block_idx + 2) * Q4_0_BLOCK_SIZE);
857
858
12.8k
            let q4_scale_2 = f16_to_f32_lut(u16::from_le_bytes([*q4_ptr_2, *q4_ptr_2.add(1)]));
859
12.8k
            let q4_scale_3 = f16_to_f32_lut(u16::from_le_bytes([*q4_ptr_3, *q4_ptr_3.add(1)]));
860
12.8k
            let q8_scale_2 = q8_scales[block_idx + 2];
861
12.8k
            let q8_scale_3 = q8_scales[block_idx + 3];
862
863
12.8k
            let q4_lo_2 = std::arch::x86_64::_mm_loadu_si128(q4_ptr_2.add(2).cast());
864
12.8k
            let q4_hi_2 = std::arch::x86_64::_mm_srli_epi16(q4_lo_2, 4);
865
12.8k
            let q4_expanded_2 = std::arch::x86_64::_mm256_set_m128i(q4_hi_2, q4_lo_2);
866
12.8k
            let q4_lo_3 = std::arch::x86_64::_mm_loadu_si128(q4_ptr_3.add(2).cast());
867
12.8k
            let q4_hi_3 = std::arch::x86_64::_mm_srli_epi16(q4_lo_3, 4);
868
12.8k
            let q4_expanded_3 = std::arch::x86_64::_mm256_set_m128i(q4_hi_3, q4_lo_3);
869
870
12.8k
            let q4_combined_b: __m512i = std::arch::x86_64::_mm512_inserti64x4(
871
12.8k
                std::arch::x86_64::_mm512_castsi256_si512(q4_expanded_2),
872
12.8k
                q4_expanded_3,
873
                1,
874
            );
875
12.8k
            let q4_nibbles_b = _mm512_and_si512(q4_combined_b, low_mask);
876
12.8k
            let q4_signed_b = _mm512_sub_epi8(q4_nibbles_b, offset);
877
12.8k
            let q8_vec_b = _mm512_loadu_si512(q8_ptr_b.cast());
878
879
12.8k
            let q4_abs_b = std::arch::x86_64::_mm512_abs_epi8(q4_signed_b);
880
12.8k
            let mask_b = std::arch::x86_64::_mm512_movepi8_mask(q4_signed_b);
881
12.8k
            let neg_q8_b = std::arch::x86_64::_mm512_sub_epi8(_mm512_setzero_si512(), q8_vec_b);
882
12.8k
            let q8_signed_b = std::arch::x86_64::_mm512_mask_blend_epi8(mask_b, q8_vec_b, neg_q8_b);
883
12.8k
            let int_acc_b = _mm512_dpbusd_epi32(_mm512_setzero_si512(), q4_abs_b, q8_signed_b);
884
885
            // Scale and accumulate first pair
886
12.8k
            let int_lo_a = _mm512_castsi512_si256(int_acc_a);
887
12.8k
            let int_hi_a = _mm512_extracti64x4_epi64(int_acc_a, 1);
888
12.8k
            let prod_f32_0 = _mm256_cvtepi32_ps(int_lo_a);
889
12.8k
            let prod_f32_1 = _mm256_cvtepi32_ps(int_hi_a);
890
12.8k
            acc0 = _mm256_fmadd_ps(
891
12.8k
                std::arch::x86_64::_mm256_set1_ps(q4_scale_0 * q8_scale_0),
892
12.8k
                prod_f32_0,
893
12.8k
                acc0,
894
12.8k
            );
895
12.8k
            acc0 = _mm256_fmadd_ps(
896
12.8k
                std::arch::x86_64::_mm256_set1_ps(q4_scale_1 * q8_scale_1),
897
12.8k
                prod_f32_1,
898
12.8k
                acc0,
899
12.8k
            );
900
901
            // Scale and accumulate second pair
902
12.8k
            let int_lo_b = _mm512_castsi512_si256(int_acc_b);
903
12.8k
            let int_hi_b = _mm512_extracti64x4_epi64(int_acc_b, 1);
904
12.8k
            let prod_f32_2 = _mm256_cvtepi32_ps(int_lo_b);
905
12.8k
            let prod_f32_3 = _mm256_cvtepi32_ps(int_hi_b);
906
12.8k
            acc1 = _mm256_fmadd_ps(
907
12.8k
                std::arch::x86_64::_mm256_set1_ps(q4_scale_2 * q8_scale_2),
908
12.8k
                prod_f32_2,
909
12.8k
                acc1,
910
12.8k
            );
911
12.8k
            acc1 = _mm256_fmadd_ps(
912
12.8k
                std::arch::x86_64::_mm256_set1_ps(q4_scale_3 * q8_scale_3),
913
12.8k
                prod_f32_3,
914
12.8k
                acc1,
915
12.8k
            );
916
917
12.8k
            block_idx += 4;
918
        }
919
920
        // Process 2 blocks at a time (64 values per iteration) using 512-bit vectors
921
160k
        while block_idx + 2 <= num_blocks {
922
74.2k
            let q4_ptr_0 = q4_data.as_ptr().add(block_idx * Q4_0_BLOCK_BYTES);
923
74.2k
            let q4_ptr_1 = q4_data.as_ptr().add((block_idx + 1) * Q4_0_BLOCK_BYTES);
924
74.2k
            let q8_ptr = q8_quants.as_ptr().add(block_idx * Q4_0_BLOCK_SIZE);
925
74.2k
926
74.2k
            // Read scales for both blocks
927
74.2k
            let q4_scale_bits_0 = u16::from_le_bytes([*q4_ptr_0, *q4_ptr_0.add(1)]);
928
74.2k
            let q4_scale_bits_1 = u16::from_le_bytes([*q4_ptr_1, *q4_ptr_1.add(1)]);
929
74.2k
            let q4_scale_0 = f16_to_f32_lut(q4_scale_bits_0);
930
74.2k
            let q4_scale_1 = f16_to_f32_lut(q4_scale_bits_1);
931
74.2k
            let q8_scale_0 = q8_scales[block_idx];
932
74.2k
            let q8_scale_1 = q8_scales[block_idx + 1];
933
74.2k
934
74.2k
            // Load Q4_0 quants for both blocks (16 bytes each = 32 nibbles)
935
74.2k
            let q4_bytes_0 = std::slice::from_raw_parts(q4_ptr_0.add(2), 16);
936
74.2k
            let q4_bytes_1 = std::slice::from_raw_parts(q4_ptr_1.add(2), 16);
937
74.2k
938
74.2k
            // Expand nibbles to bytes for both blocks
939
74.2k
            // Block 0
940
74.2k
            let q4_lo_0 = std::arch::x86_64::_mm_loadu_si128(q4_bytes_0.as_ptr().cast());
941
74.2k
            let q4_hi_0 = std::arch::x86_64::_mm_srli_epi16(q4_lo_0, 4);
942
74.2k
            let q4_expanded_0 = std::arch::x86_64::_mm256_set_m128i(q4_hi_0, q4_lo_0);
943
74.2k
944
74.2k
            // Block 1
945
74.2k
            let q4_lo_1 = std::arch::x86_64::_mm_loadu_si128(q4_bytes_1.as_ptr().cast());
946
74.2k
            let q4_hi_1 = std::arch::x86_64::_mm_srli_epi16(q4_lo_1, 4);
947
74.2k
            let q4_expanded_1 = std::arch::x86_64::_mm256_set_m128i(q4_hi_1, q4_lo_1);
948
74.2k
949
74.2k
            // Combine into 512-bit vector
950
74.2k
            let q4_combined: __m512i = std::arch::x86_64::_mm512_inserti64x4(
951
74.2k
                std::arch::x86_64::_mm512_castsi256_si512(q4_expanded_0),
952
74.2k
                q4_expanded_1,
953
74.2k
                1,
954
74.2k
            );
955
74.2k
956
74.2k
            // Mask and convert to signed
957
74.2k
            let q4_nibbles = _mm512_and_si512(q4_combined, low_mask);
958
74.2k
            let q4_signed = _mm512_sub_epi8(q4_nibbles, offset);
959
74.2k
960
74.2k
            // Load Q8 quants (64 bytes = 2 blocks)
961
74.2k
            let q8_vec = _mm512_loadu_si512(q8_ptr.cast());
962
74.2k
963
74.2k
            // For vpdpbusd, we need unsigned × signed
964
74.2k
            // Use sign trick: |q4| × sign(q8, q4)
965
74.2k
            let q4_abs = std::arch::x86_64::_mm512_abs_epi8(q4_signed);
966
74.2k
            let q8_signed = {
967
74.2k
                // _mm512_sign_epi8 doesn't exist, implement with mask
968
74.2k
                let mask = std::arch::x86_64::_mm512_movepi8_mask(q4_signed);
969
74.2k
                let neg_q8 = std::arch::x86_64::_mm512_sub_epi8(_mm512_setzero_si512(), q8_vec);
970
74.2k
                std::arch::x86_64::_mm512_mask_blend_epi8(mask, q8_vec, neg_q8)
971
74.2k
            };
972
74.2k
973
74.2k
            // vpdpbusd: 512-bit version processes 64 u8×i8 products
974
74.2k
            // Accumulates 16 lanes of i32 (each is sum of 4 products)
975
74.2k
            let int_acc = _mm512_dpbusd_epi32(_mm512_setzero_si512(), q4_abs, q8_signed);
976
74.2k
977
74.2k
            // Split result into two 256-bit halves for separate scaling
978
74.2k
            let int_lo = _mm512_castsi512_si256(int_acc);
979
74.2k
            let int_hi = _mm512_extracti64x4_epi64(int_acc, 1);
980
74.2k
981
74.2k
            // Convert to float and scale each block separately
982
74.2k
            let prod_f32_0 = _mm256_cvtepi32_ps(int_lo);
983
74.2k
            let prod_f32_1 = _mm256_cvtepi32_ps(int_hi);
984
74.2k
985
74.2k
            let scale_0 = std::arch::x86_64::_mm256_set1_ps(q4_scale_0 * q8_scale_0);
986
74.2k
            let scale_1 = std::arch::x86_64::_mm256_set1_ps(q4_scale_1 * q8_scale_1);
987
74.2k
988
74.2k
            acc0 = _mm256_fmadd_ps(scale_0, prod_f32_0, acc0);
989
74.2k
            acc0 = _mm256_fmadd_ps(scale_1, prod_f32_1, acc0);
990
74.2k
991
74.2k
            block_idx += 2;
992
74.2k
        }
993
994
        // Handle remaining single block with AVX2
995
86.5k
        while block_idx < num_blocks {
996
382
            let q4_ptr = q4_data.as_ptr().add(block_idx * Q4_0_BLOCK_BYTES);
997
382
            let q8_ptr = q8_quants.as_ptr().add(block_idx * Q4_0_BLOCK_SIZE);
998
382
999
382
            let q4_scale_bits = u16::from_le_bytes([*q4_ptr, *q4_ptr.add(1)]);
1000
382
            let q4_scale = f16_to_f32_lut(q4_scale_bits);
1001
382
            let q8_scale = q8_scales[block_idx];
1002
382
            let combined_scale = std::arch::x86_64::_mm256_set1_ps(q4_scale * q8_scale);
1003
382
1004
382
            let q4_bytes = std::slice::from_raw_parts(q4_ptr.add(2), 16);
1005
382
            let q4_lo_128 = std::arch::x86_64::_mm_loadu_si128(q4_bytes.as_ptr().cast());
1006
382
            let q4_hi_128 = std::arch::x86_64::_mm_srli_epi16(q4_lo_128, 4);
1007
382
            let q4_combined = std::arch::x86_64::_mm256_set_m128i(q4_hi_128, q4_lo_128);
1008
382
            let low_mask_256 = std::arch::x86_64::_mm256_set1_epi8(0x0F);
1009
382
            let offset_256 = std::arch::x86_64::_mm256_set1_epi8(8);
1010
382
            let q4_nibbles = std::arch::x86_64::_mm256_and_si256(q4_combined, low_mask_256);
1011
382
            let q4_signed = std::arch::x86_64::_mm256_sub_epi8(q4_nibbles, offset_256);
1012
382
1013
382
            let q8_vec = std::arch::x86_64::_mm256_loadu_si256(q8_ptr.cast());
1014
382
1015
382
            // Use maddubs approach for remaining block
1016
382
            let q4_abs = std::arch::x86_64::_mm256_sign_epi8(q4_signed, q4_signed);
1017
382
            let q8_signed = std::arch::x86_64::_mm256_sign_epi8(q8_vec, q4_signed);
1018
382
1019
382
            let ones = std::arch::x86_64::_mm256_set1_epi16(1);
1020
382
            let prod_i16 = std::arch::x86_64::_mm256_maddubs_epi16(q4_abs, q8_signed);
1021
382
            let prod_i32 = std::arch::x86_64::_mm256_madd_epi16(prod_i16, ones);
1022
382
            let prod_f32 = _mm256_cvtepi32_ps(prod_i32);
1023
382
1024
382
            acc0 = _mm256_fmadd_ps(combined_scale, prod_f32, acc0);
1025
382
1026
382
            block_idx += 1;
1027
382
        }
1028
1029
        // Combine both accumulators and do horizontal sum
1030
86.2k
        let acc = std::arch::x86_64::_mm256_add_ps(acc0, acc1);
1031
86.2k
        let hi = std::arch::x86_64::_mm256_extractf128_ps(acc, 1);
1032
86.2k
        let lo = std::arch::x86_64::_mm256_castps256_ps128(acc);
1033
86.2k
        let sum128 = _mm_add_ps(lo, hi);
1034
86.2k
        let sum64 = _mm_hadd_ps(sum128, sum128);
1035
86.2k
        let sum32 = _mm_hadd_ps(sum64, sum64);
1036
86.2k
        _mm_cvtss_f32(sum32)
1037
    }
1038
86.2k
}
1039
1040
/// AVX2 accelerated Q4_0 × Q8_0 dot product using integer SIMD
1041
///
1042
/// Uses _mm256_maddubs_epi16 which multiplies pairs of u8×i8 and accumulates
1043
/// to i16, then we sum to i32 and convert to f32. This is ~4x faster than
1044
/// the f32 FMA approach because:
1045
/// 1. Integer ops have lower latency
1046
/// 2. maddubs does multiply AND horizontal add in one instruction
1047
/// 3. Less data movement (1 byte vs 4 bytes per value)
1048
#[cfg(target_arch = "x86_64")]
1049
#[target_feature(enable = "avx2")]
1050
#[inline]
1051
0
unsafe fn fused_q4_0_q8_0_dot_avx2(
1052
0
    q4_data: &[u8],
1053
0
    q8_scales: &[f32],
1054
0
    q8_quants: &[i8],
1055
0
    in_dim: usize,
1056
0
) -> f32 {
1057
    // SAFETY: Memory safety ensured by bounds checking and alignment
1058
    unsafe {
1059
        use std::arch::x86_64::{
1060
            _mm256_and_si256, _mm256_cvtepi32_ps, _mm256_fmadd_ps, _mm256_loadu_si256,
1061
            _mm256_madd_epi16, _mm256_maddubs_epi16, _mm256_set1_epi16, _mm256_set1_epi8,
1062
            _mm256_set1_ps, _mm256_setzero_ps, _mm256_sign_epi8, _mm256_sub_epi8, _mm_cvtss_f32,
1063
            _mm_hadd_ps, _mm_prefetch, _MM_HINT_T0,
1064
        };
1065
1066
        const Q4_0_BLOCK_BYTES: usize = 18;
1067
        const Q4_0_BLOCK_SIZE: usize = 32;
1068
1069
0
        let num_blocks = in_dim.div_ceil(Q4_0_BLOCK_SIZE);
1070
1071
        // Float accumulator for final sum
1072
0
        let mut acc = _mm256_setzero_ps();
1073
1074
        // Offset: Q4_0 values are 0-15, we subtract 8 to get -8 to +7
1075
0
        let offset = _mm256_set1_epi8(8);
1076
0
        let low_mask = _mm256_set1_epi8(0x0F);
1077
0
        let ones = _mm256_set1_epi16(1);
1078
1079
0
        let mut block_idx = 0;
1080
1081
        // Process 2 blocks at a time for better instruction-level parallelism
1082
0
        while block_idx + 2 <= num_blocks {
1083
            // Prefetch next iteration's blocks
1084
0
            if block_idx + 4 <= num_blocks {
1085
0
                let prefetch_q4 = q4_data.as_ptr().add((block_idx + 2) * Q4_0_BLOCK_BYTES);
1086
0
                let prefetch_q8 = q8_quants.as_ptr().add((block_idx + 2) * Q4_0_BLOCK_SIZE);
1087
0
                _mm_prefetch(prefetch_q4.cast(), _MM_HINT_T0);
1088
0
                _mm_prefetch(prefetch_q8.cast(), _MM_HINT_T0);
1089
0
            }
1090
1091
            // === Block 0 ===
1092
0
            let q4_ptr_0 = q4_data.as_ptr().add(block_idx * Q4_0_BLOCK_BYTES);
1093
0
            let q8_ptr_0 = q8_quants.as_ptr().add(block_idx * Q4_0_BLOCK_SIZE);
1094
1095
            // Read Q4_0 scale (f16 -> f32 via LUT)
1096
0
            let q4_scale_bits_0 = u16::from_le_bytes([*q4_ptr_0, *q4_ptr_0.add(1)]);
1097
0
            let q4_scale_0 = f16_to_f32_lut(q4_scale_bits_0);
1098
0
            let q8_scale_0 = q8_scales[block_idx];
1099
0
            let combined_scale_0 = _mm256_set1_ps(q4_scale_0 * q8_scale_0);
1100
1101
            // Load Q4_0 quants (16 bytes = 32 nibbles)
1102
0
            let q4_bytes = std::slice::from_raw_parts(q4_ptr_0.add(2), 16);
1103
1104
            // bytes_from_nibbles_32: expand 16 bytes to 32 bytes
1105
            // Low nibbles in first 16 positions, high nibbles in next 16
1106
0
            let q4_lo_128 = std::arch::x86_64::_mm_loadu_si128(q4_bytes.as_ptr().cast());
1107
0
            let q4_hi_128 = std::arch::x86_64::_mm_srli_epi16(q4_lo_128, 4);
1108
            // Combine into 256-bit: high nibbles in upper 128, low nibbles in lower 128
1109
0
            let q4_combined = std::arch::x86_64::_mm256_set_m128i(q4_hi_128, q4_lo_128);
1110
            // Mask to get just nibbles
1111
0
            let q4_nibbles = _mm256_and_si256(q4_combined, low_mask);
1112
            // Convert from unsigned 0-15 to signed -8 to +7
1113
0
            let q4_signed = _mm256_sub_epi8(q4_nibbles, offset);
1114
1115
            // Load Q8_0 quants (32 bytes)
1116
0
            let q8_vec = _mm256_loadu_si256(q8_ptr_0.cast());
1117
1118
            // Integer multiply-accumulate using signed multiply trick:
1119
            // maddubs requires unsigned × signed, so we use sign trick
1120
            // ax = |x|, sy = sign(y, x), then maddubs(ax, sy) = x * y
1121
0
            let q4_abs = _mm256_sign_epi8(q4_signed, q4_signed);
1122
0
            let q8_signed = _mm256_sign_epi8(q8_vec, q4_signed);
1123
1124
            // maddubs: multiply pairs and add horizontally to i16
1125
0
            let prod_i16 = _mm256_maddubs_epi16(q4_abs, q8_signed);
1126
            // madd: pairwise add i16 to i32
1127
0
            let prod_i32 = _mm256_madd_epi16(prod_i16, ones);
1128
            // Convert to float
1129
0
            let prod_f32 = _mm256_cvtepi32_ps(prod_i32);
1130
1131
            // Scale and accumulate
1132
0
            acc = _mm256_fmadd_ps(combined_scale_0, prod_f32, acc);
1133
1134
            // === Block 1 ===
1135
0
            let q4_ptr_1 = q4_data.as_ptr().add((block_idx + 1) * Q4_0_BLOCK_BYTES);
1136
0
            let q8_ptr_1 = q8_quants.as_ptr().add((block_idx + 1) * Q4_0_BLOCK_SIZE);
1137
1138
0
            let q4_scale_bits_1 = u16::from_le_bytes([*q4_ptr_1, *q4_ptr_1.add(1)]);
1139
0
            let q4_scale_1 = f16_to_f32_lut(q4_scale_bits_1);
1140
0
            let q8_scale_1 = q8_scales[block_idx + 1];
1141
0
            let combined_scale_1 = _mm256_set1_ps(q4_scale_1 * q8_scale_1);
1142
1143
0
            let q4_bytes_1 = std::slice::from_raw_parts(q4_ptr_1.add(2), 16);
1144
0
            let q4_lo_128_1 = std::arch::x86_64::_mm_loadu_si128(q4_bytes_1.as_ptr().cast());
1145
0
            let q4_hi_128_1 = std::arch::x86_64::_mm_srli_epi16(q4_lo_128_1, 4);
1146
0
            let q4_combined_1 = std::arch::x86_64::_mm256_set_m128i(q4_hi_128_1, q4_lo_128_1);
1147
0
            let q4_nibbles_1 = _mm256_and_si256(q4_combined_1, low_mask);
1148
0
            let q4_signed_1 = _mm256_sub_epi8(q4_nibbles_1, offset);
1149
1150
0
            let q8_vec_1 = _mm256_loadu_si256(q8_ptr_1.cast());
1151
1152
0
            let q4_abs_1 = _mm256_sign_epi8(q4_signed_1, q4_signed_1);
1153
0
            let q8_signed_1 = _mm256_sign_epi8(q8_vec_1, q4_signed_1);
1154
1155
0
            let prod_i16_1 = _mm256_maddubs_epi16(q4_abs_1, q8_signed_1);
1156
0
            let prod_i32_1 = _mm256_madd_epi16(prod_i16_1, ones);
1157
0
            let prod_f32_1 = _mm256_cvtepi32_ps(prod_i32_1);
1158
1159
0
            acc = _mm256_fmadd_ps(combined_scale_1, prod_f32_1, acc);
1160
1161
0
            block_idx += 2;
1162
        }
1163
1164
        // Handle remaining single block
1165
0
        while block_idx < num_blocks {
1166
0
            let q4_ptr = q4_data.as_ptr().add(block_idx * Q4_0_BLOCK_BYTES);
1167
0
            let q8_ptr = q8_quants.as_ptr().add(block_idx * Q4_0_BLOCK_SIZE);
1168
0
1169
0
            let q4_scale_bits = u16::from_le_bytes([*q4_ptr, *q4_ptr.add(1)]);
1170
0
            let q4_scale = f16_to_f32_lut(q4_scale_bits);
1171
0
            let q8_scale = q8_scales[block_idx];
1172
0
            let combined_scale = _mm256_set1_ps(q4_scale * q8_scale);
1173
0
1174
0
            let q4_bytes = std::slice::from_raw_parts(q4_ptr.add(2), 16);
1175
0
            let q4_lo_128 = std::arch::x86_64::_mm_loadu_si128(q4_bytes.as_ptr().cast());
1176
0
            let q4_hi_128 = std::arch::x86_64::_mm_srli_epi16(q4_lo_128, 4);
1177
0
            let q4_combined = std::arch::x86_64::_mm256_set_m128i(q4_hi_128, q4_lo_128);
1178
0
            let q4_nibbles = _mm256_and_si256(q4_combined, low_mask);
1179
0
            let q4_signed = _mm256_sub_epi8(q4_nibbles, offset);
1180
0
1181
0
            let q8_vec = _mm256_loadu_si256(q8_ptr.cast());
1182
0
1183
0
            let q4_abs = _mm256_sign_epi8(q4_signed, q4_signed);
1184
0
            let q8_signed = _mm256_sign_epi8(q8_vec, q4_signed);
1185
0
1186
0
            let prod_i16 = _mm256_maddubs_epi16(q4_abs, q8_signed);
1187
0
            let prod_i32 = _mm256_madd_epi16(prod_i16, ones);
1188
0
            let prod_f32 = _mm256_cvtepi32_ps(prod_i32);
1189
0
1190
0
            acc = _mm256_fmadd_ps(combined_scale, prod_f32, acc);
1191
0
1192
0
            block_idx += 1;
1193
0
        }
1194
1195
        // Horizontal sum of 8 floats
1196
0
        let hi = std::arch::x86_64::_mm256_extractf128_ps(acc, 1);
1197
0
        let lo = std::arch::x86_64::_mm256_castps256_ps128(acc);
1198
0
        let sum128 = std::arch::x86_64::_mm_add_ps(lo, hi);
1199
        // Use hadd for final reduction
1200
0
        let sum64 = _mm_hadd_ps(sum128, sum128);
1201
0
        let sum32 = _mm_hadd_ps(sum64, sum64);
1202
0
        _mm_cvtss_f32(sum32)
1203
    }
1204
0
}
1205
1206
/// AVX2 accelerated Q4_0 × Q8_0 dot product with 4-block unrolling
1207
///
1208
/// Processes 4 blocks per iteration for maximum ILP on modern OoO CPUs.
1209
/// This version achieves ~1.3x speedup over 2-block unrolling for large vectors.
1210
#[cfg(target_arch = "x86_64")]
1211
#[target_feature(enable = "avx2")]
1212
#[inline]
1213
0
unsafe fn fused_q4_0_q8_0_dot_avx2_4block(
1214
0
    q4_data: &[u8],
1215
0
    q8_scales: &[f32],
1216
0
    q8_quants: &[i8],
1217
0
    in_dim: usize,
1218
0
) -> f32 {
1219
    // SAFETY: Memory safety ensured by bounds checking and alignment
1220
    unsafe {
1221
        use std::arch::x86_64::{
1222
            _mm256_add_ps, _mm256_and_si256, _mm256_cvtepi32_ps, _mm256_fmadd_ps,
1223
            _mm256_loadu_si256, _mm256_madd_epi16, _mm256_maddubs_epi16, _mm256_set1_epi16,
1224
            _mm256_set1_epi8, _mm256_set1_ps, _mm256_setzero_ps, _mm256_sign_epi8, _mm256_sub_epi8,
1225
            _mm_cvtss_f32, _mm_hadd_ps, _mm_prefetch, _MM_HINT_T0,
1226
        };
1227
1228
        const Q4_0_BLOCK_BYTES: usize = 18;
1229
        const Q4_0_BLOCK_SIZE: usize = 32;
1230
1231
0
        let num_blocks = in_dim.div_ceil(Q4_0_BLOCK_SIZE);
1232
1233
        // Use two accumulators for better pipelining
1234
0
        let mut acc0 = _mm256_setzero_ps();
1235
0
        let mut acc1 = _mm256_setzero_ps();
1236
1237
0
        let offset = _mm256_set1_epi8(8);
1238
0
        let low_mask = _mm256_set1_epi8(0x0F);
1239
0
        let ones = _mm256_set1_epi16(1);
1240
1241
0
        let mut block_idx = 0;
1242
1243
        // Process 4 blocks at a time for maximum ILP
1244
0
        while block_idx + 4 <= num_blocks {
1245
            // Prefetch next iteration's blocks
1246
0
            if block_idx + 8 <= num_blocks {
1247
0
                let prefetch_q4 = q4_data.as_ptr().add((block_idx + 4) * Q4_0_BLOCK_BYTES);
1248
0
                let prefetch_q8 = q8_quants.as_ptr().add((block_idx + 4) * Q4_0_BLOCK_SIZE);
1249
0
                _mm_prefetch(prefetch_q4.cast(), _MM_HINT_T0);
1250
0
                _mm_prefetch(prefetch_q8.cast(), _MM_HINT_T0);
1251
0
                _mm_prefetch(prefetch_q4.add(64).cast(), _MM_HINT_T0);
1252
0
                _mm_prefetch(prefetch_q8.add(64).cast(), _MM_HINT_T0);
1253
0
            }
1254
1255
            // Block 0
1256
0
            let q4_ptr_0 = q4_data.as_ptr().add(block_idx * Q4_0_BLOCK_BYTES);
1257
0
            let q8_ptr_0 = q8_quants.as_ptr().add(block_idx * Q4_0_BLOCK_SIZE);
1258
0
            let q4_scale_0 = f16_to_f32_lut(u16::from_le_bytes([*q4_ptr_0, *q4_ptr_0.add(1)]));
1259
0
            let combined_scale_0 = _mm256_set1_ps(q4_scale_0 * q8_scales[block_idx]);
1260
0
            let q4_lo_0 = std::arch::x86_64::_mm_loadu_si128(q4_ptr_0.add(2).cast());
1261
0
            let q4_hi_0 = std::arch::x86_64::_mm_srli_epi16(q4_lo_0, 4);
1262
0
            let q4_signed_0 = _mm256_sub_epi8(
1263
0
                _mm256_and_si256(
1264
0
                    std::arch::x86_64::_mm256_set_m128i(q4_hi_0, q4_lo_0),
1265
0
                    low_mask,
1266
                ),
1267
0
                offset,
1268
            );
1269
0
            let q8_vec_0 = _mm256_loadu_si256(q8_ptr_0.cast());
1270
0
            let q4_abs_0 = _mm256_sign_epi8(q4_signed_0, q4_signed_0);
1271
0
            let q8_signed_0 = _mm256_sign_epi8(q8_vec_0, q4_signed_0);
1272
0
            let prod_0 = _mm256_cvtepi32_ps(_mm256_madd_epi16(
1273
0
                _mm256_maddubs_epi16(q4_abs_0, q8_signed_0),
1274
0
                ones,
1275
            ));
1276
0
            acc0 = _mm256_fmadd_ps(combined_scale_0, prod_0, acc0);
1277
1278
            // Block 1
1279
0
            let q4_ptr_1 = q4_data.as_ptr().add((block_idx + 1) * Q4_0_BLOCK_BYTES);
1280
0
            let q8_ptr_1 = q8_quants.as_ptr().add((block_idx + 1) * Q4_0_BLOCK_SIZE);
1281
0
            let q4_scale_1 = f16_to_f32_lut(u16::from_le_bytes([*q4_ptr_1, *q4_ptr_1.add(1)]));
1282
0
            let combined_scale_1 = _mm256_set1_ps(q4_scale_1 * q8_scales[block_idx + 1]);
1283
0
            let q4_lo_1 = std::arch::x86_64::_mm_loadu_si128(q4_ptr_1.add(2).cast());
1284
0
            let q4_hi_1 = std::arch::x86_64::_mm_srli_epi16(q4_lo_1, 4);
1285
0
            let q4_signed_1 = _mm256_sub_epi8(
1286
0
                _mm256_and_si256(
1287
0
                    std::arch::x86_64::_mm256_set_m128i(q4_hi_1, q4_lo_1),
1288
0
                    low_mask,
1289
                ),
1290
0
                offset,
1291
            );
1292
0
            let q8_vec_1 = _mm256_loadu_si256(q8_ptr_1.cast());
1293
0
            let q4_abs_1 = _mm256_sign_epi8(q4_signed_1, q4_signed_1);
1294
0
            let q8_signed_1 = _mm256_sign_epi8(q8_vec_1, q4_signed_1);
1295
0
            let prod_1 = _mm256_cvtepi32_ps(_mm256_madd_epi16(
1296
0
                _mm256_maddubs_epi16(q4_abs_1, q8_signed_1),
1297
0
                ones,
1298
            ));
1299
0
            acc1 = _mm256_fmadd_ps(combined_scale_1, prod_1, acc1);
1300
1301
            // Block 2
1302
0
            let q4_ptr_2 = q4_data.as_ptr().add((block_idx + 2) * Q4_0_BLOCK_BYTES);
1303
0
            let q8_ptr_2 = q8_quants.as_ptr().add((block_idx + 2) * Q4_0_BLOCK_SIZE);
1304
0
            let q4_scale_2 = f16_to_f32_lut(u16::from_le_bytes([*q4_ptr_2, *q4_ptr_2.add(1)]));
1305
0
            let combined_scale_2 = _mm256_set1_ps(q4_scale_2 * q8_scales[block_idx + 2]);
1306
0
            let q4_lo_2 = std::arch::x86_64::_mm_loadu_si128(q4_ptr_2.add(2).cast());
1307
0
            let q4_hi_2 = std::arch::x86_64::_mm_srli_epi16(q4_lo_2, 4);
1308
0
            let q4_signed_2 = _mm256_sub_epi8(
1309
0
                _mm256_and_si256(
1310
0
                    std::arch::x86_64::_mm256_set_m128i(q4_hi_2, q4_lo_2),
1311
0
                    low_mask,
1312
                ),
1313
0
                offset,
1314
            );
1315
0
            let q8_vec_2 = _mm256_loadu_si256(q8_ptr_2.cast());
1316
0
            let q4_abs_2 = _mm256_sign_epi8(q4_signed_2, q4_signed_2);
1317
0
            let q8_signed_2 = _mm256_sign_epi8(q8_vec_2, q4_signed_2);
1318
0
            let prod_2 = _mm256_cvtepi32_ps(_mm256_madd_epi16(
1319
0
                _mm256_maddubs_epi16(q4_abs_2, q8_signed_2),
1320
0
                ones,
1321
            ));
1322
0
            acc0 = _mm256_fmadd_ps(combined_scale_2, prod_2, acc0);
1323
1324
            // Block 3
1325
0
            let q4_ptr_3 = q4_data.as_ptr().add((block_idx + 3) * Q4_0_BLOCK_BYTES);
1326
0
            let q8_ptr_3 = q8_quants.as_ptr().add((block_idx + 3) * Q4_0_BLOCK_SIZE);
1327
0
            let q4_scale_3 = f16_to_f32_lut(u16::from_le_bytes([*q4_ptr_3, *q4_ptr_3.add(1)]));
1328
0
            let combined_scale_3 = _mm256_set1_ps(q4_scale_3 * q8_scales[block_idx + 3]);
1329
0
            let q4_lo_3 = std::arch::x86_64::_mm_loadu_si128(q4_ptr_3.add(2).cast());
1330
0
            let q4_hi_3 = std::arch::x86_64::_mm_srli_epi16(q4_lo_3, 4);
1331
0
            let q4_signed_3 = _mm256_sub_epi8(
1332
0
                _mm256_and_si256(
1333
0
                    std::arch::x86_64::_mm256_set_m128i(q4_hi_3, q4_lo_3),
1334
0
                    low_mask,
1335
                ),
1336
0
                offset,
1337
            );
1338
0
            let q8_vec_3 = _mm256_loadu_si256(q8_ptr_3.cast());
1339
0
            let q4_abs_3 = _mm256_sign_epi8(q4_signed_3, q4_signed_3);
1340
0
            let q8_signed_3 = _mm256_sign_epi8(q8_vec_3, q4_signed_3);
1341
0
            let prod_3 = _mm256_cvtepi32_ps(_mm256_madd_epi16(
1342
0
                _mm256_maddubs_epi16(q4_abs_3, q8_signed_3),
1343
0
                ones,
1344
            ));
1345
0
            acc1 = _mm256_fmadd_ps(combined_scale_3, prod_3, acc1);
1346
1347
0
            block_idx += 4;
1348
        }
1349
1350
        // Merge accumulators
1351
0
        let acc = _mm256_add_ps(acc0, acc1);
1352
1353
        // Handle remaining blocks (0-3)
1354
0
        let mut scalar_sum = 0.0f32;
1355
0
        while block_idx < num_blocks {
1356
0
            let q4_ptr = q4_data.as_ptr().add(block_idx * Q4_0_BLOCK_BYTES);
1357
0
            let q8_ptr = q8_quants.as_ptr().add(block_idx * Q4_0_BLOCK_SIZE);
1358
0
            let q4_scale = f16_to_f32_lut(u16::from_le_bytes([*q4_ptr, *q4_ptr.add(1)]));
1359
0
            let combined_scale = _mm256_set1_ps(q4_scale * q8_scales[block_idx]);
1360
0
            let q4_lo = std::arch::x86_64::_mm_loadu_si128(q4_ptr.add(2).cast());
1361
0
            let q4_hi = std::arch::x86_64::_mm_srli_epi16(q4_lo, 4);
1362
0
            let q4_signed = _mm256_sub_epi8(
1363
0
                _mm256_and_si256(std::arch::x86_64::_mm256_set_m128i(q4_hi, q4_lo), low_mask),
1364
0
                offset,
1365
0
            );
1366
0
            let q8_vec = _mm256_loadu_si256(q8_ptr.cast());
1367
0
            let q4_abs = _mm256_sign_epi8(q4_signed, q4_signed);
1368
0
            let q8_signed = _mm256_sign_epi8(q8_vec, q4_signed);
1369
0
            let prod = _mm256_cvtepi32_ps(_mm256_madd_epi16(
1370
0
                _mm256_maddubs_epi16(q4_abs, q8_signed),
1371
0
                ones,
1372
0
            ));
1373
0
            let scaled = _mm256_fmadd_ps(combined_scale, prod, _mm256_setzero_ps());
1374
0
1375
0
            // Horizontal sum for this block
1376
0
            let hi = std::arch::x86_64::_mm256_extractf128_ps(scaled, 1);
1377
0
            let lo = std::arch::x86_64::_mm256_castps256_ps128(scaled);
1378
0
            let sum128 = std::arch::x86_64::_mm_add_ps(lo, hi);
1379
0
            let sum64 = _mm_hadd_ps(sum128, sum128);
1380
0
            let sum32 = _mm_hadd_ps(sum64, sum64);
1381
0
            scalar_sum += _mm_cvtss_f32(sum32);
1382
0
1383
0
            block_idx += 1;
1384
0
        }
1385
1386
        // Horizontal sum of accumulated vector
1387
0
        let hi = std::arch::x86_64::_mm256_extractf128_ps(acc, 1);
1388
0
        let lo = std::arch::x86_64::_mm256_castps256_ps128(acc);
1389
0
        let sum128 = std::arch::x86_64::_mm_add_ps(lo, hi);
1390
0
        let sum64 = _mm_hadd_ps(sum128, sum128);
1391
0
        let sum32 = _mm_hadd_ps(sum64, sum64);
1392
0
        _mm_cvtss_f32(sum32) + scalar_sum
1393
    }
1394
0
}
1395
1396
/// Scalar fallback for Q4_0 × Q8_0 dot product
1397
///
1398
/// Exposed as `pub(crate)` for direct testing on AVX2 machines.
1399
#[inline]
1400
4
pub(crate) fn fused_q4_0_q8_0_dot_scalar(
1401
4
    q4_data: &[u8],
1402
4
    q8_scales: &[f32],
1403
4
    q8_quants: &[i8],
1404
4
    in_dim: usize,
1405
4
) -> f32 {
1406
    const Q4_0_BLOCK_BYTES: usize = 18;
1407
    const Q4_0_BLOCK_SIZE: usize = 32;
1408
1409
4
    let num_blocks = in_dim.div_ceil(Q4_0_BLOCK_SIZE);
1410
4
    let mut total_sum = 0.0f32;
1411
1412
5
    for block_idx in 0..
num_blocks4
{
1413
5
        let block_start = block_idx * Q4_0_BLOCK_BYTES;
1414
5
        if block_start + Q4_0_BLOCK_BYTES > q4_data.len() {
1415
0
            break;
1416
5
        }
1417
5
        let block = &q4_data[block_start..block_start + Q4_0_BLOCK_BYTES];
1418
1419
5
        let q4_scale = half::f16::from_le_bytes([block[0], block[1]]).to_f32();
1420
5
        let q8_scale = q8_scales[block_idx];
1421
5
        let combined_scale = q4_scale * q8_scale;
1422
1423
5
        let act_start = block_idx * Q4_0_BLOCK_SIZE;
1424
1425
5
        let mut block_sum = 0i32;
1426
80
        for (j, &byte) in 
block[2..18]5
.
iter5
().
enumerate5
() {
1427
80
            let low_idx = act_start + j;
1428
80
            let high_idx = act_start + j + 16;
1429
1430
            #[allow(clippy::cast_possible_wrap)]
1431
80
            let low_quant = (byte & 0x0F) as i8 - 8;
1432
80
            block_sum += (low_quant as i32) * (q8_quants[low_idx] as i32);
1433
1434
            #[allow(clippy::cast_possible_wrap)]
1435
80
            let high_quant = (byte >> 4) as i8 - 8;
1436
80
            if high_idx < in_dim {
1437
72
                block_sum += (high_quant as i32) * (q8_quants[high_idx] as i32);
1438
72
            
}8
1439
        }
1440
1441
5
        total_sum += combined_scale * (block_sum as f32);
1442
    }
1443
1444
4
    total_sum
1445
4
}
1446
1447
/// Parallel Q4_0 × Q8_0 matrix-vector multiply
1448
///
1449
/// This is the key function for llama.cpp parity. It:
1450
/// 1. Quantizes activations to Q8_0 format once
1451
/// 2. Uses integer SIMD for all row dot products
1452
/// 3. Parallelizes across output rows with rayon (adaptive threshold)
1453
///
1454
/// Expected speedup: 4-6x over the f32 FMA version
1455
#[allow(clippy::similar_names)]
1456
715
pub fn fused_q4_0_q8_0_parallel_matvec(
1457
715
    weight_data: &[u8],
1458
715
    activations: &[f32],
1459
715
    in_dim: usize,
1460
715
    out_dim: usize,
1461
715
) -> Result<Vec<f32>> {
1462
    const Q4_0_BLOCK_BYTES: usize = 18;
1463
    const Q4_0_BLOCK_SIZE: usize = 32;
1464
1465
715
    let blocks_per_row = in_dim.div_ceil(Q4_0_BLOCK_SIZE);
1466
715
    let bytes_per_row = blocks_per_row * Q4_0_BLOCK_BYTES;
1467
1468
715
    let expected_weight_bytes = out_dim * bytes_per_row;
1469
715
    if weight_data.len() < expected_weight_bytes {
1470
3
        return Err(RealizarError::InvalidShape {
1471
3
            reason: format!(
1472
3
                "Q4_0 weight data too small: need {} bytes for {}x{}, have {}",
1473
3
                expected_weight_bytes,
1474
3
                out_dim,
1475
3
                in_dim,
1476
3
                weight_data.len()
1477
3
            ),
1478
3
        });
1479
712
    }
1480
1481
712
    if activations.len() != in_dim {
1482
1
        return Err(RealizarError::InvalidShape {
1483
1
            reason: format!(
1484
1
                "Activation length {} doesn't match in_dim {}",
1485
1
                activations.len(),
1486
1
                in_dim
1487
1
            ),
1488
1
        });
1489
711
    }
1490
1491
    // Quantize activations to Q8_0 ONCE (amortized over all rows)
1492
711
    let (q8_scales, q8_quants) = quantize_activations_q8_0(activations);
1493
1494
    // Adaptive parallelization: sequential for small matrices, parallel for large
1495
    // Rayon overhead (~50-100µs) dominates for small out_dim
1496
    // Threshold tuned for 22-core CPU: break-even at ~1024 rows
1497
    const PARALLEL_THRESHOLD: usize = 1024;
1498
1499
711
    if out_dim < PARALLEL_THRESHOLD {
1500
        // Sequential path: avoids Rayon overhead entirely
1501
711
        let output: Vec<f32> = (0..out_dim)
1502
78.8k
            .
map711
(|o| {
1503
78.8k
                let row_start = o * bytes_per_row;
1504
78.8k
                let row_end = row_start + bytes_per_row;
1505
78.8k
                let row_data = &weight_data[row_start..row_end];
1506
78.8k
                fused_q4_0_q8_0_dot_simd(row_data, &q8_scales, &q8_quants, in_dim)
1507
78.8k
            })
1508
711
            .collect();
1509
711
        return Ok(output);
1510
0
    }
1511
1512
    // Parallel path for large matrices
1513
    use rayon::prelude::*;
1514
    // Use chunked parallel iteration to reduce Rayon scheduling overhead
1515
    // CHUNK_SIZE=128 provides good balance between parallelism and overhead
1516
    const CHUNK_SIZE: usize = 128;
1517
0
    let output: Vec<f32> = (0..out_dim)
1518
0
        .into_par_iter()
1519
0
        .with_min_len(CHUNK_SIZE)
1520
0
        .map(|o| {
1521
0
            let row_start = o * bytes_per_row;
1522
0
            let row_end = row_start + bytes_per_row;
1523
0
            let row_data = &weight_data[row_start..row_end];
1524
0
            fused_q4_0_q8_0_dot_simd(row_data, &q8_scales, &q8_quants, in_dim)
1525
0
        })
1526
0
        .collect();
1527
1528
0
    Ok(output)
1529
715
}
1530
1531
/// Zero-allocation Q4_0 × Q8_0 matrix-vector multiply.
1532
///
1533
/// Writes result directly into provided output buffer, eliminating allocation.
1534
/// Use this with scratch buffers for maximum performance.
1535
///
1536
/// # Arguments
1537
/// * `weight_data` - Q4_0 quantized weight matrix (row-major)
1538
/// * `activations` - Input activation vector (f32)
1539
/// * `in_dim` - Input dimension (columns)
1540
/// * `output` - Pre-allocated output buffer (must be exactly out_dim length)
1541
///
1542
/// # Returns
1543
/// Number of elements written (equals output.len())
1544
#[allow(clippy::similar_names)]
1545
71
pub fn fused_q4_0_q8_0_parallel_matvec_into(
1546
71
    weight_data: &[u8],
1547
71
    activations: &[f32],
1548
71
    in_dim: usize,
1549
71
    output: &mut [f32],
1550
71
) -> Result<()> {
1551
    use rayon::prelude::*;
1552
1553
    const Q4_0_BLOCK_BYTES: usize = 18;
1554
    const Q4_0_BLOCK_SIZE: usize = 32;
1555
1556
71
    let out_dim = output.len();
1557
71
    let blocks_per_row = in_dim.div_ceil(Q4_0_BLOCK_SIZE);
1558
71
    let bytes_per_row = blocks_per_row * Q4_0_BLOCK_BYTES;
1559
1560
71
    let expected_weight_bytes = out_dim * bytes_per_row;
1561
71
    if weight_data.len() < expected_weight_bytes {
1562
3
        return Err(RealizarError::InvalidShape {
1563
3
            reason: format!(
1564
3
                "Q4_0 weight data too small: need {} bytes for {}x{}, have {}",
1565
3
                expected_weight_bytes,
1566
3
                out_dim,
1567
3
                in_dim,
1568
3
                weight_data.len()
1569
3
            ),
1570
3
        });
1571
68
    }
1572
1573
68
    if activations.len() != in_dim {
1574
0
        return Err(RealizarError::InvalidShape {
1575
0
            reason: format!(
1576
0
                "Activation length {} doesn't match in_dim {}",
1577
0
                activations.len(),
1578
0
                in_dim
1579
0
            ),
1580
0
        });
1581
68
    }
1582
1583
    // Quantize activations to Q8_0 ONCE
1584
68
    let (q8_scales, q8_quants) = quantize_activations_q8_0(activations);
1585
1586
    // Use chunked parallel iteration to reduce Rayon scheduling overhead
1587
    const CHUNK_SIZE: usize = 64;
1588
68
    output
1589
68
        .par_iter_mut()
1590
68
        .with_min_len(CHUNK_SIZE)
1591
68
        .enumerate()
1592
7.25k
        .
for_each68
(|(o, out_val)| {
1593
7.25k
            let row_start = o * bytes_per_row;
1594
7.25k
            let row_end = row_start + bytes_per_row;
1595
7.25k
            let row_data = &weight_data[row_start..row_end];
1596
7.25k
            *out_val = fused_q4_0_q8_0_dot_simd(row_data, &q8_scales, &q8_quants, in_dim);
1597
7.25k
        });
1598
1599
68
    Ok(())
1600
71
}
1601
1602
// ============================================================================
1603
// FUSED Q8_0 × Q8_0 MATMUL (For Q8_0 quantized weights like Qwen2.5 LM head)
1604
// ============================================================================
1605
//
1606
// Q8_0 format: 34 bytes per block (2 byte f16 scale + 32 i8 quants)
1607
// This avoids the massive dequantization allocation that was causing
1608
// Qwen2.5's 152K vocab LM head to allocate 544MB per forward pass.
1609
// ============================================================================
1610
1611
/// AVX2 accelerated Q8_0 × Q8_0 dot product using integer SIMD
1612
///
1613
/// Uses _mm256_maddubs_epi16 with sign trick for i8×i8 multiplication.
1614
/// This is simpler than Q4_0×Q8_0 since no nibble unpacking is needed.
1615
#[cfg(target_arch = "x86_64")]
1616
#[target_feature(enable = "avx2")]
1617
#[inline]
1618
1.03k
unsafe fn fused_q8_0_q8_0_dot_avx2(
1619
1.03k
    q8_weight_data: &[u8],
1620
1.03k
    q8_act_scales: &[f32],
1621
1.03k
    q8_act_quants: &[i8],
1622
1.03k
    in_dim: usize,
1623
1.03k
) -> f32 {
1624
    // SAFETY: Memory safety ensured by bounds checking and alignment
1625
    unsafe {
1626
        use std::arch::x86_64::{
1627
            _mm256_cvtepi32_ps, _mm256_fmadd_ps, _mm256_loadu_si256, _mm256_madd_epi16,
1628
            _mm256_maddubs_epi16, _mm256_set1_epi16, _mm256_set1_ps, _mm256_setzero_ps,
1629
            _mm256_sign_epi8, _mm_cvtss_f32, _mm_hadd_ps, _mm_prefetch, _MM_HINT_T0,
1630
        };
1631
1632
        const Q8_0_BLOCK_BYTES: usize = 34; // 2 byte scale + 32 byte quants
1633
        const Q8_0_BLOCK_SIZE: usize = 32;
1634
1635
1.03k
        let num_blocks = in_dim.div_ceil(Q8_0_BLOCK_SIZE);
1636
1637
        // Float accumulator for final sum
1638
1.03k
        let mut acc = _mm256_setzero_ps();
1639
1.03k
        let ones = _mm256_set1_epi16(1);
1640
1641
1.03k
        let mut block_idx = 0;
1642
1643
        // Process 2 blocks at a time for better ILP
1644
3.59k
        while block_idx + 2 <= num_blocks {
1645
            // Prefetch next iteration's blocks
1646
2.56k
            if block_idx + 4 <= num_blocks {
1647
1.53k
                let prefetch_w = q8_weight_data
1648
1.53k
                    .as_ptr()
1649
1.53k
                    .add((block_idx + 2) * Q8_0_BLOCK_BYTES);
1650
1.53k
                let prefetch_a = q8_act_quants
1651
1.53k
                    .as_ptr()
1652
1.53k
                    .add((block_idx + 2) * Q8_0_BLOCK_SIZE);
1653
1.53k
                _mm_prefetch(prefetch_w.cast(), _MM_HINT_T0);
1654
1.53k
                _mm_prefetch(prefetch_a.cast(), _MM_HINT_T0);
1655
1.53k
            
}1.02k
1656
1657
            // === Block 0 ===
1658
2.56k
            let w_ptr_0 = q8_weight_data.as_ptr().add(block_idx * Q8_0_BLOCK_BYTES);
1659
2.56k
            let a_ptr_0 = q8_act_quants.as_ptr().add(block_idx * Q8_0_BLOCK_SIZE);
1660
1661
            // Read Q8_0 weight scale (f16 -> f32)
1662
2.56k
            let w_scale_bits_0 = u16::from_le_bytes([*w_ptr_0, *w_ptr_0.add(1)]);
1663
2.56k
            let w_scale_0 = f16_to_f32_lut(w_scale_bits_0);
1664
2.56k
            let a_scale_0 = q8_act_scales[block_idx];
1665
2.56k
            let combined_scale_0 = _mm256_set1_ps(w_scale_0 * a_scale_0);
1666
1667
            // Load Q8_0 weight quants (32 bytes at offset 2)
1668
2.56k
            let w_vec_0 = _mm256_loadu_si256(w_ptr_0.add(2).cast());
1669
            // Load Q8_0 activation quants (32 bytes)
1670
2.56k
            let a_vec_0 = _mm256_loadu_si256(a_ptr_0.cast());
1671
1672
            // Integer multiply-accumulate using signed multiply trick:
1673
            // maddubs requires unsigned × signed, so we use sign trick
1674
            // |w| * sign(a, w) = w * a
1675
2.56k
            let w_abs_0 = _mm256_sign_epi8(w_vec_0, w_vec_0);
1676
2.56k
            let a_signed_0 = _mm256_sign_epi8(a_vec_0, w_vec_0);
1677
1678
            // maddubs: multiply pairs and add horizontally to i16
1679
2.56k
            let prod_i16_0 = _mm256_maddubs_epi16(w_abs_0, a_signed_0);
1680
            // madd: pairwise add i16 to i32
1681
2.56k
            let prod_i32_0 = _mm256_madd_epi16(prod_i16_0, ones);
1682
            // Convert to float
1683
2.56k
            let prod_f32_0 = _mm256_cvtepi32_ps(prod_i32_0);
1684
1685
            // Scale and accumulate
1686
2.56k
            acc = _mm256_fmadd_ps(combined_scale_0, prod_f32_0, acc);
1687
1688
            // === Block 1 ===
1689
2.56k
            let w_ptr_1 = q8_weight_data
1690
2.56k
                .as_ptr()
1691
2.56k
                .add((block_idx + 1) * Q8_0_BLOCK_BYTES);
1692
2.56k
            let a_ptr_1 = q8_act_quants
1693
2.56k
                .as_ptr()
1694
2.56k
                .add((block_idx + 1) * Q8_0_BLOCK_SIZE);
1695
1696
2.56k
            let w_scale_bits_1 = u16::from_le_bytes([*w_ptr_1, *w_ptr_1.add(1)]);
1697
2.56k
            let w_scale_1 = f16_to_f32_lut(w_scale_bits_1);
1698
2.56k
            let a_scale_1 = q8_act_scales[block_idx + 1];
1699
2.56k
            let combined_scale_1 = _mm256_set1_ps(w_scale_1 * a_scale_1);
1700
1701
2.56k
            let w_vec_1 = _mm256_loadu_si256(w_ptr_1.add(2).cast());
1702
2.56k
            let a_vec_1 = _mm256_loadu_si256(a_ptr_1.cast());
1703
1704
2.56k
            let w_abs_1 = _mm256_sign_epi8(w_vec_1, w_vec_1);
1705
2.56k
            let a_signed_1 = _mm256_sign_epi8(a_vec_1, w_vec_1);
1706
1707
2.56k
            let prod_i16_1 = _mm256_maddubs_epi16(w_abs_1, a_signed_1);
1708
2.56k
            let prod_i32_1 = _mm256_madd_epi16(prod_i16_1, ones);
1709
2.56k
            let prod_f32_1 = _mm256_cvtepi32_ps(prod_i32_1);
1710
1711
2.56k
            acc = _mm256_fmadd_ps(combined_scale_1, prod_f32_1, acc);
1712
1713
2.56k
            block_idx += 2;
1714
        }
1715
1716
        // Handle remaining single block
1717
1.04k
        while block_idx < num_blocks {
1718
8
            let w_ptr = q8_weight_data.as_ptr().add(block_idx * Q8_0_BLOCK_BYTES);
1719
8
            let a_ptr = q8_act_quants.as_ptr().add(block_idx * Q8_0_BLOCK_SIZE);
1720
8
1721
8
            let w_scale_bits = u16::from_le_bytes([*w_ptr, *w_ptr.add(1)]);
1722
8
            let w_scale = f16_to_f32_lut(w_scale_bits);
1723
8
            let a_scale = q8_act_scales[block_idx];
1724
8
            let combined_scale = _mm256_set1_ps(w_scale * a_scale);
1725
8
1726
8
            let w_vec = _mm256_loadu_si256(w_ptr.add(2).cast());
1727
8
            let a_vec = _mm256_loadu_si256(a_ptr.cast());
1728
8
1729
8
            let w_abs = _mm256_sign_epi8(w_vec, w_vec);
1730
8
            let a_signed = _mm256_sign_epi8(a_vec, w_vec);
1731
8
1732
8
            let prod_i16 = _mm256_maddubs_epi16(w_abs, a_signed);
1733
8
            let prod_i32 = _mm256_madd_epi16(prod_i16, ones);
1734
8
            let prod_f32 = _mm256_cvtepi32_ps(prod_i32);
1735
8
1736
8
            acc = _mm256_fmadd_ps(combined_scale, prod_f32, acc);
1737
8
1738
8
            block_idx += 1;
1739
8
        }
1740
1741
        // Horizontal sum of 8 floats
1742
1.03k
        let hi = std::arch::x86_64::_mm256_extractf128_ps(acc, 1);
1743
1.03k
        let lo = std::arch::x86_64::_mm256_castps256_ps128(acc);
1744
1.03k
        let sum128 = std::arch::x86_64::_mm_add_ps(lo, hi);
1745
1.03k
        let sum64 = _mm_hadd_ps(sum128, sum128);
1746
1.03k
        let sum32 = _mm_hadd_ps(sum64, sum64);
1747
1.03k
        _mm_cvtss_f32(sum32)
1748
    }
1749
1.03k
}
1750
1751
/// Scalar fallback for Q8_0 × Q8_0 dot product
1752
///
1753
/// Exposed as `pub(crate)` for direct testing on AVX2 machines.
1754
#[inline]
1755
1
pub(crate) fn fused_q8_0_q8_0_dot_scalar(
1756
1
    q8_weight_data: &[u8],
1757
1
    q8_act_scales: &[f32],
1758
1
    q8_act_quants: &[i8],
1759
1
    in_dim: usize,
1760
1
) -> f32 {
1761
    const Q8_0_BLOCK_BYTES: usize = 34;
1762
    const Q8_0_BLOCK_SIZE: usize = 32;
1763
1764
1
    let num_blocks = in_dim.div_ceil(Q8_0_BLOCK_SIZE);
1765
1
    let mut total_sum = 0.0f32;
1766
1767
1
    for block_idx in 0..num_blocks {
1768
1
        let block_start = block_idx * Q8_0_BLOCK_BYTES;
1769
1
        if block_start + Q8_0_BLOCK_BYTES > q8_weight_data.len() {
1770
0
            break;
1771
1
        }
1772
1
        let block = &q8_weight_data[block_start..block_start + Q8_0_BLOCK_BYTES];
1773
1774
        // Read weight scale (f16)
1775
1
        let w_scale = half::f16::from_le_bytes([block[0], block[1]]).to_f32();
1776
1
        let a_scale = q8_act_scales[block_idx];
1777
1
        let combined_scale = w_scale * a_scale;
1778
1779
1
        let act_start = block_idx * Q8_0_BLOCK_SIZE;
1780
1781
        // Sum of weight_quant[i] * act_quant[i] in i32
1782
1
        let mut block_sum = 0i32;
1783
33
        for 
j32
in 0..32 {
1784
32
            if act_start + j >= in_dim {
1785
0
                break;
1786
32
            }
1787
            #[allow(clippy::cast_possible_wrap)]
1788
32
            let w_quant = block[2 + j] as i8;
1789
32
            let a_quant = q8_act_quants[act_start + j];
1790
32
            block_sum += (w_quant as i32) * (a_quant as i32);
1791
        }
1792
1793
1
        total_sum += combined_scale * (block_sum as f32);
1794
    }
1795
1796
1
    total_sum
1797
1
}
1798
1799
/// SIMD dispatcher for Q8_0 × Q8_0 dot product
1800
#[inline]
1801
1.03k
fn fused_q8_0_q8_0_dot_simd(
1802
1.03k
    q8_weight_data: &[u8],
1803
1.03k
    q8_act_scales: &[f32],
1804
1.03k
    q8_act_quants: &[i8],
1805
1.03k
    in_dim: usize,
1806
1.03k
) -> f32 {
1807
    #[cfg(target_arch = "x86_64")]
1808
    {
1809
1.03k
        if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
1810
            // SAFETY: AVX2 and FMA features checked above
1811
            unsafe {
1812
1.03k
                return fused_q8_0_q8_0_dot_avx2(
1813
1.03k
                    q8_weight_data,
1814
1.03k
                    q8_act_scales,
1815
1.03k
                    q8_act_quants,
1816
1.03k
                    in_dim,
1817
                );
1818
            }
1819
0
        }
1820
    }
1821
0
    fused_q8_0_q8_0_dot_scalar(q8_weight_data, q8_act_scales, q8_act_quants, in_dim)
1822
1.03k
}
1823
1824
/// Parallel Q8_0 × Q8_0 matrix-vector multiply
1825
///
1826
/// This avoids the massive dequantization allocation that was causing
1827
/// Qwen2.5's 152K vocab LM head (Q8_0) to allocate 544MB per forward pass.
1828
///
1829
/// For Q8_0 weights (e.g., Qwen2.5 LM head), this is ~100x faster than
1830
/// dequantize + matmul because:
1831
/// 1. No 544MB allocation per forward pass
1832
/// 2. Integer SIMD is faster than FP32
1833
/// 3. Better cache locality (34 bytes vs 128 bytes per block)
1834
#[allow(clippy::similar_names)]
1835
10
pub fn fused_q8_0_q8_0_parallel_matvec(
1836
10
    weight_data: &[u8],
1837
10
    activations: &[f32],
1838
10
    in_dim: usize,
1839
10
    out_dim: usize,
1840
10
) -> Result<Vec<f32>> {
1841
    use rayon::prelude::*;
1842
1843
    const Q8_0_BLOCK_BYTES: usize = 34;
1844
    const Q8_0_BLOCK_SIZE: usize = 32;
1845
1846
10
    let blocks_per_row = in_dim.div_ceil(Q8_0_BLOCK_SIZE);
1847
10
    let bytes_per_row = blocks_per_row * Q8_0_BLOCK_BYTES;
1848
1849
10
    let expected_weight_bytes = out_dim * bytes_per_row;
1850
10
    if weight_data.len() < expected_weight_bytes {
1851
3
        return Err(RealizarError::InvalidShape {
1852
3
            reason: format!(
1853
3
                "Q8_0 weight data too small: need {} bytes for {}x{}, have {}",
1854
3
                expected_weight_bytes,
1855
3
                out_dim,
1856
3
                in_dim,
1857
3
                weight_data.len()
1858
3
            ),
1859
3
        });
1860
7
    }
1861
1862
7
    if activations.len() != in_dim {
1863
0
        return Err(RealizarError::InvalidShape {
1864
0
            reason: format!(
1865
0
                "Activation length {} doesn't match in_dim {}",
1866
0
                activations.len(),
1867
0
                in_dim
1868
0
            ),
1869
0
        });
1870
7
    }
1871
1872
    // Quantize activations to Q8_0 ONCE (amortized over all rows)
1873
7
    let (q8_scales, q8_quants) = quantize_activations_q8_0(activations);
1874
1875
    // Parallel over output rows with chunking
1876
    const CHUNK_SIZE: usize = 64;
1877
7
    let output: Vec<f32> = (0..out_dim)
1878
7
        .into_par_iter()
1879
7
        .with_min_len(CHUNK_SIZE)
1880
1.02k
        .
map7
(|o| {
1881
1.02k
            let row_start = o * bytes_per_row;
1882
1.02k
            let row_end = row_start + bytes_per_row;
1883
1.02k
            let row_data = &weight_data[row_start..row_end];
1884
1.02k
            fused_q8_0_q8_0_dot_simd(row_data, &q8_scales, &q8_quants, in_dim)
1885
1.02k
        })
1886
7
        .collect();
1887
1888
7
    Ok(output)
1889
10
}
1890
1891
/// Fused Q8_0 × Q8_0 parallel matvec - writes to pre-allocated buffer
1892
///
1893
/// IMP-131: Zero-allocation variant for hot-path inference.
1894
#[allow(clippy::similar_names)]
1895
4
pub fn fused_q8_0_q8_0_parallel_matvec_into(
1896
4
    weight_data: &[u8],
1897
4
    activations: &[f32],
1898
4
    in_dim: usize,
1899
4
    out_dim: usize,
1900
4
    output: &mut [f32],
1901
4
) -> Result<()> {
1902
    use rayon::prelude::*;
1903
1904
    const Q8_0_BLOCK_BYTES: usize = 34;
1905
    const Q8_0_BLOCK_SIZE: usize = 32;
1906
1907
4
    let blocks_per_row = in_dim.div_ceil(Q8_0_BLOCK_SIZE);
1908
4
    let bytes_per_row = blocks_per_row * Q8_0_BLOCK_BYTES;
1909
1910
4
    let expected_weight_bytes = out_dim * bytes_per_row;
1911
4
    if weight_data.len() < expected_weight_bytes {
1912
2
        return Err(RealizarError::InvalidShape {
1913
2
            reason: format!(
1914
2
                "Q8_0 weight data too small: need {} bytes for {}x{}, have {}",
1915
2
                expected_weight_bytes,
1916
2
                out_dim,
1917
2
                in_dim,
1918
2
                weight_data.len()
1919
2
            ),
1920
2
        });
1921
2
    }
1922
1923
2
    if activations.len() != in_dim {
1924
0
        return Err(RealizarError::InvalidShape {
1925
0
            reason: format!(
1926
0
                "Activation length {} doesn't match in_dim {}",
1927
0
                activations.len(),
1928
0
                in_dim
1929
0
            ),
1930
0
        });
1931
2
    }
1932
1933
2
    if output.len() < out_dim {
1934
0
        return Err(RealizarError::InvalidShape {
1935
0
            reason: format!(
1936
0
                "Output buffer too small: need {}, have {}",
1937
0
                out_dim,
1938
0
                output.len()
1939
0
            ),
1940
0
        });
1941
2
    }
1942
1943
    // Quantize activations to Q8_0 ONCE (amortized over all rows)
1944
2
    let (q8_scales, q8_quants) = quantize_activations_q8_0(activations);
1945
1946
    // Parallel over output rows with chunking
1947
    const CHUNK_SIZE: usize = 64;
1948
2
    output[..out_dim]
1949
2
        .par_iter_mut()
1950
2
        .enumerate()
1951
2
        .with_min_len(CHUNK_SIZE)
1952
4
        .
for_each2
(|(o, out)| {
1953
4
            let row_start = o * bytes_per_row;
1954
4
            let row_end = row_start + bytes_per_row;
1955
4
            let row_data = &weight_data[row_start..row_end];
1956
4
            *out = fused_q8_0_q8_0_dot_simd(row_data, &q8_scales, &q8_quants, in_dim);
1957
4
        });
1958
1959
2
    Ok(())
1960
4
}
1961
1962
/// Helper: Extract 6-bit scale and min for a block from the packed scales array
1963
///
1964
/// PAR-001 FIX: Matches llama.cpp's get_scale_min_k4 packing scheme:
1965
/// - Blocks 0-3: scale = q[j] & 63, min = q[j+4] & 63
1966
/// - Blocks 4-7: scale = (q[j+4] & 0xF) | ((q[j-4] >> 6) << 4)
1967
///   min = (q[j+4] >> 4) | ((q[j] >> 6) << 4)
1968
#[inline]
1969
13
pub(crate) fn extract_scale_min(scales: &[u8; 12], block_idx: usize) -> (f32, f32) {
1970
13
    let j = block_idx;
1971
13
    let (scale_bits, min_bits) = if j < 4 {
1972
        // First 4 blocks: simple layout
1973
7
        let d = scales[j] & 63;
1974
7
        let m = scales[j + 4] & 63;
1975
7
        (d, m)
1976
    } else {
1977
        // Last 4 blocks: packed layout using high bits from first 4 bytes
1978
6
        let d = (scales[j + 4] & 0x0F) | ((scales[j - 4] >> 6) << 4);
1979
6
        let m = (scales[j + 4] >> 4) | ((scales[j] >> 6) << 4);
1980
6
        (d, m)
1981
    };
1982
1983
    // Return raw 6-bit values as floats
1984
    // The GGUF header's d/dmin values already include the /63 normalization
1985
13
    let scale = f32::from(scale_bits);
1986
13
    let min = f32::from(min_bits);
1987
1988
13
    (scale, min)
1989
13
}
1990
1991
1992
1993
#[cfg(test)]
1994
1995
#[cfg(test)]
1996
mod tests;