Skip to main content

trueno/backends/q6k/
gemv.rs

1#![allow(missing_docs)]
2//! Row-major Q6_K matrix-vector multiplication.
3//!
4//! This module implements row-major GEMV for Q6_K format.
5//! Includes scalar, AVX2-optimized, and parallel dispatch implementations.
6
7use super::{f16_to_f32, SUPER_BLOCK_BYTES, SUPER_BLOCK_SIZE};
8
9/// Fused Q6_K matrix-vector multiply (scalar reference)
10/// Extract a single Q6K quantized value from packed ql/qh arrays.
11#[inline(always)]
12fn extract_q6k_scalar(ql: &[u8], qh: &[u8], idx: usize) -> i8 {
13    let ql_byte = ql[idx / 2];
14    let low4 = if idx % 2 == 0 { ql_byte & 0x0F } else { ql_byte >> 4 };
15    let qh_byte = qh[idx / 4];
16    let high2 = (qh_byte >> ((idx % 4) * 2)) & 0x03;
17    (low4 | (high2 << 4)) as i8 - 32
18}
19
20/// Scalar dot product for one Q6K super-block row.
21#[inline(always)]
22fn process_q6k_superblock_scalar(
23    sb_data: &[u8],
24    input: &[f32],
25    input_offset: usize,
26    in_dim: usize,
27) -> f32 {
28    let ql = sb_data.get(0..128).expect("Q6_K: need ≥128 bytes for ql");
29    let qh = sb_data.get(128..192).expect("Q6_K: need ≥192 bytes for qh");
30    let scales = sb_data.get(192..208).expect("Q6_K: need ≥208 bytes for scales");
31    let d = f16_to_f32(u16::from_le_bytes([sb_data[208], sb_data[209]]));
32    let mut sum = 0.0f32;
33
34    for group in 0..16 {
35        let scale = (scales[group] as i8) as f32;
36        let group_offset = group * 16;
37
38        for j in 0..16 {
39            let idx = group_offset + j;
40            let input_idx = input_offset + idx;
41            if input_idx >= in_dim {
42                continue;
43            }
44            let q6 = extract_q6k_scalar(ql, qh, idx);
45            sum += d * scale * q6 as f32 * input[input_idx];
46        }
47    }
48    sum
49}
50
51pub fn matmul_q6k_f32_scalar(
52    q6k_data: &[u8],
53    input: &[f32],
54    out_dim: usize,
55    in_dim: usize,
56) -> Vec<f32> {
57    assert_eq!(input.len(), in_dim, "Input length mismatch");
58
59    let num_blocks_per_row = (in_dim + SUPER_BLOCK_SIZE - 1) / SUPER_BLOCK_SIZE;
60    let row_bytes = num_blocks_per_row * SUPER_BLOCK_BYTES;
61
62    // Uninit: output[out_idx] = sum (SET) for every out_idx.
63    let mut output: Vec<f32> = Vec::with_capacity(out_dim);
64    // SAFETY: Each output[out_idx] is SET from local accumulator sum.
65    unsafe {
66        output.set_len(out_dim);
67    }
68
69    for out_idx in 0..out_dim {
70        let row_start = out_idx * row_bytes;
71        let mut sum = 0.0f32;
72
73        for sb_idx in 0..num_blocks_per_row {
74            let sb_start = row_start + sb_idx * SUPER_BLOCK_BYTES;
75            if sb_start + SUPER_BLOCK_BYTES > q6k_data.len() {
76                break;
77            }
78            let sb_data = &q6k_data[sb_start..sb_start + SUPER_BLOCK_BYTES];
79            let input_offset = sb_idx * SUPER_BLOCK_SIZE;
80            sum += process_q6k_superblock_scalar(sb_data, input, input_offset, in_dim);
81        }
82
83        output[out_idx] = sum;
84    }
85
86    output
87}
88
89/// Extract 8 Q6K quantized values from packed ql/qh arrays.
90#[cfg(target_arch = "x86_64")]
91#[inline(always)]
92fn extract_q6k_values(ql: &[u8], qh: &[u8], idx_base: usize) -> [i32; 8] {
93    let mut q6_vals = [0i32; 8];
94    for i in 0..8 {
95        let idx = idx_base + i;
96        let ql_byte = ql[idx / 2];
97        let low4 = if idx % 2 == 0 { ql_byte & 0x0F } else { ql_byte >> 4 };
98        let qh_byte = qh[idx / 4];
99        let qh_shift = (idx % 4) * 2;
100        let high2 = (qh_byte >> qh_shift) & 0x03;
101        q6_vals[i] = ((low4 | (high2 << 4)) as i32) - 32;
102    }
103    q6_vals
104}
105
106/// AVX2 horizontal sum of 8 f32 lanes to a single f32.
107#[cfg(target_arch = "x86_64")]
108#[target_feature(enable = "avx2")]
109// SAFETY: caller verifies AVX2 support, input slices meet alignment/length requirements
110unsafe fn hsum_q6k_avx2(acc: std::arch::x86_64::__m256) -> f32 {
111    use std::arch::x86_64::*;
112    let hi128 = _mm256_extractf128_ps(acc, 1);
113    let lo128 = _mm256_castps256_ps128(acc);
114    let sum128 = _mm_add_ps(lo128, hi128);
115    let hi64 = _mm_movehl_ps(sum128, sum128);
116    let sum64 = _mm_add_ps(sum128, hi64);
117    let hi32 = _mm_shuffle_ps(sum64, sum64, 1);
118    let sum32 = _mm_add_ss(sum64, hi32);
119    _mm_cvtss_f32(sum32)
120}
121
122/// Process one Q6K super-block with AVX2, accumulating into `acc`.
123#[cfg(target_arch = "x86_64")]
124#[target_feature(enable = "avx2", enable = "fma")]
125// SAFETY: Caller ensures AVX2+FMA are available and sb_data is a valid Q6_K super-block
126unsafe fn process_q6k_superblock_avx2(
127    sb_data: &[u8],
128    input: &[f32],
129    input_offset: usize,
130    in_dim: usize,
131    acc: &mut std::arch::x86_64::__m256,
132) {
133    unsafe {
134        use std::arch::x86_64::*;
135
136        let ql = sb_data.get(0..128).expect("Q6_K: need ≥128 bytes for ql");
137        let qh = sb_data.get(128..192).expect("Q6_K: need ≥192 bytes for qh");
138        let scales = sb_data.get(192..208).expect("Q6_K: need ≥208 bytes for scales");
139        let d = f16_to_f32(u16::from_le_bytes([sb_data[208], sb_data[209]]));
140        let d_vec = _mm256_set1_ps(d);
141
142        for group in 0..16 {
143            let scale = (scales[group] as i8) as f32;
144            let ds_vec = _mm256_mul_ps(d_vec, _mm256_set1_ps(scale));
145            let group_offset = group * 16;
146            let input_group = input_offset + group_offset;
147
148            for half in 0..2 {
149                let half_offset = half * 8;
150                let input_base = input_group + half_offset;
151                if input_base + 8 > in_dim {
152                    continue;
153                }
154
155                let q6_vals = extract_q6k_values(ql, qh, group_offset + half_offset);
156                let q6_i32 = _mm256_loadu_si256(q6_vals.as_ptr() as *const __m256i);
157                let q6_f32 = _mm256_cvtepi32_ps(q6_i32);
158                let x = _mm256_loadu_ps(input.as_ptr().add(input_base));
159                let dequant = _mm256_mul_ps(ds_vec, q6_f32);
160                *acc = _mm256_fmadd_ps(dequant, x, *acc);
161            }
162        }
163    }
164}
165
166/// Fused Q6_K matrix-vector multiply with AVX2 SIMD
167///
168/// Optimized to process groups of 8 values at a time, computing
169/// dequant and dot product in one pass without intermediate buffer.
170#[cfg(target_arch = "x86_64")]
171#[target_feature(enable = "avx2", enable = "fma")]
172// SAFETY: Caller ensures AVX2+FMA are available and q6k_data is valid Q6_K layout
173unsafe fn matmul_q6k_f32_avx2(
174    q6k_data: &[u8],
175    input: &[f32],
176    out_dim: usize,
177    in_dim: usize,
178) -> Vec<f32> {
179    unsafe {
180        use std::arch::x86_64::*;
181
182        let num_blocks_per_row = (in_dim + SUPER_BLOCK_SIZE - 1) / SUPER_BLOCK_SIZE;
183        let row_bytes = num_blocks_per_row * SUPER_BLOCK_BYTES;
184
185        // Uninit: output[out_idx] = hsum_q6k_avx2(acc) (SET) for every out_idx.
186        let mut output: Vec<f32> = Vec::with_capacity(out_dim);
187        // SAFETY: Each output[out_idx] is SET from local SIMD accumulator.
188        output.set_len(out_dim);
189
190        for out_idx in 0..out_dim {
191            let row_start = out_idx * row_bytes;
192            let mut acc = _mm256_setzero_ps();
193
194            for sb_idx in 0..num_blocks_per_row {
195                let sb_start = row_start + sb_idx * SUPER_BLOCK_BYTES;
196                if sb_start + SUPER_BLOCK_BYTES > q6k_data.len() {
197                    break;
198                }
199                let sb_data = &q6k_data[sb_start..sb_start + SUPER_BLOCK_BYTES];
200                let input_offset = sb_idx * SUPER_BLOCK_SIZE;
201                process_q6k_superblock_avx2(sb_data, input, input_offset, in_dim, &mut acc);
202            }
203
204            output[out_idx] = hsum_q6k_avx2(acc);
205        }
206
207        output
208    }
209}
210
211/// Runtime dispatch for Q6K matmul - uses AVX2 if available
212///
213/// # Contract (GH-279)
214///
215/// Preconditions validated via `debug_assert!` (zero-cost in release):
216/// - `q6k_data.len() >= contracts::Q6_K.expected_bytes(out_dim, in_dim)`
217/// - `input.len() == in_dim`
218///
219/// These guarantee that inner-loop `expect()` calls on super-block sub-slices
220/// are unreachable: each super-block is sliced to exactly `SUPER_BLOCK_BYTES`
221/// (210), and all sub-accesses (`get(0..128)`, `get(128..192)`, `get(192..208)`)
222/// fit within that.
223#[inline]
224pub fn matmul_q6k_f32_dispatch(
225    q6k_data: &[u8],
226    input: &[f32],
227    out_dim: usize,
228    in_dim: usize,
229) -> Vec<f32> {
230    // GH-279: Contract validation at dispatch boundary.
231    // Inner expect() calls are defense-in-depth — provably unreachable when
232    // this precondition holds, because every sb_data slice is SUPER_BLOCK_BYTES.
233    debug_assert_eq!(input.len(), in_dim, "Q6K dispatch: input length mismatch");
234    debug_assert!(
235        q6k_data.len() >= crate::contracts::Q6_K.expected_bytes(out_dim, in_dim),
236        "Q6K dispatch: buffer too small: {} bytes for [{}, {}] (need {})",
237        q6k_data.len(),
238        out_dim,
239        in_dim,
240        crate::contracts::Q6_K.expected_bytes(out_dim, in_dim),
241    );
242
243    // For large matmuls (total work >= ~8M ops), use parallel execution
244    // This catches FFN layers (8960x1536) and lm_head (151936x1536)
245    // Also catches ffn_down (1536x8960) where out_dim is small but in_dim is large
246    let total_work = out_dim * in_dim;
247    if total_work >= 8_000_000 {
248        return matmul_q6k_f32_parallel(q6k_data, input, out_dim, in_dim);
249    }
250
251    #[cfg(target_arch = "x86_64")]
252    {
253        if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
254            // SAFETY: preconditions verified by caller
255            return unsafe { matmul_q6k_f32_avx2(q6k_data, input, out_dim, in_dim) };
256        }
257    }
258    matmul_q6k_f32_scalar(q6k_data, input, out_dim, in_dim)
259}
260
261/// Parallel Q6K matmul using multiple threads with AVX2
262#[cfg(target_arch = "x86_64")]
263fn matmul_q6k_f32_parallel(
264    q6k_data: &[u8],
265    input: &[f32],
266    out_dim: usize,
267    in_dim: usize,
268) -> Vec<f32> {
269    use std::thread;
270
271    // Use fewer threads with larger chunks for better cache efficiency
272    let num_threads = thread::available_parallelism().map(|p| p.get()).unwrap_or(4).min(12); // Use 12 threads max for better cache behavior
273
274    let chunk_size = (out_dim + num_threads - 1) / num_threads;
275    let num_blocks_per_row = (in_dim + SUPER_BLOCK_SIZE - 1) / SUPER_BLOCK_SIZE;
276    let row_bytes = num_blocks_per_row * SUPER_BLOCK_BYTES;
277
278    // Uninit: compute_chunk writes *out_val = sum/hsum(acc) (SET) for every element.
279    let mut output: Vec<f32> = Vec::with_capacity(out_dim);
280    // SAFETY: Each thread's compute_chunk writes every element in its chunk (SET).
281    unsafe {
282        output.set_len(out_dim);
283    }
284    let has_avx2 = is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma");
285
286    thread::scope(|s| {
287        let input_ref = input;
288        let q6k_ref = q6k_data;
289        // CGP-DBUF: iterate directly instead of collecting into Vec.
290        for (chunk_idx, chunk) in output.chunks_mut(chunk_size).enumerate() {
291            let start_row = chunk_idx * chunk_size;
292
293            s.spawn(move || {
294                if has_avx2 {
295                    // SAFETY: AVX2+FMA availability verified via is_x86_feature_detected!()
296                    // before thread::scope entry; has_avx2 captures that result.
297                    unsafe {
298                        compute_chunk_avx2(
299                            q6k_ref,
300                            input_ref,
301                            chunk,
302                            start_row,
303                            out_dim,
304                            in_dim,
305                            num_blocks_per_row,
306                            row_bytes,
307                        );
308                    }
309                } else {
310                    compute_chunk_scalar(
311                        q6k_ref,
312                        input_ref,
313                        chunk,
314                        start_row,
315                        out_dim,
316                        in_dim,
317                        num_blocks_per_row,
318                        row_bytes,
319                    );
320                }
321            });
322        }
323    });
324
325    output
326}
327
328/// Fallback for non-x86_64
329#[cfg(not(target_arch = "x86_64"))]
330fn matmul_q6k_f32_parallel(
331    q6k_data: &[u8],
332    input: &[f32],
333    out_dim: usize,
334    in_dim: usize,
335) -> Vec<f32> {
336    matmul_q6k_f32_scalar(q6k_data, input, out_dim, in_dim)
337}
338
339#[cfg(target_arch = "x86_64")]
340#[target_feature(enable = "avx2", enable = "fma")]
341// SAFETY: Caller ensures AVX2+FMA are available and chunk bounds are valid
342unsafe fn compute_chunk_avx2(
343    q6k_data: &[u8],
344    input: &[f32],
345    chunk: &mut [f32],
346    start_row: usize,
347    out_dim: usize,
348    in_dim: usize,
349    num_blocks_per_row: usize,
350    row_bytes: usize,
351) {
352    unsafe {
353        use std::arch::x86_64::*;
354
355        for (local_idx, out_val) in chunk.iter_mut().enumerate() {
356            let out_idx = start_row + local_idx;
357            if out_idx >= out_dim {
358                break;
359            }
360
361            let row_start = out_idx * row_bytes;
362            let mut acc = _mm256_setzero_ps();
363
364            for sb_idx in 0..num_blocks_per_row {
365                let sb_start = row_start + sb_idx * SUPER_BLOCK_BYTES;
366                if sb_start + SUPER_BLOCK_BYTES > q6k_data.len() {
367                    break;
368                }
369                let sb_data = &q6k_data[sb_start..sb_start + SUPER_BLOCK_BYTES];
370                let input_offset = sb_idx * SUPER_BLOCK_SIZE;
371                process_q6k_superblock_avx2(sb_data, input, input_offset, in_dim, &mut acc);
372            }
373
374            *out_val = hsum_q6k_avx2(acc);
375        }
376    }
377}
378
379#[cfg(any(target_arch = "x86_64", test))]
380pub(crate) fn compute_chunk_scalar(
381    q6k_data: &[u8],
382    input: &[f32],
383    chunk: &mut [f32],
384    start_row: usize,
385    out_dim: usize,
386    in_dim: usize,
387    num_blocks_per_row: usize,
388    row_bytes: usize,
389) {
390    for (local_idx, out_val) in chunk.iter_mut().enumerate() {
391        let out_idx = start_row + local_idx;
392        if out_idx >= out_dim {
393            break;
394        }
395
396        let row_start = out_idx * row_bytes;
397        let mut sum = 0.0f32;
398
399        for sb_idx in 0..num_blocks_per_row {
400            let sb_start = row_start + sb_idx * SUPER_BLOCK_BYTES;
401            if sb_start + SUPER_BLOCK_BYTES > q6k_data.len() {
402                break;
403            }
404            let sb_data = &q6k_data[sb_start..sb_start + SUPER_BLOCK_BYTES];
405            let input_offset = sb_idx * SUPER_BLOCK_SIZE;
406            sum += process_q6k_superblock_scalar(sb_data, input, input_offset, in_dim);
407        }
408
409        *out_val = sum;
410    }
411}
412
413/// Public alias for the optimized Q6K matmul
414pub fn matmul_q6k_f32(q6k_data: &[u8], input: &[f32], out_dim: usize, in_dim: usize) -> Vec<f32> {
415    matmul_q6k_f32_dispatch(q6k_data, input, out_dim, in_dim)
416}