Skip to main content

trueno/backends/q4k/gemv/
avx512.rs

1//! AVX-512 SIMD Q4_K GEMV implementation.
2//!
3//! Contract: avx512-q4k-v1.yaml (C-AVX512-Q4K-001)
4//! Processes 16 elements per iteration using zmm registers (2× throughput vs AVX2).
5//! References: [46] GPTQ, [47] QuIP# AVX-512 dequant methodology.
6
7use super::super::{parse_q4k_header, SUPER_BLOCK_BYTES, SUPER_BLOCK_SIZE};
8
9/// Fused Q4_K matrix-vector multiply with AVX-512 SIMD (16-wide)
10///
11/// Contract: avx512-q4k-v1.yaml (C-AVX512-Q4K-001, C-AVX512-Q4K-002)
12#[cfg(target_arch = "x86_64")]
13#[target_feature(enable = "avx512f", enable = "avx512bw", enable = "fma")]
14pub(crate) unsafe fn matmul_q4k_f32_avx512(
15    q4k_data: &[u8],
16    input: &[f32],
17    out_dim: usize,
18    in_dim: usize,
19) -> Vec<f32> {
20    unsafe {
21        use std::arch::x86_64::*;
22
23        let num_blocks_per_row = (in_dim + SUPER_BLOCK_SIZE - 1) / SUPER_BLOCK_SIZE;
24        let row_bytes = num_blocks_per_row * SUPER_BLOCK_BYTES;
25        let low_mask = _mm512_set1_epi32(0x0F);
26
27        // Uninit: output[out_idx] = hsum_avx512(acc) (SET) for every out_idx.
28        let mut output: Vec<f32> = Vec::with_capacity(out_dim);
29        // SAFETY: Each output[out_idx] is SET from local AVX-512 accumulator.
30        output.set_len(out_dim);
31
32        for out_idx in 0..out_dim {
33            let row_start = out_idx * row_bytes;
34            let mut acc = _mm512_setzero_ps();
35
36            for sb_idx in 0..num_blocks_per_row {
37                let sb_start = row_start + sb_idx * SUPER_BLOCK_BYTES;
38                let sb_data = &q4k_data[sb_start..sb_start + SUPER_BLOCK_BYTES];
39                let input_offset = sb_idx * SUPER_BLOCK_SIZE;
40                process_q4k_superblock_avx512(
41                    sb_data,
42                    input,
43                    input_offset,
44                    in_dim,
45                    low_mask,
46                    &mut acc,
47                );
48            }
49
50            output[out_idx] = hsum_avx512(acc);
51        }
52
53        contract_post_dequant!(output);
54        output
55    }
56}
57
58/// Bounds-aware 16-wide activation load for the Q4_K reduction tail.
59///
60/// Contract: avx512-q4k-v1.yaml (C-AVX512-Q4K-003).
61/// Lanes whose activation index is ≥ `in_dim` are masked: they are NOT read
62/// (no heap over-read past the activation buffer) and load as `0.0` (so the
63/// downstream FMA contributes nothing for those positions). Used only on the
64/// partial-chunk tail; the full-chunk hot path uses unguarded loads.
65#[cfg(target_arch = "x86_64")]
66#[target_feature(enable = "avx512f")]
67#[inline]
68unsafe fn load16_bounded(
69    input_ptr: *const f32,
70    base: usize,
71    in_dim: usize,
72) -> std::arch::x86_64::__m512 {
73    use std::arch::x86_64::*;
74    unsafe {
75        if base + 16 <= in_dim {
76            _mm512_loadu_ps(input_ptr.add(base))
77        } else if base < in_dim {
78            // 1..=15 valid low lanes; the rest are masked (unread, zeroed).
79            let mask: __mmask16 = ((1u32 << (in_dim - base)) - 1) as __mmask16;
80            _mm512_maskz_loadu_ps(mask, input_ptr.add(base))
81        } else {
82            _mm512_setzero_ps()
83        }
84    }
85}
86
87/// Process one Q4K super-block with AVX-512 (16-wide), fully unrolled.
88///
89/// Each super-block = 256 elements in 4 chunks of 64.
90/// Each chunk: 32 low nibbles + 32 high nibbles.
91/// AVX-512: 16 elements per iteration → 2 iterations per 32 nibbles.
92///
93/// Optimization (Phase 4, 2026-04-05):
94/// - Fully unrolled inner loops (was while loop with 2 iterations)
95/// - Bounds check hoisted out of hot loop (in_dim validated by caller)
96/// - Software prefetch of next superblock's quantized data
97///
98/// NOTE: Dual-accumulator (low→acc0, high→acc1) was tested (2026-04-05)
99/// but showed NO improvement. Zen 4's OOO engine already hides the FMA
100/// dependency chain across iterations — adding a second accumulator just
101/// adds merge overhead without helping the pipeline.
102#[cfg(target_arch = "x86_64")]
103#[target_feature(enable = "avx512f", enable = "avx512bw", enable = "fma")]
104unsafe fn process_q4k_superblock_avx512(
105    sb_data: &[u8],
106    input: &[f32],
107    input_offset: usize,
108    in_dim: usize,
109    low_mask: std::arch::x86_64::__m512i,
110    acc: &mut std::arch::x86_64::__m512,
111) {
112    unsafe {
113        use std::arch::x86_64::*;
114
115        let (d, dmin, scales, mins) = parse_q4k_header(sb_data);
116        let qs = sb_data.get(16..144).expect("Q4_K: need ≥144 bytes for qs");
117        let qs_ptr = qs.as_ptr();
118        let input_ptr = input.as_ptr();
119
120        // Software prefetch: next superblock's header + first quant bytes
121        // Prefetch 2 cache lines ahead (128 bytes = most of next superblock)
122        _mm_prefetch(sb_data.as_ptr().add(SUPER_BLOCK_BYTES) as *const i8, _MM_HINT_T0);
123        _mm_prefetch(sb_data.as_ptr().add(SUPER_BLOCK_BYTES + 64) as *const i8, _MM_HINT_T0);
124
125        for chunk_i in 0..4 {
126            let chunk_start = chunk_i * 64;
127            let q_start = chunk_i * 32;
128            let input_base_lo0 = input_offset + chunk_start;
129
130            // Skip chunks whose activation positions are entirely beyond the
131            // reduction dim (the original code skipped these via its bounds
132            // guard; we make it explicit so the tail branch below stays simple).
133            if input_base_lo0 >= in_dim {
134                continue;
135            }
136
137            let d1 = d * f32::from(scales[chunk_i * 2]);
138            let dm1 = dmin * f32::from(mins[chunk_i * 2]);
139            let d2 = d * f32::from(scales[chunk_i * 2 + 1]);
140            let dm2 = dmin * f32::from(mins[chunk_i * 2 + 1]);
141
142            let d1_vec = _mm512_set1_ps(d1);
143            let dm1_vec = _mm512_set1_ps(dm1);
144            let d2_vec = _mm512_set1_ps(d2);
145            let dm2_vec = _mm512_set1_ps(dm2);
146
147            // Dequant the 4×16 weight groups. `qs` is always fully in bounds (a
148            // super-block is always SUPER_BLOCK_BYTES). Low nibbles map to
149            // activation positions [base, base+32); high nibbles to
150            // [base+32, base+64) — DIFFERENT positions, which is why the high
151            // loads need their own bound (the bug this fixes).
152            let q0 = _mm_loadu_si128(qs_ptr.add(q_start) as *const __m128i);
153            let q0_i32 = _mm512_cvtepu8_epi32(q0);
154            let q1 = _mm_loadu_si128(qs_ptr.add(q_start + 16) as *const __m128i);
155            let q1_i32 = _mm512_cvtepu8_epi32(q1);
156            let dq0 = _mm512_fmsub_ps(
157                d1_vec,
158                _mm512_cvtepi32_ps(_mm512_and_si512(q0_i32, low_mask)),
159                dm1_vec,
160            );
161            let dq1 = _mm512_fmsub_ps(
162                d1_vec,
163                _mm512_cvtepi32_ps(_mm512_and_si512(q1_i32, low_mask)),
164                dm1_vec,
165            );
166            let dqh0 =
167                _mm512_fmsub_ps(d2_vec, _mm512_cvtepi32_ps(_mm512_srli_epi32(q0_i32, 4)), dm2_vec);
168            let dqh1 =
169                _mm512_fmsub_ps(d2_vec, _mm512_cvtepi32_ps(_mm512_srli_epi32(q1_i32, 4)), dm2_vec);
170
171            let input_base_hi0 = input_base_lo0 + 32;
172
173            if input_base_lo0 + 64 <= in_dim {
174                // FAST PATH: the whole 64-element chunk is in bounds (the common
175                // 256-aligned case) — unguarded 16-wide loads, hot-path perf
176                // preserved.
177                let x0 = _mm512_loadu_ps(input_ptr.add(input_base_lo0));
178                let x1 = _mm512_loadu_ps(input_ptr.add(input_base_lo0 + 16));
179                let xh0 = _mm512_loadu_ps(input_ptr.add(input_base_hi0));
180                let xh1 = _mm512_loadu_ps(input_ptr.add(input_base_hi0 + 16));
181                *acc = _mm512_fmadd_ps(dq0, x0, *acc);
182                *acc = _mm512_fmadd_ps(dq1, x1, *acc);
183                *acc = _mm512_fmadd_ps(dqh0, xh0, *acc);
184                *acc = _mm512_fmadd_ps(dqh1, xh1, *acc);
185            } else {
186                // TAIL PATH: partial chunk — bounded masked loads. Lanes ≥ in_dim
187                // are NOT read (no heap over-read) and load as 0.0 (0 FMA
188                // contribution). Fixes C-AVX512-Q4K-003: the previous single
189                // `input_base_lo0 + 32 <= in_dim` guard covered only the low
190                // loads, so the high-nibble loads at [+32,+64) over-read up to
191                // 128 bytes for any non-256-aligned in_dim (32/96/160/224 mod
192                // 256). It also silently dropped the valid high-nibble tail —
193                // masked loads compute it correctly.
194                let x0 = load16_bounded(input_ptr, input_base_lo0, in_dim);
195                let x1 = load16_bounded(input_ptr, input_base_lo0 + 16, in_dim);
196                let xh0 = load16_bounded(input_ptr, input_base_hi0, in_dim);
197                let xh1 = load16_bounded(input_ptr, input_base_hi0 + 16, in_dim);
198                *acc = _mm512_fmadd_ps(dq0, x0, *acc);
199                *acc = _mm512_fmadd_ps(dq1, x1, *acc);
200                *acc = _mm512_fmadd_ps(dqh0, xh0, *acc);
201                *acc = _mm512_fmadd_ps(dqh1, xh1, *acc);
202            }
203        }
204    }
205}
206
207/// AVX-512 horizontal sum of 16 f32 lanes.
208/// Uses avx512f-only intrinsics (no avx512dq dependency).
209#[cfg(target_arch = "x86_64")]
210#[target_feature(enable = "avx512f")]
211unsafe fn hsum_avx512(v: std::arch::x86_64::__m512) -> f32 {
212    use std::arch::x86_64::*;
213    // Reduce 512→256 using shuffle instead of extractf32x8 (which needs avx512dq)
214    let lo256 = _mm512_castps512_ps256(v);
215    // Shift upper 256 bits down: use _mm512_shuffle_f32x4 to bring lanes 8-15 into 0-7
216    let hi_shifted = _mm512_shuffle_f32x4(v, v, 0b_01_00_11_10); // swap upper and lower 256
217    let hi256 = _mm512_castps512_ps256(hi_shifted);
218    let sum256 = _mm256_add_ps(lo256, hi256);
219    // Now reduce 256→scalar
220    let hi128 = _mm256_extractf128_ps(sum256, 1);
221    let lo128 = _mm256_castps256_ps128(sum256);
222    let sum128 = _mm_add_ps(lo128, hi128);
223    let hi64 = _mm_movehl_ps(sum128, sum128);
224    let sum64 = _mm_add_ps(sum128, hi64);
225    let hi32 = _mm_shuffle_ps(sum64, sum64, 1);
226    let sum32 = _mm_add_ss(sum64, hi32);
227    _mm_cvtss_f32(sum32)
228}
229
230/// AVX-512 chunk processor for parallel dispatch.
231/// Contract: avx512-q4k-v1.yaml
232#[cfg(target_arch = "x86_64")]
233#[target_feature(enable = "avx512f", enable = "avx512bw", enable = "fma")]
234pub(crate) unsafe fn compute_chunk_q4k_avx512(
235    q4k_data: &[u8],
236    input: &[f32],
237    chunk: &mut [f32],
238    start_row: usize,
239    out_dim: usize,
240    in_dim: usize,
241    num_blocks_per_row: usize,
242    row_bytes: usize,
243) {
244    unsafe {
245        use std::arch::x86_64::*;
246
247        let low_mask = _mm512_set1_epi32(0x0F);
248
249        for (local_idx, out_val) in chunk.iter_mut().enumerate() {
250            let out_idx = start_row + local_idx;
251            if out_idx >= out_dim {
252                break;
253            }
254            let row_start = out_idx * row_bytes;
255            let mut acc = _mm512_setzero_ps();
256
257            for sb_idx in 0..num_blocks_per_row {
258                let sb_start = row_start + sb_idx * SUPER_BLOCK_BYTES;
259                let sb_data = &q4k_data[sb_start..sb_start + SUPER_BLOCK_BYTES];
260                let input_offset = sb_idx * SUPER_BLOCK_SIZE;
261                process_q4k_superblock_avx512(
262                    sb_data,
263                    input,
264                    input_offset,
265                    in_dim,
266                    low_mask,
267                    &mut acc,
268                );
269            }
270
271            *out_val = hsum_avx512(acc);
272        }
273    }
274    contract_post_dequant!(chunk);
275}