Skip to main content

cortiq_engine/
qtensor.rs

1//! QTensor — weight tensor with pluggable storage.
2//!
3//! Two backings, one interface:
4//! - `F32`   — owned dense floats (small models, tests). Every operation
5//!   is bit-identical to the historical `&[f32]` code paths.
6//! - `Mapped` — quantized bytes zero-copy from the CMF mmap (`q8_row` /
7//!   `q8_2f`). The matvec is fused: int8 rows × f32 activations, the
8//!   q8_2f column field folds into a pre-scale of the input
9//!   (`x'[i] = col[i]·x[i]`), so the inner loop is the same i8 dot as
10//!   q8_row. This is what lets a 15B file run in a few GB of RSS.
11//!
12//! Extension point: new dtypes = new match arm here, nothing else moves.
13
14use crate::pool::{matvec_rows, matvec_rows2, Pool};
15use cortiq_core::quant::{f16_to_f32, GROUP_SIZE, Q4_TILE};
16use cortiq_core::{CmfModel, TensorDtype};
17use std::sync::Arc;
18
19pub enum QTensor {
20    F32 {
21        data: Vec<f32>,
22        rows: usize,
23        cols: usize,
24    },
25    Mapped {
26        model: Arc<CmfModel>,
27        /// Index into the model's tensor directory.
28        idx: usize,
29        dtype: TensorDtype,
30        rows: usize,
31        cols: usize,
32        /// Per-row scales, dequantized to f32 up front (tiny).
33        row_scale: Vec<f32>,
34        /// q8_2f column field (θ), dequantized up front; empty for q8_row.
35        col_field: Vec<f32>,
36        /// Vbit only: byte offset of each row's packed data within the
37        /// tensor blob (`[rows + 1]`, computed once at load — the per-
38        /// matvec prefix scan over row bit-widths was O(rows) each call).
39        vbit_offsets: Vec<usize>,
40    },
41}
42
43/// Prefix-sum of vbit row payload offsets (absolute within the tensor
44/// bytes). `offsets[r]..offsets[r+1]` is row r's packed data.
45fn vbit_row_offsets(bytes: &[u8], rows: usize, cols: usize) -> Vec<usize> {
46    let ng = cols / GROUP_SIZE;
47    let bits = &bytes[..rows];
48    let mut offsets = Vec::with_capacity(rows + 1);
49    let mut off = rows + rows * ng * 2;
50    for r in 0..rows {
51        offsets.push(off);
52        off += (cols * bits[r] as usize + 7) / 8;
53    }
54    offsets.push(off);
55    offsets
56}
57
58impl QTensor {
59    pub fn from_f32(data: Vec<f32>, rows: usize, cols: usize) -> Self {
60        debug_assert_eq!(data.len(), rows * cols);
61        Self::F32 { data, rows, cols }
62    }
63
64    /// Wrap a directory tensor without dequantizing the payload.
65    /// Falls back to dequantized f32 for dtypes without a fused kernel.
66    pub fn from_model(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
67        // Indexed lookup: the linear directory scan made pipeline build
68        // O(N²) on MoE/skills files with thousands of tensors.
69        let idx = model
70            .tensor_index(name)
71            .ok_or_else(|| format!("tensor '{name}' not found in CMF directory"))?;
72        let entry = &model.tensors[idx];
73        if entry.shape.len() != 2 {
74            return Err(format!("QTensor::from_model needs 2-D, got '{name}'"));
75        }
76        let (rows, cols) = (entry.shape[0], entry.shape[1]);
77        let bytes = model.entry_bytes(entry);
78
79        match entry.dtype {
80            TensorDtype::Q8Row | TensorDtype::Q8_2f => {
81                let n = rows * cols;
82                let scales_off = n;
83                let row_scale: Vec<f32> = (0..rows)
84                    .map(|o| {
85                        f16_to_f32(u16::from_le_bytes([
86                            bytes[scales_off + o * 2],
87                            bytes[scales_off + o * 2 + 1],
88                        ]))
89                    })
90                    .collect();
91                let col_field: Vec<f32> = if entry.dtype == TensorDtype::Q8_2f {
92                    let col_off = n + rows * 2;
93                    (0..cols)
94                        .map(|i| {
95                            f16_to_f32(u16::from_le_bytes([
96                                bytes[col_off + i * 2],
97                                bytes[col_off + i * 2 + 1],
98                            ]))
99                        })
100                        .collect()
101                } else {
102                    Vec::new()
103                };
104                Ok(Self::Mapped {
105                    model: model.clone(),
106                    idx,
107                    dtype: entry.dtype,
108                    rows,
109                    cols,
110                    row_scale,
111                    col_field,
112                    vbit_offsets: Vec::new(),
113                })
114            }
115            // vbit: fused kernel unpacks variable-bit rows from mmap.
116            TensorDtype::Vbit if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
117                model: model.clone(),
118                idx,
119                dtype: entry.dtype,
120                rows,
121                cols,
122                row_scale: Vec::new(),
123                col_field: Vec::new(),
124                vbit_offsets: vbit_row_offsets(bytes, rows, cols),
125            }),
126            // vbit_ro (§4.2): the offset table comes straight from the
127            // file — no load-time prefix scan; kernels are shared with
128            // legacy vbit (they consume absolute offsets either way).
129            TensorDtype::VbitRo if cols % GROUP_SIZE == 0 => {
130                let (_, off_off, packed_off) =
131                    cortiq_core::quant::vbit_ro_sections(rows, cols);
132                let offsets: Vec<usize> = (0..=rows)
133                    .map(|r| {
134                        packed_off + cortiq_core::quant::vbit_ro_offset(bytes, off_off, r)
135                    })
136                    .collect();
137                Ok(Self::Mapped {
138                    model: model.clone(),
139                    idx,
140                    dtype: entry.dtype,
141                    rows,
142                    cols,
143                    row_scale: Vec::new(),
144                    col_field: Vec::new(),
145                    vbit_offsets: offsets,
146                })
147            }
148            // q4_block: fused kernel reads nibbles straight from mmap —
149            // a 14B q4 file no longer explodes into ×8 f32 RAM.
150            // q4_tiled (§4.3): interleaved [scale][nibbles] tiles — one
151            // sequential memory stream (measured ×1.66 ARM / ×1.13 AVX2
152            // at kernel level over the split layout).
153            TensorDtype::Q4Tiled if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
154                model: model.clone(),
155                idx,
156                dtype: entry.dtype,
157                rows,
158                cols,
159                row_scale: Vec::new(),
160                col_field: Vec::new(),
161                vbit_offsets: Vec::new(),
162            }),
163            TensorDtype::Q4Block if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
164                model: model.clone(),
165                idx,
166                dtype: entry.dtype,
167                rows,
168                cols,
169                row_scale: Vec::new(),
170                col_field: Vec::new(),
171                vbit_offsets: Vec::new(),
172            }),
173            // No fused kernel yet → dequantize once (correct, more RAM).
174            _ => {
175                let mut data = vec![0.0f32; rows * cols];
176                cortiq_core::quant::dequant_tensor(entry, bytes, &mut data)?;
177                Ok(Self::from_f32(data, rows, cols))
178            }
179        }
180    }
181
182    pub fn rows(&self) -> usize {
183        match self {
184            Self::F32 { rows, .. } | Self::Mapped { rows, .. } => *rows,
185        }
186    }
187
188    pub fn cols(&self) -> usize {
189        match self {
190            Self::F32 { cols, .. } | Self::Mapped { cols, .. } => *cols,
191        }
192    }
193
194    /// Dense f32 view — only for owned tensors. Masked/sparse execution
195    /// paths require it; quantized weights don't support masks yet.
196    pub fn as_f32(&self) -> Option<&[f32]> {
197        match self {
198            Self::F32 { data, .. } => Some(data),
199            Self::Mapped { .. } => None,
200        }
201    }
202
203    fn quant_bytes(&self) -> &[u8] {
204        match self {
205            Self::Mapped { model, idx, .. } => model.entry_bytes(&model.tensors[*idx]),
206            Self::F32 { .. } => unreachable!("quant_bytes on F32"),
207        }
208    }
209
210    /// Dequantize one row into `dst` (embedding lookup).
211    pub fn row_f32(&self, r: usize, dst: &mut [f32]) {
212        let cols = self.cols();
213        debug_assert_eq!(dst.len(), cols);
214        match self {
215            Self::F32 { data, .. } => dst.copy_from_slice(&data[r * cols..(r + 1) * cols]),
216            Self::Mapped {
217                dtype,
218                row_scale,
219                col_field,
220                vbit_offsets,
221                ..
222            } => {
223                if *dtype == TensorDtype::Q4Tiled {
224                    let bytes = self.quant_bytes();
225                    let gpr = cols / GROUP_SIZE;
226                    for gi in 0..gpr {
227                        let tile = &bytes[(r * gpr + gi) * Q4_TILE..(r * gpr + gi + 1) * Q4_TILE];
228                        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
229                        for (k, &b) in tile[2..].iter().enumerate() {
230                            dst[gi * GROUP_SIZE + k * 2] = ((b & 0x0F) as f32 - 8.0) * s;
231                            dst[gi * GROUP_SIZE + k * 2 + 1] = (((b >> 4) & 0x0F) as f32 - 8.0) * s;
232                        }
233                    }
234                    return;
235                }
236                if *dtype == TensorDtype::Q4Block {
237                    let (packed, scales) = q4_split(self.quant_bytes(), self.rows(), cols);
238                    let gpr = cols / GROUP_SIZE;
239                    for gi in 0..gpr {
240                        let g = r * gpr + gi;
241                        let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
242                        for (k, &b) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
243                            dst[gi * GROUP_SIZE + k * 2] = ((b & 0x0F) as f32 - 8.0) * s;
244                            dst[gi * GROUP_SIZE + k * 2 + 1] = (((b >> 4) & 0x0F) as f32 - 8.0) * s;
245                        }
246                    }
247                    return;
248                }
249                if matches!(dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
250                    let bytes = self.quant_bytes();
251                    let rows = self.rows();
252                    let ng = cols / GROUP_SIZE;
253                    let bits = &bytes[..rows];
254                    let sc_off = rows;
255                    // Precomputed at load — embedding lookup used to scan
256                    // the bit-widths of every preceding row (O(token_id)).
257                    let off = vbit_offsets[r];
258                    let b = bits[r] as usize;
259                    let l = ((1usize << (b - 1)) - 1) as f32;
260                    let data = &bytes[off..];
261                    let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
262                    for (i, d) in dst.iter_mut().enumerate() {
263                        while nbits < b {
264                            acc = (acc << 8) | data[idx] as u64;
265                            idx += 1;
266                            nbits += 8;
267                        }
268                        let u = ((acc >> (nbits - b)) & ((1u64 << b) - 1)) as f32;
269                        nbits -= b;
270                        let so = (r * ng + i / GROUP_SIZE) * 2;
271                        let sv = f16_to_f32(u16::from_le_bytes([
272                            bytes[sc_off + so],
273                            bytes[sc_off + so + 1],
274                        ]));
275                        *d = (u - l) * sv;
276                    }
277                    return;
278                }
279                let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
280                let s = row_scale[r];
281                match dtype {
282                    TensorDtype::Q8Row => {
283                        for (d, &b) in dst.iter_mut().zip(q) {
284                            *d = (b as i8) as f32 * s;
285                        }
286                    }
287                    TensorDtype::Q8_2f => {
288                        for (i, (d, &b)) in dst.iter_mut().zip(q).enumerate() {
289                            *d = (b as i8) as f32 * s * col_field[i];
290                        }
291                    }
292                    _ => unreachable!(),
293                }
294            }
295        }
296    }
297
298    /// Can this tensor's columns be read cheaply (for sparse down_proj)?
299    /// True for F32/Q8Row/Q8_2f (per-row scale, direct strided access);
300    /// false for group-packed q4/vbit (column access would unpack whole
301    /// groups — sparse execution falls back to f32 for those).
302    pub fn sparse_col_ok(&self) -> bool {
303        match self {
304            Self::F32 { .. } => true,
305            Self::Mapped { dtype, .. } => {
306                matches!(dtype, TensorDtype::Q8Row | TensorDtype::Q8_2f)
307            }
308        }
309    }
310
311    /// down_proj [hidden, inter]: accumulate `w · col(c)` into `out`
312    /// [hidden] — reads ONLY column `c` (one neuron) from the mmap,
313    /// no full-matrix dequant. `out[k] += w · down[k, c]`.
314    pub fn add_col_scaled(&self, c: usize, w: f32, out: &mut [f32]) {
315        let inter = self.cols();
316        let hidden = self.rows();
317        debug_assert_eq!(out.len(), hidden);
318        match self {
319            Self::F32 { data, .. } => {
320                for (k, o) in out.iter_mut().enumerate() {
321                    *o += w * data[k * inter + c];
322                }
323            }
324            Self::Mapped {
325                dtype, row_scale, col_field, ..
326            } => {
327                let q = self.quant_bytes();
328                let colf = if *dtype == TensorDtype::Q8_2f {
329                    col_field[c]
330                } else {
331                    1.0
332                };
333                let wc = w * colf;
334                for (k, o) in out.iter_mut().enumerate() {
335                    let b = q[k * inter + c] as i8 as f32;
336                    *o += wc * b * row_scale[k];
337                }
338            }
339        }
340    }
341
342    /// Dot of row `r` with `x` (gate/up active-neuron path). Reads only
343    /// row `r` from the mmap — no full dequant. q4/vbit dequant the row
344    /// into `scratch` first (rare for active-FFN weights).
345    pub fn row_dot(&self, r: usize, x: &[f32], scratch: &mut [f32]) -> f32 {
346        let cols = self.cols();
347        match self {
348            Self::F32 { data, .. } => {
349                let row = &data[r * cols..(r + 1) * cols];
350                row.iter().zip(x).map(|(w, v)| w * v).sum()
351            }
352            Self::Mapped { dtype, row_scale, col_field, .. } => match dtype {
353                TensorDtype::Q8Row => {
354                    let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
355                    dot_i8_f32(q, x) * row_scale[r]
356                }
357                TensorDtype::Q8_2f => {
358                    let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
359                    dot_i8_col_f32(q, x, col_field) * row_scale[r]
360                }
361                _ => {
362                    self.row_f32(r, scratch);
363                    scratch.iter().zip(x).map(|(w, v)| w * v).sum()
364                }
365            },
366        }
367    }
368
369    /// `out = W · x` (row-major). F32 delegates to the historical
370    /// bit-exact path; Mapped runs the fused int8 kernel.
371    pub fn matvec(&self, x: &[f32], out: &mut [f32], pool: Option<&Pool>) {
372        match self {
373            Self::F32 { data, .. } => matvec_rows(pool, data, x, out),
374            Self::Mapped {
375                model,
376                idx,
377                dtype,
378                rows,
379                cols,
380                row_scale,
381                col_field,
382                vbit_offsets,
383            } => {
384                let _ = (model, idx);
385                if *dtype == TensorDtype::Q4Block {
386                    q4matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
387                    return;
388                }
389                if *dtype == TensorDtype::Q4Tiled {
390                    q4t_matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
391                    return;
392                }
393                if matches!(dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
394                    vbitmatvec(self.quant_bytes(), vbit_offsets, x, *rows, *cols, out, pool);
395                    return;
396                }
397                let xs = prescale(x, col_field, *dtype);
398                // D5: large q8 matrices (lm_head-class) — hybrid
399                // CPU∥GPU: split the rows, both sides compute
400                // SIMULTANEOUSLY (same math, shared prescale).
401                // GPU share: CMF_GPU_SPLIT (0..1, default 0.5).
402                if *rows >= crate::gpu::min_rows()
403                    && matches!(dtype, TensorDtype::Q8Row | TensorDtype::Q8_2f)
404                    && std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0").unwrap_or(true)
405                    && crate::gpu::enabled_here()
406                {
407                    // Runtime probe: alternate the hybrid against the
408                    // pure-CPU matvec, keep whichever is faster HERE.
409                    let t0 = std::time::Instant::now();
410                    match crate::gpu::probe_arm(crate::gpu::OpClass::Matvec) {
411                        crate::gpu::ProbeArm::Gpu => {}
412                        crate::gpu::ProbeArm::CpuTimed => {
413                            qmatvec(self.quant_bytes(), row_scale, &xs, *rows, *cols, out, pool);
414                            crate::gpu::probe_record(
415                                crate::gpu::OpClass::Matvec,
416                                false,
417                                t0.elapsed(),
418                            );
419                            return;
420                        }
421                        crate::gpu::ProbeArm::Cpu => {
422                            qmatvec(self.quant_bytes(), row_scale, &xs, *rows, *cols, out, pool);
423                            return;
424                        }
425                    }
426                    let frac = std::env::var("CMF_GPU_SPLIT")
427                        .ok()
428                        .and_then(|v| v.parse::<f32>().ok())
429                        .unwrap_or(0.5)
430                        .clamp(0.0, 1.0);
431                    let cpu_rows = ((*rows as f32) * (1.0 - frac)) as usize;
432                    let (out_cpu, out_gpu) = out.split_at_mut(cpu_rows);
433                    let bytes = self.quant_bytes();
434                    let ok = std::thread::scope(|sc| {
435                        let g = sc.spawn(|| {
436                            crate::gpu::q8_matvec_range(
437                                model,
438                                *idx,
439                                cpu_rows,
440                                &row_scale[cpu_rows..],
441                                &xs,
442                                *rows - cpu_rows,
443                                *cols,
444                                out_gpu,
445                            )
446                        });
447                        if cpu_rows > 0 {
448                            qmatvec(
449                                &bytes[..cpu_rows * *cols],
450                                &row_scale[..cpu_rows],
451                                &xs,
452                                cpu_rows,
453                                *cols,
454                                out_cpu,
455                                pool,
456                            );
457                        }
458                        g.join().unwrap_or(false)
459                    });
460                    if ok {
461                        crate::gpu::probe_record(crate::gpu::OpClass::Matvec, true, t0.elapsed());
462                        return;
463                    }
464                    // GPU failed — CPU finishes its half.
465                    qmatvec(
466                        &bytes[cpu_rows * *cols..(*rows) * *cols],
467                        &row_scale[cpu_rows..],
468                        &xs,
469                        *rows - cpu_rows,
470                        *cols,
471                        out_gpu,
472                        pool,
473                    );
474                    return;
475                }
476                qmatvec(self.quant_bytes(), row_scale, &xs, *rows, *cols, out, pool);
477            }
478        }
479    }
480
481    /// Fused two-input matvec (MTP verify pair): weights streamed once.
482    pub fn matvec2(&self, x1: &[f32], x2: &[f32], o1: &mut [f32], o2: &mut [f32], pool: Option<&Pool>) {
483        match self {
484            Self::F32 { data, .. } => matvec_rows2(pool, data, x1, x2, o1, o2),
485            Self::Mapped {
486                dtype,
487                rows,
488                cols,
489                row_scale,
490                col_field,
491                vbit_offsets,
492                ..
493            } => {
494                if *dtype == TensorDtype::Q4Block {
495                    q4matvec2(self.quant_bytes(), x1, x2, *rows, *cols, o1, o2, pool);
496                    return;
497                }
498                if *dtype == TensorDtype::Q4Tiled {
499                    q4t_matvec2(self.quant_bytes(), x1, x2, *rows, *cols, o1, o2, pool);
500                    return;
501                }
502                if matches!(dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
503                    vbitmatvec2(self.quant_bytes(), vbit_offsets, x1, x2, *rows, *cols, o1, o2, pool);
504                    return;
505                }
506                let x1s = prescale(x1, col_field, *dtype);
507                let x2s = prescale(x2, col_field, *dtype);
508                qmatvec2(self.quant_bytes(), row_scale, &x1s, &x2s, *rows, *cols, o1, o2, pool);
509            }
510        }
511    }
512}
513
514impl QTensor {
515    /// Batched matvec (prefill-GEMM): xs — row-major [b, cols],
516    /// out — row-major [b, rows]. Element-wise semantics are IDENTICAL
517    /// to b matvec calls (same dot kernels in the same order); the win —
518    /// the weight row streams from DRAM once per batch, not b times.
519    pub fn matmat(&self, xs_all: &[f32], b: usize, out: &mut [f32], pool: Option<&Pool>) {
520        let cols = self.cols();
521        let rows = self.rows();
522        debug_assert_eq!(xs_all.len(), b * cols);
523        debug_assert_eq!(out.len(), b * rows);
524        match self {
525            Self::F32 { data, .. } => {
526                let out_addr = SendMut(out.as_mut_ptr());
527                let run = |start: usize, end: usize| {
528                    for o in start..end {
529                        let row = &data[o * cols..(o + 1) * cols];
530                        for bi in 0..b {
531                            let x = &xs_all[bi * cols..(bi + 1) * cols];
532                            let mut acc = 0f32;
533                            for j in 0..cols {
534                                acc += row[j] * x[j];
535                            }
536                            unsafe { *out_addr.at(bi * rows + o) = acc };
537                        }
538                    }
539                };
540                dispatch_rows(pool, rows, &run);
541            }
542            Self::Mapped {
543                dtype,
544                row_scale,
545                col_field,
546                vbit_offsets,
547                ..
548            } => {
549                if *dtype == TensorDtype::Q4Block {
550                    q4matmat(self.quant_bytes(), xs_all, b, rows, cols, out, pool);
551                    return;
552                }
553                if *dtype == TensorDtype::Q4Tiled {
554                    q4t_matmat(self.quant_bytes(), xs_all, b, rows, cols, out, pool);
555                    return;
556                }
557                if matches!(dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
558                    vbitmatmat(self.quant_bytes(), vbit_offsets, xs_all, b, rows, cols, out, pool);
559                    return;
560                }
561                let pre: Vec<std::borrow::Cow<'_, [f32]>> = (0..b)
562                    .map(|bi| {
563                        prescale(&xs_all[bi * cols..(bi + 1) * cols], col_field, *dtype)
564                    })
565                    .collect();
566                // D5: large prefill-batch GEMMs — on the GPU (threshold by
567                // work volume: submission carries b×rows×cols MACs).
568                // Runtime probe: the naive GEMM shader + sync readback
569                // lose to the CPU GEMM on slow driver stacks — alternate
570                // both arms and keep the winner.
571                if b >= 8
572                    && b * rows * cols >= 128_000_000
573                    && crate::gpu::enabled_here()
574                {
575                    if let Self::Mapped { model, idx, .. } = self {
576                        let t0 = std::time::Instant::now();
577                        match crate::gpu::probe_arm(crate::gpu::OpClass::Matmat) {
578                            crate::gpu::ProbeArm::Gpu
579                                if crate::gpu::probe_deciding(crate::gpu::OpClass::Matmat)
580                                    && !crate::gpu::q8_resident_or_upload(model, *idx) =>
581                            {
582                                // Cold weights during probing: the upload
583                                // has started, the count runs on the CPU —
584                                // the GPU arm samples on the next touch.
585                                let q = self.quant_bytes();
586                                qmatmat(q, row_scale, &pre, rows, cols, out, pool);
587                                return;
588                            }
589                            crate::gpu::ProbeArm::Gpu => {
590                                let flat: Vec<f32> =
591                                    pre.iter().flat_map(|v| v.iter().copied()).collect();
592                                if crate::gpu::q8_matmat(
593                                    model, *idx, row_scale, &flat, b, rows, cols, out)
594                                {
595                                    crate::gpu::probe_record(
596                                        crate::gpu::OpClass::Matmat,
597                                        true,
598                                        t0.elapsed(),
599                                    );
600                                    return;
601                                }
602                            }
603                            crate::gpu::ProbeArm::CpuTimed => {
604                                let q = self.quant_bytes();
605                                qmatmat(q, row_scale, &pre, rows, cols, out, pool);
606                                crate::gpu::probe_record(
607                                    crate::gpu::OpClass::Matmat,
608                                    false,
609                                    t0.elapsed(),
610                                );
611                                return;
612                            }
613                            crate::gpu::ProbeArm::Cpu => {}
614                        }
615                    }
616                }
617                let q = self.quant_bytes();
618                qmatmat(q, row_scale, &pre, rows, cols, out, pool);
619            }
620        }
621    }
622}
623
624impl QTensor {
625    /// Multi-matrix job (roadmap §3 P0): N tensors sharing one input
626    /// run under a SINGLE pool dispatch — QKV or gate+up cost one
627    /// barrier instead of N. Per-row math is the exact same kernel as
628    /// `matvec` (bit-identical outputs); only the dispatch is fused.
629    /// Falls back to N sequential matvecs when the set is not a uniform
630    /// q8-family/F32 group or there is no pool.
631    pub fn matvec_many<const N: usize>(
632        ts: [&QTensor; N],
633        x: &[f32],
634        mut outs: [&mut [f32]; N],
635        pool: Option<&Pool>,
636    ) {
637        let total_rows: usize = ts.iter().map(|t| t.rows()).sum();
638        let uniform_q8 = ts.iter().all(|t| {
639            matches!(
640                t,
641                Self::Mapped { dtype: TensorDtype::Q8Row | TensorDtype::Q8_2f, .. }
642            )
643        });
644        let uniform_f32 = ts.iter().all(|t| matches!(t, Self::F32 { .. }));
645        let uniform_q4 = ts
646            .iter()
647            .all(|t| matches!(t, Self::Mapped { dtype: TensorDtype::Q4Block, .. }));
648        let uniform_vbit = ts
649            .iter()
650            .all(|t| matches!(
651                t,
652                Self::Mapped { dtype: TensorDtype::Vbit | TensorDtype::VbitRo, .. }
653            ));
654        let Some(pool) = pool else {
655            for (t, o) in ts.iter().zip(outs.iter_mut()) {
656                t.matvec(x, o, None);
657            }
658            return;
659        };
660        if total_rows < 256 || !(uniform_q8 || uniform_f32 || uniform_q4 || uniform_vbit) {
661            for (t, o) in ts.iter().zip(outs.iter_mut()) {
662                t.matvec(x, o, Some(pool));
663            }
664            return;
665        }
666
667        if uniform_q4 || uniform_vbit {
668            let outs_addr: [SendMut; N] = std::array::from_fn(|i| SendMut(outs[i].as_mut_ptr()));
669            // q4/vbit share one activation split — no per-tensor col field.
670            if a8w8_enabled() {
671                let act = split_act(x);
672                let act = &act;
673                if uniform_q4 {
674                    let closures: [_; N] = std::array::from_fn(|i| {
675                        let (packed, scales) =
676                            q4_split(ts[i].quant_bytes(), ts[i].rows(), ts[i].cols());
677                        let (gpr, cols, out) =
678                            (ts[i].cols() / GROUP_SIZE, ts[i].cols(), outs_addr[i]);
679                        move |s: usize, e: usize| {
680                            q4_range_a8w8(packed, scales, gpr, cols, act, out, s, e)
681                        }
682                    });
683                    let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
684                        std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
685                    pool.run_many(&parts);
686                } else {
687                    let closures: [_; N] = std::array::from_fn(|i| {
688                        let Self::Mapped { vbit_offsets, .. } = ts[i] else { unreachable!() };
689                        let (bytes, rows, cols, out) =
690                            (ts[i].quant_bytes(), ts[i].rows(), ts[i].cols(), outs_addr[i]);
691                        move |s: usize, e: usize| {
692                            vbit_range_a8w8(bytes, vbit_offsets, x, act, rows, cols, out, s, e)
693                        }
694                    });
695                    let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
696                        std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
697                    pool.run_many(&parts);
698                }
699                return;
700            }
701            if uniform_q4 {
702                let closures: [_; N] = std::array::from_fn(|i| {
703                    let (packed, scales) =
704                        q4_split(ts[i].quant_bytes(), ts[i].rows(), ts[i].cols());
705                    let (gpr, out) = (ts[i].cols() / GROUP_SIZE, outs_addr[i]);
706                    move |s: usize, e: usize| q4_range_f32(packed, scales, gpr, x, out, s, e)
707                });
708                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
709                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
710                pool.run_many(&parts);
711            } else {
712                let closures: [_; N] = std::array::from_fn(|i| {
713                    let Self::Mapped { vbit_offsets, .. } = ts[i] else { unreachable!() };
714                    let (bytes, rows, cols, out) =
715                        (ts[i].quant_bytes(), ts[i].rows(), ts[i].cols(), outs_addr[i]);
716                    move |s: usize, e: usize| {
717                        vbit_range_f32(bytes, vbit_offsets, x, rows, cols, out, s, e)
718                    }
719                });
720                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
721                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
722                pool.run_many(&parts);
723            }
724            return;
725        }
726
727        if uniform_f32 {
728            let outs_addr: [SendMut; N] = std::array::from_fn(|i| SendMut(outs[i].as_mut_ptr()));
729            let closures: [_; N] = std::array::from_fn(|i| {
730                let Self::F32 { data, cols, .. } = ts[i] else { unreachable!() };
731                let out = outs_addr[i];
732                move |start: usize, end: usize| {
733                    for o in start..end {
734                        let row = &data[o * cols..(o + 1) * cols];
735                        let mut sum = 0.0f32;
736                        for j in 0..*cols {
737                            sum += row[j] * x[j];
738                        }
739                        // SAFETY: disjoint (tensor, row) cells per worker.
740                        unsafe { *out.at(o) = sum };
741                    }
742                }
743            });
744            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
745                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
746            pool.run_many(&parts);
747            return;
748        }
749
750        // Uniform q8-family: per-tensor prescale (q8_2f col fields
751        // differ per tensor) + the shared range kernels.
752        struct Ctx<'a> {
753            bytes: &'a [u8],
754            row_scale: &'a [f32],
755            cols: usize,
756            xs: std::borrow::Cow<'a, [f32]>,
757        }
758        let ctxs: [Ctx<'_>; N] = std::array::from_fn(|i| {
759            let Self::Mapped { dtype, cols, row_scale, col_field, .. } = ts[i] else {
760                unreachable!()
761            };
762            Ctx {
763                bytes: ts[i].quant_bytes(),
764                row_scale,
765                cols: *cols,
766                xs: prescale(x, col_field, *dtype),
767            }
768        });
769        let outs_addr: [SendMut; N] = std::array::from_fn(|i| SendMut(outs[i].as_mut_ptr()));
770        #[cfg(target_arch = "aarch64")]
771        if sdot_enabled() {
772            let acts: [SplitAct; N] = std::array::from_fn(|i| split_act(&ctxs[i].xs));
773            let closures: [_; N] = std::array::from_fn(|i| {
774                let (c, act, out) = (&ctxs[i], &acts[i], outs_addr[i]);
775                move |start: usize, end: usize| {
776                    q8_range_sdot(c.bytes, c.row_scale, act, c.cols, out, start, end)
777                }
778            });
779            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
780                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
781            pool.run_many(&parts);
782            return;
783        }
784        #[cfg(target_arch = "x86_64")]
785        if avx2_a8w8_enabled() {
786            let acts: [SplitAct; N] = std::array::from_fn(|i| split_act(&ctxs[i].xs));
787            let closures: [_; N] = std::array::from_fn(|i| {
788                let (c, act, out) = (&ctxs[i], &acts[i], outs_addr[i]);
789                move |start: usize, end: usize| {
790                    q8_range_avx2(c.bytes, c.row_scale, act, c.cols, out, start, end)
791                }
792            });
793            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
794                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
795            pool.run_many(&parts);
796            return;
797        }
798        let closures: [_; N] = std::array::from_fn(|i| {
799            let (c, out) = (&ctxs[i], outs_addr[i]);
800            move |start: usize, end: usize| {
801                q8_range_f32(c.bytes, c.row_scale, &c.xs, c.cols, out, start, end)
802            }
803        });
804        let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
805            std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
806        pool.run_many(&parts);
807    }
808}
809
810impl QTensor {
811    /// Pair-input multi-matrix job: N tensors × 2 shared inputs under a
812    /// single pool dispatch — the MTP/pair decode path publishes one job
813    /// for Q/K/V (and one for gate+up) instead of one per tensor.
814    /// Per-row math is exactly `matvec2`'s kernels; bit-identical.
815    #[allow(clippy::needless_range_loop)]
816    pub fn matvec2_many<const N: usize>(
817        ts: [&QTensor; N],
818        x1: &[f32],
819        x2: &[f32],
820        mut o1s: [&mut [f32]; N],
821        mut o2s: [&mut [f32]; N],
822        pool: Option<&Pool>,
823    ) {
824        let total_rows: usize = ts.iter().map(|t| t.rows()).sum();
825        let uniform_q8 = ts.iter().all(|t| {
826            matches!(
827                t,
828                Self::Mapped { dtype: TensorDtype::Q8Row | TensorDtype::Q8_2f, .. }
829            )
830        });
831        let uniform_f32 = ts.iter().all(|t| matches!(t, Self::F32 { .. }));
832        let uniform_q4 = ts
833            .iter()
834            .all(|t| matches!(t, Self::Mapped { dtype: TensorDtype::Q4Block, .. }));
835        let uniform_vbit = ts
836            .iter()
837            .all(|t| matches!(
838                t,
839                Self::Mapped { dtype: TensorDtype::Vbit | TensorDtype::VbitRo, .. }
840            ));
841        let fusable = pool.is_some()
842            && total_rows >= 256
843            && (uniform_q8 || uniform_f32 || uniform_q4 || uniform_vbit);
844        if !fusable {
845            for i in 0..N {
846                ts[i].matvec2(x1, x2, o1s[i], o2s[i], pool);
847            }
848            return;
849        }
850        let pool = pool.unwrap();
851
852        if uniform_q4 || uniform_vbit {
853            let p1: [SendMut; N] = std::array::from_fn(|i| SendMut(o1s[i].as_mut_ptr()));
854            let p2: [SendMut; N] = std::array::from_fn(|i| SendMut(o2s[i].as_mut_ptr()));
855            // q4/vbit share activation splits — no per-tensor col field.
856            if a8w8_enabled() {
857                let a1 = split_act(x1);
858                let a2 = split_act(x2);
859                let (a1, a2) = (&a1, &a2);
860                if uniform_q4 {
861                    let closures: [_; N] = std::array::from_fn(|i| {
862                        let (packed, scales) =
863                            q4_split(ts[i].quant_bytes(), ts[i].rows(), ts[i].cols());
864                        let (gpr, cols, o1, o2) =
865                            (ts[i].cols() / GROUP_SIZE, ts[i].cols(), p1[i], p2[i]);
866                        move |s: usize, e: usize| {
867                            q4_range2_a8w8(packed, scales, gpr, cols, a1, a2, o1, o2, s, e)
868                        }
869                    });
870                    let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
871                        std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
872                    pool.run_many(&parts);
873                } else {
874                    let closures: [_; N] = std::array::from_fn(|i| {
875                        let Self::Mapped { vbit_offsets, .. } = ts[i] else { unreachable!() };
876                        let (bytes, rows, cols, o1, o2) =
877                            (ts[i].quant_bytes(), ts[i].rows(), ts[i].cols(), p1[i], p2[i]);
878                        move |s: usize, e: usize| {
879                            vbit_range2_a8w8(
880                                bytes, vbit_offsets, x1, x2, a1, a2, rows, cols, o1, o2, s, e,
881                            )
882                        }
883                    });
884                    let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
885                        std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
886                    pool.run_many(&parts);
887                }
888                return;
889            }
890            if uniform_q4 {
891                let closures: [_; N] = std::array::from_fn(|i| {
892                    let (packed, scales) =
893                        q4_split(ts[i].quant_bytes(), ts[i].rows(), ts[i].cols());
894                    let (gpr, o1, o2) = (ts[i].cols() / GROUP_SIZE, p1[i], p2[i]);
895                    move |s: usize, e: usize| {
896                        q4_range2_f32(packed, scales, gpr, x1, x2, o1, o2, s, e)
897                    }
898                });
899                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
900                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
901                pool.run_many(&parts);
902            } else {
903                let closures: [_; N] = std::array::from_fn(|i| {
904                    let Self::Mapped { vbit_offsets, .. } = ts[i] else { unreachable!() };
905                    let (bytes, rows, cols, o1, o2) =
906                        (ts[i].quant_bytes(), ts[i].rows(), ts[i].cols(), p1[i], p2[i]);
907                    move |s: usize, e: usize| {
908                        vbit_range2_f32(bytes, vbit_offsets, x1, x2, rows, cols, o1, o2, s, e)
909                    }
910                });
911                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
912                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
913                pool.run_many(&parts);
914            }
915            return;
916        }
917
918        if uniform_f32 {
919            let p1: [SendMut; N] = std::array::from_fn(|i| SendMut(o1s[i].as_mut_ptr()));
920            let p2: [SendMut; N] = std::array::from_fn(|i| SendMut(o2s[i].as_mut_ptr()));
921            let closures: [_; N] = std::array::from_fn(|i| {
922                let Self::F32 { data, cols, .. } = ts[i] else { unreachable!() };
923                let (o1, o2) = (p1[i], p2[i]);
924                move |start: usize, end: usize| {
925                    for o in start..end {
926                        let row = &data[o * cols..(o + 1) * cols];
927                        let (mut s1, mut s2) = (0.0f32, 0.0f32);
928                        for j in 0..*cols {
929                            s1 += row[j] * x1[j];
930                            s2 += row[j] * x2[j];
931                        }
932                        // SAFETY: disjoint (tensor, row) cells per worker.
933                        unsafe {
934                            *o1.at(o) = s1;
935                            *o2.at(o) = s2;
936                        }
937                    }
938                }
939            });
940            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
941                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
942            pool.run_many(&parts);
943            return;
944        }
945
946        struct Ctx<'a> {
947            bytes: &'a [u8],
948            row_scale: &'a [f32],
949            cols: usize,
950            xs1: std::borrow::Cow<'a, [f32]>,
951            xs2: std::borrow::Cow<'a, [f32]>,
952        }
953        let ctxs: [Ctx<'_>; N] = std::array::from_fn(|i| {
954            let Self::Mapped { dtype, cols, row_scale, col_field, .. } = ts[i] else {
955                unreachable!()
956            };
957            Ctx {
958                bytes: ts[i].quant_bytes(),
959                row_scale,
960                cols: *cols,
961                xs1: prescale(x1, col_field, *dtype),
962                xs2: prescale(x2, col_field, *dtype),
963            }
964        });
965        let p1: [SendMut; N] = std::array::from_fn(|i| SendMut(o1s[i].as_mut_ptr()));
966        let p2: [SendMut; N] = std::array::from_fn(|i| SendMut(o2s[i].as_mut_ptr()));
967        #[cfg(target_arch = "aarch64")]
968        if sdot_enabled() {
969            let acts: [(SplitAct, SplitAct); N] =
970                std::array::from_fn(|i| (split_act(&ctxs[i].xs1), split_act(&ctxs[i].xs2)));
971            let closures: [_; N] = std::array::from_fn(|i| {
972                let (c, a, o1, o2) = (&ctxs[i], &acts[i], p1[i], p2[i]);
973                move |start: usize, end: usize| {
974                    q8_range2_sdot(c.bytes, c.row_scale, &a.0, &a.1, c.cols, o1, o2, start, end)
975                }
976            });
977            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
978                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
979            pool.run_many(&parts);
980            return;
981        }
982        #[cfg(target_arch = "x86_64")]
983        if avx2_a8w8_enabled() {
984            let acts: [(SplitAct, SplitAct); N] =
985                std::array::from_fn(|i| (split_act(&ctxs[i].xs1), split_act(&ctxs[i].xs2)));
986            let closures: [_; N] = std::array::from_fn(|i| {
987                let (c, a, o1, o2) = (&ctxs[i], &acts[i], p1[i], p2[i]);
988                move |start: usize, end: usize| {
989                    q8_range2_avx2(c.bytes, c.row_scale, &a.0, &a.1, c.cols, o1, o2, start, end)
990                }
991            });
992            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
993                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
994            pool.run_many(&parts);
995            return;
996        }
997        let closures: [_; N] = std::array::from_fn(|i| {
998            let (c, o1, o2) = (&ctxs[i], p1[i], p2[i]);
999            move |start: usize, end: usize| {
1000                q8_range2_f32(c.bytes, c.row_scale, &c.xs1, &c.xs2, c.cols, o1, o2, start, end)
1001            }
1002        });
1003        let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1004            std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1005        pool.run_many(&parts);
1006    }
1007}
1008
1009/// Batched q8 kernel: same math as qmatvec, the row makes a single
1010/// pass from memory for the whole batch.
1011fn qmatmat(
1012    q: &[u8],
1013    row_scale: &[f32],
1014    pre: &[std::borrow::Cow<'_, [f32]>],
1015    rows: usize,
1016    cols: usize,
1017    out: &mut [f32],
1018    pool: Option<&Pool>,
1019) {
1020    let b = pre.len();
1021    debug_assert_eq!(out.len(), b * rows);
1022    #[cfg(target_arch = "aarch64")]
1023    if sdot_enabled() {
1024        let acts: Vec<SplitAct> = pre.iter().map(|x| split_act(x)).collect();
1025        let out_addr = SendMut(out.as_mut_ptr());
1026        let run = |start: usize, end: usize| {
1027            for o in start..end {
1028                let row = &q[o * cols..(o + 1) * cols];
1029                for (bi, act) in acts.iter().enumerate() {
1030                    let v = row_dot_sdot(row, act) * row_scale[o];
1031                    unsafe { *out_addr.at(bi * rows + o) = v };
1032                }
1033            }
1034        };
1035        dispatch_rows(pool, rows, &run);
1036        return;
1037    }
1038    // x86 A8W8 batch: each weight row streams once for the whole chunk
1039    // through the AVX2/VNNI row dot (prefill q8 was the last
1040    // aarch64-only batched path).
1041    #[cfg(target_arch = "x86_64")]
1042    if avx2_a8w8_enabled() {
1043        let acts: Vec<SplitAct> = pre.iter().map(|x| split_act(x)).collect();
1044        let out_addr = SendMut(out.as_mut_ptr());
1045        let run = |start: usize, end: usize| {
1046            for o in start..end {
1047                let row = &q[o * cols..(o + 1) * cols];
1048                for (bi, act) in acts.iter().enumerate() {
1049                    let v = row_dot_avx2(row, act) * row_scale[o];
1050                    unsafe { *out_addr.at(bi * rows + o) = v };
1051                }
1052            }
1053        };
1054        dispatch_rows(pool, rows, &run);
1055        return;
1056    }
1057    let out_addr = SendMut(out.as_mut_ptr());
1058    let run = |start: usize, end: usize| {
1059        for o in start..end {
1060            let row = &q[o * cols..(o + 1) * cols];
1061            for (bi, x) in pre.iter().enumerate() {
1062                let mut acc = 0f32;
1063                for j in 0..cols {
1064                    acc += (row[j] as i8) as f32 * x[j];
1065                }
1066                unsafe { *out_addr.at(bi * rows + o) = acc * row_scale[o] };
1067            }
1068        }
1069    };
1070    dispatch_rows(pool, rows, &run);
1071}
1072
1073/// Split rows across pool workers (shared qmatvec pattern). Self-balancing
1074/// — see `Pool::run_rows` for why a static 1/n split is wrong here.
1075fn dispatch_rows(pool: Option<&Pool>, rows: usize, run: &(dyn Fn(usize, usize) + Sync)) {
1076    match pool {
1077        Some(pool) if rows >= 256 => pool.run_rows(rows, run),
1078        _ => run(0, rows),
1079    }
1080}
1081
1082/// Split a q4_block blob into (packed nibbles, f16 group scales).
1083fn q4_split(bytes: &[u8], rows: usize, cols: usize) -> (&[u8], &[u8]) {
1084    let groups = rows * cols / GROUP_SIZE;
1085    bytes.split_at(groups * 16)
1086}
1087
1088/// SIMD unpack for the dominant vbit width B=4 (94% of rows on the
1089/// log2-shape calibration): 16 packed bytes -> 32 centered i8 values.
1090/// vbit packs MSB-first, so the HIGH nibble is the even element
1091/// (opposite of q4_block's lo-first interleave). Centering is u-7.
1092#[inline]
1093fn vbit_fill4(data: &[u8], buf: &mut [u8]) {
1094    #[cfg(target_arch = "aarch64")]
1095    unsafe {
1096        return vbit_fill4_neon(data, buf);
1097    }
1098    #[cfg(target_arch = "x86_64")]
1099    if avx2_enabled() {
1100        return unsafe { vbit_fill4_avx2(data, buf) };
1101    }
1102    #[allow(unreachable_code)]
1103    for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
1104        let u = unpack8::<4>(&data[blk * 4..]);
1105        for k in 0..8 {
1106            chunk[k] = (u[k] - 7) as i8 as u8;
1107        }
1108    }
1109}
1110
1111#[cfg(target_arch = "aarch64")]
1112#[target_feature(enable = "neon")]
1113unsafe fn vbit_fill4_neon(data: &[u8], buf: &mut [u8]) {
1114    // SAFETY: buf.len() is a multiple of GROUP_SIZE=32; data holds
1115    // buf.len()/2 packed bytes (validated at load).
1116    unsafe {
1117        use core::arch::aarch64::*;
1118        let n = buf.len();
1119        let mask = vdupq_n_u8(0x0F);
1120        let seven = vdupq_n_s8(7);
1121        let mut g = 0usize;
1122        while g * 32 + 32 <= n {
1123            let b = vld1q_u8(data.as_ptr().add(g * 16));
1124            let hi = vshrq_n_u8::<4>(b);
1125            let lo = vandq_u8(b, mask);
1126            let z0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(hi, lo)), seven);
1127            let z1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(hi, lo)), seven);
1128            vst1q_u8(buf.as_mut_ptr().add(g * 32), vreinterpretq_u8_s8(z0));
1129            vst1q_u8(buf.as_mut_ptr().add(g * 32 + 16), vreinterpretq_u8_s8(z1));
1130            g += 1;
1131        }
1132    }
1133}
1134
1135#[cfg(target_arch = "x86_64")]
1136#[target_feature(enable = "avx2")]
1137unsafe fn vbit_fill4_avx2(data: &[u8], buf: &mut [u8]) {
1138    // SAFETY: see vbit_fill4_neon.
1139    unsafe {
1140        use core::arch::x86_64::*;
1141        let n = buf.len();
1142        let mask = _mm_set1_epi8(0x0F);
1143        let seven = _mm256_set1_epi8(7);
1144        let mut g = 0usize;
1145        while g * 32 + 32 <= n {
1146            let b = _mm_loadu_si128(data.as_ptr().add(g * 16) as *const __m128i);
1147            let hi = _mm_and_si128(_mm_srli_epi16::<4>(b), mask);
1148            let lo = _mm_and_si128(b, mask);
1149            let z = _mm256_sub_epi8(
1150                _mm256_set_m128i(_mm_unpackhi_epi8(hi, lo), _mm_unpacklo_epi8(hi, lo)),
1151                seven,
1152            );
1153            _mm256_storeu_si256(buf.as_mut_ptr().add(g * 32) as *mut __m256i, z);
1154            g += 1;
1155        }
1156    }
1157}
1158
1159/// Unpack 8 MSB-first B-bit values from exactly B bytes (fixed shifts —
1160/// no serial bit-buffer, auto-vectorizable). Every 32-value group starts
1161/// byte-aligned (32·B/8 is integral for B∈3..8), so groups decompose
1162/// into 4 such blocks.
1163#[inline(always)]
1164fn unpack8<const B: usize>(data: &[u8]) -> [i32; 8] {
1165    let mut acc = 0u64;
1166    for i in 0..B {
1167        acc = (acc << 8) | data[i] as u64;
1168    }
1169    let mask = (1u64 << B) - 1;
1170    let mut out = [0i32; 8];
1171    for (k, o) in out.iter_mut().enumerate() {
1172        *o = ((acc >> ((7 - k) * B)) & mask) as i32;
1173    }
1174    out
1175}
1176
1177/// Fused vbit matvec straight from the mapped bytes (spec §3, P13
1178/// FIG.3): [u8 bits: rows][f16 scales: rows·cols/32][bit-packed rows,
1179/// MSB-first, byte-padded]. Row data offsets are precomputed at load
1180/// (`vbit_row_offsets`) — the per-call prefix scan was O(rows) pure
1181/// overhead on every matvec.
1182#[allow(clippy::too_many_arguments)]
1183fn vbitmatvec(
1184    bytes: &[u8],
1185    offsets: &[usize],
1186    x: &[f32],
1187    rows: usize,
1188    cols: usize,
1189    out: &mut [f32],
1190    pool: Option<&Pool>,
1191) {
1192    debug_assert_eq!(out.len(), rows);
1193    debug_assert_eq!(offsets.len(), rows + 1);
1194
1195    // SDOT path: unpack the row to centered i8 once, then per-group
1196    // int8 dot against the quantized activations — same A8W8 contract
1197    // as q8 (bounded noise; CMF_SDOT=0 keeps the exact scalar path).
1198    if a8w8_enabled() {
1199        let act = split_act(x);
1200        let out_addr = SendMut(out.as_mut_ptr());
1201        let run = move |start: usize, end: usize| {
1202            vbit_range_a8w8(bytes, offsets, x, &act, rows, cols, out_addr, start, end)
1203        };
1204        dispatch_rows(pool, rows, &run);
1205        return;
1206    }
1207
1208    let out_addr = SendMut(out.as_mut_ptr());
1209    let run = move |start: usize, end: usize| {
1210        vbit_range_f32(bytes, offsets, x, rows, cols, out_addr, start, end)
1211    };
1212    dispatch_rows(pool, rows, &run);
1213}
1214
1215/// One vbit row range via the A8W8 int8 path — kernel body of
1216/// `vbitmatvec`, extracted so multi-matrix jobs can drive it for
1217/// several tensors in one dispatch (b=8 rows go exact f32).
1218#[allow(clippy::too_many_arguments)]
1219fn vbit_range_a8w8(
1220    bytes: &[u8],
1221    offsets: &[usize],
1222    x: &[f32],
1223    act: &SplitAct,
1224    rows: usize,
1225    cols: usize,
1226    out: SendMut,
1227    start: usize,
1228    end: usize,
1229) {
1230    let ng = cols / GROUP_SIZE;
1231    let bits = &bytes[..rows];
1232    let sc_off = rows;
1233    let row_dot = |r: usize| -> f32 {
1234            let b = bits[r] as usize;
1235            let l = ((1i32 << (b - 1)) - 1) as i32;
1236            let mask = (1u64 << b) - 1;
1237            let data = &bytes[offsets[r]..offsets[r + 1]];
1238            if b == 8 {
1239                // u−L reaches 128 → does not fit i8; exact f32 path.
1240                let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
1241                let mut dot = 0f32;
1242                for g in 0..ng {
1243                    let so = (r * ng + g) * 2;
1244                    let sgf = f16_to_f32(u16::from_le_bytes([
1245                        bytes[sc_off + so],
1246                        bytes[sc_off + so + 1],
1247                    ]));
1248                    let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
1249                    let mut gd = 0f32;
1250                    for &xv in xg.iter() {
1251                        if nbits < 8 {
1252                            acc = (acc << 8) | data[idx] as u64;
1253                            idx += 1;
1254                            nbits += 8;
1255                        }
1256                        let u = ((acc >> (nbits - 8)) & 0xFF) as i32;
1257                        nbits -= 8;
1258                        gd += (u - l) as f32 * xv;
1259                    }
1260                    dot += gd * sgf;
1261                }
1262                return dot;
1263            }
1264            // Per-worker scratch: this closure runs for every row of the
1265            // tensor (lm_head ≈ 150k rows/token) — a heap allocation per
1266            // row was measurable pure overhead.
1267            thread_local! {
1268                static VBIT_SCRATCH: std::cell::RefCell<Vec<u8>> =
1269                    const { std::cell::RefCell::new(Vec::new()) };
1270            }
1271            #[inline(always)]
1272            fn fill<const B: usize>(data: &[u8], l: i32, buf: &mut [u8]) {
1273                for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
1274                    let u = unpack8::<B>(&data[blk * B..]);
1275                    for k in 0..8 {
1276                        chunk[k] = (u[k] - l) as i8 as u8;
1277                    }
1278                }
1279            }
1280            let _ = mask;
1281            VBIT_SCRATCH.with(|scratch| {
1282                let mut buf = scratch.borrow_mut();
1283                buf.resize(cols, 0);
1284                match b {
1285                    3 => fill::<3>(data, l, &mut buf),
1286                    4 => vbit_fill4(data, &mut buf),
1287                    5 => fill::<5>(data, l, &mut buf),
1288                    6 => fill::<6>(data, l, &mut buf),
1289                    _ => unreachable!(),
1290                }
1291                let mut dot = 0f32;
1292                for g in 0..ng {
1293                    let so = (r * ng + g) * 2;
1294                    let s = f16_to_f32(u16::from_le_bytes([
1295                        bytes[sc_off + so],
1296                        bytes[sc_off + so + 1],
1297                    ]));
1298                    let d = dot_i8_i8(
1299                        &buf[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
1300                        &act.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
1301                    ) as f32
1302                        * act.sx;
1303                    dot += d * s;
1304                }
1305                for &(j, xv) in &act.outliers {
1306                    let so = (r * ng + j / GROUP_SIZE) * 2;
1307                    let s = f16_to_f32(u16::from_le_bytes([
1308                        bytes[sc_off + so],
1309                        bytes[sc_off + so + 1],
1310                    ]));
1311                    // xq is zeroed at outlier slots — add the exact term.
1312                    dot += (buf[j] as i8) as f32 * s * xv;
1313                }
1314                dot
1315            })
1316    };
1317    for r in start..end {
1318        // SAFETY: disjoint row ranges per worker.
1319        unsafe { *out.at(r) = row_dot(r) };
1320    }
1321}
1322
1323/// Exact scalar vbit row range (same extraction, non-SDOT path).
1324#[allow(clippy::too_many_arguments)]
1325fn vbit_range_f32(
1326    bytes: &[u8],
1327    offsets: &[usize],
1328    x: &[f32],
1329    rows: usize,
1330    cols: usize,
1331    out: SendMut,
1332    start: usize,
1333    end: usize,
1334) {
1335    let ng = cols / GROUP_SIZE;
1336    let bits = &bytes[..rows];
1337    let sc_off = rows;
1338    // Per-bit-width specialized inner loops: the compiler unrolls the
1339    // constant shifts (the generic bit-buffer loop was branch-bound —
1340    // 5.6 vs 13.2 tok/s q4 on the 0.8B).
1341    #[inline(always)]
1342    fn dot_row<const B: usize>(
1343        data: &[u8],
1344        bytes: &[u8],
1345        sc_off: usize,
1346        r: usize,
1347        ng: usize,
1348        x: &[f32],
1349    ) -> f32 {
1350        let l = ((1i32 << (B - 1)) - 1) as f32;
1351        let gbytes = GROUP_SIZE * B / 8;
1352        let mut dot = 0f32;
1353        for g in 0..ng {
1354            let so = (r * ng + g) * 2;
1355            let s = f16_to_f32(u16::from_le_bytes([bytes[sc_off + so], bytes[sc_off + so + 1]]));
1356            let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
1357            let gd0 = &data[g * gbytes..(g + 1) * gbytes];
1358            let mut gd = 0f32;
1359            for blk in 0..GROUP_SIZE / 8 {
1360                let u = unpack8::<B>(&gd0[blk * B..]);
1361                let xb = &xg[blk * 8..blk * 8 + 8];
1362                for k in 0..8 {
1363                    gd += (u[k] as f32 - l) * xb[k];
1364                }
1365            }
1366            dot += gd * s;
1367        }
1368        dot
1369    }
1370    for r in start..end {
1371        let data = &bytes[offsets[r]..offsets[r + 1]];
1372        let v = match bits[r] {
1373            3 => dot_row::<3>(data, bytes, sc_off, r, ng, x),
1374            4 => dot_row::<4>(data, bytes, sc_off, r, ng, x),
1375            5 => dot_row::<5>(data, bytes, sc_off, r, ng, x),
1376            6 => dot_row::<6>(data, bytes, sc_off, r, ng, x),
1377            8 => dot_row::<8>(data, bytes, sc_off, r, ng, x),
1378            b => unreachable!("vbit bit-width {b} (validated at load)"),
1379        };
1380        // SAFETY: disjoint row ranges per worker.
1381        unsafe { *out.at(r) = v };
1382    }
1383}
1384
1385/// Fused two-input vbit matvec: each row is unpacked from the mmap ONCE
1386/// and dotted against BOTH activations (MTP verify / pair prefill used
1387/// to run two full matvecs — double weight traffic and double unpack).
1388/// Per-input math is identical to `vbitmatvec` → same accuracy contract.
1389#[allow(clippy::too_many_arguments)]
1390fn vbitmatvec2(
1391    bytes: &[u8],
1392    offsets: &[usize],
1393    x1: &[f32],
1394    x2: &[f32],
1395    rows: usize,
1396    cols: usize,
1397    o1: &mut [f32],
1398    o2: &mut [f32],
1399    pool: Option<&Pool>,
1400) {
1401    debug_assert_eq!(o1.len(), rows);
1402    debug_assert_eq!(o2.len(), rows);
1403
1404    if a8w8_enabled() {
1405        let a1 = split_act(x1);
1406        let a2 = split_act(x2);
1407        let p1 = SendMut(o1.as_mut_ptr());
1408        let p2 = SendMut(o2.as_mut_ptr());
1409        let run = move |start: usize, end: usize| {
1410            vbit_range2_a8w8(bytes, offsets, x1, x2, &a1, &a2, rows, cols, p1, p2, start, end)
1411        };
1412        dispatch_rows(pool, rows, &run);
1413        return;
1414    }
1415
1416    let p1 = SendMut(o1.as_mut_ptr());
1417    let p2 = SendMut(o2.as_mut_ptr());
1418    let run = move |start: usize, end: usize| {
1419        vbit_range2_f32(bytes, offsets, x1, x2, rows, cols, p1, p2, start, end)
1420    };
1421    dispatch_rows(pool, rows, &run);
1422}
1423
1424/// Two-input vbit row range via the A8W8 int8 path — kernel body of
1425/// `vbitmatvec2`, extracted for pair multi-matrix jobs (b=8 rows go
1426/// exact f32 for both lanes, bits streamed once).
1427#[allow(clippy::too_many_arguments)]
1428fn vbit_range2_a8w8(
1429    bytes: &[u8],
1430    offsets: &[usize],
1431    x1: &[f32],
1432    x2: &[f32],
1433    a1: &SplitAct,
1434    a2: &SplitAct,
1435    rows: usize,
1436    cols: usize,
1437    p1: SendMut,
1438    p2: SendMut,
1439    start: usize,
1440    end: usize,
1441) {
1442    let ng = cols / GROUP_SIZE;
1443    let bits = &bytes[..rows];
1444    let sc_off = rows;
1445    let row_dots = |r: usize| -> (f32, f32) {
1446            let b = bits[r] as usize;
1447            let l = (1i32 << (b - 1)) - 1;
1448            let data = &bytes[offsets[r]..offsets[r + 1]];
1449            if b == 8 {
1450                // u−L reaches 128 → does not fit i8; exact f32 path,
1451                // bits still streamed once for both lanes.
1452                let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
1453                let (mut d1, mut d2) = (0f32, 0f32);
1454                for g in 0..ng {
1455                    let so = (r * ng + g) * 2;
1456                    let sgf = f16_to_f32(u16::from_le_bytes([
1457                        bytes[sc_off + so],
1458                        bytes[sc_off + so + 1],
1459                    ]));
1460                    let (mut g1, mut g2) = (0f32, 0f32);
1461                    for k in 0..GROUP_SIZE {
1462                        if nbits < 8 {
1463                            acc = (acc << 8) | data[idx] as u64;
1464                            idx += 1;
1465                            nbits += 8;
1466                        }
1467                        let u = ((acc >> (nbits - 8)) & 0xFF) as i32;
1468                        nbits -= 8;
1469                        let w = (u - l) as f32;
1470                        g1 += w * x1[g * GROUP_SIZE + k];
1471                        g2 += w * x2[g * GROUP_SIZE + k];
1472                    }
1473                    d1 += g1 * sgf;
1474                    d2 += g2 * sgf;
1475                }
1476                return (d1, d2);
1477            }
1478            thread_local! {
1479                static VBIT_SCRATCH2: std::cell::RefCell<Vec<u8>> =
1480                    const { std::cell::RefCell::new(Vec::new()) };
1481            }
1482            #[inline(always)]
1483            fn fill<const B: usize>(data: &[u8], l: i32, buf: &mut [u8]) {
1484                for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
1485                    let u = unpack8::<B>(&data[blk * B..]);
1486                    for k in 0..8 {
1487                        chunk[k] = (u[k] - l) as i8 as u8;
1488                    }
1489                }
1490            }
1491            VBIT_SCRATCH2.with(|scratch| {
1492                let mut buf = scratch.borrow_mut();
1493                buf.resize(cols, 0);
1494                match b {
1495                    3 => fill::<3>(data, l, &mut buf),
1496                    4 => vbit_fill4(data, &mut buf),
1497                    5 => fill::<5>(data, l, &mut buf),
1498                    6 => fill::<6>(data, l, &mut buf),
1499                    _ => unreachable!(),
1500                }
1501                let (mut d1, mut d2) = (0f32, 0f32);
1502                for g in 0..ng {
1503                    let so = (r * ng + g) * 2;
1504                    let s = f16_to_f32(u16::from_le_bytes([
1505                        bytes[sc_off + so],
1506                        bytes[sc_off + so + 1],
1507                    ]));
1508                    let wg = &buf[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
1509                    let v1 =
1510                        dot_i8_i8(wg, &a1.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE]) as f32 * a1.sx;
1511                    let v2 =
1512                        dot_i8_i8(wg, &a2.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE]) as f32 * a2.sx;
1513                    d1 += v1 * s;
1514                    d2 += v2 * s;
1515                }
1516                for &(j, xv) in &a1.outliers {
1517                    let so = (r * ng + j / GROUP_SIZE) * 2;
1518                    let s = f16_to_f32(u16::from_le_bytes([
1519                        bytes[sc_off + so],
1520                        bytes[sc_off + so + 1],
1521                    ]));
1522                    d1 += (buf[j] as i8) as f32 * s * xv;
1523                }
1524                for &(j, xv) in &a2.outliers {
1525                    let so = (r * ng + j / GROUP_SIZE) * 2;
1526                    let s = f16_to_f32(u16::from_le_bytes([
1527                        bytes[sc_off + so],
1528                        bytes[sc_off + so + 1],
1529                    ]));
1530                    d2 += (buf[j] as i8) as f32 * s * xv;
1531                }
1532                (d1, d2)
1533            })
1534    };
1535    for r in start..end {
1536        let (v1, v2) = row_dots(r);
1537        // SAFETY: disjoint row ranges per worker.
1538        unsafe {
1539            *p1.at(r) = v1;
1540            *p2.at(r) = v2;
1541        }
1542    }
1543}
1544
1545/// Two-input exact scalar vbit row range (same extraction) —
1546/// per-bit-width specialized, two accumulators per row; per-lane
1547/// accumulation order matches `vbitmatvec` exactly.
1548#[allow(clippy::too_many_arguments)]
1549fn vbit_range2_f32(
1550    bytes: &[u8],
1551    offsets: &[usize],
1552    x1: &[f32],
1553    x2: &[f32],
1554    rows: usize,
1555    cols: usize,
1556    p1: SendMut,
1557    p2: SendMut,
1558    start: usize,
1559    end: usize,
1560) {
1561    let ng = cols / GROUP_SIZE;
1562    let bits = &bytes[..rows];
1563    let sc_off = rows;
1564    #[inline(always)]
1565    #[allow(clippy::too_many_arguments)]
1566    fn dot_row2<const B: usize>(
1567        data: &[u8],
1568        bytes: &[u8],
1569        sc_off: usize,
1570        r: usize,
1571        ng: usize,
1572        x1: &[f32],
1573        x2: &[f32],
1574    ) -> (f32, f32) {
1575        let l = ((1i32 << (B - 1)) - 1) as f32;
1576        let gbytes = GROUP_SIZE * B / 8;
1577        let (mut d1, mut d2) = (0f32, 0f32);
1578        for g in 0..ng {
1579            let so = (r * ng + g) * 2;
1580            let s = f16_to_f32(u16::from_le_bytes([bytes[sc_off + so], bytes[sc_off + so + 1]]));
1581            let x1g = &x1[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
1582            let x2g = &x2[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
1583            let gd0 = &data[g * gbytes..(g + 1) * gbytes];
1584            let (mut g1, mut g2) = (0f32, 0f32);
1585            for blk in 0..GROUP_SIZE / 8 {
1586                let u = unpack8::<B>(&gd0[blk * B..]);
1587                for k in 0..8 {
1588                    let w = u[k] as f32 - l;
1589                    g1 += w * x1g[blk * 8 + k];
1590                    g2 += w * x2g[blk * 8 + k];
1591                }
1592            }
1593            d1 += g1 * s;
1594            d2 += g2 * s;
1595        }
1596        (d1, d2)
1597    }
1598    for r in start..end {
1599        let data = &bytes[offsets[r]..offsets[r + 1]];
1600        let (v1, v2) = match bits[r] {
1601            3 => dot_row2::<3>(data, bytes, sc_off, r, ng, x1, x2),
1602            4 => dot_row2::<4>(data, bytes, sc_off, r, ng, x1, x2),
1603            5 => dot_row2::<5>(data, bytes, sc_off, r, ng, x1, x2),
1604            6 => dot_row2::<6>(data, bytes, sc_off, r, ng, x1, x2),
1605            8 => dot_row2::<8>(data, bytes, sc_off, r, ng, x1, x2),
1606            b => unreachable!("vbit bit-width {b} (validated at load)"),
1607        };
1608        // SAFETY: disjoint row ranges per worker.
1609        unsafe {
1610            *p1.at(r) = v1;
1611            *p2.at(r) = v2;
1612        }
1613    }
1614}
1615
1616// ───────────────────── q4_tiled kernels (§4.3) ─────────────────────
1617
1618/// One q4_tiled row dot on the A8W8 int8 path: per 32-group the tile
1619/// is ONE sequential read — [f16 scale][16B nibbles] — versus the two
1620/// distant streams of the split layout. Values/order identical to the
1621/// split kernels.
1622#[inline]
1623#[allow(unreachable_code)]
1624fn dot_q4t_row_i8(bytes: &[u8], r: usize, gpr: usize, xq: &[i8]) -> f32 {
1625    #[cfg(target_arch = "aarch64")]
1626    unsafe {
1627        return dot_q4t_row_sdot(bytes, r, gpr, xq);
1628    }
1629    #[cfg(target_arch = "x86_64")]
1630    unsafe {
1631        return dot_q4t_row_avx2(bytes, r, gpr, xq);
1632    }
1633    let mut acc = 0f32;
1634    for gi in 0..gpr {
1635        let tile = &bytes[(r * gpr + gi) * Q4_TILE..(r * gpr + gi + 1) * Q4_TILE];
1636        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
1637        let mut d = 0i32;
1638        for (k, &b) in tile[2..].iter().enumerate() {
1639            d += ((b & 0x0F) as i32 - 8) * xq[gi * GROUP_SIZE + k * 2] as i32
1640                + (((b >> 4) & 0x0F) as i32 - 8) * xq[gi * GROUP_SIZE + k * 2 + 1] as i32;
1641        }
1642        acc += d as f32 * s;
1643    }
1644    acc
1645}
1646
1647#[cfg(target_arch = "aarch64")]
1648#[target_feature(enable = "neon,dotprod")]
1649unsafe fn dot_q4t_row_sdot(bytes: &[u8], r: usize, gpr: usize, xq: &[i8]) -> f32 {
1650    // SAFETY: callers uphold slice-length contracts (18B tile per group,
1651    // xq.len() == gpr·GROUP_SIZE).
1652    unsafe {
1653        use core::arch::aarch64::*;
1654        use core::arch::asm;
1655        let lomask = vdupq_n_u8(0x0F);
1656        let eight = vdupq_n_s8(8);
1657        let mut acc = 0f32;
1658        for gi in 0..gpr {
1659            let t = bytes.as_ptr().add((r * gpr + gi) * Q4_TILE);
1660            let s = f16_to_f32(u16::from_le_bytes([*t, *t.add(1)]));
1661            let b = vld1q_u8(t.add(2));
1662            let lo = vandq_u8(b, lomask);
1663            let hi = vshrq_n_u8::<4>(b);
1664            let e0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(lo, hi)), eight);
1665            let e1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(lo, hi)), eight);
1666            let x0 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE));
1667            let x1 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE + 16));
1668            let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
1669            asm!(
1670                "sdot {a0:v}.4s, {e0:v}.16b, {x0:v}.16b",
1671                "sdot {a1:v}.4s, {e1:v}.16b, {x1:v}.16b",
1672                a0 = inout(vreg) a0, a1 = inout(vreg) a1,
1673                e0 = in(vreg) e0, x0 = in(vreg) x0, e1 = in(vreg) e1, x1 = in(vreg) x1,
1674                options(pure, nomem, nostack),
1675            );
1676            acc += vaddvq_s32(vaddq_s32(a0, a1)) as f32 * s;
1677        }
1678        acc
1679    }
1680}
1681
1682#[cfg(target_arch = "x86_64")]
1683#[target_feature(enable = "avx2")]
1684unsafe fn dot_q4t_row_avx2(bytes: &[u8], r: usize, gpr: usize, xq: &[i8]) -> f32 {
1685    // SAFETY: see dot_q4t_row_sdot.
1686    unsafe {
1687        use core::arch::x86_64::*;
1688        let lomask = _mm_set1_epi8(0x0F);
1689        let eight = _mm256_set1_epi8(8);
1690        let ones = _mm256_set1_epi16(1);
1691        let mut acc = 0f32;
1692        for gi in 0..gpr {
1693            let t = bytes.as_ptr().add((r * gpr + gi) * Q4_TILE);
1694            let s = f16_to_f32(u16::from_le_bytes([*t, *t.add(1)]));
1695            let b = _mm_loadu_si128(t.add(2) as *const __m128i);
1696            let lo = _mm_and_si128(b, lomask);
1697            let hi = _mm_and_si128(_mm_srli_epi16::<4>(b), lomask);
1698            let w = _mm256_sub_epi8(
1699                _mm256_set_m128i(_mm_unpackhi_epi8(lo, hi), _mm_unpacklo_epi8(lo, hi)),
1700                eight,
1701            );
1702            let x = _mm256_loadu_si256(xq.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
1703            let p16 = _mm256_maddubs_epi16(_mm256_abs_epi8(w), _mm256_sign_epi8(x, w));
1704            let d = _mm256_madd_epi16(p16, ones);
1705            let hi128 = _mm256_extracti128_si256::<1>(d);
1706            let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
1707            let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
1708            let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
1709            acc += _mm_cvtsi128_si32(s32) as f32 * s;
1710        }
1711        acc
1712    }
1713}
1714
1715/// Exact-term correction for A8W8 outliers on a tiled row.
1716#[inline]
1717fn q4t_outlier(bytes: &[u8], r: usize, gpr: usize, j: usize) -> (f32, f32) {
1718    let gi = j / GROUP_SIZE;
1719    let k = j % GROUP_SIZE;
1720    let tile = &bytes[(r * gpr + gi) * Q4_TILE..(r * gpr + gi + 1) * Q4_TILE];
1721    let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
1722    let byte = tile[2 + k / 2];
1723    let nib = if k & 1 == 0 { byte & 0x0F } else { byte >> 4 };
1724    ((nib as i32 - 8) as f32, s)
1725}
1726
1727/// Exact scalar q4_tiled row (CMF_SDOT=0 contract) — same pairwise
1728/// accumulation shape as `q4_range_f32`.
1729#[inline]
1730fn q4t_row_exact(bytes: &[u8], r: usize, gpr: usize, x: &[f32]) -> f32 {
1731    let mut acc = 0f32;
1732    for gi in 0..gpr {
1733        let tile = &bytes[(r * gpr + gi) * Q4_TILE..(r * gpr + gi + 1) * Q4_TILE];
1734        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
1735        let xg = &x[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
1736        let mut ga = 0f32;
1737        for (k, &b) in tile[2..].iter().enumerate() {
1738            ga += ((b & 0x0F) as f32 - 8.0) * xg[k * 2]
1739                + (((b >> 4) & 0x0F) as f32 - 8.0) * xg[k * 2 + 1];
1740        }
1741        acc += ga * s;
1742    }
1743    acc
1744}
1745
1746/// Fused q4_tiled matvec (dispatch mirrors `q4matvec`).
1747fn q4t_matvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
1748    debug_assert_eq!(out.len(), rows);
1749    let gpr = cols / GROUP_SIZE;
1750    let out_addr = SendMut(out.as_mut_ptr());
1751    if a8w8_enabled() {
1752        let act = split_act(x);
1753        let run = move |start: usize, end: usize| {
1754            for r in start..end {
1755                let mut acc = dot_q4t_row_i8(bytes, r, gpr, &act.xq) * act.sx;
1756                for &(j, xv) in &act.outliers {
1757                    let (w, s) = q4t_outlier(bytes, r, gpr, j);
1758                    acc += w * s * xv;
1759                }
1760                // SAFETY: disjoint row ranges per worker.
1761                unsafe { *out_addr.at(r) = acc };
1762            }
1763        };
1764        dispatch_rows(pool, rows, &run);
1765        return;
1766    }
1767    let run = move |start: usize, end: usize| {
1768        for r in start..end {
1769            // SAFETY: disjoint row ranges per worker.
1770            unsafe { *out_addr.at(r) = q4t_row_exact(bytes, r, gpr, x) };
1771        }
1772    };
1773    dispatch_rows(pool, rows, &run);
1774}
1775
1776/// Fused two-input q4_tiled matvec (weights read once per pair).
1777#[allow(clippy::too_many_arguments)]
1778fn q4t_matvec2(
1779    bytes: &[u8],
1780    x1: &[f32],
1781    x2: &[f32],
1782    rows: usize,
1783    cols: usize,
1784    o1: &mut [f32],
1785    o2: &mut [f32],
1786    pool: Option<&Pool>,
1787) {
1788    let gpr = cols / GROUP_SIZE;
1789    let p1 = SendMut(o1.as_mut_ptr());
1790    let p2 = SendMut(o2.as_mut_ptr());
1791    if a8w8_enabled() {
1792        let a1 = split_act(x1);
1793        let a2 = split_act(x2);
1794        let run = move |start: usize, end: usize| {
1795            for r in start..end {
1796                let mut v1 = dot_q4t_row_i8(bytes, r, gpr, &a1.xq) * a1.sx;
1797                let mut v2 = dot_q4t_row_i8(bytes, r, gpr, &a2.xq) * a2.sx;
1798                for &(j, xv) in &a1.outliers {
1799                    let (w, s) = q4t_outlier(bytes, r, gpr, j);
1800                    v1 += w * s * xv;
1801                }
1802                for &(j, xv) in &a2.outliers {
1803                    let (w, s) = q4t_outlier(bytes, r, gpr, j);
1804                    v2 += w * s * xv;
1805                }
1806                // SAFETY: disjoint row ranges per worker.
1807                unsafe {
1808                    *p1.at(r) = v1;
1809                    *p2.at(r) = v2;
1810                }
1811            }
1812        };
1813        dispatch_rows(pool, rows, &run);
1814        return;
1815    }
1816    let run = move |start: usize, end: usize| {
1817        for r in start..end {
1818            // SAFETY: disjoint row ranges per worker.
1819            unsafe {
1820                *p1.at(r) = q4t_row_exact(bytes, r, gpr, x1);
1821                *p2.at(r) = q4t_row_exact(bytes, r, gpr, x2);
1822            }
1823        }
1824    };
1825    dispatch_rows(pool, rows, &run);
1826}
1827
1828/// Batched q4_tiled matmat: each row's tiles stream once per microbatch.
1829#[allow(clippy::too_many_arguments)]
1830fn q4t_matmat(
1831    bytes: &[u8],
1832    xs_all: &[f32],
1833    b: usize,
1834    rows: usize,
1835    cols: usize,
1836    out: &mut [f32],
1837    pool: Option<&Pool>,
1838) {
1839    debug_assert_eq!(out.len(), b * rows);
1840    let gpr = cols / GROUP_SIZE;
1841    let out_addr = SendMut(out.as_mut_ptr());
1842    if a8w8_enabled() {
1843        let acts: Vec<SplitAct> =
1844            (0..b).map(|bi| split_act(&xs_all[bi * cols..(bi + 1) * cols])).collect();
1845        let acts = &acts;
1846        let run = move |start: usize, end: usize| {
1847            for r in start..end {
1848                for (bi, act) in acts.iter().enumerate() {
1849                    let mut acc = dot_q4t_row_i8(bytes, r, gpr, &act.xq) * act.sx;
1850                    for &(j, xv) in &act.outliers {
1851                        let (w, s) = q4t_outlier(bytes, r, gpr, j);
1852                        acc += w * s * xv;
1853                    }
1854                    // SAFETY: disjoint (bi, r) cells per worker range.
1855                    unsafe { *out_addr.at(bi * rows + r) = acc };
1856                }
1857            }
1858        };
1859        dispatch_rows(pool, rows, &run);
1860        return;
1861    }
1862    let run = move |start: usize, end: usize| {
1863        for r in start..end {
1864            for bi in 0..b {
1865                let x = &xs_all[bi * cols..(bi + 1) * cols];
1866                // SAFETY: disjoint (bi, r) cells per worker range.
1867                unsafe { *out_addr.at(bi * rows + r) = q4t_row_exact(bytes, r, gpr, x) };
1868            }
1869        }
1870    };
1871    dispatch_rows(pool, rows, &run);
1872}
1873
1874/// Fused q4_block matvec straight from the mapped bytes. SDOT path when
1875/// dotprod is available (port of vmfcore `dot_q4_block_sdot`, measured
1876/// +23% on q4 decode): nibbles → centered i8, int8×int8 `sdot` per
1877/// 32-group, exact outlier correction — the same A8W8 contract as q8.
1878/// `CMF_SDOT=0` keeps the exact scalar path.
1879fn q4matvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
1880    debug_assert_eq!(out.len(), rows);
1881    let (packed, scales) = q4_split(bytes, rows, cols);
1882    let gpr = cols / GROUP_SIZE;
1883    let out_addr = SendMut(out.as_mut_ptr());
1884
1885    if a8w8_enabled() {
1886        let act = split_act(x);
1887        let run = move |start: usize, end: usize| {
1888            q4_range_a8w8(packed, scales, gpr, cols, &act, out_addr, start, end)
1889        };
1890        dispatch_rows(pool, rows, &run);
1891        return;
1892    }
1893
1894    let run = move |start: usize, end: usize| {
1895        q4_range_f32(packed, scales, gpr, x, out_addr, start, end)
1896    };
1897    dispatch_rows(pool, rows, &run);
1898}
1899
1900/// One q4 row via the A8W8 int8 path — SDOT on ARM, AVX2 maddubs on
1901/// x86 (scalar fallback is unreachable: callers gate on a8w8_enabled).
1902#[inline]
1903#[allow(unreachable_code)]
1904fn dot_q4_row_i8(packed: &[u8], scales: &[u8], g0: usize, gpr: usize, xq: &[i8]) -> f32 {
1905    #[cfg(target_arch = "aarch64")]
1906    unsafe {
1907        return dot_q4_row_sdot(packed, scales, g0, gpr, xq);
1908    }
1909    #[cfg(target_arch = "x86_64")]
1910    unsafe {
1911        return dot_q4_row_avx2(packed, scales, g0, gpr, xq);
1912    }
1913    let mut acc = 0f32;
1914    for gi in 0..gpr {
1915        let g = g0 + gi;
1916        let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
1917        let mut d = 0i32;
1918        for (k, &b) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
1919            d += ((b & 0x0F) as i32 - 8) * xq[gi * GROUP_SIZE + k * 2] as i32
1920                + (((b >> 4) & 0x0F) as i32 - 8) * xq[gi * GROUP_SIZE + k * 2 + 1] as i32;
1921        }
1922        acc += d as f32 * s;
1923    }
1924    acc
1925}
1926
1927/// Two-activation q4 row via the A8W8 int8 path (see `dot_q4_row_i8`).
1928#[inline]
1929#[allow(unreachable_code)]
1930fn dot_q4_row_i8_2(
1931    packed: &[u8],
1932    scales: &[u8],
1933    g0: usize,
1934    gpr: usize,
1935    xq1: &[i8],
1936    xq2: &[i8],
1937) -> (f32, f32) {
1938    #[cfg(target_arch = "aarch64")]
1939    unsafe {
1940        return dot_q4_row_sdot2(packed, scales, g0, gpr, xq1, xq2);
1941    }
1942    #[cfg(target_arch = "x86_64")]
1943    unsafe {
1944        return dot_q4_row_avx2_2(packed, scales, g0, gpr, xq1, xq2);
1945    }
1946    (
1947        dot_q4_row_i8(packed, scales, g0, gpr, xq1),
1948        dot_q4_row_i8(packed, scales, g0, gpr, xq2),
1949    )
1950}
1951
1952/// One q4 row range via SDOT (kernel body of `q4matvec`, extracted so
1953/// multi-matrix jobs can drive it for several tensors in one dispatch).
1954#[allow(clippy::too_many_arguments)]
1955fn q4_range_a8w8(
1956    packed: &[u8],
1957    scales: &[u8],
1958    gpr: usize,
1959    cols: usize,
1960    act: &SplitAct,
1961    out: SendMut,
1962    start: usize,
1963    end: usize,
1964) {
1965    for r in start..end {
1966        let mut acc = dot_q4_row_i8(packed, scales, r * gpr, gpr, &act.xq) * act.sx;
1967        // xq is zeroed at outlier slots — add the exact terms.
1968        for &(j, xv) in &act.outliers {
1969            let flat = r * cols + j;
1970            let byte = packed[flat / 2];
1971            let nib = if flat & 1 == 0 { byte & 0x0F } else { byte >> 4 };
1972            let s = f16_to_f32(u16::from_le_bytes([
1973                scales[(flat / GROUP_SIZE) * 2],
1974                scales[(flat / GROUP_SIZE) * 2 + 1],
1975            ]));
1976            acc += ((nib as i32 - 8) as f32) * s * xv;
1977        }
1978        // SAFETY: disjoint row ranges per worker.
1979        unsafe { *out.at(r) = acc };
1980    }
1981}
1982
1983/// Two-input q4 row range via the A8W8 int8 path — kernel body of
1984/// `q4matvec2`, extracted for pair multi-matrix jobs.
1985#[allow(clippy::too_many_arguments)]
1986fn q4_range2_a8w8(
1987    packed: &[u8],
1988    scales: &[u8],
1989    gpr: usize,
1990    cols: usize,
1991    a1: &SplitAct,
1992    a2: &SplitAct,
1993    p1: SendMut,
1994    p2: SendMut,
1995    start: usize,
1996    end: usize,
1997) {
1998    for r in start..end {
1999        let (s1, s2) = dot_q4_row_i8_2(packed, scales, r * gpr, gpr, &a1.xq, &a2.xq);
2000        let mut acc1 = s1 * a1.sx;
2001        let mut acc2 = s2 * a2.sx;
2002        // xq is zeroed at outlier slots — add the exact terms.
2003        let fix = |outliers: &[(usize, f32)], acc: &mut f32| {
2004            for &(j, xv) in outliers {
2005                let flat = r * cols + j;
2006                let byte = packed[flat / 2];
2007                let nib = if flat & 1 == 0 { byte & 0x0F } else { byte >> 4 };
2008                let s = f16_to_f32(u16::from_le_bytes([
2009                    scales[(flat / GROUP_SIZE) * 2],
2010                    scales[(flat / GROUP_SIZE) * 2 + 1],
2011                ]));
2012                *acc += ((nib as i32 - 8) as f32) * s * xv;
2013            }
2014        };
2015        fix(&a1.outliers, &mut acc1);
2016        fix(&a2.outliers, &mut acc2);
2017        // SAFETY: disjoint row ranges per worker.
2018        unsafe {
2019            *p1.at(r) = acc1;
2020            *p2.at(r) = acc2;
2021        }
2022    }
2023}
2024
2025/// Exact scalar q4 row range (same extraction, non-SDOT path).
2026fn q4_range_f32(
2027    packed: &[u8],
2028    scales: &[u8],
2029    gpr: usize,
2030    x: &[f32],
2031    out: SendMut,
2032    start: usize,
2033    end: usize,
2034) {
2035    for r in start..end {
2036        let mut acc = 0f32;
2037        for gi in 0..gpr {
2038            let g = r * gpr + gi;
2039            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
2040            let pk = &packed[g * 16..(g + 1) * 16];
2041            let xg = &x[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
2042            let mut ga = 0f32;
2043            for (k, &b) in pk.iter().enumerate() {
2044                ga += ((b & 0x0F) as f32 - 8.0) * xg[k * 2]
2045                    + (((b >> 4) & 0x0F) as f32 - 8.0) * xg[k * 2 + 1];
2046            }
2047            acc += ga * s;
2048        }
2049        // SAFETY: disjoint row ranges per worker.
2050        unsafe { *out.at(r) = acc };
2051    }
2052}
2053
2054/// Fused two-input q4 matvec: nibbles are unpacked ONCE per group and
2055/// dotted against both activations (was: two full matvecs — double
2056/// weight traffic). Per-lane math matches `q4matvec` exactly.
2057#[allow(clippy::too_many_arguments)]
2058fn q4matvec2(
2059    bytes: &[u8],
2060    x1: &[f32],
2061    x2: &[f32],
2062    rows: usize,
2063    cols: usize,
2064    o1: &mut [f32],
2065    o2: &mut [f32],
2066    pool: Option<&Pool>,
2067) {
2068    debug_assert_eq!(o1.len(), rows);
2069    debug_assert_eq!(o2.len(), rows);
2070    let (packed, scales) = q4_split(bytes, rows, cols);
2071    let gpr = cols / GROUP_SIZE;
2072
2073    if a8w8_enabled() {
2074        let a1 = split_act(x1);
2075        let a2 = split_act(x2);
2076        let p1 = SendMut(o1.as_mut_ptr());
2077        let p2 = SendMut(o2.as_mut_ptr());
2078        let run = move |start: usize, end: usize| {
2079            q4_range2_a8w8(packed, scales, gpr, cols, &a1, &a2, p1, p2, start, end)
2080        };
2081        dispatch_rows(pool, rows, &run);
2082        return;
2083    }
2084
2085    let p1 = SendMut(o1.as_mut_ptr());
2086    let p2 = SendMut(o2.as_mut_ptr());
2087    let run = move |start: usize, end: usize| {
2088        q4_range2_f32(packed, scales, gpr, x1, x2, p1, p2, start, end)
2089    };
2090    dispatch_rows(pool, rows, &run);
2091}
2092
2093/// Two-input exact scalar q4 row range (same extraction).
2094#[allow(clippy::too_many_arguments)]
2095fn q4_range2_f32(
2096    packed: &[u8],
2097    scales: &[u8],
2098    gpr: usize,
2099    x1: &[f32],
2100    x2: &[f32],
2101    p1: SendMut,
2102    p2: SendMut,
2103    start: usize,
2104    end: usize,
2105) {
2106    for r in start..end {
2107        let (mut acc1, mut acc2) = (0f32, 0f32);
2108        for gi in 0..gpr {
2109            let g = r * gpr + gi;
2110            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
2111            let pk = &packed[g * 16..(g + 1) * 16];
2112            let x1g = &x1[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
2113            let x2g = &x2[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
2114            let (mut g1, mut g2) = (0f32, 0f32);
2115            for (k, &b) in pk.iter().enumerate() {
2116                let wl = (b & 0x0F) as f32 - 8.0;
2117                let wh = ((b >> 4) & 0x0F) as f32 - 8.0;
2118                g1 += wl * x1g[k * 2] + wh * x1g[k * 2 + 1];
2119                g2 += wl * x2g[k * 2] + wh * x2g[k * 2 + 1];
2120            }
2121            acc1 += g1 * s;
2122            acc2 += g2 * s;
2123        }
2124        // SAFETY: disjoint row ranges per worker.
2125        unsafe {
2126            *p1.at(r) = acc1;
2127            *p2.at(r) = acc2;
2128        }
2129    }
2130}
2131
2132thread_local! {
2133    /// Per-worker decoded-row scratch for the batched q4/vbit kernels
2134    /// (centered i8 for SDOT, f32 for the exact/scalar paths).
2135    static ROW_I8: std::cell::RefCell<Vec<u8>> = const { std::cell::RefCell::new(Vec::new()) };
2136    static ROW_F32: std::cell::RefCell<Vec<f32>> = const { std::cell::RefCell::new(Vec::new()) };
2137}
2138
2139/// Batched q4 matmat: each weight row is unpacked from the mmap ONCE
2140/// and dotted against ALL b activations (prefill used to fall back to b
2141/// full matvecs — b× weight traffic and b× nibble decode). Per-position
2142/// math matches `q4matvec` exactly: same group order, same accumulation.
2143/// `out` is row-major [b, rows] like `qmatmat`.
2144#[allow(clippy::too_many_arguments)]
2145fn q4matmat(
2146    bytes: &[u8],
2147    xs_all: &[f32],
2148    b: usize,
2149    rows: usize,
2150    cols: usize,
2151    out: &mut [f32],
2152    pool: Option<&Pool>,
2153) {
2154    debug_assert_eq!(xs_all.len(), b * cols);
2155    debug_assert_eq!(out.len(), b * rows);
2156    let (packed, scales) = q4_split(bytes, rows, cols);
2157    let gpr = cols / GROUP_SIZE;
2158    let gscale = |g: usize| f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
2159
2160    if a8w8_enabled() {
2161        let acts: Vec<SplitAct> =
2162            (0..b).map(|bi| split_act(&xs_all[bi * cols..(bi + 1) * cols])).collect();
2163        let acts = &acts;
2164        let out_addr = SendMut(out.as_mut_ptr());
2165        let run = move |start: usize, end: usize| {
2166            ROW_I8.with(|rb| {
2167                let mut buf = rb.borrow_mut();
2168                buf.resize(cols, 0);
2169                for r in start..end {
2170                    // Unpack the row's nibbles to centered i8 once
2171                    // (element 2k = low nibble, 2k+1 = high — flat order,
2172                    // same as dot_q4_row_sdot's zip).
2173                    for gi in 0..gpr {
2174                        let g = r * gpr + gi;
2175                        for (k, &bt) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
2176                            buf[gi * GROUP_SIZE + k * 2] = ((bt & 0x0F) as i32 - 8) as i8 as u8;
2177                            buf[gi * GROUP_SIZE + k * 2 + 1] =
2178                                (((bt >> 4) & 0x0F) as i32 - 8) as i8 as u8;
2179                        }
2180                    }
2181                    for (bi, act) in acts.iter().enumerate() {
2182                        let mut acc = 0f32;
2183                        for gi in 0..gpr {
2184                            let d = dot_i8_i8(
2185                                &buf[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE],
2186                                &act.xq[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE],
2187                            );
2188                            acc += d as f32 * gscale(r * gpr + gi);
2189                        }
2190                        acc *= act.sx;
2191                        // xq is zeroed at outlier slots — exact terms.
2192                        for &(j, xv) in &act.outliers {
2193                            acc += (buf[j] as i8) as f32 * gscale((r * cols + j) / GROUP_SIZE) * xv;
2194                        }
2195                        // SAFETY: disjoint (bi, r) cells per worker row range.
2196                        unsafe { *out_addr.at(bi * rows + r) = acc };
2197                    }
2198                }
2199            })
2200        };
2201        dispatch_rows(pool, rows, &run);
2202        return;
2203    }
2204
2205    let out_addr = SendMut(out.as_mut_ptr());
2206    let run = move |start: usize, end: usize| {
2207        ROW_F32.with(|rb| {
2208            let mut buf = rb.borrow_mut();
2209            buf.resize(cols, 0.0);
2210            for r in start..end {
2211                // Decode raw (nib − 8) values once; scales stay per-group
2212                // so the accumulation order matches q4matvec bit-for-bit.
2213                for gi in 0..gpr {
2214                    let g = r * gpr + gi;
2215                    for (k, &bt) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
2216                        buf[gi * GROUP_SIZE + k * 2] = (bt & 0x0F) as f32 - 8.0;
2217                        buf[gi * GROUP_SIZE + k * 2 + 1] = ((bt >> 4) & 0x0F) as f32 - 8.0;
2218                    }
2219                }
2220                for bi in 0..b {
2221                    let x = &xs_all[bi * cols..(bi + 1) * cols];
2222                    let mut acc = 0f32;
2223                    for gi in 0..gpr {
2224                        let mut ga = 0f32;
2225                        // Pairwise (lo + hi) addition, matching
2226                        // q4matvec's `ga += lo·x + hi·x` shape exactly —
2227                        // a flat one-per-element loop rounds differently
2228                        // and broke bit-parity on the scalar (x86) path.
2229                        for k in 0..GROUP_SIZE / 2 {
2230                            let e = gi * GROUP_SIZE + k * 2;
2231                            ga += buf[e] * x[e] + buf[e + 1] * x[e + 1];
2232                        }
2233                        acc += ga * gscale(r * gpr + gi);
2234                    }
2235                    // SAFETY: disjoint (bi, r) cells per worker row range.
2236                    unsafe { *out_addr.at(bi * rows + r) = acc };
2237                }
2238            }
2239        })
2240    };
2241    dispatch_rows(pool, rows, &run);
2242}
2243
2244/// Batched vbit matmat: each variable-bit row is decoded from the mmap
2245/// ONCE for the whole microbatch. Same per-position math as
2246/// `vbitmatvec` (SDOT A8W8 with exact outliers / exact f32 for b=8 rows
2247/// and the scalar path).
2248#[allow(clippy::too_many_arguments)]
2249fn vbitmatmat(
2250    bytes: &[u8],
2251    offsets: &[usize],
2252    xs_all: &[f32],
2253    b: usize,
2254    rows: usize,
2255    cols: usize,
2256    out: &mut [f32],
2257    pool: Option<&Pool>,
2258) {
2259    debug_assert_eq!(xs_all.len(), b * cols);
2260    debug_assert_eq!(out.len(), b * rows);
2261    debug_assert_eq!(offsets.len(), rows + 1);
2262    let ng = cols / GROUP_SIZE;
2263    let bits = &bytes[..rows];
2264    let sc_off = rows;
2265    let gscale = |r: usize, g: usize| {
2266        let so = (r * ng + g) * 2;
2267        f16_to_f32(u16::from_le_bytes([bytes[sc_off + so], bytes[sc_off + so + 1]]))
2268    };
2269
2270    // Decode row r's raw (u − L) values into `dst` (f32, unscaled).
2271    let decode_f32 = |r: usize, dst: &mut [f32]| {
2272        let bw = bits[r] as usize;
2273        let l = ((1i32 << (bw - 1)) - 1) as f32;
2274        let data = &bytes[offsets[r]..offsets[r + 1]];
2275        let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
2276        for d in dst.iter_mut() {
2277            while nbits < bw {
2278                acc = (acc << 8) | data[idx] as u64;
2279                idx += 1;
2280                nbits += 8;
2281            }
2282            let u = ((acc >> (nbits - bw)) & ((1u64 << bw) - 1)) as f32;
2283            nbits -= bw;
2284            *d = u - l;
2285        }
2286    };
2287
2288    if a8w8_enabled() {
2289        let acts: Vec<SplitAct> =
2290            (0..b).map(|bi| split_act(&xs_all[bi * cols..(bi + 1) * cols])).collect();
2291        let acts = &acts;
2292        let out_addr = SendMut(out.as_mut_ptr());
2293        let run = move |start: usize, end: usize| {
2294            for r in start..end {
2295                let bw = bits[r] as usize;
2296                if bw == 8 {
2297                    // u−L reaches 128 → no i8 path; decode once, exact
2298                    // f32 dots for every position (same as vbitmatvec).
2299                    ROW_F32.with(|rb| {
2300                        let mut buf = rb.borrow_mut();
2301                        buf.resize(cols, 0.0);
2302                        decode_f32(r, &mut buf);
2303                        for bi in 0..b {
2304                            let x = &xs_all[bi * cols..(bi + 1) * cols];
2305                            let mut dot = 0f32;
2306                            for g in 0..ng {
2307                                let mut gd = 0f32;
2308                                for k in 0..GROUP_SIZE {
2309                                    gd += buf[g * GROUP_SIZE + k] * x[g * GROUP_SIZE + k];
2310                                }
2311                                dot += gd * gscale(r, g);
2312                            }
2313                            // SAFETY: disjoint (bi, r) cells per worker range.
2314                            unsafe { *out_addr.at(bi * rows + r) = dot };
2315                        }
2316                    });
2317                    continue;
2318                }
2319                let l = (1i32 << (bw - 1)) - 1;
2320                let data = &bytes[offsets[r]..offsets[r + 1]];
2321                ROW_I8.with(|rb| {
2322                    let mut buf = rb.borrow_mut();
2323                    buf.resize(cols, 0);
2324                    #[inline(always)]
2325                    fn fill<const B: usize>(data: &[u8], l: i32, buf: &mut [u8]) {
2326                        for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
2327                            let u = unpack8::<B>(&data[blk * B..]);
2328                            for k in 0..8 {
2329                                chunk[k] = (u[k] - l) as i8 as u8;
2330                            }
2331                        }
2332                    }
2333                    match bw {
2334                        3 => fill::<3>(data, l, &mut buf),
2335                        4 => vbit_fill4(data, &mut buf),
2336                        5 => fill::<5>(data, l, &mut buf),
2337                        6 => fill::<6>(data, l, &mut buf),
2338                        _ => unreachable!("vbit bit-width {bw} (validated at load)"),
2339                    }
2340                    for (bi, act) in acts.iter().enumerate() {
2341                        let mut dot = 0f32;
2342                        for g in 0..ng {
2343                            let d = dot_i8_i8(
2344                                &buf[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
2345                                &act.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
2346                            ) as f32
2347                                * act.sx;
2348                            dot += d * gscale(r, g);
2349                        }
2350                        for &(j, xv) in &act.outliers {
2351                            dot += (buf[j] as i8) as f32 * gscale(r, j / GROUP_SIZE) * xv;
2352                        }
2353                        // SAFETY: disjoint (bi, r) cells per worker range.
2354                        unsafe { *out_addr.at(bi * rows + r) = dot };
2355                    }
2356                });
2357            }
2358        };
2359        dispatch_rows(pool, rows, &run);
2360        return;
2361    }
2362
2363    let out_addr = SendMut(out.as_mut_ptr());
2364    let run = move |start: usize, end: usize| {
2365        ROW_F32.with(|rb| {
2366            let mut buf = rb.borrow_mut();
2367            buf.resize(cols, 0.0);
2368            for r in start..end {
2369                decode_f32(r, &mut buf);
2370                for bi in 0..b {
2371                    let x = &xs_all[bi * cols..(bi + 1) * cols];
2372                    let mut dot = 0f32;
2373                    for g in 0..ng {
2374                        let mut gd = 0f32;
2375                        for k in 0..GROUP_SIZE {
2376                            gd += buf[g * GROUP_SIZE + k] * x[g * GROUP_SIZE + k];
2377                        }
2378                        dot += gd * gscale(r, g);
2379                    }
2380                    // SAFETY: disjoint (bi, r) cells per worker range.
2381                    unsafe { *out_addr.at(bi * rows + r) = dot };
2382                }
2383            }
2384        })
2385    };
2386    dispatch_rows(pool, rows, &run);
2387}
2388
2389/// Build a GPU batch job for a q8-family mapped tensor (primary
2390/// shard): prescaled input + directory coordinates. None → not
2391/// GPU-eligible, caller stays on the CPU.
2392pub(crate) fn gpu_batch_job<'a>(
2393    t: &'a QTensor,
2394    x: &[f32],
2395) -> Option<(std::sync::Arc<CmfModel>, crate::gpu::BatchJob<'a>)> {
2396    match t {
2397        QTensor::Mapped {
2398            model,
2399            idx,
2400            dtype: dt @ (TensorDtype::Q8Row | TensorDtype::Q8_2f),
2401            rows,
2402            cols,
2403            row_scale,
2404            col_field,
2405            ..
2406        } => Some((
2407            model.clone(),
2408            crate::gpu::BatchJob {
2409                idx: *idx,
2410                rows: *rows,
2411                cols: *cols,
2412                row_scale,
2413                xs: prescale(x, col_field, *dt).into_owned(),
2414            },
2415        )),
2416        _ => None,
2417    }
2418}
2419
2420/// θ col-field fold for q8_2f activations. Borrowed pass-through for
2421/// every other dtype — the old unconditional `x.to_vec()` was a pure
2422/// per-matvec allocation on the q8_row hot path.
2423pub(crate) fn prescale<'a>(
2424    x: &'a [f32],
2425    col_field: &[f32],
2426    dtype: TensorDtype,
2427) -> std::borrow::Cow<'a, [f32]> {
2428    if dtype == TensorDtype::Q8_2f {
2429        x.iter().zip(col_field).map(|(a, c)| a * c).collect()
2430    } else {
2431        std::borrow::Cow::Borrowed(x)
2432    }
2433}
2434
2435// ───────────────────── x86-64 AVX2 kernels (roadmap этап 2) ─────────────────────
2436
2437/// AVX2+FMA available? Default ON when the CPU supports both;
2438/// `CMF_AVX2=0` disables (falls back to the autovectorized loops).
2439#[cfg(target_arch = "x86_64")]
2440pub(crate) fn avx2_enabled() -> bool {
2441    use std::sync::OnceLock;
2442    static ON: OnceLock<bool> = OnceLock::new();
2443    *ON.get_or_init(|| {
2444        std::env::var("CMF_AVX2").map(|v| v != "0").unwrap_or(true)
2445            && std::arch::is_x86_feature_detected!("avx2")
2446            && std::arch::is_x86_feature_detected!("fma")
2447    })
2448}
2449
2450/// AVX2 A8W8 allowed? The quantized-activation contract is switched by
2451/// the SAME env as the ARM SDOT path: `CMF_SDOT=0` keeps exact kernels
2452/// (the golden-parity exact gate relies on it) — AVX2 f32 kernels stay
2453/// active either way, they are exact (regrouped sums only).
2454#[cfg(target_arch = "x86_64")]
2455fn avx2_a8w8_enabled() -> bool {
2456    use std::sync::OnceLock;
2457    static ON: OnceLock<bool> = OnceLock::new();
2458    *ON.get_or_init(|| {
2459        avx2_enabled() && std::env::var("CMF_SDOT").map(|v| v != "0").unwrap_or(true)
2460    })
2461}
2462
2463/// A8W8 quantized-activation path available on THIS machine? One
2464/// switch across architectures: ARM dotprod (CMF_SDOT) or x86 AVX2
2465/// (CMF_AVX2 + the same CMF_SDOT exact-contract override).
2466#[inline]
2467pub(crate) fn a8w8_enabled() -> bool {
2468    #[cfg(target_arch = "aarch64")]
2469    {
2470        sdot_enabled()
2471    }
2472    #[cfg(target_arch = "x86_64")]
2473    {
2474        avx2_a8w8_enabled()
2475    }
2476    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
2477    {
2478        false
2479    }
2480}
2481
2482/// int8·int8 dot dispatch: SDOT on ARM; AVX-512 VNNI (vpdpbusd) or AVX2
2483/// maddubs on x86. Callers are gated by `a8w8_enabled()`.
2484#[inline]
2485#[allow(unreachable_code)]
2486fn dot_i8_i8(w: &[u8], xq: &[i8]) -> i32 {
2487    #[cfg(target_arch = "aarch64")]
2488    unsafe {
2489        return dot_i8_sdot(w, xq);
2490    }
2491    #[cfg(target_arch = "x86_64")]
2492    unsafe {
2493        if avx512vnni_enabled() {
2494            return dot_i8_i8_vnni(w, xq);
2495        }
2496        return dot_i8_i8_avx2(w, xq);
2497    }
2498    w.iter().zip(xq).map(|(&a, &b)| (a as i8) as i32 * b as i32).sum()
2499}
2500
2501/// AVX-512 VNNI available? (F+BW+VL+VNNI; `CMF_AVX512=0` falls back to
2502/// AVX2.) VL matters: short 32-byte groups (q4/vbit) ride the 256-bit
2503/// `vpdpbusd` encoding.
2504#[cfg(target_arch = "x86_64")]
2505fn avx512vnni_enabled() -> bool {
2506    use std::sync::OnceLock;
2507    static ON: OnceLock<bool> = OnceLock::new();
2508    *ON.get_or_init(|| {
2509        std::env::var("CMF_AVX512").map(|v| v != "0").unwrap_or(true)
2510            && std::arch::is_x86_feature_detected!("avx512f")
2511            && std::arch::is_x86_feature_detected!("avx512bw")
2512            && std::arch::is_x86_feature_detected!("avx512vl")
2513            && std::arch::is_x86_feature_detected!("avx512vnni")
2514    })
2515}
2516
2517/// int8·int8 via AVX-512 VNNI: `vpdpbusd` fuses the maddubs+madd+add
2518/// triple into one u8×i8 dot-accumulate. AVX-512 has no vpsignb, so the
2519/// |w|·sign(x,w) trick becomes |w| × (x negated where w<0) via a mask
2520/// subtract — w==0 lanes contribute 0 through |w|=0 either way.
2521#[cfg(target_arch = "x86_64")]
2522#[target_feature(enable = "avx2,avx512f,avx512bw,avx512vl,avx512vnni")]
2523unsafe fn dot_i8_i8_vnni(w: &[u8], xq: &[i8]) -> i32 {
2524    // SAFETY: callers uphold slice-length contracts (see call sites).
2525    unsafe {
2526        use core::arch::x86_64::*;
2527        let n = w.len();
2528        let mut j = 0usize;
2529        let mut total: i32;
2530        // 4 independent accumulators: vpdpbusd is its own loop-carried
2531        // dependency (~5-cycle latency) — a single-acc loop runs
2532        // latency-bound and LOSES to the AVX2 maddubs kernel, measured
2533        // on Granite Rapids.
2534        {
2535            #[inline(always)]
2536            unsafe fn step(
2537                w: *const u8,
2538                x: *const i8,
2539                acc: core::arch::x86_64::__m512i,
2540            ) -> core::arch::x86_64::__m512i {
2541                unsafe {
2542                    use core::arch::x86_64::*;
2543                    let wv = _mm512_loadu_si512(w as *const _);
2544                    let xv = _mm512_loadu_si512(x as *const _);
2545                    let aw = _mm512_abs_epi8(wv);
2546                    let neg = _mm512_movepi8_mask(wv);
2547                    let sx = _mm512_mask_sub_epi8(xv, neg, _mm512_setzero_si512(), xv);
2548                    _mm512_dpbusd_epi32(acc, aw, sx)
2549                }
2550            }
2551            let (mut a0, mut a1, mut a2, mut a3) = (
2552                _mm512_setzero_si512(),
2553                _mm512_setzero_si512(),
2554                _mm512_setzero_si512(),
2555                _mm512_setzero_si512(),
2556            );
2557            while j + 256 <= n {
2558                a0 = step(w.as_ptr().add(j), xq.as_ptr().add(j), a0);
2559                a1 = step(w.as_ptr().add(j + 64), xq.as_ptr().add(j + 64), a1);
2560                a2 = step(w.as_ptr().add(j + 128), xq.as_ptr().add(j + 128), a2);
2561                a3 = step(w.as_ptr().add(j + 192), xq.as_ptr().add(j + 192), a3);
2562                j += 256;
2563            }
2564            while j + 64 <= n {
2565                a0 = step(w.as_ptr().add(j), xq.as_ptr().add(j), a0);
2566                j += 64;
2567            }
2568            let s01 = _mm512_add_epi32(a0, a1);
2569            let s23 = _mm512_add_epi32(a2, a3);
2570            total = _mm512_reduce_add_epi32(_mm512_add_epi32(s01, s23));
2571        }
2572        // 32-wide (q4/vbit groups are exactly 32 bytes).
2573        if j + 32 <= n {
2574            let wv = _mm256_loadu_si256(w.as_ptr().add(j) as *const __m256i);
2575            let xv = _mm256_loadu_si256(xq.as_ptr().add(j) as *const __m256i);
2576            let d = _mm256_dpbusd_epi32(
2577                _mm256_setzero_si256(),
2578                _mm256_abs_epi8(wv),
2579                _mm256_sign_epi8(xv, wv),
2580            );
2581            let hi128 = _mm256_extracti128_si256::<1>(d);
2582            let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
2583            let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
2584            let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
2585            total += _mm_cvtsi128_si32(s32);
2586            j += 32;
2587        }
2588        while j < n {
2589            total += (w[j] as i8) as i32 * xq[j] as i32;
2590            j += 1;
2591        }
2592        total
2593    }
2594}
2595
2596/// i8 row · f32 x via AVX2/FMA (x86 mirror of `dot_i8_f32_neon`).
2597#[cfg(target_arch = "x86_64")]
2598#[target_feature(enable = "avx2,fma")]
2599unsafe fn dot_i8_f32_avx2(w: &[u8], x: &[f32]) -> f32 {
2600    // SAFETY: callers uphold slice-length contracts (see call sites).
2601    unsafe {
2602        use core::arch::x86_64::*;
2603        let n = x.len();
2604        let wp = w.as_ptr();
2605        let xp = x.as_ptr();
2606        let (mut a0, mut a1) = (_mm256_setzero_ps(), _mm256_setzero_ps());
2607        let mut j = 0usize;
2608        while j + 16 <= n {
2609            let wb = _mm_loadu_si128(wp.add(j) as *const __m128i);
2610            let lo = _mm256_cvtepi8_epi32(wb);
2611            let hi = _mm256_cvtepi8_epi32(_mm_srli_si128::<8>(wb));
2612            a0 = _mm256_fmadd_ps(_mm256_cvtepi32_ps(lo), _mm256_loadu_ps(xp.add(j)), a0);
2613            a1 = _mm256_fmadd_ps(_mm256_cvtepi32_ps(hi), _mm256_loadu_ps(xp.add(j + 8)), a1);
2614            j += 16;
2615        }
2616        let acc = _mm256_add_ps(a0, a1);
2617        let hi128 = _mm256_extractf128_ps::<1>(acc);
2618        let s128 = _mm_add_ps(_mm256_castps256_ps128(acc), hi128);
2619        let s64 = _mm_add_ps(s128, _mm_movehl_ps(s128, s128));
2620        let s32 = _mm_add_ss(s64, _mm_shuffle_ps::<1>(s64, s64));
2621        let mut sum = _mm_cvtss_f32(s32);
2622        while j < n {
2623            sum += (*wp.add(j) as i8) as f32 * *xp.add(j);
2624            j += 1;
2625        }
2626        sum
2627    }
2628}
2629
2630/// int8(weight)·int8(activation) → i32 via AVX2 maddubs — the x86
2631/// analogue of the SDOT A8W8 path. `maddubs` takes u8×i8, so the
2632/// standard sign trick applies: |w| × sign(x, w) ≡ w × x per lane.
2633/// Pair saturation is safe: |w|≤128, |x|≤127 → 2·128·127 < 32767.
2634#[cfg(target_arch = "x86_64")]
2635#[target_feature(enable = "avx2")]
2636unsafe fn dot_i8_i8_avx2(w: &[u8], xq: &[i8]) -> i32 {
2637    // SAFETY: callers uphold slice-length contracts (see call sites).
2638    unsafe {
2639        use core::arch::x86_64::*;
2640        let n = w.len();
2641        let ones = _mm256_set1_epi16(1);
2642        let mut acc = _mm256_setzero_si256();
2643        let mut j = 0usize;
2644        while j + 32 <= n {
2645            let wv = _mm256_loadu_si256(w.as_ptr().add(j) as *const __m256i);
2646            let xv = _mm256_loadu_si256(xq.as_ptr().add(j) as *const __m256i);
2647            let p16 = _mm256_maddubs_epi16(_mm256_abs_epi8(wv), _mm256_sign_epi8(xv, wv));
2648            acc = _mm256_add_epi32(acc, _mm256_madd_epi16(p16, ones));
2649            j += 32;
2650        }
2651        let hi128 = _mm256_extracti128_si256::<1>(acc);
2652        let s128 = _mm_add_epi32(_mm256_castsi256_si128(acc), hi128);
2653        let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
2654        let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
2655        let mut s = _mm_cvtsi128_si32(s32);
2656        while j < n {
2657            s += (w[j] as i8) as i32 * xq[j] as i32;
2658            j += 1;
2659        }
2660        s
2661    }
2662}
2663
2664/// AVX2/VNNI q8 row dot with exact outlier correction (x86 mirror of
2665/// `row_dot_sdot` — same A8W8 contract). With AVX-512 VNNI the row goes
2666/// through the bias trick: Σ(w+128)·x via pure `vpdpbusd` (no per-lane
2667/// sign fixups), corrected by −128·Σx with Σx precomputed per split.
2668#[cfg(target_arch = "x86_64")]
2669#[inline]
2670fn row_dot_avx2(row: &[u8], act: &SplitAct) -> f32 {
2671    let dot = if avx512vnni_enabled() && row.len() >= 64 {
2672        (unsafe { dot_u8p128_i8_vnni(row, &act.xq) }) - 128 * act.xsum
2673    } else {
2674        unsafe { dot_i8_i8_avx2(row, &act.xq) }
2675    };
2676    let mut acc = dot as f32 * act.sx;
2677    for &(j, xv) in &act.outliers {
2678        acc += (row[j] as i8) as f32 * xv;
2679    }
2680    acc
2681}
2682
2683/// Σ (w[i]+128)·x[i] via pure `vpdpbusd` — the caller subtracts
2684/// 128·Σx. Four independent accumulators (dpbusd is ~5-cycle latency;
2685/// a single-acc loop runs latency-bound, measured on Granite Rapids).
2686#[cfg(target_arch = "x86_64")]
2687#[target_feature(enable = "avx2,avx512f,avx512bw,avx512vl,avx512vnni")]
2688unsafe fn dot_u8p128_i8_vnni(w: &[u8], xq: &[i8]) -> i32 {
2689    // SAFETY: callers uphold slice-length contracts (see call sites).
2690    unsafe {
2691        use core::arch::x86_64::*;
2692        let n = w.len();
2693        let flip = _mm512_set1_epi8(-128); // XOR 0x80: i8 w → u8 (w+128)
2694        #[inline(always)]
2695        unsafe fn step(
2696            w: *const u8,
2697            x: *const i8,
2698            flip: core::arch::x86_64::__m512i,
2699            acc: core::arch::x86_64::__m512i,
2700        ) -> core::arch::x86_64::__m512i {
2701            unsafe {
2702                use core::arch::x86_64::*;
2703                let wv = _mm512_xor_si512(_mm512_loadu_si512(w as *const _), flip);
2704                _mm512_dpbusd_epi32(acc, wv, _mm512_loadu_si512(x as *const _))
2705            }
2706        }
2707        let (mut a0, mut a1, mut a2, mut a3) = (
2708            _mm512_setzero_si512(),
2709            _mm512_setzero_si512(),
2710            _mm512_setzero_si512(),
2711            _mm512_setzero_si512(),
2712        );
2713        let mut j = 0usize;
2714        while j + 256 <= n {
2715            a0 = step(w.as_ptr().add(j), xq.as_ptr().add(j), flip, a0);
2716            a1 = step(w.as_ptr().add(j + 64), xq.as_ptr().add(j + 64), flip, a1);
2717            a2 = step(w.as_ptr().add(j + 128), xq.as_ptr().add(j + 128), flip, a2);
2718            a3 = step(w.as_ptr().add(j + 192), xq.as_ptr().add(j + 192), flip, a3);
2719            j += 256;
2720        }
2721        while j + 64 <= n {
2722            a0 = step(w.as_ptr().add(j), xq.as_ptr().add(j), flip, a0);
2723            j += 64;
2724        }
2725        let mut total = _mm512_reduce_add_epi32(_mm512_add_epi32(
2726            _mm512_add_epi32(a0, a1),
2727            _mm512_add_epi32(a2, a3),
2728        ));
2729        // Scalar tail: (w as i8) + 128 ≡ (w as u8) ^ 0x80.
2730        while j < n {
2731            total += ((w[j] ^ 0x80) as i32) * xq[j] as i32;
2732            j += 1;
2733        }
2734        total
2735    }
2736}
2737
2738/// One q4 row via AVX2: nibbles → centered i8 (unpacklo/hi restores the
2739/// writer's flat order, same as the NEON vzip pair), maddubs against
2740/// the pre-quantized activation group, × the group's f16 scale. Pair
2741/// saturation safe: |w|≤8, |x|≤127 → 2·8·127 ≪ 32767. Mirror of
2742/// `dot_q4_row_sdot`.
2743#[cfg(target_arch = "x86_64")]
2744#[target_feature(enable = "avx2")]
2745unsafe fn dot_q4_row_avx2(packed: &[u8], scales: &[u8], g0: usize, gpr: usize, xq: &[i8]) -> f32 {
2746    // SAFETY: callers uphold slice-length contracts (16 packed bytes and
2747    // 2 scale bytes per group; xq.len() == gpr·GROUP_SIZE).
2748    unsafe {
2749        use core::arch::x86_64::*;
2750        let lomask = _mm_set1_epi8(0x0F);
2751        let eight = _mm256_set1_epi8(8);
2752        let ones = _mm256_set1_epi16(1);
2753        let mut acc = 0f32;
2754        for gi in 0..gpr {
2755            let g = g0 + gi;
2756            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
2757            let b = _mm_loadu_si128(packed.as_ptr().add(g * 16) as *const __m128i);
2758            let lo = _mm_and_si128(b, lomask);
2759            let hi = _mm_and_si128(_mm_srli_epi16::<4>(b), lomask);
2760            let w = _mm256_sub_epi8(
2761                _mm256_set_m128i(_mm_unpackhi_epi8(lo, hi), _mm_unpacklo_epi8(lo, hi)),
2762                eight,
2763            );
2764            let x = _mm256_loadu_si256(xq.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
2765            let p16 = _mm256_maddubs_epi16(_mm256_abs_epi8(w), _mm256_sign_epi8(x, w));
2766            let d = _mm256_madd_epi16(p16, ones);
2767            let hi128 = _mm256_extracti128_si256::<1>(d);
2768            let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
2769            let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
2770            let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
2771            acc += _mm_cvtsi128_si32(s32) as f32 * s;
2772        }
2773        acc
2774    }
2775}
2776
2777/// Two-activation q4 row via AVX2: nibbles unpacked ONCE per group,
2778/// both activations dotted against the same centered i8 register.
2779#[cfg(target_arch = "x86_64")]
2780#[target_feature(enable = "avx2")]
2781unsafe fn dot_q4_row_avx2_2(
2782    packed: &[u8],
2783    scales: &[u8],
2784    g0: usize,
2785    gpr: usize,
2786    xq1: &[i8],
2787    xq2: &[i8],
2788) -> (f32, f32) {
2789    // SAFETY: callers uphold slice-length contracts (see dot_q4_row_avx2).
2790    unsafe {
2791        use core::arch::x86_64::*;
2792        let lomask = _mm_set1_epi8(0x0F);
2793        let eight = _mm256_set1_epi8(8);
2794        let ones = _mm256_set1_epi16(1);
2795        let (mut acc1, mut acc2) = (0f32, 0f32);
2796        #[inline(always)]
2797        unsafe fn hsum(d: core::arch::x86_64::__m256i) -> i32 {
2798            unsafe {
2799                use core::arch::x86_64::*;
2800                let hi128 = _mm256_extracti128_si256::<1>(d);
2801                let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
2802                let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
2803                let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
2804                _mm_cvtsi128_si32(s32)
2805            }
2806        }
2807        for gi in 0..gpr {
2808            let g = g0 + gi;
2809            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
2810            let b = _mm_loadu_si128(packed.as_ptr().add(g * 16) as *const __m128i);
2811            let lo = _mm_and_si128(b, lomask);
2812            let hi = _mm_and_si128(_mm_srli_epi16::<4>(b), lomask);
2813            let w = _mm256_sub_epi8(
2814                _mm256_set_m128i(_mm_unpackhi_epi8(lo, hi), _mm_unpacklo_epi8(lo, hi)),
2815                eight,
2816            );
2817            let aw = _mm256_abs_epi8(w);
2818            let x1 = _mm256_loadu_si256(xq1.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
2819            let x2 = _mm256_loadu_si256(xq2.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
2820            let d1 = _mm256_madd_epi16(_mm256_maddubs_epi16(aw, _mm256_sign_epi8(x1, w)), ones);
2821            let d2 = _mm256_madd_epi16(_mm256_maddubs_epi16(aw, _mm256_sign_epi8(x2, w)), ones);
2822            acc1 += hsum(d1) as f32 * s;
2823            acc2 += hsum(d2) as f32 * s;
2824        }
2825        (acc1, acc2)
2826    }
2827}
2828
2829/// One q8 row range via AVX2 (x86 mirror of `q8_range_sdot`).
2830#[cfg(target_arch = "x86_64")]
2831fn q8_range_avx2(
2832    q: &[u8],
2833    row_scale: &[f32],
2834    act: &SplitAct,
2835    cols: usize,
2836    out_addr: SendMut,
2837    start: usize,
2838    end: usize,
2839) {
2840    for o in start..end {
2841        let v = row_dot_avx2(&q[o * cols..(o + 1) * cols], act) * row_scale[o];
2842        // SAFETY: disjoint row ranges per worker.
2843        unsafe { *out_addr.at(o) = v };
2844    }
2845}
2846
2847/// Two-input q8 row range via AVX2 (x86 mirror of `q8_range2_sdot`).
2848#[cfg(target_arch = "x86_64")]
2849#[allow(clippy::too_many_arguments)]
2850fn q8_range2_avx2(
2851    q: &[u8],
2852    row_scale: &[f32],
2853    a1: &SplitAct,
2854    a2: &SplitAct,
2855    cols: usize,
2856    p1: SendMut,
2857    p2: SendMut,
2858    start: usize,
2859    end: usize,
2860) {
2861    for o in start..end {
2862        let row = &q[o * cols..(o + 1) * cols];
2863        // SAFETY: disjoint row ranges per worker.
2864        unsafe {
2865            *p1.at(o) = row_dot_avx2(row, a1) * row_scale[o];
2866            *p2.at(o) = row_dot_avx2(row, a2) * row_scale[o];
2867        }
2868    }
2869}
2870
2871// ───────────────────── A8W8 SDOT path (port of vmfcore, ×1.78 decode) ─────────────────────
2872
2873/// SDOT enabled? Default ON when the CPU has ARMv8.2 dotprod;
2874/// `CMF_SDOT=0` disables (falls back to i8×f32 NEON).
2875/// (On non-ARM release builds only the test tolerance switch calls it.)
2876#[cfg_attr(not(target_arch = "aarch64"), allow(dead_code))]
2877fn sdot_enabled() -> bool {
2878    use std::sync::OnceLock;
2879    static ON: OnceLock<bool> = OnceLock::new();
2880    *ON.get_or_init(|| {
2881        let want = std::env::var("CMF_SDOT").map(|v| v != "0").unwrap_or(true);
2882        #[cfg(target_arch = "aarch64")]
2883        {
2884            want && std::arch::is_aarch64_feature_detected!("dotprod")
2885        }
2886        #[cfg(not(target_arch = "aarch64"))]
2887        {
2888            let _ = want;
2889            false
2890        }
2891    })
2892}
2893
2894/// Two-field activation split (≡ vmfcore `q8_split_prep`): outlier
2895/// channels (>8·rms) are computed exactly in f32; the bulk (outliers
2896/// zeroed → clean absmax) goes through int8 SDOT. Computed ONCE per
2897/// matvec, shared by all rows/workers.
2898struct SplitAct {
2899    xq: Vec<i8>,
2900    sx: f32,
2901    outliers: Vec<(usize, f32)>,
2902    /// Σ xq — the VNNI bias-trick correction (`(w+128)·x` sums need
2903    /// `−128·Σx`); one i32 per split, computed once per matvec.
2904    #[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
2905    xsum: i32,
2906}
2907
2908thread_local! {
2909    /// Recycled xq buffers: split_act runs for every matvec (~200/token)
2910    /// and its hidden-size allocation was steady-state heap churn.
2911    static XQ_FREE: std::cell::RefCell<Vec<Vec<i8>>> =
2912        const { std::cell::RefCell::new(Vec::new()) };
2913}
2914
2915impl Drop for SplitAct {
2916    fn drop(&mut self) {
2917        let buf = std::mem::take(&mut self.xq);
2918        if buf.capacity() > 0 {
2919            XQ_FREE.with(|f| {
2920                let mut f = f.borrow_mut();
2921                if f.len() < 16 {
2922                    f.push(buf);
2923                }
2924            });
2925        }
2926    }
2927}
2928
2929fn split_act(x: &[f32]) -> SplitAct {
2930    let n = x.len();
2931    let rms = (x.iter().map(|&v| (v * v) as f64).sum::<f64>() / n.max(1) as f64).sqrt() as f32;
2932    let thr = 8.0 * rms;
2933    // One pass: collect outliers and the bulk absmax (outliers excluded —
2934    // identical to the old zero-then-fold over a copied buffer, minus the
2935    // full-vector copy).
2936    let mut outliers: Vec<(usize, f32)> = Vec::new();
2937    let mut amax = 0f32;
2938    for (j, &v) in x.iter().enumerate() {
2939        let a = v.abs();
2940        if a > thr {
2941            outliers.push((j, v));
2942        } else if a > amax {
2943            amax = a;
2944        }
2945    }
2946    let sx = if amax > 0.0 { amax / 127.0 } else { 1.0 };
2947    let inv = 1.0 / sx;
2948    let mut xq = XQ_FREE.with(|f| f.borrow_mut().pop()).unwrap_or_default();
2949    xq.clear();
2950    xq.reserve(n);
2951    if outliers.is_empty() {
2952        xq.extend(x.iter().map(|&v| (v * inv).round().clamp(-127.0, 127.0) as i8));
2953    } else {
2954        // Outlier slots quantize to 0 (their exact term is added later).
2955        xq.extend(x.iter().map(|&v| {
2956            if v.abs() > thr {
2957                0
2958            } else {
2959                (v * inv).round().clamp(-127.0, 127.0) as i8
2960            }
2961        }));
2962    }
2963    let xsum = xq.iter().map(|&v| v as i32).sum();
2964    SplitAct { xq, sx, outliers, xsum }
2965}
2966
2967/// int8(weight)·int8(activation) → i32 via `sdot` (inline asm — the
2968/// vdotq intrinsic is unstable; port of vmfcore `dot_i8_sdot`).
2969#[cfg(target_arch = "aarch64")]
2970#[target_feature(enable = "neon,dotprod")]
2971unsafe fn dot_i8_sdot(w: &[u8], xq: &[i8]) -> i32 {
2972    // SAFETY: callers uphold slice-length contracts (see call sites).
2973    unsafe {
2974        use core::arch::aarch64::*;
2975        use core::arch::asm;
2976        let wp = w.as_ptr() as *const i8;
2977        let n = w.len();
2978        let (mut a0, mut a1, mut a2, mut a3) =
2979            (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
2980        let mut i = 0;
2981        while i + 64 <= n {
2982            let (w0, x0) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
2983            let (w1, x1) = (vld1q_s8(wp.add(i + 16)), vld1q_s8(xq.as_ptr().add(i + 16)));
2984            let (w2, x2) = (vld1q_s8(wp.add(i + 32)), vld1q_s8(xq.as_ptr().add(i + 32)));
2985            let (w3, x3) = (vld1q_s8(wp.add(i + 48)), vld1q_s8(xq.as_ptr().add(i + 48)));
2986            asm!(
2987                "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
2988                "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
2989                "sdot {a2:v}.4s, {w2:v}.16b, {x2:v}.16b",
2990                "sdot {a3:v}.4s, {w3:v}.16b, {x3:v}.16b",
2991                a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
2992                w0 = in(vreg) w0, x0 = in(vreg) x0, w1 = in(vreg) w1, x1 = in(vreg) x1,
2993                w2 = in(vreg) w2, x2 = in(vreg) x2, w3 = in(vreg) w3, x3 = in(vreg) x3,
2994                options(pure, nomem, nostack),
2995            );
2996            i += 64;
2997        }
2998        while i + 16 <= n {
2999            let (wv, xv) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
3000            asm!("sdot {a:v}.4s, {w:v}.16b, {x:v}.16b",
3001                 a = inout(vreg) a0, w = in(vreg) wv, x = in(vreg) xv, options(pure, nomem, nostack));
3002            i += 16;
3003        }
3004        let mut s = vaddvq_s32(vaddq_s32(vaddq_s32(a0, a1), vaddq_s32(a2, a3)));
3005        while i < n {
3006            s += (*wp.add(i)) as i32 * xq[i] as i32;
3007            i += 1;
3008        }
3009        s
3010}
3011}
3012
3013/// Row-blocked SDOT: 4 output rows per pass — the activation chunk is
3014/// loaded once and reused, 4 independent accumulators hide sdot latency
3015/// (port of vmfcore `dot_i8_sdot_4rows`).
3016#[cfg(target_arch = "aarch64")]
3017#[target_feature(enable = "neon,dotprod")]
3018unsafe fn dot_i8_sdot_4rows(w0: &[u8], w1: &[u8], w2: &[u8], w3: &[u8], xq: &[i8]) -> [i32; 4] {
3019    // SAFETY: callers uphold slice-length contracts (see call sites).
3020    unsafe {
3021        use core::arch::aarch64::*;
3022        use core::arch::asm;
3023        let n = xq.len();
3024        let px = xq.as_ptr();
3025        let (p0, p1, p2, p3) = (
3026            w0.as_ptr() as *const i8,
3027            w1.as_ptr() as *const i8,
3028            w2.as_ptr() as *const i8,
3029            w3.as_ptr() as *const i8,
3030        );
3031        let (mut a0, mut a1, mut a2, mut a3) =
3032            (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
3033        let mut i = 0;
3034        while i + 16 <= n {
3035            let x = vld1q_s8(px.add(i));
3036            let v0 = vld1q_s8(p0.add(i));
3037            let v1 = vld1q_s8(p1.add(i));
3038            let v2 = vld1q_s8(p2.add(i));
3039            let v3 = vld1q_s8(p3.add(i));
3040            asm!(
3041                "sdot {a0:v}.4s, {v0:v}.16b, {x:v}.16b",
3042                "sdot {a1:v}.4s, {v1:v}.16b, {x:v}.16b",
3043                "sdot {a2:v}.4s, {v2:v}.16b, {x:v}.16b",
3044                "sdot {a3:v}.4s, {v3:v}.16b, {x:v}.16b",
3045                a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
3046                v0 = in(vreg) v0, v1 = in(vreg) v1, v2 = in(vreg) v2, v3 = in(vreg) v3, x = in(vreg) x,
3047                options(pure, nomem, nostack),
3048            );
3049            i += 16;
3050        }
3051        let mut r = [vaddvq_s32(a0), vaddvq_s32(a1), vaddvq_s32(a2), vaddvq_s32(a3)];
3052        while i < n {
3053            let xi = *px.add(i) as i32;
3054            r[0] += (*p0.add(i)) as i32 * xi;
3055            r[1] += (*p1.add(i)) as i32 * xi;
3056            r[2] += (*p2.add(i)) as i32 * xi;
3057            r[3] += (*p3.add(i)) as i32 * xi;
3058            i += 1;
3059        }
3060        r
3061}
3062}
3063
3064/// One q8 row range via SDOT (4-row blocks + tail) — the body of
3065/// `qmatvec`'s hot loop, extracted so multi-matrix jobs can drive the
3066/// SAME kernel for several tensors under one pool dispatch.
3067#[cfg(target_arch = "aarch64")]
3068fn q8_range_sdot(
3069    q: &[u8],
3070    row_scale: &[f32],
3071    act: &SplitAct,
3072    cols: usize,
3073    out_addr: SendMut,
3074    start: usize,
3075    end: usize,
3076) {
3077    let mut o = start;
3078    while o + 4 <= end {
3079        let r = unsafe {
3080            dot_i8_sdot_4rows(
3081                &q[o * cols..(o + 1) * cols],
3082                &q[(o + 1) * cols..(o + 2) * cols],
3083                &q[(o + 2) * cols..(o + 3) * cols],
3084                &q[(o + 3) * cols..(o + 4) * cols],
3085                &act.xq,
3086            )
3087        };
3088        for k in 0..4 {
3089            let mut acc = r[k] as f32 * act.sx;
3090            for &(j, xv) in &act.outliers {
3091                acc += (q[(o + k) * cols + j] as i8) as f32 * xv;
3092            }
3093            // SAFETY: disjoint row ranges per worker.
3094            unsafe { *out_addr.at(o + k) = acc * row_scale[o + k] };
3095        }
3096        o += 4;
3097    }
3098    while o < end {
3099        let v = row_dot_sdot(&q[o * cols..(o + 1) * cols], act) * row_scale[o];
3100        unsafe { *out_addr.at(o) = v };
3101        o += 1;
3102    }
3103}
3104
3105/// Two-input q8 row range via SDOT — `qmatvec2`'s hot loop, extracted
3106/// for the fused pair multi-matrix job (`matvec2_many`).
3107#[cfg(target_arch = "aarch64")]
3108#[allow(clippy::too_many_arguments)]
3109fn q8_range2_sdot(
3110    q: &[u8],
3111    row_scale: &[f32],
3112    a1: &SplitAct,
3113    a2: &SplitAct,
3114    cols: usize,
3115    p1: SendMut,
3116    p2: SendMut,
3117    start: usize,
3118    end: usize,
3119) {
3120    for o in start..end {
3121        let row = &q[o * cols..(o + 1) * cols];
3122        // SAFETY: disjoint row ranges per worker.
3123        unsafe {
3124            *p1.at(o) = row_dot_sdot(row, a1) * row_scale[o];
3125            *p2.at(o) = row_dot_sdot(row, a2) * row_scale[o];
3126        }
3127    }
3128}
3129
3130/// Two-input q8 row range, f32 kernel (non-SDOT) — same extraction.
3131#[allow(clippy::too_many_arguments)]
3132fn q8_range2_f32(
3133    q: &[u8],
3134    row_scale: &[f32],
3135    x1: &[f32],
3136    x2: &[f32],
3137    cols: usize,
3138    p1: SendMut,
3139    p2: SendMut,
3140    start: usize,
3141    end: usize,
3142) {
3143    for o in start..end {
3144        let row = &q[o * cols..(o + 1) * cols];
3145        // SAFETY: disjoint row ranges per worker.
3146        unsafe {
3147            *p1.at(o) = dot_i8_f32(row, x1) * row_scale[o];
3148            *p2.at(o) = dot_i8_f32(row, x2) * row_scale[o];
3149        }
3150    }
3151}
3152
3153/// Scalar/NEON-f32 q8 row range (non-SDOT platforms) — same extraction.
3154fn q8_range_f32(
3155    q: &[u8],
3156    row_scale: &[f32],
3157    xs: &[f32],
3158    cols: usize,
3159    out_addr: SendMut,
3160    start: usize,
3161    end: usize,
3162) {
3163    for o in start..end {
3164        let v = dot_i8_f32(&q[o * cols..(o + 1) * cols], xs) * row_scale[o];
3165        // SAFETY: disjoint row ranges per worker.
3166        unsafe { *out_addr.at(o) = v };
3167    }
3168}
3169
3170/// SDOT row dot with exact outlier correction:
3171/// `dot = sdot(w, xq)·sx + Σ_outl w[j]·x[j]` (then × row_scale by caller).
3172#[cfg(target_arch = "aarch64")]
3173#[inline]
3174fn row_dot_sdot(row: &[u8], act: &SplitAct) -> f32 {
3175    let mut acc = unsafe { dot_i8_sdot(row, &act.xq) } as f32 * act.sx;
3176    for &(j, xv) in &act.outliers {
3177        acc += (row[j] as i8) as f32 * xv;
3178    }
3179    acc
3180}
3181
3182/// One q4 row via SDOT: each 32-group's nibbles unpack to centered i8
3183/// (nib−8 ∈ [−8,7]), int8×int8 `sdot` against the pre-quantized
3184/// activation group, × the group's f16 scale. Returns Σ_g dot_g·s_g;
3185/// the caller multiplies by the activation scale and adds the exact
3186/// outlier terms (port of vmfcore `dot_q4_block_sdot`, +23% measured).
3187/// Nibble order matches the writer: element 2k = low nibble, 2k+1 = high
3188/// → zip(lo,hi) restores flat order.
3189#[cfg(target_arch = "aarch64")]
3190#[target_feature(enable = "neon,dotprod")]
3191unsafe fn dot_q4_row_sdot(packed: &[u8], scales: &[u8], g0: usize, gpr: usize, xq: &[i8]) -> f32 {
3192    // SAFETY: callers uphold slice-length contracts (16 packed bytes and
3193    // 2 scale bytes per group; xq.len() == gpr·GROUP_SIZE).
3194    unsafe {
3195        use core::arch::aarch64::*;
3196        use core::arch::asm;
3197        let lomask = vdupq_n_u8(0x0F);
3198        let eight = vdupq_n_s8(8);
3199        let mut acc = 0f32;
3200        for gi in 0..gpr {
3201            let g = g0 + gi;
3202            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
3203            let b = vld1q_u8(packed.as_ptr().add(g * 16));
3204            let lo = vandq_u8(b, lomask);
3205            let hi = vshrq_n_u8::<4>(b);
3206            let e0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(lo, hi)), eight);
3207            let e1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(lo, hi)), eight);
3208            let x0 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE));
3209            let x1 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE + 16));
3210            let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
3211            asm!(
3212                "sdot {a0:v}.4s, {e0:v}.16b, {x0:v}.16b",
3213                "sdot {a1:v}.4s, {e1:v}.16b, {x1:v}.16b",
3214                a0 = inout(vreg) a0, a1 = inout(vreg) a1,
3215                e0 = in(vreg) e0, x0 = in(vreg) x0, e1 = in(vreg) e1, x1 = in(vreg) x1,
3216                options(pure, nomem, nostack),
3217            );
3218            acc += vaddvq_s32(vaddq_s32(a0, a1)) as f32 * s;
3219        }
3220        acc
3221    }
3222}
3223
3224/// Two-activation q4 row via SDOT: the nibble unpack (the expensive
3225/// part) happens ONCE per group; both pre-quantized activations are
3226/// dotted against the same centered i8 registers. Per-lane math matches
3227/// `dot_q4_row_sdot` exactly.
3228#[cfg(target_arch = "aarch64")]
3229#[target_feature(enable = "neon,dotprod")]
3230unsafe fn dot_q4_row_sdot2(
3231    packed: &[u8],
3232    scales: &[u8],
3233    g0: usize,
3234    gpr: usize,
3235    xq1: &[i8],
3236    xq2: &[i8],
3237) -> (f32, f32) {
3238    // SAFETY: callers uphold slice-length contracts (16 packed bytes and
3239    // 2 scale bytes per group; xq*.len() == gpr·GROUP_SIZE).
3240    unsafe {
3241        use core::arch::aarch64::*;
3242        use core::arch::asm;
3243        let lomask = vdupq_n_u8(0x0F);
3244        let eight = vdupq_n_s8(8);
3245        let (mut acc1, mut acc2) = (0f32, 0f32);
3246        for gi in 0..gpr {
3247            let g = g0 + gi;
3248            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
3249            let b = vld1q_u8(packed.as_ptr().add(g * 16));
3250            let lo = vandq_u8(b, lomask);
3251            let hi = vshrq_n_u8::<4>(b);
3252            let e0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(lo, hi)), eight);
3253            let e1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(lo, hi)), eight);
3254            let x10 = vld1q_s8(xq1.as_ptr().add(gi * GROUP_SIZE));
3255            let x11 = vld1q_s8(xq1.as_ptr().add(gi * GROUP_SIZE + 16));
3256            let x20 = vld1q_s8(xq2.as_ptr().add(gi * GROUP_SIZE));
3257            let x21 = vld1q_s8(xq2.as_ptr().add(gi * GROUP_SIZE + 16));
3258            let (mut a0, mut a1, mut b0, mut b1) =
3259                (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
3260            asm!(
3261                "sdot {a0:v}.4s, {e0:v}.16b, {x10:v}.16b",
3262                "sdot {a1:v}.4s, {e1:v}.16b, {x11:v}.16b",
3263                "sdot {b0:v}.4s, {e0:v}.16b, {x20:v}.16b",
3264                "sdot {b1:v}.4s, {e1:v}.16b, {x21:v}.16b",
3265                a0 = inout(vreg) a0, a1 = inout(vreg) a1,
3266                b0 = inout(vreg) b0, b1 = inout(vreg) b1,
3267                e0 = in(vreg) e0, e1 = in(vreg) e1,
3268                x10 = in(vreg) x10, x11 = in(vreg) x11,
3269                x20 = in(vreg) x20, x21 = in(vreg) x21,
3270                options(pure, nomem, nostack),
3271            );
3272            acc1 += vaddvq_s32(vaddq_s32(a0, a1)) as f32 * s;
3273            acc2 += vaddvq_s32(vaddq_s32(b0, b1)) as f32 * s;
3274        }
3275        (acc1, acc2)
3276    }
3277}
3278
3279// ───────────────────── fused int8 kernels ─────────────────────
3280
3281/// `acc += w · row` where the row is centered i8 — NEON widen+fma on
3282/// aarch64, scalar elsewhere. The KV-cache q8 value path rides on this.
3283#[inline]
3284pub(crate) fn axpy_i8_f32(acc: &mut [f32], row: &[i8], w: f32) {
3285    #[cfg(target_arch = "aarch64")]
3286    unsafe {
3287        return axpy_i8_f32_neon(acc, row, w);
3288    }
3289    #[cfg(target_arch = "x86_64")]
3290    if avx2_enabled() {
3291        return unsafe { axpy_i8_f32_avx2(acc, row, w) };
3292    }
3293    #[allow(unreachable_code)]
3294    {
3295        for (a, &b) in acc.iter_mut().zip(row) {
3296            *a += w * b as f32;
3297        }
3298    }
3299}
3300
3301/// i8→f32 axpy via AVX2/FMA (x86 mirror of `axpy_i8_f32_neon`).
3302#[cfg(target_arch = "x86_64")]
3303#[target_feature(enable = "avx2,fma")]
3304unsafe fn axpy_i8_f32_avx2(acc: &mut [f32], row: &[i8], w: f32) {
3305    // SAFETY: callers uphold slice-length contracts (see call sites).
3306    unsafe {
3307        use core::arch::x86_64::*;
3308        let n = acc.len().min(row.len());
3309        let ap = acc.as_mut_ptr();
3310        let rp = row.as_ptr();
3311        let wv = _mm256_set1_ps(w);
3312        let mut j = 0usize;
3313        while j + 16 <= n {
3314            let rb = _mm_loadu_si128(rp.add(j) as *const __m128i);
3315            let lo = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(rb));
3316            let hi = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(_mm_srli_si128::<8>(rb)));
3317            let v0 = _mm256_fmadd_ps(wv, lo, _mm256_loadu_ps(ap.add(j)));
3318            let v1 = _mm256_fmadd_ps(wv, hi, _mm256_loadu_ps(ap.add(j + 8)));
3319            _mm256_storeu_ps(ap.add(j), v0);
3320            _mm256_storeu_ps(ap.add(j + 8), v1);
3321            j += 16;
3322        }
3323        while j < n {
3324            *ap.add(j) += w * (*rp.add(j)) as f32;
3325            j += 1;
3326        }
3327    }
3328}
3329
3330#[cfg(target_arch = "aarch64")]
3331#[target_feature(enable = "neon")]
3332unsafe fn axpy_i8_f32_neon(acc: &mut [f32], row: &[i8], w: f32) {
3333    // SAFETY: callers uphold slice-length contracts (see call sites).
3334    unsafe {
3335        use core::arch::aarch64::*;
3336        let n = acc.len().min(row.len());
3337        let ap = acc.as_mut_ptr();
3338        let rp = row.as_ptr();
3339        let wv = vdupq_n_f32(w);
3340        let mut j = 0usize;
3341        while j + 16 <= n {
3342            let rb = vld1q_s8(rp.add(j));
3343            let lo = vmovl_s8(vget_low_s8(rb));
3344            let hi = vmovl_s8(vget_high_s8(rb));
3345            for (off, half) in [(0, lo), (8, hi)] {
3346                let f0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(half)));
3347                let f1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(half)));
3348                let o = j + off;
3349                vst1q_f32(ap.add(o), vfmaq_f32(vld1q_f32(ap.add(o)), wv, f0));
3350                vst1q_f32(ap.add(o + 4), vfmaq_f32(vld1q_f32(ap.add(o + 4)), wv, f1));
3351            }
3352            j += 16;
3353        }
3354        while j < n {
3355            *ap.add(j) += w * (*rp.add(j)) as f32;
3356            j += 1;
3357        }
3358}
3359}
3360
3361/// i8 row · f32 x. NEON on aarch64 (ported from vmfcore `dot_i8_f32_neon`,
3362/// ≈9× scalar), scalar elsewhere.
3363#[inline]
3364pub(crate) fn dot_i8_f32(w: &[u8], x: &[f32]) -> f32 {
3365    #[cfg(target_arch = "aarch64")]
3366    unsafe {
3367        return dot_i8_f32_neon(w, x);
3368    }
3369    #[cfg(target_arch = "x86_64")]
3370    if avx2_enabled() {
3371        return unsafe { dot_i8_f32_avx2(w, x) };
3372    }
3373    #[allow(unreachable_code)]
3374    {
3375        let mut sum = 0.0f32;
3376        for (j, &b) in w.iter().enumerate() {
3377            sum += (b as i8) as f32 * x[j];
3378        }
3379        sum
3380    }
3381}
3382
3383/// i8 row · (x ⊙ col_field) — the q8_2f row dot with the θ col-field
3384/// folded into the product (no prescaled copy of x). NEON on aarch64,
3385/// scalar elsewhere. Used by the active-neuron path `row_dot`.
3386#[inline]
3387fn dot_i8_col_f32(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
3388    #[cfg(target_arch = "aarch64")]
3389    unsafe {
3390        return dot_i8_col_f32_neon(w, x, col);
3391    }
3392    #[allow(unreachable_code)]
3393    {
3394        let mut sum = 0.0f32;
3395        for (j, &b) in w.iter().enumerate() {
3396            sum += (b as i8) as f32 * x[j] * col[j];
3397        }
3398        sum
3399    }
3400}
3401
3402#[cfg(target_arch = "aarch64")]
3403#[target_feature(enable = "neon")]
3404unsafe fn dot_i8_col_f32_neon(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
3405    // SAFETY: callers uphold slice-length contracts (see call sites).
3406    unsafe {
3407        use core::arch::aarch64::*;
3408        let n = x.len();
3409        let wp = w.as_ptr() as *const i8;
3410        let xp = x.as_ptr();
3411        let cp = col.as_ptr();
3412        let (mut a0, mut a1, mut a2, mut a3) =
3413            (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
3414        let mut j = 0usize;
3415        while j + 16 <= n {
3416            let wb = vld1q_s8(wp.add(j));
3417            let lo = vmovl_s8(vget_low_s8(wb));
3418            let hi = vmovl_s8(vget_high_s8(wb));
3419            let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
3420            let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
3421            let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
3422            let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
3423            a0 = vfmaq_f32(a0, w0, vmulq_f32(vld1q_f32(xp.add(j)), vld1q_f32(cp.add(j))));
3424            a1 = vfmaq_f32(a1, w1, vmulq_f32(vld1q_f32(xp.add(j + 4)), vld1q_f32(cp.add(j + 4))));
3425            a2 = vfmaq_f32(a2, w2, vmulq_f32(vld1q_f32(xp.add(j + 8)), vld1q_f32(cp.add(j + 8))));
3426            a3 = vfmaq_f32(a3, w3, vmulq_f32(vld1q_f32(xp.add(j + 12)), vld1q_f32(cp.add(j + 12))));
3427            j += 16;
3428        }
3429        let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
3430        while j < n {
3431            sum += (*wp.add(j)) as f32 * *xp.add(j) * *cp.add(j);
3432            j += 1;
3433        }
3434        sum
3435}
3436}
3437
3438#[cfg(target_arch = "aarch64")]
3439#[target_feature(enable = "neon")]
3440unsafe fn dot_i8_f32_neon(w: &[u8], x: &[f32]) -> f32 {
3441    // SAFETY: callers uphold slice-length contracts (see call sites).
3442    unsafe {
3443        use core::arch::aarch64::*;
3444        let n = x.len();
3445        let wp = w.as_ptr() as *const i8;
3446        let xp = x.as_ptr();
3447        let (mut a0, mut a1, mut a2, mut a3) =
3448            (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
3449        let mut j = 0usize;
3450        while j + 16 <= n {
3451            let wb = vld1q_s8(wp.add(j));
3452            let lo = vmovl_s8(vget_low_s8(wb));
3453            let hi = vmovl_s8(vget_high_s8(wb));
3454            let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
3455            let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
3456            let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
3457            let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
3458            a0 = vfmaq_f32(a0, w0, vld1q_f32(xp.add(j)));
3459            a1 = vfmaq_f32(a1, w1, vld1q_f32(xp.add(j + 4)));
3460            a2 = vfmaq_f32(a2, w2, vld1q_f32(xp.add(j + 8)));
3461            a3 = vfmaq_f32(a3, w3, vld1q_f32(xp.add(j + 12)));
3462            j += 16;
3463        }
3464        let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
3465        while j < n {
3466            sum += (*wp.add(j)) as f32 * *xp.add(j);
3467            j += 1;
3468        }
3469        sum
3470}
3471}
3472
3473#[allow(clippy::too_many_arguments)]
3474fn qmatvec(
3475    q: &[u8],
3476    row_scale: &[f32],
3477    xs: &[f32],
3478    rows: usize,
3479    cols: usize,
3480    out: &mut [f32],
3481    pool: Option<&Pool>,
3482) {
3483    debug_assert_eq!(out.len(), rows);
3484
3485    #[cfg(target_arch = "aarch64")]
3486    if sdot_enabled() {
3487        let act = split_act(xs);
3488        let out_addr = SendMut(out.as_mut_ptr());
3489        let run_range =
3490            |start: usize, end: usize| q8_range_sdot(q, row_scale, &act, cols, out_addr, start, end);
3491        match pool {
3492            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
3493            _ => run_range(0, rows),
3494        }
3495        return;
3496    }
3497    // x86 A8W8 via AVX2 maddubs — same quantized-activation contract as
3498    // the SDOT path (CMF_AVX2=0 keeps the exact i8×f32 loop).
3499    #[cfg(target_arch = "x86_64")]
3500    if avx2_a8w8_enabled() {
3501        let act = split_act(xs);
3502        let out_addr = SendMut(out.as_mut_ptr());
3503        let run_range =
3504            |start: usize, end: usize| q8_range_avx2(q, row_scale, &act, cols, out_addr, start, end);
3505        match pool {
3506            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
3507            _ => run_range(0, rows),
3508        }
3509        return;
3510    }
3511
3512    let row_dot = |o: usize| -> f32 { dot_i8_f32(&q[o * cols..(o + 1) * cols], xs) * row_scale[o] };
3513    match pool {
3514        Some(pool) if rows >= 256 => {
3515            let out_addr = SendMut(out.as_mut_ptr());
3516            pool.run(&move |widx, n| {
3517                let chunk = rows.div_ceil(n);
3518                let start = widx * chunk;
3519                let end = (start + chunk).min(rows);
3520                for o in start..end {
3521                    // SAFETY: disjoint row ranges per worker.
3522                    unsafe { *out_addr.at(o) = row_dot(o) };
3523                }
3524            });
3525        }
3526        _ => {
3527            for (o, dst) in out.iter_mut().enumerate() {
3528                *dst = row_dot(o);
3529            }
3530        }
3531    }
3532}
3533
3534#[allow(clippy::too_many_arguments)]
3535fn qmatvec2(
3536    q: &[u8],
3537    row_scale: &[f32],
3538    x1: &[f32],
3539    x2: &[f32],
3540    rows: usize,
3541    cols: usize,
3542    o1: &mut [f32],
3543    o2: &mut [f32],
3544    pool: Option<&Pool>,
3545) {
3546    #[cfg(target_arch = "aarch64")]
3547    if sdot_enabled() {
3548        let a1s = split_act(x1);
3549        let a2s = split_act(x2);
3550        let p1 = SendMut(o1.as_mut_ptr());
3551        let p2 = SendMut(o2.as_mut_ptr());
3552        let run_range = |start: usize, end: usize| {
3553            q8_range2_sdot(q, row_scale, &a1s, &a2s, cols, p1, p2, start, end)
3554        };
3555        match pool {
3556            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
3557            _ => run_range(0, rows),
3558        }
3559        return;
3560    }
3561    #[cfg(target_arch = "x86_64")]
3562    if avx2_a8w8_enabled() {
3563        let a1s = split_act(x1);
3564        let a2s = split_act(x2);
3565        let p1 = SendMut(o1.as_mut_ptr());
3566        let p2 = SendMut(o2.as_mut_ptr());
3567        let run_range = |start: usize, end: usize| {
3568            q8_range2_avx2(q, row_scale, &a1s, &a2s, cols, p1, p2, start, end)
3569        };
3570        match pool {
3571            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
3572            _ => run_range(0, rows),
3573        }
3574        return;
3575    }
3576
3577    let row_dots = |o: usize| -> (f32, f32) {
3578        let row = &q[o * cols..(o + 1) * cols];
3579        (dot_i8_f32(row, x1) * row_scale[o], dot_i8_f32(row, x2) * row_scale[o])
3580    };
3581    match pool {
3582        Some(pool) if rows >= 256 => {
3583            let p1 = SendMut(o1.as_mut_ptr());
3584            let p2 = SendMut(o2.as_mut_ptr());
3585            pool.run(&move |widx, n| {
3586                let chunk = rows.div_ceil(n);
3587                let start = widx * chunk;
3588                let end = (start + chunk).min(rows);
3589                for o in start..end {
3590                    let (s1, s2) = row_dots(o);
3591                    // SAFETY: disjoint row ranges per worker.
3592                    unsafe {
3593                        *p1.at(o) = s1;
3594                        *p2.at(o) = s2;
3595                    }
3596                }
3597            });
3598        }
3599        _ => {
3600            for o in 0..rows {
3601                let (s1, s2) = row_dots(o);
3602                o1[o] = s1;
3603                o2[o] = s2;
3604            }
3605        }
3606    }
3607}
3608
3609#[derive(Clone, Copy)]
3610struct SendMut(*mut f32);
3611unsafe impl Send for SendMut {}
3612unsafe impl Sync for SendMut {}
3613
3614impl SendMut {
3615    #[inline]
3616    fn at(self, i: usize) -> *mut f32 {
3617        unsafe { self.0.add(i) }
3618    }
3619}
3620
3621#[cfg(test)]
3622mod tests {
3623    use super::*;
3624
3625    #[test]
3626    fn f32_matvec_matches_matvec_rows_bitexact() {
3627        let (rows, cols) = (300, 40);
3628        let w: Vec<f32> = (0..rows * cols).map(|i| (i as f32 * 0.017).sin()).collect();
3629        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.05).cos()).collect();
3630        let qt = QTensor::from_f32(w.clone(), rows, cols);
3631
3632        let mut a = vec![0.0f32; rows];
3633        matvec_rows(None, &w, &x, &mut a);
3634        let mut b = vec![0.0f32; rows];
3635        qt.matvec(&x, &mut b, None);
3636        assert_eq!(a, b);
3637    }
3638
3639    #[test]
3640    fn sdot_kernel_exact_on_grid() {
3641        // Activations already on the i8 grid (±1 with amax=1 → sx=1/127,
3642        // xq=±127 dequantizes EXACTLY) → the SDOT path must match the
3643        // exact f32 dot to float rounding. This isolates kernel
3644        // correctness from quantization noise.
3645        eprintln!("sdot_enabled = {}", sdot_enabled());
3646        let (rows, cols) = (9, 80); // odd rows → exercises 4-row + tail
3647        let w: Vec<u8> = (0..rows * cols)
3648            .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
3649            .collect();
3650        let scales: Vec<f32> = (0..rows).map(|o| 0.005 + o as f32 * 0.001).collect();
3651        let x: Vec<f32> = (0..cols)
3652            .map(|i| match i % 3 {
3653                0 => 1.0,
3654                1 => -1.0,
3655                _ => 0.0,
3656            })
3657            .collect();
3658        let mut a = vec![0.0f32; rows];
3659        qmatvec(&w, &scales, &x, rows, cols, &mut a, None);
3660        for o in 0..rows {
3661            let mut acc = 0.0f32;
3662            for j in 0..cols {
3663                acc += (w[o * cols + j] as i8) as f32 * x[j];
3664            }
3665            let expect = acc * scales[o];
3666            assert!(
3667                (a[o] - expect).abs() < 1e-3 * expect.abs().max(1e-3),
3668                "row {o}: {} vs {expect}",
3669                a[o]
3670            );
3671        }
3672    }
3673
3674    #[test]
3675    fn sdot_a8w8_noise_is_bounded() {
3676        // Off-grid activations: A8 quantization noise must stay small in
3677        // relative L2 over the whole output (realistic accuracy contract;
3678        // vmfcore measured argmax-identical decode on real models).
3679        let (rows, cols) = (16, 512);
3680        let w: Vec<u8> = (0..rows * cols)
3681            .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
3682            .collect();
3683        let scales = vec![0.01f32; rows];
3684        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.21).sin()).collect();
3685        let mut a = vec![0.0f32; rows];
3686        qmatvec(&w, &scales, &x, rows, cols, &mut a, None);
3687        let (mut num, mut den) = (0f64, 0f64);
3688        for o in 0..rows {
3689            let mut acc = 0.0f32;
3690            for j in 0..cols {
3691                acc += (w[o * cols + j] as i8) as f32 * x[j];
3692            }
3693            let expect = acc * scales[o];
3694            num += ((a[o] - expect) as f64).powi(2);
3695            den += (expect as f64).powi(2);
3696        }
3697        let rel = (num / den.max(1e-12)).sqrt();
3698        assert!(rel < 0.05, "A8W8 relative L2 error too high: {rel}");
3699    }
3700
3701    #[test]
3702    fn i8_dot_neon_matches_scalar() {
3703        let n = 100;
3704        let w: Vec<u8> = (0..n).map(|i| ((i * 37 + 11) % 251) as u8).collect();
3705        let x: Vec<f32> = (0..n).map(|i| (i as f32 * 0.13).sin()).collect();
3706        let mut scalar = 0.0f32;
3707        for j in 0..n {
3708            scalar += (w[j] as i8) as f32 * x[j];
3709        }
3710        let fast = dot_i8_f32(&w, &x);
3711        assert!((scalar - fast).abs() < 1e-3 * scalar.abs().max(1.0));
3712    }
3713
3714    /// Fused vbit matvec must match full dequant_vbit + dense matvec.
3715    #[test]
3716    fn vbitmatvec_matches_full_dequant() {
3717        let (rows, cols) = (6, 64);
3718        let ng = cols / GROUP_SIZE;
3719        // Hand-craft: bits per row, f16 scales, packed rows.
3720        let bits: Vec<u8> = vec![3, 4, 5, 6, 8, 4];
3721        let mut bytes = bits.clone();
3722        for g in 0..rows * ng {
3723            let s = 0.02 + 0.001 * g as f32;
3724            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
3725        }
3726        for r in 0..rows {
3727            let b = bits[r] as usize;
3728            let (mut acc, mut nb) = (0u64, 0usize);
3729            let mut rowbytes = Vec::new();
3730            for i in 0..cols {
3731                let v = ((i * 7 + r * 13) % (1 << b)) as u64;
3732                acc = (acc << b) | v;
3733                nb += b;
3734                while nb >= 8 {
3735                    nb -= 8;
3736                    rowbytes.push(((acc >> nb) & 0xFF) as u8);
3737                }
3738            }
3739            if nb > 0 {
3740                rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
3741            }
3742            bytes.extend_from_slice(&rowbytes);
3743        }
3744        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.19).sin()).collect();
3745
3746        let mut reference = vec![0f32; rows * cols];
3747        cortiq_core::quant::dequant_vbit(&bytes, rows, cols, &mut reference).unwrap();
3748        let mut expect = vec![0f32; rows];
3749        for r in 0..rows {
3750            expect[r] = reference[r * cols..(r + 1) * cols]
3751                .iter()
3752                .zip(&x)
3753                .map(|(w, xv)| w * xv)
3754                .sum();
3755        }
3756        let mut got = vec![0f32; rows];
3757        let offsets = vbit_row_offsets(&bytes, rows, cols);
3758        vbitmatvec(&bytes, &offsets, &x, rows, cols, &mut got, None);
3759        // SDOT path quantizes activations to i8 (A8W8): bounded noise,
3760        // same contract as q8 (exact path is pinned by CMF_SDOT=0 in
3761        // the golden-parity gate).
3762        let tol = if a8w8_enabled() { 6e-2 } else { 1e-4 };
3763        let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1e-6);
3764        for r in 0..rows {
3765            assert!(
3766                (got[r] - expect[r]).abs() < tol * scale,
3767                "row {r}: {} vs {}",
3768                got[r],
3769                expect[r]
3770            );
3771        }
3772    }
3773
3774    /// Fused q4 matvec must match the reference full-dequant + dense
3775    /// matvec bit-for-bit in structure (same f32 math, group order).
3776    #[test]
3777    fn q4matvec_matches_full_dequant() {
3778        let (rows, cols) = (8, 64);
3779        let groups = rows * cols / GROUP_SIZE;
3780        // Hand-craft a q4_block blob: nibbles then f16 scales.
3781        let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
3782        for i in 0..groups * 16 {
3783            bytes.push((((i * 7 + 3) % 256) & 0xFF) as u8);
3784        }
3785        for g in 0..groups {
3786            let s = 0.01 + 0.003 * g as f32;
3787            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
3788        }
3789        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.17).sin()).collect();
3790
3791        let mut reference = vec![0.0f32; rows * cols];
3792        cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
3793        let mut expect = vec![0.0f32; rows];
3794        for r in 0..rows {
3795            expect[r] = reference[r * cols..(r + 1) * cols]
3796                .iter()
3797                .zip(&x)
3798                .map(|(w, xv)| w * xv)
3799                .sum();
3800        }
3801
3802        let mut got = vec![0.0f32; rows];
3803        q4matvec(&bytes, &x, rows, cols, &mut got, None);
3804        // SDOT path quantizes activations to i8 (A8W8): bounded noise,
3805        // same contract as q8/vbit (exact path is pinned by CMF_SDOT=0
3806        // in the golden-parity gate).
3807        let tol = if a8w8_enabled() { 6e-2 } else { 1e-4 };
3808        let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
3809        for r in 0..rows {
3810            assert!(
3811                (got[r] - expect[r]).abs() < tol * scale,
3812                "row {r}: {} vs {}",
3813                got[r],
3814                expect[r]
3815            );
3816        }
3817    }
3818
3819    /// Fused two-input vbit matvec must equal two single matvecs exactly
3820    /// (same per-lane accumulation order on both scalar and SDOT paths).
3821    #[test]
3822    fn vbitmatvec2_equals_two_singles() {
3823        let (rows, cols) = (6, 64);
3824        let ng = cols / GROUP_SIZE;
3825        let bits: Vec<u8> = vec![3, 4, 5, 6, 8, 4];
3826        let mut bytes = bits.clone();
3827        for g in 0..rows * ng {
3828            let s = 0.02 + 0.001 * g as f32;
3829            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
3830        }
3831        for r in 0..rows {
3832            let b = bits[r] as usize;
3833            let (mut acc, mut nb) = (0u64, 0usize);
3834            let mut rowbytes = Vec::new();
3835            for i in 0..cols {
3836                let v = ((i * 7 + r * 13) % (1 << b)) as u64;
3837                acc = (acc << b) | v;
3838                nb += b;
3839                while nb >= 8 {
3840                    nb -= 8;
3841                    rowbytes.push(((acc >> nb) & 0xFF) as u8);
3842                }
3843            }
3844            if nb > 0 {
3845                rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
3846            }
3847            bytes.extend_from_slice(&rowbytes);
3848        }
3849        let x1: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.19).sin()).collect();
3850        let x2: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.11).cos()).collect();
3851        let offsets = vbit_row_offsets(&bytes, rows, cols);
3852
3853        let (mut a1, mut a2) = (vec![0f32; rows], vec![0f32; rows]);
3854        vbitmatvec(&bytes, &offsets, &x1, rows, cols, &mut a1, None);
3855        vbitmatvec(&bytes, &offsets, &x2, rows, cols, &mut a2, None);
3856        let (mut b1, mut b2) = (vec![0f32; rows], vec![0f32; rows]);
3857        vbitmatvec2(&bytes, &offsets, &x1, &x2, rows, cols, &mut b1, &mut b2, None);
3858        assert_eq!(a1, b1, "fused vbit lane 1 must be bit-identical");
3859        assert_eq!(a2, b2, "fused vbit lane 2 must be bit-identical");
3860    }
3861
3862    /// Fused two-input q4 matvec must equal two single matvecs exactly.
3863    #[test]
3864    fn q4matvec2_equals_two_singles() {
3865        let (rows, cols) = (8, 128);
3866        let groups = rows * cols / GROUP_SIZE;
3867        let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
3868        for i in 0..groups * 16 {
3869            bytes.push((((i * 7 + 3) % 256) & 0xFF) as u8);
3870        }
3871        for g in 0..groups {
3872            let s = 0.01 + 0.003 * g as f32;
3873            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
3874        }
3875        // Include an outlier channel so the SDOT correction path is
3876        // exercised in the pair kernel too.
3877        let mut x1: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.17).sin()).collect();
3878        x1[9] = 250.0;
3879        let x2: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.23).cos()).collect();
3880
3881        let (mut a1, mut a2) = (vec![0f32; rows], vec![0f32; rows]);
3882        q4matvec(&bytes, &x1, rows, cols, &mut a1, None);
3883        q4matvec(&bytes, &x2, rows, cols, &mut a2, None);
3884        let (mut b1, mut b2) = (vec![0f32; rows], vec![0f32; rows]);
3885        q4matvec2(&bytes, &x1, &x2, rows, cols, &mut b1, &mut b2, None);
3886        assert_eq!(a1, b1, "fused q4 lane 1 must be bit-identical");
3887        assert_eq!(a2, b2, "fused q4 lane 2 must be bit-identical");
3888    }
3889
3890    /// Multi-matrix job must equal separate matvecs exactly — same
3891    /// kernels, only the dispatch is fused.
3892    #[test]
3893    fn matvec_many_equals_separate_matvecs() {
3894        use crate::pool::Pool;
3895        let (r1, r2, cols) = (300, 200, 64);
3896        let mk = |salt: usize, rows: usize| {
3897            QTensor::from_f32(
3898                (0..rows * cols).map(|i| ((i * 7 + salt) % 97) as f32 / 97.0 - 0.5).collect(),
3899                rows,
3900                cols,
3901            )
3902        };
3903        let (a, b) = (mk(1, r1), mk(5, r2));
3904        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.11).sin()).collect();
3905        let pool = Pool::new(3);
3906
3907        let (mut ea, mut eb) = (vec![0f32; r1], vec![0f32; r2]);
3908        a.matvec(&x, &mut ea, Some(&pool));
3909        b.matvec(&x, &mut eb, Some(&pool));
3910        let (mut ga, mut gb) = (vec![0f32; r1], vec![0f32; r2]);
3911        QTensor::matvec_many([&a, &b], &x, [&mut ga, &mut gb], Some(&pool));
3912        assert_eq!(ea, ga, "fused multi-matrix lane 1 must be bit-identical");
3913        assert_eq!(eb, gb, "fused multi-matrix lane 2 must be bit-identical");
3914    }
3915
3916    /// Batched q4/vbit matmat must equal per-position matvec calls
3917    /// exactly (the fallback it replaced) — same kernels, same order.
3918    #[test]
3919    fn batched_matmat_equals_per_position_matvec() {
3920        let (rows, cols, b) = (8, 64, 5);
3921        // q4 blob.
3922        let groups = rows * cols / GROUP_SIZE;
3923        let mut q4 = Vec::new();
3924        for i in 0..groups * 16 {
3925            q4.push((((i * 7 + 3) % 256) & 0xFF) as u8);
3926        }
3927        for g in 0..groups {
3928            q4.extend_from_slice(
3929                &cortiq_core::quant::f32_to_f16(0.01 + 0.003 * g as f32).to_le_bytes(),
3930            );
3931        }
3932        // vbit blob (mixed widths incl. 8).
3933        let ng = cols / GROUP_SIZE;
3934        let bits: Vec<u8> = vec![3, 4, 5, 6, 8, 4, 5, 3];
3935        let mut vb = bits.clone();
3936        for g in 0..rows * ng {
3937            vb.extend_from_slice(
3938                &cortiq_core::quant::f32_to_f16(0.02 + 0.001 * g as f32).to_le_bytes(),
3939            );
3940        }
3941        for r in 0..rows {
3942            let bw = bits[r] as usize;
3943            let (mut acc, mut nb) = (0u64, 0usize);
3944            let mut rowbytes = Vec::new();
3945            for i in 0..cols {
3946                let v = ((i * 7 + r * 13) % (1 << bw)) as u64;
3947                acc = (acc << bw) | v;
3948                nb += bw;
3949                while nb >= 8 {
3950                    nb -= 8;
3951                    rowbytes.push(((acc >> nb) & 0xFF) as u8);
3952                }
3953            }
3954            if nb > 0 {
3955                rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
3956            }
3957            vb.extend_from_slice(&rowbytes);
3958        }
3959        let offsets = vbit_row_offsets(&vb, rows, cols);
3960
3961        let xs: Vec<f32> = (0..b * cols).map(|i| (i as f32 * 0.13).sin()).collect();
3962
3963        // q4: batch vs singles.
3964        let mut got = vec![0f32; b * rows];
3965        q4matmat(&q4, &xs, b, rows, cols, &mut got, None);
3966        for bi in 0..b {
3967            let mut expect = vec![0f32; rows];
3968            q4matvec(&q4, &xs[bi * cols..(bi + 1) * cols], rows, cols, &mut expect, None);
3969            assert_eq!(&got[bi * rows..(bi + 1) * rows], &expect[..], "q4 batch pos {bi}");
3970        }
3971
3972        // vbit: batch vs singles.
3973        let mut got = vec![0f32; b * rows];
3974        vbitmatmat(&vb, &offsets, &xs, b, rows, cols, &mut got, None);
3975        for bi in 0..b {
3976            let mut expect = vec![0f32; rows];
3977            vbitmatvec(
3978                &vb, &offsets, &xs[bi * cols..(bi + 1) * cols], rows, cols, &mut expect, None,
3979            );
3980            assert_eq!(&got[bi * rows..(bi + 1) * rows], &expect[..], "vbit batch pos {bi}");
3981        }
3982    }
3983
3984    /// q4_tiled kernels must produce BIT-identical outputs to the q4
3985    /// split kernels on the same values (same ints, same order — only
3986    /// the byte placement differs).
3987    #[test]
3988    fn q4_tiled_matches_q4_block_bitexact() {
3989        let (rows, cols, b) = (8usize, 128usize, 3usize);
3990        let groups = rows * cols / GROUP_SIZE;
3991        let mut split = Vec::with_capacity(groups * 18);
3992        for i in 0..groups * 16 {
3993            split.push((((i * 7 + 3) % 256) & 0xFF) as u8);
3994        }
3995        for g in 0..groups {
3996            split.extend_from_slice(
3997                &cortiq_core::quant::f32_to_f16(0.01 + 0.003 * g as f32).to_le_bytes(),
3998            );
3999        }
4000        // Re-tile: [scale][nibbles] per group.
4001        let (packed, scales) = split.split_at(groups * 16);
4002        let mut tiled = Vec::with_capacity(groups * Q4_TILE);
4003        for g in 0..groups {
4004            tiled.extend_from_slice(&scales[g * 2..g * 2 + 2]);
4005            tiled.extend_from_slice(&packed[g * 16..(g + 1) * 16]);
4006        }
4007
4008        let mut x1: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.17).sin()).collect();
4009        x1[9] = 250.0; // exercise the outlier path
4010        let x2: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.23).cos()).collect();
4011
4012        let (mut a, mut t) = (vec![0f32; rows], vec![0f32; rows]);
4013        q4matvec(&split, &x1, rows, cols, &mut a, None);
4014        q4t_matvec(&tiled, &x1, rows, cols, &mut t, None);
4015        assert_eq!(a, t, "q4t matvec must match q4 bit-for-bit");
4016
4017        let (mut a1, mut a2) = (vec![0f32; rows], vec![0f32; rows]);
4018        let (mut t1, mut t2) = (vec![0f32; rows], vec![0f32; rows]);
4019        q4matvec2(&split, &x1, &x2, rows, cols, &mut a1, &mut a2, None);
4020        q4t_matvec2(&tiled, &x1, &x2, rows, cols, &mut t1, &mut t2, None);
4021        assert_eq!(a1, t1);
4022        assert_eq!(a2, t2);
4023
4024        let xs: Vec<f32> = (0..b * cols).map(|i| (i as f32 * 0.13).sin()).collect();
4025        let (mut am, mut tm) = (vec![0f32; b * rows], vec![0f32; b * rows]);
4026        q4matmat(&split, &xs, b, rows, cols, &mut am, None);
4027        q4t_matmat(&tiled, &xs, b, rows, cols, &mut tm, None);
4028        assert_eq!(am, tm, "q4t matmat must match q4 bit-for-bit");
4029    }
4030
4031    /// q4 SDOT outlier correction: a single huge activation channel
4032    /// (>8·rms → outlier, zeroed in xq) must still contribute its EXACT
4033    /// term. On-grid bulk (±1/0 → xq dequantizes exactly) isolates the
4034    /// correction from A8W8 noise. cols must exceed 64: at n=64 the
4035    /// 8·rms threshold equals sqrt(v²+rest) ≥ v, so a single outlier
4036    /// can never qualify (8² = n).
4037    #[test]
4038    fn q4matvec_sdot_outlier_exact() {
4039        let (rows, cols) = (4, 128);
4040        let groups = rows * cols / GROUP_SIZE;
4041        let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
4042        for i in 0..groups * 16 {
4043            bytes.push(((i * 11 + 5) % 256) as u8);
4044        }
4045        for g in 0..groups {
4046            let s = 0.02 + 0.002 * g as f32;
4047            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
4048        }
4049        let mut x: Vec<f32> = (0..cols)
4050            .map(|i| match i % 3 {
4051                0 => 1.0,
4052                1 => -1.0,
4053                _ => 0.0,
4054            })
4055            .collect();
4056        x[17] = 300.0; // ≫ 8·rms → outlier channel
4057
4058        let mut reference = vec![0.0f32; rows * cols];
4059        cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
4060        let mut expect = vec![0.0f32; rows];
4061        for r in 0..rows {
4062            expect[r] = reference[r * cols..(r + 1) * cols]
4063                .iter()
4064                .zip(&x)
4065                .map(|(w, xv)| w * xv)
4066                .sum();
4067        }
4068        let mut got = vec![0.0f32; rows];
4069        q4matvec(&bytes, &x, rows, cols, &mut got, None);
4070        let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
4071        for r in 0..rows {
4072            assert!(
4073                (got[r] - expect[r]).abs() < 2e-3 * scale,
4074                "row {r}: {} vs {} (outlier term must be exact)",
4075                got[r],
4076                expect[r]
4077            );
4078        }
4079    }
4080}