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, Q1_TILE, 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        /// q8-family decode repack (load-time, optional): rows in groups
41        /// of 4, interleaved in 16-byte units — one 64-byte line per
42        /// iteration feeds all 4 sdot lanes, ONE sequential weight
43        /// stream per worker instead of four (this is where llama.cpp's
44        /// repacked Q8 kernels get their bandwidth). Empty = off
45        /// (CMF_REPACK=0, non-SDOT arch, or an ineligible shape). Trades
46        /// an anonymous copy of the quants for mmap pages that go cold.
47        repack: Vec<u8>,
48    },
49}
50
51/// Load-time q8 repack gate (see `Mapped::repack`). OPT-IN
52/// (`CMF_REPACK=1`): the single-stream hypothesis LOST on Apple Silicon
53/// (M4, interleaved A/B: decode 101 vs 94 tok/s — four adjacent row
54/// streams per worker feed the prefetcher MORE memory-level parallelism
55/// than one); kept as an experiment flag for x86, where the tradeoff
56/// may land differently.
57fn repack_enabled() -> bool {
58    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
59    *ON.get_or_init(|| {
60        std::env::var("CMF_REPACK").map(|v| v == "1").unwrap_or(cfg!(target_os = "android"))
61    })
62}
63
64/// Interleave q8 rows for the decode kernel: group g holds rows
65/// 4g..4g+4 as [r0[c], r1[c], r2[c], r3[c]] per 16-byte chunk c. Only
66/// full groups are packed — tail rows keep reading the mmap layout.
67fn q8_repack(bytes: &[u8], rows: usize, cols: usize) -> Vec<u8> {
68    #[cfg(target_arch = "aarch64")]
69    let arch_ok = sdot_enabled();
70    #[cfg(not(target_arch = "aarch64"))]
71    let arch_ok = false;
72    if !arch_ok || !repack_enabled() || rows < 256 || cols % 16 != 0 {
73        return Vec::new();
74    }
75    q8_repack_layout(bytes, rows, cols)
76}
77
78/// The pure layout transform behind `q8_repack` (tested directly —
79/// the gate depends on arch and env).
80fn q8_repack_layout(bytes: &[u8], rows: usize, cols: usize) -> Vec<u8> {
81    let groups = rows / 4;
82    let mut rep = vec![0u8; groups * 4 * cols];
83    for g in 0..groups {
84        let dst = &mut rep[g * 4 * cols..(g + 1) * 4 * cols];
85        for c in 0..cols / 16 {
86            for lane in 0..4 {
87                let src = (g * 4 + lane) * cols + c * 16;
88                dst[c * 64 + lane * 16..c * 64 + lane * 16 + 16]
89                    .copy_from_slice(&bytes[src..src + 16]);
90            }
91        }
92    }
93    rep
94}
95
96/// Prefix-sum of vbit row payload offsets (absolute within the tensor
97/// bytes). `offsets[r]..offsets[r+1]` is row r's packed data.
98fn vbit_row_offsets(bytes: &[u8], rows: usize, cols: usize) -> Vec<usize> {
99    let ng = cols / GROUP_SIZE;
100    let bits = &bytes[..rows];
101    let mut offsets = Vec::with_capacity(rows + 1);
102    let mut off = rows + rows * ng * 2;
103    for r in 0..rows {
104        offsets.push(off);
105        off += (cols * bits[r] as usize).div_ceil(8);
106    }
107    offsets.push(off);
108    offsets
109}
110
111impl QTensor {
112    pub fn from_f32(data: Vec<f32>, rows: usize, cols: usize) -> Self {
113        debug_assert_eq!(data.len(), rows * cols);
114        Self::F32 { data, rows, cols }
115    }
116
117    /// Wrap a directory tensor without dequantizing the payload.
118    /// Falls back to dequantized f32 for dtypes without a fused kernel.
119    pub fn from_model(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
120        // Indexed lookup: the linear directory scan made pipeline build
121        // O(N²) on MoE/skills files with thousands of tensors.
122        let idx = model
123            .tensor_index(name)
124            .ok_or_else(|| format!("tensor '{name}' not found in CMF directory"))?;
125        let entry = &model.tensors[idx];
126        if entry.shape.len() != 2 {
127            return Err(format!("QTensor::from_model needs 2-D, got '{name}'"));
128        }
129        let (rows, cols) = (entry.shape[0], entry.shape[1]);
130        let bytes = model.entry_bytes(entry);
131
132        match entry.dtype {
133            TensorDtype::Q8Row | TensorDtype::Q8_2f => {
134                let n = rows * cols;
135                let scales_off = n;
136                let row_scale: Vec<f32> = (0..rows)
137                    .map(|o| {
138                        f16_to_f32(u16::from_le_bytes([
139                            bytes[scales_off + o * 2],
140                            bytes[scales_off + o * 2 + 1],
141                        ]))
142                    })
143                    .collect();
144                let col_field: Vec<f32> = if entry.dtype == TensorDtype::Q8_2f {
145                    let col_off = n + rows * 2;
146                    (0..cols)
147                        .map(|i| {
148                            f16_to_f32(u16::from_le_bytes([
149                                bytes[col_off + i * 2],
150                                bytes[col_off + i * 2 + 1],
151                            ]))
152                        })
153                        .collect()
154                } else {
155                    Vec::new()
156                };
157                Ok(Self::Mapped {
158                    model: model.clone(),
159                    idx,
160                    dtype: entry.dtype,
161                    rows,
162                    cols,
163                    row_scale,
164                    col_field,
165                    vbit_offsets: Vec::new(),
166                    repack: q8_repack(bytes, rows, cols),
167                })
168            }
169            // vbit: fused kernel unpacks variable-bit rows from mmap.
170            TensorDtype::Vbit if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
171                model: model.clone(),
172                idx,
173                dtype: entry.dtype,
174                rows,
175                cols,
176                row_scale: Vec::new(),
177                col_field: Vec::new(),
178                vbit_offsets: vbit_row_offsets(bytes, rows, cols),
179                repack: Vec::new(),
180            }),
181            // vbit_ro (§4.2): the offset table comes straight from the
182            // file — no load-time prefix scan; kernels are shared with
183            // legacy vbit (they consume absolute offsets either way).
184            TensorDtype::VbitRo if cols % GROUP_SIZE == 0 => {
185                let (_, off_off, packed_off) =
186                    cortiq_core::quant::vbit_ro_sections(rows, cols);
187                let offsets: Vec<usize> = (0..=rows)
188                    .map(|r| {
189                        packed_off + cortiq_core::quant::vbit_ro_offset(bytes, off_off, r)
190                    })
191                    .collect();
192                Ok(Self::Mapped {
193                    model: model.clone(),
194                    idx,
195                    dtype: entry.dtype,
196                    rows,
197                    cols,
198                    row_scale: Vec::new(),
199                    col_field: Vec::new(),
200                    vbit_offsets: offsets,
201                    repack: Vec::new(),
202                })
203            }
204            // q4_block: fused kernel reads nibbles straight from mmap —
205            // a 14B q4 file no longer explodes into ×8 f32 RAM.
206            // q4_tiled (§4.3): interleaved [scale][nibbles] tiles — one
207            // sequential memory stream (measured ×1.66 ARM / ×1.13 AVX2
208            // at kernel level over the split layout).
209            TensorDtype::Q4Tiled if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
210                model: model.clone(),
211                idx,
212                dtype: entry.dtype,
213                rows,
214                cols,
215                row_scale: Vec::new(),
216                col_field: Vec::new(),
217                vbit_offsets: Vec::new(),
218                repack: Vec::new(),
219            }),
220            TensorDtype::Q4Block if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
221                model: model.clone(),
222                idx,
223                dtype: entry.dtype,
224                rows,
225                cols,
226                row_scale: Vec::new(),
227                col_field: Vec::new(),
228                vbit_offsets: Vec::new(),
229                repack: Vec::new(),
230            }),
231            // q1: binary sign-bit tiles from mmap (1-bit-trained models).
232            TensorDtype::Q1 if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
233                model: model.clone(),
234                idx,
235                dtype: entry.dtype,
236                rows,
237                cols,
238                row_scale: Vec::new(),
239                col_field: Vec::new(),
240                vbit_offsets: Vec::new(),
241                repack: Vec::new(),
242            }),
243            // q1t (ternary + outlier overlay): fused per-row dequant kernel
244            // reads straight from mmap — a 12B q1t stays ~its file size in
245            // RAM instead of dequantizing to ~48 GB of f32.
246            TensorDtype::Q1T if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
247                model: model.clone(),
248                idx,
249                dtype: entry.dtype,
250                rows,
251                cols,
252                row_scale: Vec::new(),
253                col_field: Vec::new(),
254                vbit_offsets: Vec::new(),
255                repack: Vec::new(),
256            }),
257            // No fused kernel yet → dequantize once (correct, more RAM).
258            _ => {
259                let mut data = vec![0.0f32; rows * cols];
260                cortiq_core::quant::dequant_tensor(entry, bytes, &mut data)?;
261                Ok(Self::from_f32(data, rows, cols))
262            }
263        }
264    }
265
266    /// q1-mapped tensor? (GPU gates: the q1 CPU kernel is
267    /// compute-bound, so offload pays at much smaller shapes than q8.)
268    pub(crate) fn is_q1(&self) -> bool {
269        matches!(self, Self::Mapped { dtype: TensorDtype::Q1, .. })
270    }
271
272    /// Owned-f32 view (data, rows, cols) — the GDN a/b gate projections
273    /// arrive dequantized (force-f16 in the converter → F32 in RAM).
274    pub(crate) fn f32_parts(&self) -> Option<(&[f32], usize, usize)> {
275        match self {
276            Self::F32 { data, rows, cols } => Some((data, *rows, *cols)),
277            _ => None,
278        }
279    }
280
281    /// (directory idx, rows, cols) of a q1-mapped tensor — the
282    /// whole-block GPU path resolves offsets itself.
283    /// (idx, rows, cols) of a mapped tensor the whole-token GPU graph can drive
284    /// — Q1, Q1T or Q4-block (it resolves the offset and picks the kernel by
285    /// dtype). Q4-block lets a precise down_proj/lm_head stay on-device.
286    /// Named `q1_parts` for historical reasons.
287    pub(crate) fn q1_parts(&self) -> Option<(usize, usize, usize)> {
288        match self {
289            #[cfg(target_os = "macos")]
290            Self::Mapped {
291                dtype: TensorDtype::Q1T,
292                ..
293            } if !crate::gpu::metal_q1t_enabled() => None,
294            Self::Mapped {
295                idx,
296                dtype: TensorDtype::Q1
297                    | TensorDtype::Q1T
298                    | TensorDtype::Q4Block
299                    | TensorDtype::Q8Row
300                    | TensorDtype::Q8_2f,
301                rows,
302                cols,
303                ..
304            } => Some((*idx, *rows, *cols)),
305            _ => None,
306        }
307    }
308
309    /// (directory idx, rows, cols, row_scale) of a plain q8_row mapped
310    /// tensor — the chunk-prefill GPU graph resolves offsets itself.
311    /// q8_2f is excluded on purpose: its column field would need a
312    /// prescale stage on the device.
313    pub(crate) fn q8_row_parts(&self) -> Option<(usize, usize, usize, &[f32])> {
314        match self {
315            Self::Mapped {
316                idx,
317                dtype: TensorDtype::Q8Row,
318                rows,
319                cols,
320                row_scale,
321                col_field,
322                ..
323            } if col_field.is_empty() => Some((*idx, *rows, *cols, row_scale)),
324            _ => None,
325        }
326    }
327
328    pub fn rows(&self) -> usize {
329        match self {
330            Self::F32 { rows, .. } | Self::Mapped { rows, .. } => *rows,
331        }
332    }
333
334    pub fn cols(&self) -> usize {
335        match self {
336            Self::F32 { cols, .. } | Self::Mapped { cols, .. } => *cols,
337        }
338    }
339
340    /// (model, tensor idx) for a q1 mapped weight — the wgpu token graph
341    /// keys its resident VRAM cache by idx. None for any other dtype/kind.
342    pub fn mapped_q1(&self) -> Option<(&std::sync::Arc<CmfModel>, usize)> {
343        match self {
344            Self::Mapped { model, idx, dtype: TensorDtype::Q1, .. } => Some((model, *idx)),
345            _ => None,
346        }
347    }
348
349    /// (model, idx, kind, row_scale) for a graph-capable mapped weight. kind:
350    /// 0=q8_row (per-row scales), 1=q1, 2=q4_tiled, 3=q1t (tile-embedded, no
351    /// rs). None for dtypes the token graph does not handle (q8_2f/q4_block/vbit).
352    pub fn graph_weight(&self) -> Option<(&std::sync::Arc<CmfModel>, usize, u8, &[f32])> {
353        match self {
354            Self::Mapped { model, idx, dtype: TensorDtype::Q8Row, row_scale, .. } => {
355                Some((model, *idx, 0, row_scale.as_slice()))
356            }
357            Self::Mapped { model, idx, dtype: TensorDtype::Q1, .. } => Some((model, *idx, 1, &[])),
358            Self::Mapped { model, idx, dtype: TensorDtype::Q4Tiled, .. } => Some((model, *idx, 2, &[])),
359            Self::Mapped { model, idx, dtype: TensorDtype::Q4Block, .. } => Some((model, *idx, 2, &[])),
360            Self::Mapped { model, idx, dtype: TensorDtype::Q1T, .. } => Some((model, *idx, 3, &[])),
361            _ => None,
362        }
363    }
364
365    /// Dense f32 view — only for owned tensors. Masked/sparse execution
366    /// paths require it; quantized weights don't support masks yet.
367    pub fn as_f32(&self) -> Option<&[f32]> {
368        match self {
369            Self::F32 { data, .. } => Some(data),
370            Self::Mapped { .. } => None,
371        }
372    }
373
374    fn quant_bytes(&self) -> &[u8] {
375        match self {
376            Self::Mapped { model, idx, .. } => model.entry_bytes(&model.tensors[*idx]),
377            Self::F32 { .. } => unreachable!("quant_bytes on F32"),
378        }
379    }
380
381    /// Dequantize one row into `dst` (embedding lookup).
382    pub fn row_f32(&self, r: usize, dst: &mut [f32]) {
383        let cols = self.cols();
384        debug_assert_eq!(dst.len(), cols);
385        match self {
386            Self::F32 { data, .. } => dst.copy_from_slice(&data[r * cols..(r + 1) * cols]),
387            Self::Mapped {
388                dtype,
389                row_scale,
390                col_field,
391                vbit_offsets,
392                ..
393            } => {
394                if *dtype == TensorDtype::Q4Tiled {
395                    let bytes = self.quant_bytes();
396                    let gpr = cols / GROUP_SIZE;
397                    for gi in 0..gpr {
398                        let tile = &bytes[(r * gpr + gi) * Q4_TILE..(r * gpr + gi + 1) * Q4_TILE];
399                        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
400                        for (k, &b) in tile[2..].iter().enumerate() {
401                            dst[gi * GROUP_SIZE + k * 2] = ((b & 0x0F) as f32 - 8.0) * s;
402                            dst[gi * GROUP_SIZE + k * 2 + 1] = (((b >> 4) & 0x0F) as f32 - 8.0) * s;
403                        }
404                    }
405                    return;
406                }
407                if *dtype == TensorDtype::Q4Block {
408                    let (packed, scales) = q4_split(self.quant_bytes(), self.rows(), cols);
409                    let gpr = cols / GROUP_SIZE;
410                    for gi in 0..gpr {
411                        let g = r * gpr + gi;
412                        let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
413                        for (k, &b) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
414                            dst[gi * GROUP_SIZE + k * 2] = ((b & 0x0F) as f32 - 8.0) * s;
415                            dst[gi * GROUP_SIZE + k * 2 + 1] = (((b >> 4) & 0x0F) as f32 - 8.0) * s;
416                        }
417                    }
418                    return;
419                }
420                if *dtype == TensorDtype::Q1 {
421                    let bytes = self.quant_bytes();
422                    let gpr = cols / GROUP_SIZE;
423                    for gi in 0..gpr {
424                        let tile =
425                            &bytes[(r * gpr + gi) * Q1_TILE..(r * gpr + gi + 1) * Q1_TILE];
426                        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
427                        for (j, &b) in tile[2..].iter().enumerate() {
428                            for k in 0..8 {
429                                dst[gi * GROUP_SIZE + j * 8 + k] =
430                                    (((b >> k) & 1) as f32 * 2.0 - 1.0) * s;
431                            }
432                        }
433                    }
434                    return;
435                }
436                if *dtype == TensorDtype::Q1T {
437                    let bytes = self.quant_bytes();
438                    let gpr = cols / GROUP_SIZE;
439                    let base_len = self.rows() * gpr * cortiq_core::quant::Q1T_TILE;
440                    for gi in 0..gpr {
441                        let off = (r * gpr + gi) * cortiq_core::quant::Q1T_TILE;
442                        let s = cortiq_core::quant::f16_to_f32(u16::from_le_bytes([bytes[off], bytes[off + 1]]));
443                        let codes = &bytes[off + 2..off + cortiq_core::quant::Q1T_TILE];
444                        for k in 0..GROUP_SIZE {
445                            dst[gi * GROUP_SIZE + k] = match cortiq_core::quant::q1t_code(codes, k) {
446                                1 => s,
447                                2 => -s,
448                                _ => 0.0,
449                            };
450                        }
451                    }
452                    // Overlay
453                    let rows = self.rows();
454                    let entries = base_len + (rows + 1) * 4;
455                    if entries <= bytes.len() {
456                        let ptrs = &bytes[base_len..base_len + (rows + 1) * 4];
457                        let r0 = u32::from_le_bytes([ptrs[r * 4], ptrs[r * 4 + 1], ptrs[r * 4 + 2], ptrs[r * 4 + 3]]) as usize;
458                        let r1 = u32::from_le_bytes([ptrs[(r + 1) * 4], ptrs[(r + 1) * 4 + 1], ptrs[(r + 1) * 4 + 2], ptrs[(r + 1) * 4 + 3]]) as usize;
459                        let off = entries + r0 * 4;
460                        for i in 0..r1 - r0 {
461                            let item = &bytes[off + i * 4..off + i * 4 + 4];
462                            let c = u16::from_le_bytes([item[0], item[1]]) as usize;
463                            let v = cortiq_core::quant::f16_to_f32(u16::from_le_bytes([item[2], item[3]]));
464                            if c < cols {
465                                dst[c] = v;
466                            }
467                        }
468                    }
469                    return;
470                }
471                if matches!(dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
472                    let bytes = self.quant_bytes();
473                    let rows = self.rows();
474                    let ng = cols / GROUP_SIZE;
475                    let bits = &bytes[..rows];
476                    let sc_off = rows;
477                    // Precomputed at load — embedding lookup used to scan
478                    // the bit-widths of every preceding row (O(token_id)).
479                    let off = vbit_offsets[r];
480                    let b = bits[r] as usize;
481                    let l = ((1usize << (b - 1)) - 1) as f32;
482                    let data = &bytes[off..];
483                    let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
484                    for (i, d) in dst.iter_mut().enumerate() {
485                        while nbits < b {
486                            acc = (acc << 8) | data[idx] as u64;
487                            idx += 1;
488                            nbits += 8;
489                        }
490                        let u = ((acc >> (nbits - b)) & ((1u64 << b) - 1)) as f32;
491                        nbits -= b;
492                        let so = (r * ng + i / GROUP_SIZE) * 2;
493                        let sv = f16_to_f32(u16::from_le_bytes([
494                            bytes[sc_off + so],
495                            bytes[sc_off + so + 1],
496                        ]));
497                        *d = (u - l) * sv;
498                    }
499                    return;
500                }
501                let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
502                let s = row_scale[r];
503                match dtype {
504                    TensorDtype::Q8Row => {
505                        for (d, &b) in dst.iter_mut().zip(q) {
506                            *d = (b as i8) as f32 * s;
507                        }
508                    }
509                    TensorDtype::Q8_2f => {
510                        for (i, (d, &b)) in dst.iter_mut().zip(q).enumerate() {
511                            *d = (b as i8) as f32 * s * col_field[i];
512                        }
513                    }
514                    _ => unreachable!(),
515                }
516            }
517        }
518    }
519
520    /// Can this tensor's columns be read cheaply (for sparse down_proj)?
521    /// True for F32/Q8Row/Q8_2f (per-row scale, direct strided access);
522    /// false for group-packed q4/vbit (column access would unpack whole
523    /// groups — sparse execution falls back to f32 for those).
524    pub fn sparse_col_ok(&self) -> bool {
525        match self {
526            Self::F32 { .. } => true,
527            Self::Mapped { dtype, .. } => {
528                matches!(dtype, TensorDtype::Q8Row | TensorDtype::Q8_2f)
529            }
530        }
531    }
532
533    /// down_proj [hidden, inter]: accumulate `w · col(c)` into `out`
534    /// [hidden] — reads ONLY column `c` (one neuron) from the mmap,
535    /// no full-matrix dequant. `out[k] += w · down[k, c]`.
536    pub fn add_col_scaled(&self, c: usize, w: f32, out: &mut [f32]) {
537        let inter = self.cols();
538        let hidden = self.rows();
539        debug_assert_eq!(out.len(), hidden);
540        match self {
541            Self::F32 { data, .. } => {
542                for (k, o) in out.iter_mut().enumerate() {
543                    *o += w * data[k * inter + c];
544                }
545            }
546            Self::Mapped {
547                dtype, row_scale, col_field, ..
548            } => {
549                let q = self.quant_bytes();
550                let colf = if *dtype == TensorDtype::Q8_2f {
551                    col_field[c]
552                } else {
553                    1.0
554                };
555                let wc = w * colf;
556                for (k, o) in out.iter_mut().enumerate() {
557                    let b = q[k * inter + c] as i8 as f32;
558                    *o += wc * b * row_scale[k];
559                }
560            }
561        }
562    }
563
564    /// Dot of row `r` with `x` (gate/up active-neuron path). Reads only
565    /// row `r` from the mmap — no full dequant. q4/vbit dequant the row
566    /// into `scratch` first (rare for active-FFN weights).
567    pub fn row_dot(&self, r: usize, x: &[f32], scratch: &mut [f32]) -> f32 {
568        let cols = self.cols();
569        match self {
570            Self::F32 { data, .. } => {
571                let row = &data[r * cols..(r + 1) * cols];
572                row.iter().zip(x).map(|(w, v)| w * v).sum()
573            }
574            Self::Mapped { dtype, row_scale, col_field, .. } => match dtype {
575                TensorDtype::Q8Row => {
576                    let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
577                    dot_i8_f32(q, x) * row_scale[r]
578                }
579                TensorDtype::Q8_2f => {
580                    let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
581                    dot_i8_col_f32(q, x, col_field) * row_scale[r]
582                }
583                _ => {
584                    self.row_f32(r, scratch);
585                    scratch.iter().zip(x).map(|(w, v)| w * v).sum()
586                }
587            },
588        }
589    }
590
591    /// `out = W · x` (row-major). F32 delegates to the historical
592    /// bit-exact path; Mapped runs the fused int8 kernel.
593    pub fn matvec(&self, x: &[f32], out: &mut [f32], pool: Option<&Pool>) {
594        match self {
595            Self::F32 { data, .. } => matvec_rows(pool, data, x, out),
596            Self::Mapped {
597                model,
598                idx,
599                dtype,
600                rows,
601                cols,
602                row_scale,
603                col_field,
604                vbit_offsets,
605                repack,
606            } => {
607                let _ = (model, idx);
608                if *dtype == TensorDtype::Q4Block {
609                    // GPU route (wgpu q4b kernel) for large q4_block matvecs —
610                    // gives NVIDIA/AMD/Intel q4 models a GPU path. Probe keeps
611                    // the winner; Metal returns false → the CPU kernel below.
612                    if *rows * *cols >= 8_388_608 && crate::gpu::enabled_here() {
613                        let t0 = std::time::Instant::now();
614                        match crate::gpu::probe_arm(crate::gpu::OpClass::Matvec) {
615                            crate::gpu::ProbeArm::Gpu => {
616                                if crate::gpu::q4b_matvec(model, *idx, x, *rows, *cols, out) {
617                                    crate::gpu::probe_record(
618                                        crate::gpu::OpClass::Matvec,
619                                        true,
620                                        t0.elapsed(),
621                                    );
622                                    return;
623                                }
624                            }
625                            crate::gpu::ProbeArm::CpuTimed => {
626                                q4matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
627                                crate::gpu::probe_record(
628                                    crate::gpu::OpClass::Matvec,
629                                    false,
630                                    t0.elapsed(),
631                                );
632                                return;
633                            }
634                            crate::gpu::ProbeArm::Cpu => {}
635                        }
636                    }
637                    q4matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
638                    return;
639                }
640                if *dtype == TensorDtype::Q4Tiled {
641                    q4t_matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
642                    return;
643                }
644                if *dtype == TensorDtype::Q1 {
645                    // GPU route for large q1 matvecs (out_proj / lm_head
646                    // class): the CPU q1 kernel is load-port-bound at
647                    // ~4 GB/s/core, the GPU one is bandwidth-bound — the
648                    // probe measures both arms and keeps the winner.
649                    if *rows * *cols >= 8_388_608 && crate::gpu::enabled_here() {
650                        let t0 = std::time::Instant::now();
651                        let arm = if crate::gpu::q1_force() {
652                            crate::gpu::ProbeArm::Gpu
653                        } else {
654                            crate::gpu::probe_arm(crate::gpu::OpClass::Matvec)
655                        };
656                        match arm {
657                            crate::gpu::ProbeArm::Gpu => {
658                                if crate::gpu::q1_matvec(model, *idx, x, *rows, *cols, out) {
659                                    crate::gpu::probe_record(
660                                        crate::gpu::OpClass::Matvec,
661                                        true,
662                                        t0.elapsed(),
663                                    );
664                                    return;
665                                }
666                            }
667                            crate::gpu::ProbeArm::CpuTimed => {
668                                q1_matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
669                                crate::gpu::probe_record(
670                                    crate::gpu::OpClass::Matvec,
671                                    false,
672                                    t0.elapsed(),
673                                );
674                                return;
675                            }
676                            crate::gpu::ProbeArm::Cpu => {}
677                        }
678                    }
679                    q1_matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
680                    return;
681                }
682                if *dtype == TensorDtype::Q1T {
683                    // GPU route for large q1t matvecs: the ternary BASE dot runs
684                    // on the GPU (load-port-bound on CPU, like q1), then the
685                    // sparse overlay is added on the CPU. Probe keeps the winner.
686                    if *rows * *cols >= 8_388_608 && crate::gpu::enabled_here() {
687                        let t0 = std::time::Instant::now();
688                        match crate::gpu::probe_arm(crate::gpu::OpClass::Matvec) {
689                            crate::gpu::ProbeArm::Gpu => {
690                                if crate::gpu::q1t_matvec(model, *idx, x, *rows, *cols, out) {
691                                    q1t_add_overlay(self.quant_bytes(), x, *rows, *cols, out, pool);
692                                    crate::gpu::probe_record(
693                                        crate::gpu::OpClass::Matvec,
694                                        true,
695                                        t0.elapsed(),
696                                    );
697                                    return;
698                                }
699                            }
700                            crate::gpu::ProbeArm::CpuTimed => {
701                                q1t_matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
702                                crate::gpu::probe_record(
703                                    crate::gpu::OpClass::Matvec,
704                                    false,
705                                    t0.elapsed(),
706                                );
707                                return;
708                            }
709                            crate::gpu::ProbeArm::Cpu => {}
710                        }
711                    }
712                    q1t_matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
713                    return;
714                }
715                if matches!(dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
716                    vbitmatvec(self.quant_bytes(), vbit_offsets, x, *rows, *cols, out, pool);
717                    return;
718                }
719                let xs = prescale(x, col_field, *dtype);
720                // D5: large q8 matrices (lm_head-class) — hybrid
721                // CPU∥GPU: split the rows, both sides compute
722                // SIMULTANEOUSLY (same math, shared prescale).
723                // GPU share: CMF_GPU_SPLIT (0..1, default 0.5).
724                if *rows >= crate::gpu::min_rows()
725                    && matches!(dtype, TensorDtype::Q8Row | TensorDtype::Q8_2f)
726                    && std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0").unwrap_or(true)
727                    && crate::gpu::enabled_here()
728                {
729                    // Runtime probe: alternate the hybrid against the
730                    // pure-CPU matvec, keep whichever is faster HERE.
731                    let t0 = std::time::Instant::now();
732                    match crate::gpu::probe_arm(crate::gpu::OpClass::Matvec) {
733                        crate::gpu::ProbeArm::Gpu => {}
734                        crate::gpu::ProbeArm::CpuTimed => {
735                            qmatvec(self.quant_bytes(), repack, row_scale, x, col_field, *dtype, *rows, *cols, out, pool);
736                            crate::gpu::probe_record(
737                                crate::gpu::OpClass::Matvec,
738                                false,
739                                t0.elapsed(),
740                            );
741                            return;
742                        }
743                        crate::gpu::ProbeArm::Cpu => {
744                            qmatvec(self.quant_bytes(), repack, row_scale, x, col_field, *dtype, *rows, *cols, out, pool);
745                            return;
746                        }
747                    }
748                    let frac = std::env::var("CMF_GPU_SPLIT")
749                        .ok()
750                        .and_then(|v| v.parse::<f32>().ok())
751                        .unwrap_or(0.5)
752                        .clamp(0.0, 1.0);
753                    let cpu_rows = ((*rows as f32) * (1.0 - frac)) as usize;
754                    let (out_cpu, out_gpu) = out.split_at_mut(cpu_rows);
755                    let bytes = self.quant_bytes();
756                    let ok = std::thread::scope(|sc| {
757                        let g = sc.spawn(|| {
758                            crate::gpu::q8_matvec_range(
759                                model,
760                                *idx,
761                                cpu_rows,
762                                &row_scale[cpu_rows..],
763                                &xs,
764                                *rows - cpu_rows,
765                                *cols,
766                                out_gpu,
767                            )
768                        });
769                        if cpu_rows > 0 {
770                            // Repack prefix covers the full groups of the
771                            // CPU half (the split starts at row 0).
772                            let rep_cpu = if repack.is_empty() {
773                                &[][..]
774                            } else {
775                                &repack[..(cpu_rows / 4) * 4 * *cols]
776                            };
777                            qmatvec(
778                                &bytes[..cpu_rows * *cols],
779                                rep_cpu,
780                                &row_scale[..cpu_rows],
781                                x,
782                                col_field,
783                                *dtype,
784                                cpu_rows,
785                                *cols,
786                                out_cpu,
787                                pool,
788                            );
789                        }
790                        g.join().unwrap_or(false)
791                    });
792                    if ok {
793                        crate::gpu::probe_record(crate::gpu::OpClass::Matvec, true, t0.elapsed());
794                        return;
795                    }
796                    // GPU failed — CPU finishes its half (rows rebased —
797                    // group offsets don't line up, mmap layout only).
798                    qmatvec(
799                        &bytes[cpu_rows * *cols..(*rows) * *cols],
800                        &[],
801                        &row_scale[cpu_rows..],
802                        x,
803                        col_field,
804                        *dtype,
805                        *rows - cpu_rows,
806                        *cols,
807                        out_gpu,
808                        pool,
809                    );
810                    return;
811                }
812                qmatvec(self.quant_bytes(), repack, row_scale, x, col_field, *dtype, *rows, *cols, out, pool);
813            }
814        }
815    }
816
817    /// Fused two-input matvec (MTP verify pair): weights streamed once.
818    pub fn matvec2(&self, x1: &[f32], x2: &[f32], o1: &mut [f32], o2: &mut [f32], pool: Option<&Pool>) {
819        match self {
820            Self::F32 { data, .. } => matvec_rows2(pool, data, x1, x2, o1, o2),
821            Self::Mapped {
822                dtype,
823                rows,
824                cols,
825                row_scale,
826                col_field,
827                vbit_offsets,
828                ..
829            } => {
830                if *dtype == TensorDtype::Q4Block {
831                    q4matvec2(self.quant_bytes(), x1, x2, *rows, *cols, o1, o2, pool);
832                    return;
833                }
834                if *dtype == TensorDtype::Q4Tiled {
835                    q4t_matvec2(self.quant_bytes(), x1, x2, *rows, *cols, o1, o2, pool);
836                    return;
837                }
838                if *dtype == TensorDtype::Q1 {
839                    q1_matvec2(self.quant_bytes(), x1, x2, *rows, *cols, o1, o2, pool);
840                    return;
841                }
842                if *dtype == TensorDtype::Q1T {
843                    // No fused ternary pair kernel; the two passes are still
844                    // the fused decode+dot each. (Q1T lacks a row_scale array —
845                    // scales live inline in the tiles — so it must not fall
846                    // through to the q8 qmatvec2 below.)
847                    q1t_matvec(self.quant_bytes(), x1, *rows, *cols, o1, pool);
848                    q1t_matvec(self.quant_bytes(), x2, *rows, *cols, o2, pool);
849                    return;
850                }
851                if matches!(dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
852                    vbitmatvec2(self.quant_bytes(), vbit_offsets, x1, x2, *rows, *cols, o1, o2, pool);
853                    return;
854                }
855                qmatvec2(self.quant_bytes(), row_scale, x1, x2, col_field, *dtype, *rows, *cols, o1, o2, pool);
856            }
857        }
858    }
859}
860
861impl QTensor {
862    /// Batched matvec (prefill-GEMM): xs — row-major [b, cols],
863    /// out — row-major [b, rows]. Element-wise semantics are IDENTICAL
864    /// to b matvec calls (same dot kernels in the same order); the win —
865    /// the weight row streams from DRAM once per batch, not b times.
866    pub fn matmat(&self, xs_all: &[f32], b: usize, out: &mut [f32], pool: Option<&Pool>) {
867        let cols = self.cols();
868        let rows = self.rows();
869        debug_assert_eq!(xs_all.len(), b * cols);
870        debug_assert_eq!(out.len(), b * rows);
871        // GPTQ calibration: fold this layer's inputs into its Hessian. Only
872        // Mapped tensors carry a directory name; the check is a relaxed
873        // atomic load, free when not calibrating.
874        if crate::gptq_capture::capturing() {
875            if let Self::Mapped { model, idx, .. } = self {
876                crate::gptq_capture::accumulate(&model.tensors[*idx].name, xs_all, b, cols);
877            }
878        }
879        match self {
880            Self::F32 { data, .. } => {
881                let out_addr = SendMut(out.as_mut_ptr());
882                let run = |start: usize, end: usize| {
883                    for o in start..end {
884                        let row = &data[o * cols..(o + 1) * cols];
885                        for bi in 0..b {
886                            let x = &xs_all[bi * cols..(bi + 1) * cols];
887                            let mut acc = 0f32;
888                            for j in 0..cols {
889                                acc += row[j] * x[j];
890                            }
891                            unsafe { *out_addr.at(bi * rows + o) = acc };
892                        }
893                    }
894                };
895                dispatch_rows(pool, rows, &run);
896            }
897            Self::Mapped {
898                dtype,
899                row_scale,
900                col_field,
901                vbit_offsets,
902                ..
903            } => {
904                if *dtype == TensorDtype::Q4Block {
905                    q4matmat(self.quant_bytes(), xs_all, b, rows, cols, out, pool);
906                    return;
907                }
908                if *dtype == TensorDtype::Q4Tiled {
909                    q4t_matmat(self.quant_bytes(), xs_all, b, rows, cols, out, pool);
910                    return;
911                }
912                if *dtype == TensorDtype::Q1 {
913                    // GPU batched q1 GEMM for wide prefill (q1_mul_mm on the
914                    // device); the probe keeps whichever beats the CPU matmat.
915                    if b >= 32
916                        && b * rows * cols >= 128_000_000
917                        && cols % 64 == 0
918                        && crate::gpu::enabled_here()
919                    {
920                        if let Self::Mapped { model, idx, .. } = self {
921                            let t0 = std::time::Instant::now();
922                            match crate::gpu::probe_arm(crate::gpu::OpClass::Matmat) {
923                                crate::gpu::ProbeArm::Gpu => {
924                                    if crate::gpu::q1_matmat(model, *idx, xs_all, b, rows, cols, out) {
925                                        crate::gpu::probe_record(
926                                            crate::gpu::OpClass::Matmat,
927                                            true,
928                                            t0.elapsed(),
929                                        );
930                                        return;
931                                    }
932                                }
933                                crate::gpu::ProbeArm::CpuTimed => {
934                                    q1_matmat(self.quant_bytes(), xs_all, b, rows, cols, out, pool);
935                                    crate::gpu::probe_record(
936                                        crate::gpu::OpClass::Matmat,
937                                        false,
938                                        t0.elapsed(),
939                                    );
940                                    return;
941                                }
942                                crate::gpu::ProbeArm::Cpu => {}
943                            }
944                        }
945                    }
946                    q1_matmat(self.quant_bytes(), xs_all, b, rows, cols, out, pool);
947                    return;
948                }
949                if *dtype == TensorDtype::Q1T {
950                    // GPU batched GEMM for wide prefill (base + overlay on the
951                    // device); probe keeps the winner vs the CPU matmat.
952                    if b >= 32
953                        && b * rows * cols >= 128_000_000
954                        && crate::gpu::enabled_here()
955                    {
956                        if let Self::Mapped { model, idx, .. } = self {
957                            let t0 = std::time::Instant::now();
958                            match crate::gpu::probe_arm(crate::gpu::OpClass::Matmat) {
959                                crate::gpu::ProbeArm::Gpu => {
960                                    if crate::gpu::q1t_matmat(model, *idx, xs_all, b, rows, cols, out) {
961                                        crate::gpu::probe_record(
962                                            crate::gpu::OpClass::Matmat,
963                                            true,
964                                            t0.elapsed(),
965                                        );
966                                        return;
967                                    }
968                                }
969                                crate::gpu::ProbeArm::CpuTimed => {
970                                    q1t_matmat(self.quant_bytes(), xs_all, b, rows, cols, out, pool);
971                                    crate::gpu::probe_record(
972                                        crate::gpu::OpClass::Matmat,
973                                        false,
974                                        t0.elapsed(),
975                                    );
976                                    return;
977                                }
978                                crate::gpu::ProbeArm::Cpu => {}
979                            }
980                        }
981                    }
982                    q1t_matmat(self.quant_bytes(), xs_all, b, rows, cols, out, pool);
983                    return;
984                }
985                if matches!(dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
986                    vbitmatmat(self.quant_bytes(), vbit_offsets, xs_all, b, rows, cols, out, pool);
987                    return;
988                }
989                let pre: Vec<std::borrow::Cow<'_, [f32]>> = (0..b)
990                    .map(|bi| {
991                        prescale(&xs_all[bi * cols..(bi + 1) * cols], col_field, *dtype)
992                    })
993                    .collect();
994                // D5: large prefill-batch GEMMs — on the GPU (threshold by
995                // work volume: submission carries b×rows×cols MACs).
996                // Runtime probe: the naive GEMM shader + sync readback
997                // lose to the CPU GEMM on slow driver stacks — alternate
998                // both arms and keep the winner.
999                if b >= 8
1000                    && b * rows * cols >= 128_000_000
1001                    && crate::gpu::enabled_here()
1002                {
1003                    if let Self::Mapped { model, idx, .. } = self {
1004                        let t0 = std::time::Instant::now();
1005                        match crate::gpu::probe_arm(crate::gpu::OpClass::Matmat) {
1006                            crate::gpu::ProbeArm::Gpu
1007                                if crate::gpu::probe_deciding(crate::gpu::OpClass::Matmat)
1008                                    && !crate::gpu::q8_resident_or_upload(model, *idx) =>
1009                            {
1010                                // Cold weights during probing: the upload
1011                                // has started, the count runs on the CPU —
1012                                // the GPU arm samples on the next touch.
1013                                let q = self.quant_bytes();
1014                                qmatmat(q, row_scale, &pre, rows, cols, out, pool);
1015                                return;
1016                            }
1017                            crate::gpu::ProbeArm::Gpu => {
1018                                let flat: Vec<f32> =
1019                                    pre.iter().flat_map(|v| v.iter().copied()).collect();
1020                                if crate::gpu::q8_matmat(
1021                                    model, *idx, row_scale, &flat, b, rows, cols, out)
1022                                {
1023                                    crate::gpu::probe_record(
1024                                        crate::gpu::OpClass::Matmat,
1025                                        true,
1026                                        t0.elapsed(),
1027                                    );
1028                                    return;
1029                                }
1030                            }
1031                            crate::gpu::ProbeArm::CpuTimed => {
1032                                let q = self.quant_bytes();
1033                                qmatmat(q, row_scale, &pre, rows, cols, out, pool);
1034                                crate::gpu::probe_record(
1035                                    crate::gpu::OpClass::Matmat,
1036                                    false,
1037                                    t0.elapsed(),
1038                                );
1039                                return;
1040                            }
1041                            crate::gpu::ProbeArm::Cpu => {}
1042                        }
1043                    }
1044                }
1045                let q = self.quant_bytes();
1046                qmatmat(q, row_scale, &pre, rows, cols, out, pool);
1047            }
1048        }
1049    }
1050}
1051
1052impl QTensor {
1053    /// Multi-matrix job (roadmap §3 P0): N tensors sharing one input
1054    /// run under a SINGLE pool dispatch — QKV or gate+up cost one
1055    /// barrier instead of N. Per-row math is the exact same kernel as
1056    /// `matvec` (bit-identical outputs); only the dispatch is fused.
1057    /// Falls back to N sequential matvecs when the set is not a uniform
1058    /// q8-family/F32 group or there is no pool.
1059    pub fn matvec_many<const N: usize>(
1060        ts: [&QTensor; N],
1061        x: &[f32],
1062        mut outs: [&mut [f32]; N],
1063        pool: Option<&Pool>,
1064    ) {
1065        let total_rows: usize = ts.iter().map(|t| t.rows()).sum();
1066        let uniform_q8 = ts.iter().all(|t| {
1067            matches!(
1068                t,
1069                Self::Mapped { dtype: TensorDtype::Q8Row | TensorDtype::Q8_2f, .. }
1070            )
1071        });
1072        let uniform_f32 = ts.iter().all(|t| matches!(t, Self::F32 { .. }));
1073        let uniform_q4 = ts
1074            .iter()
1075            .all(|t| matches!(t, Self::Mapped { dtype: TensorDtype::Q4Block, .. }));
1076        let uniform_vbit = ts
1077            .iter()
1078            .all(|t| matches!(
1079                t,
1080                Self::Mapped { dtype: TensorDtype::Vbit | TensorDtype::VbitRo, .. }
1081            ));
1082        let uniform_q1 = ts
1083            .iter()
1084            .all(|t| matches!(t, Self::Mapped { dtype: TensorDtype::Q1, .. }));
1085        let uniform_q1t = ts
1086            .iter()
1087            .all(|t| matches!(t, Self::Mapped { dtype: TensorDtype::Q1T, .. }));
1088        let Some(pool) = pool else {
1089            for (t, o) in ts.iter().zip(outs.iter_mut()) {
1090                t.matvec(x, o, None);
1091            }
1092            return;
1093        };
1094        if total_rows < 256
1095            || !(uniform_q8 || uniform_f32 || uniform_q4 || uniform_vbit || uniform_q1 || uniform_q1t)
1096        {
1097            for (t, o) in ts.iter().zip(outs.iter_mut()) {
1098                t.matvec(x, o, Some(pool));
1099            }
1100            return;
1101        }
1102
1103        if uniform_q1 {
1104            // One shared activation split + group sums (q1 has no col
1105            // field; the same input feeds every tensor).
1106            let outs_addr: [SendMut; N] = std::array::from_fn(|i| SendMut(outs[i].as_mut_ptr()));
1107            if a8w8_enabled() {
1108                let act = split_act(x);
1109                let gsum = q1_group_sums(&act.xq, ts[0].cols() / GROUP_SIZE);
1110                let (act, gsum) = (&act, &gsum);
1111                let closures: [_; N] = std::array::from_fn(|i| {
1112                    let (bytes, gpr, out) =
1113                        (ts[i].quant_bytes(), ts[i].cols() / GROUP_SIZE, outs_addr[i]);
1114                    move |s: usize, e: usize| q1_range_a8w8(bytes, gpr, act, gsum, out, s, e)
1115                });
1116                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1117                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1118                pool.run_many(&parts);
1119            } else {
1120                let closures: [_; N] = std::array::from_fn(|i| {
1121                    let (bytes, gpr, out) =
1122                        (ts[i].quant_bytes(), ts[i].cols() / GROUP_SIZE, outs_addr[i]);
1123                    move |s: usize, e: usize| q1_range_f32(bytes, gpr, x, out, s, e)
1124                });
1125                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1126                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1127                pool.run_many(&parts);
1128            }
1129            return;
1130        }
1131
1132        if uniform_q1t {
1133            // Q1T batched: one shared activation split + overlay decode,
1134            // all tensors' rows in ONE pool dispatch (saves N−1 dispatches
1135            // and N−1 redundant split_act calls per layer).
1136            let outs_addr: [SendMut; N] = std::array::from_fn(|i| SendMut(outs[i].as_mut_ptr()));
1137            const TILE: usize = cortiq_core::quant::Q1T_TILE;
1138            if a8w8_enabled() {
1139                let act = split_act(x);
1140                let act = &act;
1141                let x_ref = x;
1142                let closures: [_; N] = std::array::from_fn(|i| {
1143                    let bytes = ts[i].quant_bytes();
1144                    let (rows, cols) = (ts[i].rows(), ts[i].cols());
1145                    let gpr = cols / GROUP_SIZE;
1146                    let (rp_off, ent_off, has_ov) = q1t_overlay(bytes, rows * gpr * TILE, rows);
1147                    let out = outs_addr[i];
1148                    move |s: usize, e: usize| {
1149                        q1t_range_a8w8(bytes, gpr, rp_off, ent_off, has_ov, act, x_ref, out, s, e)
1150                    }
1151                });
1152                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1153                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1154                pool.run_many(&parts);
1155            } else {
1156                let x_ref = x;
1157                let closures: [_; N] = std::array::from_fn(|i| {
1158                    let bytes = ts[i].quant_bytes();
1159                    let (rows, cols) = (ts[i].rows(), ts[i].cols());
1160                    let gpr = cols / GROUP_SIZE;
1161                    let (rp_off, ent_off, has_ov) = q1t_overlay(bytes, rows * gpr * TILE, rows);
1162                    let out = outs_addr[i];
1163                    move |s: usize, e: usize| {
1164                        q1t_range_f32_batch(bytes, gpr, rp_off, ent_off, has_ov, x_ref, out, s, e)
1165                    }
1166                });
1167                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1168                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1169                pool.run_many(&parts);
1170            }
1171            return;
1172        }
1173
1174        if uniform_q4 || uniform_vbit {
1175            let outs_addr: [SendMut; N] = std::array::from_fn(|i| SendMut(outs[i].as_mut_ptr()));
1176            // q4/vbit share one activation split — no per-tensor col field.
1177            if a8w8_enabled() {
1178                let act = split_act(x);
1179                let act = &act;
1180                if uniform_q4 {
1181                    let closures: [_; N] = std::array::from_fn(|i| {
1182                        let (packed, scales) =
1183                            q4_split(ts[i].quant_bytes(), ts[i].rows(), ts[i].cols());
1184                        let (gpr, cols, out) =
1185                            (ts[i].cols() / GROUP_SIZE, ts[i].cols(), outs_addr[i]);
1186                        move |s: usize, e: usize| {
1187                            q4_range_a8w8(packed, scales, gpr, cols, act, out, s, e)
1188                        }
1189                    });
1190                    let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1191                        std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1192                    pool.run_many(&parts);
1193                } else {
1194                    let closures: [_; N] = std::array::from_fn(|i| {
1195                        let Self::Mapped { vbit_offsets, .. } = ts[i] else { unreachable!() };
1196                        let (bytes, rows, cols, out) =
1197                            (ts[i].quant_bytes(), ts[i].rows(), ts[i].cols(), outs_addr[i]);
1198                        move |s: usize, e: usize| {
1199                            vbit_range_a8w8(bytes, vbit_offsets, x, act, rows, cols, out, s, e)
1200                        }
1201                    });
1202                    let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1203                        std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1204                    pool.run_many(&parts);
1205                }
1206                return;
1207            }
1208            if uniform_q4 {
1209                let closures: [_; N] = std::array::from_fn(|i| {
1210                    let (packed, scales) =
1211                        q4_split(ts[i].quant_bytes(), ts[i].rows(), ts[i].cols());
1212                    let (gpr, out) = (ts[i].cols() / GROUP_SIZE, outs_addr[i]);
1213                    move |s: usize, e: usize| q4_range_f32(packed, scales, gpr, x, out, s, e)
1214                });
1215                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1216                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1217                pool.run_many(&parts);
1218            } else {
1219                let closures: [_; N] = std::array::from_fn(|i| {
1220                    let Self::Mapped { vbit_offsets, .. } = ts[i] else { unreachable!() };
1221                    let (bytes, rows, cols, out) =
1222                        (ts[i].quant_bytes(), ts[i].rows(), ts[i].cols(), outs_addr[i]);
1223                    move |s: usize, e: usize| {
1224                        vbit_range_f32(bytes, vbit_offsets, x, rows, cols, out, s, e)
1225                    }
1226                });
1227                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1228                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1229                pool.run_many(&parts);
1230            }
1231            return;
1232        }
1233
1234        if uniform_f32 {
1235            let outs_addr: [SendMut; N] = std::array::from_fn(|i| SendMut(outs[i].as_mut_ptr()));
1236            let closures: [_; N] = std::array::from_fn(|i| {
1237                let Self::F32 { data, cols, .. } = ts[i] else { unreachable!() };
1238                let out = outs_addr[i];
1239                move |start: usize, end: usize| {
1240                    for o in start..end {
1241                        let row = &data[o * cols..(o + 1) * cols];
1242                        let mut sum = 0.0f32;
1243                        for j in 0..*cols {
1244                            sum += row[j] * x[j];
1245                        }
1246                        // SAFETY: disjoint (tensor, row) cells per worker.
1247                        unsafe { *out.at(o) = sum };
1248                    }
1249                }
1250            });
1251            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1252                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1253            pool.run_many(&parts);
1254            return;
1255        }
1256
1257        // Uniform q8-family: per-tensor prescale (q8_2f col fields
1258        // differ per tensor) + the shared range kernels.
1259        struct Ctx<'a> {
1260            bytes: &'a [u8],
1261            #[cfg_attr(not(target_arch = "aarch64"), allow(dead_code))]
1262            rep: &'a [u8],
1263            row_scale: &'a [f32],
1264            cols: usize,
1265            xs: std::borrow::Cow<'a, [f32]>,
1266        }
1267        let ctxs: [Ctx<'_>; N] = std::array::from_fn(|i| {
1268            let Self::Mapped { dtype, cols, row_scale, col_field, repack, .. } = ts[i] else {
1269                unreachable!()
1270            };
1271            Ctx {
1272                bytes: ts[i].quant_bytes(),
1273                rep: repack,
1274                row_scale,
1275                cols: *cols,
1276                xs: prescale(x, col_field, *dtype),
1277            }
1278        });
1279        let outs_addr: [SendMut; N] = std::array::from_fn(|i| SendMut(outs[i].as_mut_ptr()));
1280        #[cfg(target_arch = "aarch64")]
1281        if sdot_enabled() {
1282            let acts: [SplitAct; N] = std::array::from_fn(|i| split_act(&ctxs[i].xs));
1283            let closures: [_; N] = std::array::from_fn(|i| {
1284                let (c, act, out) = (&ctxs[i], &acts[i], outs_addr[i]);
1285                move |start: usize, end: usize| {
1286                    q8_range_sdot(c.bytes, c.rep, c.row_scale, act, c.cols, out, start, end)
1287                }
1288            });
1289            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1290                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1291            pool.run_many(&parts);
1292            return;
1293        }
1294        #[cfg(target_arch = "x86_64")]
1295        if avx2_a8w8_enabled() {
1296            let acts: [SplitAct; N] = std::array::from_fn(|i| split_act(&ctxs[i].xs));
1297            let closures: [_; N] = std::array::from_fn(|i| {
1298                let (c, act, out) = (&ctxs[i], &acts[i], outs_addr[i]);
1299                move |start: usize, end: usize| {
1300                    q8_range_avx2(c.bytes, c.row_scale, act, c.cols, out, start, end)
1301                }
1302            });
1303            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1304                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1305            pool.run_many(&parts);
1306            return;
1307        }
1308        let closures: [_; N] = std::array::from_fn(|i| {
1309            let (c, out) = (&ctxs[i], outs_addr[i]);
1310            move |start: usize, end: usize| {
1311                q8_range_f32(c.bytes, c.row_scale, &c.xs, c.cols, out, start, end)
1312            }
1313        });
1314        let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1315            std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1316        pool.run_many(&parts);
1317    }
1318}
1319
1320impl QTensor {
1321    /// Pair-input multi-matrix job: N tensors × 2 shared inputs under a
1322    /// single pool dispatch — the MTP/pair decode path publishes one job
1323    /// for Q/K/V (and one for gate+up) instead of one per tensor.
1324    /// Per-row math is exactly `matvec2`'s kernels; bit-identical.
1325    #[allow(clippy::needless_range_loop)]
1326    pub fn matvec2_many<const N: usize>(
1327        ts: [&QTensor; N],
1328        x1: &[f32],
1329        x2: &[f32],
1330        mut o1s: [&mut [f32]; N],
1331        mut o2s: [&mut [f32]; N],
1332        pool: Option<&Pool>,
1333    ) {
1334        let total_rows: usize = ts.iter().map(|t| t.rows()).sum();
1335        let uniform_q8 = ts.iter().all(|t| {
1336            matches!(
1337                t,
1338                Self::Mapped { dtype: TensorDtype::Q8Row | TensorDtype::Q8_2f, .. }
1339            )
1340        });
1341        let uniform_f32 = ts.iter().all(|t| matches!(t, Self::F32 { .. }));
1342        let uniform_q4 = ts
1343            .iter()
1344            .all(|t| matches!(t, Self::Mapped { dtype: TensorDtype::Q4Block, .. }));
1345        let uniform_vbit = ts
1346            .iter()
1347            .all(|t| matches!(
1348                t,
1349                Self::Mapped { dtype: TensorDtype::Vbit | TensorDtype::VbitRo, .. }
1350            ));
1351        let fusable = pool.is_some()
1352            && total_rows >= 256
1353            && (uniform_q8 || uniform_f32 || uniform_q4 || uniform_vbit);
1354        if !fusable {
1355            for i in 0..N {
1356                ts[i].matvec2(x1, x2, o1s[i], o2s[i], pool);
1357            }
1358            return;
1359        }
1360        let pool = pool.unwrap();
1361
1362        if uniform_q4 || uniform_vbit {
1363            let p1: [SendMut; N] = std::array::from_fn(|i| SendMut(o1s[i].as_mut_ptr()));
1364            let p2: [SendMut; N] = std::array::from_fn(|i| SendMut(o2s[i].as_mut_ptr()));
1365            // q4/vbit share activation splits — no per-tensor col field.
1366            if a8w8_enabled() {
1367                let a1 = split_act(x1);
1368                let a2 = split_act(x2);
1369                let (a1, a2) = (&a1, &a2);
1370                if uniform_q4 {
1371                    let closures: [_; N] = std::array::from_fn(|i| {
1372                        let (packed, scales) =
1373                            q4_split(ts[i].quant_bytes(), ts[i].rows(), ts[i].cols());
1374                        let (gpr, cols, o1, o2) =
1375                            (ts[i].cols() / GROUP_SIZE, ts[i].cols(), p1[i], p2[i]);
1376                        move |s: usize, e: usize| {
1377                            q4_range2_a8w8(packed, scales, gpr, cols, a1, a2, o1, o2, s, e)
1378                        }
1379                    });
1380                    let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1381                        std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1382                    pool.run_many(&parts);
1383                } else {
1384                    let closures: [_; N] = std::array::from_fn(|i| {
1385                        let Self::Mapped { vbit_offsets, .. } = ts[i] else { unreachable!() };
1386                        let (bytes, rows, cols, o1, o2) =
1387                            (ts[i].quant_bytes(), ts[i].rows(), ts[i].cols(), p1[i], p2[i]);
1388                        move |s: usize, e: usize| {
1389                            vbit_range2_a8w8(
1390                                bytes, vbit_offsets, x1, x2, a1, a2, rows, cols, o1, o2, s, e,
1391                            )
1392                        }
1393                    });
1394                    let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1395                        std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1396                    pool.run_many(&parts);
1397                }
1398                return;
1399            }
1400            if uniform_q4 {
1401                let closures: [_; N] = std::array::from_fn(|i| {
1402                    let (packed, scales) =
1403                        q4_split(ts[i].quant_bytes(), ts[i].rows(), ts[i].cols());
1404                    let (gpr, o1, o2) = (ts[i].cols() / GROUP_SIZE, p1[i], p2[i]);
1405                    move |s: usize, e: usize| {
1406                        q4_range2_f32(packed, scales, gpr, x1, x2, o1, o2, s, e)
1407                    }
1408                });
1409                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1410                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1411                pool.run_many(&parts);
1412            } else {
1413                let closures: [_; N] = std::array::from_fn(|i| {
1414                    let Self::Mapped { vbit_offsets, .. } = ts[i] else { unreachable!() };
1415                    let (bytes, rows, cols, o1, o2) =
1416                        (ts[i].quant_bytes(), ts[i].rows(), ts[i].cols(), p1[i], p2[i]);
1417                    move |s: usize, e: usize| {
1418                        vbit_range2_f32(bytes, vbit_offsets, x1, x2, rows, cols, o1, o2, s, e)
1419                    }
1420                });
1421                let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1422                    std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1423                pool.run_many(&parts);
1424            }
1425            return;
1426        }
1427
1428        if uniform_f32 {
1429            let p1: [SendMut; N] = std::array::from_fn(|i| SendMut(o1s[i].as_mut_ptr()));
1430            let p2: [SendMut; N] = std::array::from_fn(|i| SendMut(o2s[i].as_mut_ptr()));
1431            let closures: [_; N] = std::array::from_fn(|i| {
1432                let Self::F32 { data, cols, .. } = ts[i] else { unreachable!() };
1433                let (o1, o2) = (p1[i], p2[i]);
1434                move |start: usize, end: usize| {
1435                    for o in start..end {
1436                        let row = &data[o * cols..(o + 1) * cols];
1437                        let (mut s1, mut s2) = (0.0f32, 0.0f32);
1438                        for j in 0..*cols {
1439                            s1 += row[j] * x1[j];
1440                            s2 += row[j] * x2[j];
1441                        }
1442                        // SAFETY: disjoint (tensor, row) cells per worker.
1443                        unsafe {
1444                            *o1.at(o) = s1;
1445                            *o2.at(o) = s2;
1446                        }
1447                    }
1448                }
1449            });
1450            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1451                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1452            pool.run_many(&parts);
1453            return;
1454        }
1455
1456        struct Ctx<'a> {
1457            bytes: &'a [u8],
1458            row_scale: &'a [f32],
1459            cols: usize,
1460            xs1: std::borrow::Cow<'a, [f32]>,
1461            xs2: std::borrow::Cow<'a, [f32]>,
1462        }
1463        let ctxs: [Ctx<'_>; N] = std::array::from_fn(|i| {
1464            let Self::Mapped { dtype, cols, row_scale, col_field, .. } = ts[i] else {
1465                unreachable!()
1466            };
1467            Ctx {
1468                bytes: ts[i].quant_bytes(),
1469                row_scale,
1470                cols: *cols,
1471                xs1: prescale(x1, col_field, *dtype),
1472                xs2: prescale(x2, col_field, *dtype),
1473            }
1474        });
1475        let p1: [SendMut; N] = std::array::from_fn(|i| SendMut(o1s[i].as_mut_ptr()));
1476        let p2: [SendMut; N] = std::array::from_fn(|i| SendMut(o2s[i].as_mut_ptr()));
1477        #[cfg(target_arch = "aarch64")]
1478        if sdot_enabled() {
1479            let acts: [(SplitAct, SplitAct); N] =
1480                std::array::from_fn(|i| (split_act(&ctxs[i].xs1), split_act(&ctxs[i].xs2)));
1481            let closures: [_; N] = std::array::from_fn(|i| {
1482                let (c, a, o1, o2) = (&ctxs[i], &acts[i], p1[i], p2[i]);
1483                move |start: usize, end: usize| {
1484                    q8_range2_sdot(c.bytes, c.row_scale, &a.0, &a.1, c.cols, o1, o2, start, end)
1485                }
1486            });
1487            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1488                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1489            pool.run_many(&parts);
1490            return;
1491        }
1492        #[cfg(target_arch = "x86_64")]
1493        if avx2_a8w8_enabled() {
1494            let acts: [(SplitAct, SplitAct); N] =
1495                std::array::from_fn(|i| (split_act(&ctxs[i].xs1), split_act(&ctxs[i].xs2)));
1496            let closures: [_; N] = std::array::from_fn(|i| {
1497                let (c, a, o1, o2) = (&ctxs[i], &acts[i], p1[i], p2[i]);
1498                move |start: usize, end: usize| {
1499                    q8_range2_avx2(c.bytes, c.row_scale, &a.0, &a.1, c.cols, o1, o2, start, end)
1500                }
1501            });
1502            let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1503                std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1504            pool.run_many(&parts);
1505            return;
1506        }
1507        let closures: [_; N] = std::array::from_fn(|i| {
1508            let (c, o1, o2) = (&ctxs[i], p1[i], p2[i]);
1509            move |start: usize, end: usize| {
1510                q8_range2_f32(c.bytes, c.row_scale, &c.xs1, &c.xs2, c.cols, o1, o2, start, end)
1511            }
1512        });
1513        let parts: [(usize, &(dyn Fn(usize, usize) + Sync)); N] =
1514            std::array::from_fn(|i| (ts[i].rows(), &closures[i] as _));
1515        pool.run_many(&parts);
1516    }
1517
1518    /// Fused gate+up matvec with SiLU·mul: for each row r, computes
1519    /// `silu(gate·x) * (up·x)` and writes to `out[r]`. ONE pool dispatch,
1520    /// no intermediate g/u buffers, no separate silu pass. Falls back
1521    /// (returns false) for unsupported dtype combos.
1522    pub fn matvec_silu_mul(
1523        gate: &QTensor,
1524        up: &QTensor,
1525        x: &[f32],
1526        out: &mut [f32],
1527        pool: Option<&Pool>,
1528    ) -> bool {
1529        let inter = gate.rows();
1530        debug_assert_eq!(up.rows(), inter);
1531        debug_assert_eq!(out.len(), inter);
1532        debug_assert_eq!(gate.cols(), up.cols());
1533        if !a8w8_enabled() {
1534            return false;
1535        }
1536        let act = split_act(x);
1537        let act = &act;
1538        let x_ref = x;
1539        let out_addr = SendMut(out.as_mut_ptr());
1540
1541        match (gate, up) {
1542            // Q4Block gate + Q4Block up (most common mobile q4 models)
1543            (
1544                Self::Mapped { dtype: TensorDtype::Q4Block, .. },
1545                Self::Mapped { dtype: TensorDtype::Q4Block, .. },
1546            ) => {
1547                let (gp, gs) = q4_split(gate.quant_bytes(), gate.rows(), gate.cols());
1548                let (up_p, up_s) = q4_split(up.quant_bytes(), up.rows(), up.cols());
1549                let gpr = gate.cols() / GROUP_SIZE;
1550                let cols = gate.cols();
1551                let run = move |start: usize, end: usize| {
1552                    for r in start..end {
1553                        let mut gv = dot_q4_row_i8(gp, gs, r * gpr, gpr, &act.xq) * act.sx;
1554                        let mut uv = dot_q4_row_i8(up_p, up_s, r * gpr, gpr, &act.xq) * act.sx;
1555                        for &(j, xv) in &act.outliers {
1556                            let flat = r * cols + j;
1557                            let gb = gp[flat / 2];
1558                            let gn = if flat & 1 == 0 { gb & 0x0F } else { gb >> 4 };
1559                            let gsc = f16_to_f32(u16::from_le_bytes([
1560                                gs[(flat / GROUP_SIZE) * 2],
1561                                gs[(flat / GROUP_SIZE) * 2 + 1],
1562                            ]));
1563                            gv += ((gn as i32 - 8) as f32) * gsc * xv;
1564                            let ub = up_p[flat / 2];
1565                            let un = if flat & 1 == 0 { ub & 0x0F } else { ub >> 4 };
1566                            let usc = f16_to_f32(u16::from_le_bytes([
1567                                up_s[(flat / GROUP_SIZE) * 2],
1568                                up_s[(flat / GROUP_SIZE) * 2 + 1],
1569                            ]));
1570                            uv += ((un as i32 - 8) as f32) * usc * xv;
1571                        }
1572                        let silu_g = gv / (1.0 + (-gv).exp());
1573                        // SAFETY: disjoint row ranges per worker.
1574                        unsafe { *out_addr.at(r) = silu_g * uv };
1575                    }
1576                };
1577                dispatch_rows(pool, inter, &run);
1578                true
1579            }
1580            // Q1T gate + Q1T up
1581            (
1582                Self::Mapped { dtype: TensorDtype::Q1T, .. },
1583                Self::Mapped { dtype: TensorDtype::Q1T, .. },
1584            ) => {
1585                const TILE: usize = cortiq_core::quant::Q1T_TILE;
1586                let g_bytes = gate.quant_bytes();
1587                let u_bytes = up.quant_bytes();
1588                let gpr = gate.cols() / GROUP_SIZE;
1589                let (g_rp, g_ent, g_ov) = q1t_overlay(g_bytes, inter * gpr * TILE, inter);
1590                let (u_rp, u_ent, u_ov) = q1t_overlay(u_bytes, inter * gpr * TILE, inter);
1591                let run = move |start: usize, end: usize| {
1592                    for r in start..end {
1593                        let mut gv = q1t_dot_row_i8(g_bytes, r, gpr, &act.xq) * act.sx;
1594                        let mut uv = q1t_dot_row_i8(u_bytes, r, gpr, &act.xq) * act.sx;
1595                        for &(j, xv) in &act.outliers {
1596                            gv += q1t_base_weight(g_bytes, r, gpr, j) * xv;
1597                            uv += q1t_base_weight(u_bytes, r, gpr, j) * xv;
1598                        }
1599                        gv += q1t_row_outlier_correction(g_bytes, r, g_rp, g_ent, g_ov, x_ref);
1600                        uv += q1t_row_outlier_correction(u_bytes, r, u_rp, u_ent, u_ov, x_ref);
1601                        let silu_g = gv / (1.0 + (-gv).exp());
1602                        // SAFETY: disjoint row ranges per worker.
1603                        unsafe { *out_addr.at(r) = silu_g * uv };
1604                    }
1605                };
1606                dispatch_rows(pool, inter, &run);
1607                true
1608            }
1609            _ => false,
1610        }
1611    }
1612}
1613
1614/// Batched q8 kernel: same math as qmatvec, the row makes a single
1615/// pass from memory for the whole batch.
1616/// Accelerate CBLAS — the Apple AMX matrix units, the same engine
1617/// llama.cpp's `-ngl 0` prefill rides via ggml-blas.
1618#[cfg(target_os = "macos")]
1619mod accel_blas {
1620    #[link(name = "Accelerate", kind = "framework")]
1621    unsafe extern "C" {
1622        pub fn cblas_sgemm(
1623            order: i32,
1624            trans_a: i32,
1625            trans_b: i32,
1626            m: i32,
1627            n: i32,
1628            k: i32,
1629            alpha: f32,
1630            a: *const f32,
1631            lda: i32,
1632            b: *const f32,
1633            ldb: i32,
1634            beta: f32,
1635            c: *mut f32,
1636            ldc: i32,
1637        );
1638    }
1639}
1640
1641#[cfg(target_os = "macos")]
1642pub(crate) fn accel_gemm_enabled() -> bool {
1643    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1644    *ON.get_or_init(|| std::env::var("CMF_ACCEL").map(|v| v != "0").unwrap_or(true))
1645}
1646
1647/// Off macOS the "accel" GEMM is the portable NEON micro-kernel below —
1648/// same entry point, so the batched-attention path opens on mobile.
1649#[cfg(all(target_arch = "aarch64", not(target_os = "macos")))]
1650pub(crate) fn accel_gemm_enabled() -> bool {
1651    true
1652}
1653
1654/// Portable NEON f32 GEMM (row-major, optional Bᵀ): a 4×8 fmla
1655/// micro-kernel with A broadcast against B panels — the mobile stand-in
1656/// for Accelerate in the batched causal attention (QKᵀ and P·V). Not a
1657/// BLAS: shapes here are the attention panels (m ≤ heads·chunk,
1658/// k = head_dim or context), and the goal is removing the per-position
1659/// quadratic wall, not peak GEMM.
1660#[cfg(target_arch = "aarch64")]
1661#[allow(clippy::too_many_arguments)]
1662pub(crate) fn neon_gemm_rm(
1663    m: usize,
1664    n: usize,
1665    k: usize,
1666    alpha: f32,
1667    a: &[f32],
1668    lda: usize,
1669    b_mat: &[f32],
1670    ldb: usize,
1671    b_rows_are_n: bool,
1672    c: &mut [f32],
1673    ldc: usize,
1674) {
1675    debug_assert!(a.len() >= (m - 1) * lda + k);
1676    debug_assert!(c.len() >= (m - 1) * ldc + n);
1677    // SAFETY: bounds asserted above; NEON is baseline on aarch64.
1678    unsafe {
1679        use core::arch::aarch64::*;
1680        let mut i = 0usize;
1681        while i < m {
1682            let mi = (m - i).min(4);
1683            let mut j = 0usize;
1684            while j < n {
1685                let nj = (n - j).min(8);
1686                if mi == 4 && nj == 8 {
1687                    let (mut c0a, mut c0b) = (vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1688                    let (mut c1a, mut c1b) = (vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1689                    let (mut c2a, mut c2b) = (vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1690                    let (mut c3a, mut c3b) = (vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1691                    for p in 0..k {
1692                        let (b0, b1) = if b_rows_are_n {
1693                            // B is [n, k]: column p of Bᵀ = element p of
1694                            // eight consecutive B rows — gathered.
1695                            let base = b_mat.as_ptr().add(j * ldb + p);
1696                            let g = |o: usize| *base.add(o * ldb);
1697                            (
1698                                [g(0), g(1), g(2), g(3)],
1699                                [g(4), g(5), g(6), g(7)],
1700                            )
1701                        } else {
1702                            let base = b_mat.as_ptr().add(p * ldb + j);
1703                            (
1704                                [*base, *base.add(1), *base.add(2), *base.add(3)],
1705                                [*base.add(4), *base.add(5), *base.add(6), *base.add(7)],
1706                            )
1707                        };
1708                        let bv0 = vld1q_f32(b0.as_ptr());
1709                        let bv1 = vld1q_f32(b1.as_ptr());
1710                        let a0 = vdupq_n_f32(*a.as_ptr().add(i * lda + p));
1711                        let a1 = vdupq_n_f32(*a.as_ptr().add((i + 1) * lda + p));
1712                        let a2 = vdupq_n_f32(*a.as_ptr().add((i + 2) * lda + p));
1713                        let a3 = vdupq_n_f32(*a.as_ptr().add((i + 3) * lda + p));
1714                        c0a = vfmaq_f32(c0a, a0, bv0);
1715                        c0b = vfmaq_f32(c0b, a0, bv1);
1716                        c1a = vfmaq_f32(c1a, a1, bv0);
1717                        c1b = vfmaq_f32(c1b, a1, bv1);
1718                        c2a = vfmaq_f32(c2a, a2, bv0);
1719                        c2b = vfmaq_f32(c2b, a2, bv1);
1720                        c3a = vfmaq_f32(c3a, a3, bv0);
1721                        c3b = vfmaq_f32(c3b, a3, bv1);
1722                    }
1723                    let al = vdupq_n_f32(alpha);
1724                    for (r, (ca, cb)) in
1725                        [(c0a, c0b), (c1a, c1b), (c2a, c2b), (c3a, c3b)].iter().enumerate()
1726                    {
1727                        let dst = c.as_mut_ptr().add((i + r) * ldc + j);
1728                        vst1q_f32(dst, vmulq_f32(*ca, al));
1729                        vst1q_f32(dst.add(4), vmulq_f32(*cb, al));
1730                    }
1731                } else {
1732                    for r in 0..mi {
1733                        for q in 0..nj {
1734                            let mut acc = 0f32;
1735                            for p in 0..k {
1736                                let bv = if b_rows_are_n {
1737                                    b_mat[(j + q) * ldb + p]
1738                                } else {
1739                                    b_mat[p * ldb + j + q]
1740                                };
1741                                acc += a[(i + r) * lda + p] * bv;
1742                            }
1743                            c[(i + r) * ldc + j + q] = acc * alpha;
1744                        }
1745                    }
1746                }
1747                j += nj;
1748            }
1749            i += mi;
1750        }
1751    }
1752}
1753
1754/// Off-macOS aarch64: the batched attention rides the NEON micro-GEMM.
1755#[cfg(all(target_arch = "aarch64", not(target_os = "macos")))]
1756#[allow(clippy::too_many_arguments)]
1757pub(crate) fn sgemm_rm(
1758    m: usize,
1759    n: usize,
1760    k: usize,
1761    alpha: f32,
1762    a: &[f32],
1763    lda: usize,
1764    b_mat: &[f32],
1765    ldb: usize,
1766    b_rows_are_n: bool,
1767    c: &mut [f32],
1768    ldc: usize,
1769) {
1770    neon_gemm_rm(m, n, k, alpha, a, lda, b_mat, ldb, b_rows_are_n, c, ldc);
1771}
1772
1773/// Row-major f32 GEMM on Accelerate: C[m,n] = alpha·A[m,k] × B(ᵀ).
1774/// `b_rows_are_n` = true multiplies by Bᵀ where B is stored [n, k].
1775#[cfg(target_os = "macos")]
1776#[allow(clippy::too_many_arguments)]
1777pub(crate) fn sgemm_rm(
1778    m: usize,
1779    n: usize,
1780    k: usize,
1781    alpha: f32,
1782    a: &[f32],
1783    lda: usize,
1784    b_mat: &[f32],
1785    ldb: usize,
1786    b_rows_are_n: bool,
1787    c: &mut [f32],
1788    ldc: usize,
1789) {
1790    debug_assert!(a.len() >= (m - 1) * lda + k);
1791    debug_assert!(c.len() >= (m - 1) * ldc + n);
1792    // Test hook: route the attention GEMMs through the portable NEON
1793    // micro-kernel ON APPLE SILICON — how the mobile batched attend is
1794    // measured without a phone in the loop. (Intel macOS has no NEON —
1795    // the hook is a no-op there, Accelerate continues below.)
1796    #[cfg(target_arch = "aarch64")]
1797    if std::env::var("CMF_FORCE_NEON_GEMM").map(|v| v == "1").unwrap_or(false) {
1798        return neon_gemm_rm(m, n, k, alpha, a, lda, b_mat, ldb, b_rows_are_n, c, ldc);
1799    }
1800    unsafe {
1801        accel_blas::cblas_sgemm(
1802            101, // RowMajor
1803            111, // NoTrans A
1804            if b_rows_are_n { 112 } else { 111 },
1805            m as i32,
1806            n as i32,
1807            k as i32,
1808            alpha,
1809            a.as_ptr(),
1810            lda as i32,
1811            b_mat.as_ptr(),
1812            ldb as i32,
1813            0.0,
1814            c.as_mut_ptr(),
1815            ldc as i32,
1816        );
1817    }
1818}
1819
1820/// Prefill GEMM through Accelerate (macOS): dequantize q8 rows into
1821/// f32 tiles (scale folded in, pool-parallel) and multiply each tile
1822/// on the AMX with one row-major sgemm. Tiles live in cache, weights
1823/// stream once. Numerics are f32-GEMM (not the int8 dot): prefill
1824/// logits shift within f32 rounding — tolerance-class, like every
1825/// reduction-order change; decode (M=1) never takes this path.
1826#[cfg(target_os = "macos")]
1827fn qmatmat_accel(
1828    q: &[u8],
1829    row_scale: &[f32],
1830    pre: &[std::borrow::Cow<'_, [f32]>],
1831    rows: usize,
1832    cols: usize,
1833    out: &mut [f32],
1834    pool: Option<&Pool>,
1835) {
1836    // NOTE: double-buffering the dequant against the sgemm (a scoped
1837    // thread driving the pool on tile k+1 while the caller multiplies
1838    // tile k) was tried and LOST ~6%: Accelerate's sgemm is itself
1839    // multithreaded, and the dequant workers just steal its cores.
1840    const TR: usize = 2048;
1841    let b = pre.len();
1842    thread_local! {
1843        static XPANEL: std::cell::RefCell<Vec<f32>> = const { std::cell::RefCell::new(Vec::new()) };
1844        static WTILE: std::cell::RefCell<Vec<f32>> = const { std::cell::RefCell::new(Vec::new()) };
1845    }
1846    XPANEL.with(|xp| {
1847        WTILE.with(|wt| {
1848            let mut xpanel = xp.borrow_mut();
1849            xpanel.clear();
1850            for x in pre {
1851                xpanel.extend_from_slice(x);
1852            }
1853            let mut wtile = wt.borrow_mut();
1854            wtile.resize(TR * cols, 0.0);
1855            let mut r0 = 0usize;
1856            while r0 < rows {
1857                let tr = TR.min(rows - r0);
1858                // Dequant the tile (scale folded) — pool-parallel.
1859                let wt_addr = SendMut(wtile.as_mut_ptr());
1860                let run = |start: usize, end: usize| {
1861                    for r in start..end {
1862                        let row = &q[(r0 + r) * cols..(r0 + r + 1) * cols];
1863                        let s = row_scale[r0 + r];
1864                        // SAFETY: workers cover disjoint r ranges.
1865                        let dst = unsafe {
1866                            std::slice::from_raw_parts_mut(wt_addr.at(r * cols), cols)
1867                        };
1868                        for (d, &v) in dst.iter_mut().zip(row) {
1869                            *d = (v as i8) as f32 * s;
1870                        }
1871                    }
1872                };
1873                dispatch_rows(pool, tr, &run);
1874                // C[b, tr] (at column r0 of out[b, rows]) = X · Wtileᵀ
1875                unsafe {
1876                    accel_blas::cblas_sgemm(
1877                        101, // RowMajor
1878                        111, // NoTrans A
1879                        112, // Trans B
1880                        b as i32,
1881                        tr as i32,
1882                        cols as i32,
1883                        1.0,
1884                        xpanel.as_ptr(),
1885                        cols as i32,
1886                        wtile.as_ptr(),
1887                        cols as i32,
1888                        0.0,
1889                        out.as_mut_ptr().add(r0),
1890                        rows as i32,
1891                    );
1892                }
1893                r0 += tr;
1894            }
1895        })
1896    });
1897}
1898
1899fn qmatmat(
1900    q: &[u8],
1901    row_scale: &[f32],
1902    pre: &[std::borrow::Cow<'_, [f32]>],
1903    rows: usize,
1904    cols: usize,
1905    out: &mut [f32],
1906    pool: Option<&Pool>,
1907) {
1908    let b = pre.len();
1909    debug_assert_eq!(out.len(), b * rows);
1910    // Big prefill batches ride the AMX (roadmap PR3): the row×batch
1911    // SDOT loop below peaks near the CPU's dot throughput, an order
1912    // below the matrix units. Small tensors and tiny test models stay
1913    // on the exact integer path.
1914    #[cfg(target_os = "macos")]
1915    if b >= 8 && rows * cols >= 500_000 && accel_gemm_enabled() {
1916        qmatmat_accel(q, row_scale, pre, rows, cols, out, pool);
1917        return;
1918    }
1919    #[cfg(target_arch = "aarch64")]
1920    if sdot_enabled() {
1921        let acts: Vec<SplitAct> = pre.iter().map(|x| split_act(x)).collect();
1922        let out_addr = SendMut(out.as_mut_ptr());
1923        // Blocked 2×4 (mobile prefill: no AMX to fall back on — this
1924        // path IS the ARM prefill GEMM off Apple silicon).
1925        let blocked_ok =
1926            std::env::var("CMF_X86_BLOCKED").map(|v| v != "0").unwrap_or(true);
1927        let use_i8mm = i8mm_enabled();
1928        if blocked_ok {
1929            let run = |start: usize, end: usize| {
1930                let mut o = start;
1931                while o < end {
1932                    if o + 2 <= end {
1933                        let r0 = &q[o * cols..(o + 1) * cols];
1934                        let r1 = &q[(o + 1) * cols..(o + 2) * cols];
1935                        let mut bi = 0usize;
1936                        while bi + 4 <= acts.len() {
1937                            let xs = [
1938                                acts[bi].xq.as_slice(),
1939                                acts[bi + 1].xq.as_slice(),
1940                                acts[bi + 2].xq.as_slice(),
1941                                acts[bi + 3].xq.as_slice(),
1942                            ];
1943                            let d = if use_i8mm {
1944                                unsafe { dot_i8_smmla_2x4(r0, r1, xs) }
1945                            } else {
1946                                unsafe { dot_i8_sdot_2x4(r0, r1, xs) }
1947                            };
1948                            for (r, row) in [r0, r1].into_iter().enumerate() {
1949                                for k in 0..4 {
1950                                    let act = &acts[bi + k];
1951                                    let mut v = d[r][k] as f32 * act.sx;
1952                                    for &(j, xv) in &act.outliers {
1953                                        v += (row[j] as i8) as f32 * xv;
1954                                    }
1955                                    unsafe {
1956                                        *out_addr.at((bi + k) * rows + o + r) =
1957                                            v * row_scale[o + r]
1958                                    };
1959                                }
1960                            }
1961                            bi += 4;
1962                        }
1963                        while bi < acts.len() {
1964                            for (r, row) in [r0, r1].into_iter().enumerate() {
1965                                let v = row_dot_sdot(row, &acts[bi]) * row_scale[o + r];
1966                                unsafe { *out_addr.at(bi * rows + o + r) = v };
1967                            }
1968                            bi += 1;
1969                        }
1970                        o += 2;
1971                    } else {
1972                        let row = &q[o * cols..(o + 1) * cols];
1973                        for (bi, act) in acts.iter().enumerate() {
1974                            let v = row_dot_sdot(row, act) * row_scale[o];
1975                            unsafe { *out_addr.at(bi * rows + o) = v };
1976                        }
1977                        o += 1;
1978                    }
1979                }
1980            };
1981            dispatch_rows(pool, rows, &run);
1982            return;
1983        }
1984        let run = |start: usize, end: usize| {
1985            for o in start..end {
1986                let row = &q[o * cols..(o + 1) * cols];
1987                for (bi, act) in acts.iter().enumerate() {
1988                    let v = row_dot_sdot(row, act) * row_scale[o];
1989                    unsafe { *out_addr.at(bi * rows + o) = v };
1990                }
1991            }
1992        };
1993        dispatch_rows(pool, rows, &run);
1994        return;
1995    }
1996    // x86 A8W8 batch. Non-VNNI parts take the BLOCKED 2×4 kernel
1997    // (roadmap P0: two weight rows' abs() stay in registers across four
1998    // activation streams); VNNI machines keep the per-row bias-trick
1999    // dot, which is already throughput-bound there.
2000    #[cfg(target_arch = "x86_64")]
2001    if avx2_a8w8_enabled() {
2002        let acts: Vec<SplitAct> = pre.iter().map(|x| split_act(x)).collect();
2003        let out_addr = SendMut(out.as_mut_ptr());
2004        // CMF_X86_BLOCKED=0 forces the per-row path (paired in-process
2005        // A/B on noisy shared-vCPU hosts).
2006        let blocked_ok = std::env::var("CMF_X86_BLOCKED").map(|v| v != "0").unwrap_or(true);
2007        if !avx512vnni_enabled() && blocked_ok {
2008            let run = |start: usize, end: usize| {
2009                let mut o = start;
2010                while o < end {
2011                    if o + 2 <= end {
2012                        let r0 = &q[o * cols..(o + 1) * cols];
2013                        let r1 = &q[(o + 1) * cols..(o + 2) * cols];
2014                        let mut bi = 0usize;
2015                        while bi + 4 <= acts.len() {
2016                            let xs = [
2017                                acts[bi].xq.as_slice(),
2018                                acts[bi + 1].xq.as_slice(),
2019                                acts[bi + 2].xq.as_slice(),
2020                                acts[bi + 3].xq.as_slice(),
2021                            ];
2022                            let d = unsafe { dot_i8_i8_avx2_2x4(r0, r1, xs) };
2023                            for (r, row) in [r0, r1].into_iter().enumerate() {
2024                                for k in 0..4 {
2025                                    let act = &acts[bi + k];
2026                                    let mut v = d[r][k] as f32 * act.sx;
2027                                    for &(j, xv) in &act.outliers {
2028                                        v += (row[j] as i8) as f32 * xv;
2029                                    }
2030                                    unsafe {
2031                                        *out_addr.at((bi + k) * rows + o + r) =
2032                                            v * row_scale[o + r]
2033                                    };
2034                                }
2035                            }
2036                            bi += 4;
2037                        }
2038                        while bi < acts.len() {
2039                            for (r, row) in [r0, r1].into_iter().enumerate() {
2040                                let v = row_dot_avx2(row, &acts[bi]) * row_scale[o + r];
2041                                unsafe { *out_addr.at(bi * rows + o + r) = v };
2042                            }
2043                            bi += 1;
2044                        }
2045                        o += 2;
2046                    } else {
2047                        let row = &q[o * cols..(o + 1) * cols];
2048                        for (bi, act) in acts.iter().enumerate() {
2049                            let v = row_dot_avx2(row, act) * row_scale[o];
2050                            unsafe { *out_addr.at(bi * rows + o) = v };
2051                        }
2052                        o += 1;
2053                    }
2054                }
2055            };
2056            dispatch_rows(pool, rows, &run);
2057            return;
2058        }
2059        let run = |start: usize, end: usize| {
2060            for o in start..end {
2061                let row = &q[o * cols..(o + 1) * cols];
2062                for (bi, act) in acts.iter().enumerate() {
2063                    let v = row_dot_avx2(row, act) * row_scale[o];
2064                    unsafe { *out_addr.at(bi * rows + o) = v };
2065                }
2066            }
2067        };
2068        dispatch_rows(pool, rows, &run);
2069        return;
2070    }
2071    let out_addr = SendMut(out.as_mut_ptr());
2072    let run = |start: usize, end: usize| {
2073        for o in start..end {
2074            let row = &q[o * cols..(o + 1) * cols];
2075            for (bi, x) in pre.iter().enumerate() {
2076                let mut acc = 0f32;
2077                for j in 0..cols {
2078                    acc += (row[j] as i8) as f32 * x[j];
2079                }
2080                unsafe { *out_addr.at(bi * rows + o) = acc * row_scale[o] };
2081            }
2082        }
2083    };
2084    dispatch_rows(pool, rows, &run);
2085}
2086
2087/// Split rows across pool workers (shared qmatvec pattern). Self-balancing
2088/// — see `Pool::run_rows` for why a static 1/n split is wrong here.
2089fn dispatch_rows(pool: Option<&Pool>, rows: usize, run: &(dyn Fn(usize, usize) + Sync)) {
2090    match pool {
2091        Some(pool) if rows >= 256 => pool.run_rows(rows, run),
2092        _ => run(0, rows),
2093    }
2094}
2095
2096/// Split a q4_block blob into (packed nibbles, f16 group scales).
2097fn q4_split(bytes: &[u8], rows: usize, cols: usize) -> (&[u8], &[u8]) {
2098    let groups = rows * cols / GROUP_SIZE;
2099    bytes.split_at(groups * 16)
2100}
2101
2102/// SIMD unpack for the dominant vbit width B=4 (94% of rows on the
2103/// log2-shape calibration): 16 packed bytes -> 32 centered i8 values.
2104/// vbit packs MSB-first, so the HIGH nibble is the even element
2105/// (opposite of q4_block's lo-first interleave). Centering is u-7.
2106#[inline]
2107fn vbit_fill4(data: &[u8], buf: &mut [u8]) {
2108    #[cfg(target_arch = "aarch64")]
2109    unsafe {
2110        return vbit_fill4_neon(data, buf);
2111    }
2112    #[cfg(target_arch = "x86_64")]
2113    if avx2_enabled() {
2114        return unsafe { vbit_fill4_avx2(data, buf) };
2115    }
2116    #[allow(unreachable_code)]
2117    for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
2118        let u = unpack8::<4>(&data[blk * 4..]);
2119        for k in 0..8 {
2120            chunk[k] = (u[k] - 7) as i8 as u8;
2121        }
2122    }
2123}
2124
2125#[cfg(target_arch = "aarch64")]
2126#[target_feature(enable = "neon")]
2127unsafe fn vbit_fill4_neon(data: &[u8], buf: &mut [u8]) {
2128    // SAFETY: buf.len() is a multiple of GROUP_SIZE=32; data holds
2129    // buf.len()/2 packed bytes (validated at load).
2130    unsafe {
2131        use core::arch::aarch64::*;
2132        let n = buf.len();
2133        let mask = vdupq_n_u8(0x0F);
2134        let seven = vdupq_n_s8(7);
2135        let mut g = 0usize;
2136        while g * 32 + 32 <= n {
2137            let b = vld1q_u8(data.as_ptr().add(g * 16));
2138            let hi = vshrq_n_u8::<4>(b);
2139            let lo = vandq_u8(b, mask);
2140            let z0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(hi, lo)), seven);
2141            let z1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(hi, lo)), seven);
2142            vst1q_u8(buf.as_mut_ptr().add(g * 32), vreinterpretq_u8_s8(z0));
2143            vst1q_u8(buf.as_mut_ptr().add(g * 32 + 16), vreinterpretq_u8_s8(z1));
2144            g += 1;
2145        }
2146    }
2147}
2148
2149#[cfg(target_arch = "x86_64")]
2150#[target_feature(enable = "avx2")]
2151unsafe fn vbit_fill4_avx2(data: &[u8], buf: &mut [u8]) {
2152    // SAFETY: see vbit_fill4_neon.
2153    unsafe {
2154        use core::arch::x86_64::*;
2155        let n = buf.len();
2156        let mask = _mm_set1_epi8(0x0F);
2157        let seven = _mm256_set1_epi8(7);
2158        let mut g = 0usize;
2159        while g * 32 + 32 <= n {
2160            let b = _mm_loadu_si128(data.as_ptr().add(g * 16) as *const __m128i);
2161            let hi = _mm_and_si128(_mm_srli_epi16::<4>(b), mask);
2162            let lo = _mm_and_si128(b, mask);
2163            let z = _mm256_sub_epi8(
2164                _mm256_set_m128i(_mm_unpackhi_epi8(hi, lo), _mm_unpacklo_epi8(hi, lo)),
2165                seven,
2166            );
2167            _mm256_storeu_si256(buf.as_mut_ptr().add(g * 32) as *mut __m256i, z);
2168            g += 1;
2169        }
2170    }
2171}
2172
2173/// Unpack 8 MSB-first B-bit values from exactly B bytes (fixed shifts —
2174/// no serial bit-buffer, auto-vectorizable). Every 32-value group starts
2175/// byte-aligned (32·B/8 is integral for B∈3..8), so groups decompose
2176/// into 4 such blocks.
2177#[inline(always)]
2178fn unpack8<const B: usize>(data: &[u8]) -> [i32; 8] {
2179    let mut acc = 0u64;
2180    for i in 0..B {
2181        acc = (acc << 8) | data[i] as u64;
2182    }
2183    let mask = (1u64 << B) - 1;
2184    let mut out = [0i32; 8];
2185    for (k, o) in out.iter_mut().enumerate() {
2186        *o = ((acc >> ((7 - k) * B)) & mask) as i32;
2187    }
2188    out
2189}
2190
2191/// Fused vbit matvec straight from the mapped bytes (spec §3, P13
2192/// FIG.3): [u8 bits: rows][f16 scales: rows·cols/32][bit-packed rows,
2193/// MSB-first, byte-padded]. Row data offsets are precomputed at load
2194/// (`vbit_row_offsets`) — the per-call prefix scan was O(rows) pure
2195/// overhead on every matvec.
2196#[allow(clippy::too_many_arguments)]
2197fn vbitmatvec(
2198    bytes: &[u8],
2199    offsets: &[usize],
2200    x: &[f32],
2201    rows: usize,
2202    cols: usize,
2203    out: &mut [f32],
2204    pool: Option<&Pool>,
2205) {
2206    debug_assert_eq!(out.len(), rows);
2207    debug_assert_eq!(offsets.len(), rows + 1);
2208
2209    // SDOT path: unpack the row to centered i8 once, then per-group
2210    // int8 dot against the quantized activations — same A8W8 contract
2211    // as q8 (bounded noise; CMF_SDOT=0 keeps the exact scalar path).
2212    if a8w8_enabled() {
2213        let act = split_act(x);
2214        let out_addr = SendMut(out.as_mut_ptr());
2215        let run = move |start: usize, end: usize| {
2216            vbit_range_a8w8(bytes, offsets, x, &act, rows, cols, out_addr, start, end)
2217        };
2218        dispatch_rows(pool, rows, &run);
2219        return;
2220    }
2221
2222    let out_addr = SendMut(out.as_mut_ptr());
2223    let run = move |start: usize, end: usize| {
2224        vbit_range_f32(bytes, offsets, x, rows, cols, out_addr, start, end)
2225    };
2226    dispatch_rows(pool, rows, &run);
2227}
2228
2229/// One vbit row range via the A8W8 int8 path — kernel body of
2230/// `vbitmatvec`, extracted so multi-matrix jobs can drive it for
2231/// several tensors in one dispatch (b=8 rows go exact f32).
2232#[allow(clippy::too_many_arguments)]
2233fn vbit_range_a8w8(
2234    bytes: &[u8],
2235    offsets: &[usize],
2236    x: &[f32],
2237    act: &SplitAct,
2238    rows: usize,
2239    cols: usize,
2240    out: SendMut,
2241    start: usize,
2242    end: usize,
2243) {
2244    let ng = cols / GROUP_SIZE;
2245    let bits = &bytes[..rows];
2246    let sc_off = rows;
2247    let row_dot = |r: usize| -> f32 {
2248            let b = bits[r] as usize;
2249            let l = (1i32 << (b - 1)) - 1;
2250            let mask = (1u64 << b) - 1;
2251            let data = &bytes[offsets[r]..offsets[r + 1]];
2252            if b == 8 {
2253                // u−L reaches 128 → does not fit i8; exact f32 path.
2254                let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
2255                let mut dot = 0f32;
2256                for g in 0..ng {
2257                    let so = (r * ng + g) * 2;
2258                    let sgf = f16_to_f32(u16::from_le_bytes([
2259                        bytes[sc_off + so],
2260                        bytes[sc_off + so + 1],
2261                    ]));
2262                    let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
2263                    let mut gd = 0f32;
2264                    for &xv in xg.iter() {
2265                        if nbits < 8 {
2266                            acc = (acc << 8) | data[idx] as u64;
2267                            idx += 1;
2268                            nbits += 8;
2269                        }
2270                        let u = ((acc >> (nbits - 8)) & 0xFF) as i32;
2271                        nbits -= 8;
2272                        gd += (u - l) as f32 * xv;
2273                    }
2274                    dot += gd * sgf;
2275                }
2276                return dot;
2277            }
2278            // Per-worker scratch: this closure runs for every row of the
2279            // tensor (lm_head ≈ 150k rows/token) — a heap allocation per
2280            // row was measurable pure overhead.
2281            thread_local! {
2282                static VBIT_SCRATCH: std::cell::RefCell<Vec<u8>> =
2283                    const { std::cell::RefCell::new(Vec::new()) };
2284            }
2285            #[inline(always)]
2286            fn fill<const B: usize>(data: &[u8], l: i32, buf: &mut [u8]) {
2287                for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
2288                    let u = unpack8::<B>(&data[blk * B..]);
2289                    for k in 0..8 {
2290                        chunk[k] = (u[k] - l) as i8 as u8;
2291                    }
2292                }
2293            }
2294            let _ = mask;
2295            VBIT_SCRATCH.with(|scratch| {
2296                let mut buf = scratch.borrow_mut();
2297                buf.resize(cols, 0);
2298                match b {
2299                    3 => fill::<3>(data, l, &mut buf),
2300                    4 => vbit_fill4(data, &mut buf),
2301                    5 => fill::<5>(data, l, &mut buf),
2302                    6 => fill::<6>(data, l, &mut buf),
2303                    _ => unreachable!(),
2304                }
2305                let mut dot = 0f32;
2306                for g in 0..ng {
2307                    let so = (r * ng + g) * 2;
2308                    let s = f16_to_f32(u16::from_le_bytes([
2309                        bytes[sc_off + so],
2310                        bytes[sc_off + so + 1],
2311                    ]));
2312                    let d = dot_i8_i8(
2313                        &buf[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
2314                        &act.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
2315                    ) as f32
2316                        * act.sx;
2317                    dot += d * s;
2318                }
2319                for &(j, xv) in &act.outliers {
2320                    let so = (r * ng + j / GROUP_SIZE) * 2;
2321                    let s = f16_to_f32(u16::from_le_bytes([
2322                        bytes[sc_off + so],
2323                        bytes[sc_off + so + 1],
2324                    ]));
2325                    // xq is zeroed at outlier slots — add the exact term.
2326                    dot += (buf[j] as i8) as f32 * s * xv;
2327                }
2328                dot
2329            })
2330    };
2331    for r in start..end {
2332        // SAFETY: disjoint row ranges per worker.
2333        unsafe { *out.at(r) = row_dot(r) };
2334    }
2335}
2336
2337/// Exact scalar vbit row range (same extraction, non-SDOT path).
2338#[allow(clippy::too_many_arguments)]
2339fn vbit_range_f32(
2340    bytes: &[u8],
2341    offsets: &[usize],
2342    x: &[f32],
2343    rows: usize,
2344    cols: usize,
2345    out: SendMut,
2346    start: usize,
2347    end: usize,
2348) {
2349    let ng = cols / GROUP_SIZE;
2350    let bits = &bytes[..rows];
2351    let sc_off = rows;
2352    // Per-bit-width specialized inner loops: the compiler unrolls the
2353    // constant shifts (the generic bit-buffer loop was branch-bound —
2354    // 5.6 vs 13.2 tok/s q4 on the 0.8B).
2355    #[inline(always)]
2356    fn dot_row<const B: usize>(
2357        data: &[u8],
2358        bytes: &[u8],
2359        sc_off: usize,
2360        r: usize,
2361        ng: usize,
2362        x: &[f32],
2363    ) -> f32 {
2364        let l = ((1i32 << (B - 1)) - 1) as f32;
2365        let gbytes = GROUP_SIZE * B / 8;
2366        let mut dot = 0f32;
2367        for g in 0..ng {
2368            let so = (r * ng + g) * 2;
2369            let s = f16_to_f32(u16::from_le_bytes([bytes[sc_off + so], bytes[sc_off + so + 1]]));
2370            let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
2371            let gd0 = &data[g * gbytes..(g + 1) * gbytes];
2372            let mut gd = 0f32;
2373            for blk in 0..GROUP_SIZE / 8 {
2374                let u = unpack8::<B>(&gd0[blk * B..]);
2375                let xb = &xg[blk * 8..blk * 8 + 8];
2376                for k in 0..8 {
2377                    gd += (u[k] as f32 - l) * xb[k];
2378                }
2379            }
2380            dot += gd * s;
2381        }
2382        dot
2383    }
2384    for r in start..end {
2385        let data = &bytes[offsets[r]..offsets[r + 1]];
2386        let v = match bits[r] {
2387            3 => dot_row::<3>(data, bytes, sc_off, r, ng, x),
2388            4 => dot_row::<4>(data, bytes, sc_off, r, ng, x),
2389            5 => dot_row::<5>(data, bytes, sc_off, r, ng, x),
2390            6 => dot_row::<6>(data, bytes, sc_off, r, ng, x),
2391            8 => dot_row::<8>(data, bytes, sc_off, r, ng, x),
2392            b => unreachable!("vbit bit-width {b} (validated at load)"),
2393        };
2394        // SAFETY: disjoint row ranges per worker.
2395        unsafe { *out.at(r) = v };
2396    }
2397}
2398
2399/// Fused two-input vbit matvec: each row is unpacked from the mmap ONCE
2400/// and dotted against BOTH activations (MTP verify / pair prefill used
2401/// to run two full matvecs — double weight traffic and double unpack).
2402/// Per-input math is identical to `vbitmatvec` → same accuracy contract.
2403#[allow(clippy::too_many_arguments)]
2404fn vbitmatvec2(
2405    bytes: &[u8],
2406    offsets: &[usize],
2407    x1: &[f32],
2408    x2: &[f32],
2409    rows: usize,
2410    cols: usize,
2411    o1: &mut [f32],
2412    o2: &mut [f32],
2413    pool: Option<&Pool>,
2414) {
2415    debug_assert_eq!(o1.len(), rows);
2416    debug_assert_eq!(o2.len(), rows);
2417
2418    if a8w8_enabled() {
2419        let a1 = split_act(x1);
2420        let a2 = split_act(x2);
2421        let p1 = SendMut(o1.as_mut_ptr());
2422        let p2 = SendMut(o2.as_mut_ptr());
2423        let run = move |start: usize, end: usize| {
2424            vbit_range2_a8w8(bytes, offsets, x1, x2, &a1, &a2, rows, cols, p1, p2, start, end)
2425        };
2426        dispatch_rows(pool, rows, &run);
2427        return;
2428    }
2429
2430    let p1 = SendMut(o1.as_mut_ptr());
2431    let p2 = SendMut(o2.as_mut_ptr());
2432    let run = move |start: usize, end: usize| {
2433        vbit_range2_f32(bytes, offsets, x1, x2, rows, cols, p1, p2, start, end)
2434    };
2435    dispatch_rows(pool, rows, &run);
2436}
2437
2438/// Two-input vbit row range via the A8W8 int8 path — kernel body of
2439/// `vbitmatvec2`, extracted for pair multi-matrix jobs (b=8 rows go
2440/// exact f32 for both lanes, bits streamed once).
2441#[allow(clippy::too_many_arguments)]
2442fn vbit_range2_a8w8(
2443    bytes: &[u8],
2444    offsets: &[usize],
2445    x1: &[f32],
2446    x2: &[f32],
2447    a1: &SplitAct,
2448    a2: &SplitAct,
2449    rows: usize,
2450    cols: usize,
2451    p1: SendMut,
2452    p2: SendMut,
2453    start: usize,
2454    end: usize,
2455) {
2456    let ng = cols / GROUP_SIZE;
2457    let bits = &bytes[..rows];
2458    let sc_off = rows;
2459    let row_dots = |r: usize| -> (f32, f32) {
2460            let b = bits[r] as usize;
2461            let l = (1i32 << (b - 1)) - 1;
2462            let data = &bytes[offsets[r]..offsets[r + 1]];
2463            if b == 8 {
2464                // u−L reaches 128 → does not fit i8; exact f32 path,
2465                // bits still streamed once for both lanes.
2466                let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
2467                let (mut d1, mut d2) = (0f32, 0f32);
2468                for g in 0..ng {
2469                    let so = (r * ng + g) * 2;
2470                    let sgf = f16_to_f32(u16::from_le_bytes([
2471                        bytes[sc_off + so],
2472                        bytes[sc_off + so + 1],
2473                    ]));
2474                    let (mut g1, mut g2) = (0f32, 0f32);
2475                    for k in 0..GROUP_SIZE {
2476                        if nbits < 8 {
2477                            acc = (acc << 8) | data[idx] as u64;
2478                            idx += 1;
2479                            nbits += 8;
2480                        }
2481                        let u = ((acc >> (nbits - 8)) & 0xFF) as i32;
2482                        nbits -= 8;
2483                        let w = (u - l) as f32;
2484                        g1 += w * x1[g * GROUP_SIZE + k];
2485                        g2 += w * x2[g * GROUP_SIZE + k];
2486                    }
2487                    d1 += g1 * sgf;
2488                    d2 += g2 * sgf;
2489                }
2490                return (d1, d2);
2491            }
2492            thread_local! {
2493                static VBIT_SCRATCH2: std::cell::RefCell<Vec<u8>> =
2494                    const { std::cell::RefCell::new(Vec::new()) };
2495            }
2496            #[inline(always)]
2497            fn fill<const B: usize>(data: &[u8], l: i32, buf: &mut [u8]) {
2498                for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
2499                    let u = unpack8::<B>(&data[blk * B..]);
2500                    for k in 0..8 {
2501                        chunk[k] = (u[k] - l) as i8 as u8;
2502                    }
2503                }
2504            }
2505            VBIT_SCRATCH2.with(|scratch| {
2506                let mut buf = scratch.borrow_mut();
2507                buf.resize(cols, 0);
2508                match b {
2509                    3 => fill::<3>(data, l, &mut buf),
2510                    4 => vbit_fill4(data, &mut buf),
2511                    5 => fill::<5>(data, l, &mut buf),
2512                    6 => fill::<6>(data, l, &mut buf),
2513                    _ => unreachable!(),
2514                }
2515                let (mut d1, mut d2) = (0f32, 0f32);
2516                for g in 0..ng {
2517                    let so = (r * ng + g) * 2;
2518                    let s = f16_to_f32(u16::from_le_bytes([
2519                        bytes[sc_off + so],
2520                        bytes[sc_off + so + 1],
2521                    ]));
2522                    let wg = &buf[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
2523                    let v1 =
2524                        dot_i8_i8(wg, &a1.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE]) as f32 * a1.sx;
2525                    let v2 =
2526                        dot_i8_i8(wg, &a2.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE]) as f32 * a2.sx;
2527                    d1 += v1 * s;
2528                    d2 += v2 * s;
2529                }
2530                for &(j, xv) in &a1.outliers {
2531                    let so = (r * ng + j / GROUP_SIZE) * 2;
2532                    let s = f16_to_f32(u16::from_le_bytes([
2533                        bytes[sc_off + so],
2534                        bytes[sc_off + so + 1],
2535                    ]));
2536                    d1 += (buf[j] as i8) as f32 * s * xv;
2537                }
2538                for &(j, xv) in &a2.outliers {
2539                    let so = (r * ng + j / GROUP_SIZE) * 2;
2540                    let s = f16_to_f32(u16::from_le_bytes([
2541                        bytes[sc_off + so],
2542                        bytes[sc_off + so + 1],
2543                    ]));
2544                    d2 += (buf[j] as i8) as f32 * s * xv;
2545                }
2546                (d1, d2)
2547            })
2548    };
2549    for r in start..end {
2550        let (v1, v2) = row_dots(r);
2551        // SAFETY: disjoint row ranges per worker.
2552        unsafe {
2553            *p1.at(r) = v1;
2554            *p2.at(r) = v2;
2555        }
2556    }
2557}
2558
2559/// Two-input exact scalar vbit row range (same extraction) —
2560/// per-bit-width specialized, two accumulators per row; per-lane
2561/// accumulation order matches `vbitmatvec` exactly.
2562#[allow(clippy::too_many_arguments)]
2563fn vbit_range2_f32(
2564    bytes: &[u8],
2565    offsets: &[usize],
2566    x1: &[f32],
2567    x2: &[f32],
2568    rows: usize,
2569    cols: usize,
2570    p1: SendMut,
2571    p2: SendMut,
2572    start: usize,
2573    end: usize,
2574) {
2575    let ng = cols / GROUP_SIZE;
2576    let bits = &bytes[..rows];
2577    let sc_off = rows;
2578    #[inline(always)]
2579    #[allow(clippy::too_many_arguments)]
2580    fn dot_row2<const B: usize>(
2581        data: &[u8],
2582        bytes: &[u8],
2583        sc_off: usize,
2584        r: usize,
2585        ng: usize,
2586        x1: &[f32],
2587        x2: &[f32],
2588    ) -> (f32, f32) {
2589        let l = ((1i32 << (B - 1)) - 1) as f32;
2590        let gbytes = GROUP_SIZE * B / 8;
2591        let (mut d1, mut d2) = (0f32, 0f32);
2592        for g in 0..ng {
2593            let so = (r * ng + g) * 2;
2594            let s = f16_to_f32(u16::from_le_bytes([bytes[sc_off + so], bytes[sc_off + so + 1]]));
2595            let x1g = &x1[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
2596            let x2g = &x2[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
2597            let gd0 = &data[g * gbytes..(g + 1) * gbytes];
2598            let (mut g1, mut g2) = (0f32, 0f32);
2599            for blk in 0..GROUP_SIZE / 8 {
2600                let u = unpack8::<B>(&gd0[blk * B..]);
2601                for k in 0..8 {
2602                    let w = u[k] as f32 - l;
2603                    g1 += w * x1g[blk * 8 + k];
2604                    g2 += w * x2g[blk * 8 + k];
2605                }
2606            }
2607            d1 += g1 * s;
2608            d2 += g2 * s;
2609        }
2610        (d1, d2)
2611    }
2612    for r in start..end {
2613        let data = &bytes[offsets[r]..offsets[r + 1]];
2614        let (v1, v2) = match bits[r] {
2615            3 => dot_row2::<3>(data, bytes, sc_off, r, ng, x1, x2),
2616            4 => dot_row2::<4>(data, bytes, sc_off, r, ng, x1, x2),
2617            5 => dot_row2::<5>(data, bytes, sc_off, r, ng, x1, x2),
2618            6 => dot_row2::<6>(data, bytes, sc_off, r, ng, x1, x2),
2619            8 => dot_row2::<8>(data, bytes, sc_off, r, ng, x1, x2),
2620            b => unreachable!("vbit bit-width {b} (validated at load)"),
2621        };
2622        // SAFETY: disjoint row ranges per worker.
2623        unsafe {
2624            *p1.at(r) = v1;
2625            *p2.at(r) = v2;
2626        }
2627    }
2628}
2629
2630// ───────────────────── q4_tiled kernels (§4.3) ─────────────────────
2631
2632/// One q4_tiled row dot on the A8W8 int8 path: per 32-group the tile
2633/// is ONE sequential read — [f16 scale][16B nibbles] — versus the two
2634/// distant streams of the split layout. Values/order identical to the
2635/// split kernels.
2636#[inline]
2637#[allow(unreachable_code)]
2638fn dot_q4t_row_i8(bytes: &[u8], r: usize, gpr: usize, xq: &[i8]) -> f32 {
2639    #[cfg(target_arch = "aarch64")]
2640    unsafe {
2641        return dot_q4t_row_sdot(bytes, r, gpr, xq);
2642    }
2643    #[cfg(target_arch = "x86_64")]
2644    unsafe {
2645        return dot_q4t_row_avx2(bytes, r, gpr, xq);
2646    }
2647    let mut acc = 0f32;
2648    for gi in 0..gpr {
2649        let tile = &bytes[(r * gpr + gi) * Q4_TILE..(r * gpr + gi + 1) * Q4_TILE];
2650        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
2651        let mut d = 0i32;
2652        for (k, &b) in tile[2..].iter().enumerate() {
2653            d += ((b & 0x0F) as i32 - 8) * xq[gi * GROUP_SIZE + k * 2] as i32
2654                + (((b >> 4) & 0x0F) as i32 - 8) * xq[gi * GROUP_SIZE + k * 2 + 1] as i32;
2655        }
2656        acc += d as f32 * s;
2657    }
2658    acc
2659}
2660
2661#[cfg(target_arch = "aarch64")]
2662#[target_feature(enable = "neon,dotprod")]
2663unsafe fn dot_q4t_row_sdot(bytes: &[u8], r: usize, gpr: usize, xq: &[i8]) -> f32 {
2664    // SAFETY: callers uphold slice-length contracts (18B tile per group,
2665    // xq.len() == gpr·GROUP_SIZE).
2666    unsafe {
2667        use core::arch::aarch64::*;
2668        use core::arch::asm;
2669        let lomask = vdupq_n_u8(0x0F);
2670        let eight = vdupq_n_s8(8);
2671        let mut acc = 0f32;
2672        for gi in 0..gpr {
2673            let t = bytes.as_ptr().add((r * gpr + gi) * Q4_TILE);
2674            let s = f16_to_f32(u16::from_le_bytes([*t, *t.add(1)]));
2675            let b = vld1q_u8(t.add(2));
2676            let lo = vandq_u8(b, lomask);
2677            let hi = vshrq_n_u8::<4>(b);
2678            let e0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(lo, hi)), eight);
2679            let e1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(lo, hi)), eight);
2680            let x0 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE));
2681            let x1 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE + 16));
2682            let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
2683            asm!(
2684                "sdot {a0:v}.4s, {e0:v}.16b, {x0:v}.16b",
2685                "sdot {a1:v}.4s, {e1:v}.16b, {x1:v}.16b",
2686                a0 = inout(vreg) a0, a1 = inout(vreg) a1,
2687                e0 = in(vreg) e0, x0 = in(vreg) x0, e1 = in(vreg) e1, x1 = in(vreg) x1,
2688                options(pure, nomem, nostack),
2689            );
2690            acc += vaddvq_s32(vaddq_s32(a0, a1)) as f32 * s;
2691        }
2692        acc
2693    }
2694}
2695
2696#[cfg(target_arch = "x86_64")]
2697#[target_feature(enable = "avx2")]
2698unsafe fn dot_q4t_row_avx2(bytes: &[u8], r: usize, gpr: usize, xq: &[i8]) -> f32 {
2699    // SAFETY: see dot_q4t_row_sdot.
2700    unsafe {
2701        use core::arch::x86_64::*;
2702        let lomask = _mm_set1_epi8(0x0F);
2703        let eight = _mm256_set1_epi8(8);
2704        let ones = _mm256_set1_epi16(1);
2705        let mut acc = 0f32;
2706        for gi in 0..gpr {
2707            let t = bytes.as_ptr().add((r * gpr + gi) * Q4_TILE);
2708            let s = f16_to_f32(u16::from_le_bytes([*t, *t.add(1)]));
2709            let b = _mm_loadu_si128(t.add(2) as *const __m128i);
2710            let lo = _mm_and_si128(b, lomask);
2711            let hi = _mm_and_si128(_mm_srli_epi16::<4>(b), lomask);
2712            let w = _mm256_sub_epi8(
2713                _mm256_set_m128i(_mm_unpackhi_epi8(lo, hi), _mm_unpacklo_epi8(lo, hi)),
2714                eight,
2715            );
2716            let x = _mm256_loadu_si256(xq.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
2717            let p16 = _mm256_maddubs_epi16(_mm256_abs_epi8(w), _mm256_sign_epi8(x, w));
2718            let d = _mm256_madd_epi16(p16, ones);
2719            let hi128 = _mm256_extracti128_si256::<1>(d);
2720            let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
2721            let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
2722            let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
2723            acc += _mm_cvtsi128_si32(s32) as f32 * s;
2724        }
2725        acc
2726    }
2727}
2728
2729/// One q4_tiled row against FOUR activation streams: the nibble unpack
2730/// and abs() happen once per group instead of once per (group,
2731/// activation) — the unpack is the dominant per-element cost of the
2732/// tiled format (roadmap P0 portable blocking, q4t leg).
2733#[cfg(target_arch = "x86_64")]
2734#[target_feature(enable = "avx2")]
2735unsafe fn dot_q4t_row_1x4_avx2(
2736    bytes: &[u8],
2737    r: usize,
2738    gpr: usize,
2739    xs: [&[i8]; 4],
2740) -> [f32; 4] {
2741    // SAFETY: callers uphold the 18B-tile and xq-length contracts.
2742    unsafe {
2743        use core::arch::x86_64::*;
2744        let lomask = _mm_set1_epi8(0x0F);
2745        let eight = _mm256_set1_epi8(8);
2746        let ones = _mm256_set1_epi16(1);
2747        let mut acc = [0f32; 4];
2748        for gi in 0..gpr {
2749            let t = bytes.as_ptr().add((r * gpr + gi) * Q4_TILE);
2750            let s = f16_to_f32(u16::from_le_bytes([*t, *t.add(1)]));
2751            let bb = _mm_loadu_si128(t.add(2) as *const __m128i);
2752            let lo = _mm_and_si128(bb, lomask);
2753            let hi = _mm_and_si128(_mm_srli_epi16::<4>(bb), lomask);
2754            let w = _mm256_sub_epi8(
2755                _mm256_set_m128i(_mm_unpackhi_epi8(lo, hi), _mm_unpacklo_epi8(lo, hi)),
2756                eight,
2757            );
2758            let aw = _mm256_abs_epi8(w);
2759            for (k, xq) in xs.iter().enumerate() {
2760                let x =
2761                    _mm256_loadu_si256(xq.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
2762                let p16 = _mm256_maddubs_epi16(aw, _mm256_sign_epi8(x, w));
2763                let d = _mm256_madd_epi16(p16, ones);
2764                let hi128 = _mm256_extracti128_si256::<1>(d);
2765                let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
2766                let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
2767                let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
2768                acc[k] += _mm_cvtsi128_si32(s32) as f32 * s;
2769            }
2770        }
2771        acc
2772    }
2773}
2774
2775/// Exact-term correction for A8W8 outliers on a tiled row.
2776#[inline]
2777fn q4t_outlier(bytes: &[u8], r: usize, gpr: usize, j: usize) -> (f32, f32) {
2778    let gi = j / GROUP_SIZE;
2779    let k = j % GROUP_SIZE;
2780    let tile = &bytes[(r * gpr + gi) * Q4_TILE..(r * gpr + gi + 1) * Q4_TILE];
2781    let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
2782    let byte = tile[2 + k / 2];
2783    let nib = if k & 1 == 0 { byte & 0x0F } else { byte >> 4 };
2784    ((nib as i32 - 8) as f32, s)
2785}
2786
2787/// Exact scalar q4_tiled row (CMF_SDOT=0 contract) — same pairwise
2788/// accumulation shape as `q4_range_f32`.
2789#[inline]
2790fn q4t_row_exact(bytes: &[u8], r: usize, gpr: usize, x: &[f32]) -> f32 {
2791    let mut acc = 0f32;
2792    for gi in 0..gpr {
2793        let tile = &bytes[(r * gpr + gi) * Q4_TILE..(r * gpr + gi + 1) * Q4_TILE];
2794        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
2795        let xg = &x[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
2796        let mut ga = 0f32;
2797        for (k, &b) in tile[2..].iter().enumerate() {
2798            ga += ((b & 0x0F) as f32 - 8.0) * xg[k * 2]
2799                + (((b >> 4) & 0x0F) as f32 - 8.0) * xg[k * 2 + 1];
2800        }
2801        acc += ga * s;
2802    }
2803    acc
2804}
2805
2806/// Fused q4_tiled matvec (dispatch mirrors `q4matvec`).
2807fn q4t_matvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
2808    debug_assert_eq!(out.len(), rows);
2809    let gpr = cols / GROUP_SIZE;
2810    let out_addr = SendMut(out.as_mut_ptr());
2811    if a8w8_enabled() {
2812        let act = split_act(x);
2813        let run = move |start: usize, end: usize| {
2814            for r in start..end {
2815                let mut acc = dot_q4t_row_i8(bytes, r, gpr, &act.xq) * act.sx;
2816                for &(j, xv) in &act.outliers {
2817                    let (w, s) = q4t_outlier(bytes, r, gpr, j);
2818                    acc += w * s * xv;
2819                }
2820                // SAFETY: disjoint row ranges per worker.
2821                unsafe { *out_addr.at(r) = acc };
2822            }
2823        };
2824        dispatch_rows(pool, rows, &run);
2825        return;
2826    }
2827    let run = move |start: usize, end: usize| {
2828        for r in start..end {
2829            // SAFETY: disjoint row ranges per worker.
2830            unsafe { *out_addr.at(r) = q4t_row_exact(bytes, r, gpr, x) };
2831        }
2832    };
2833    dispatch_rows(pool, rows, &run);
2834}
2835
2836/// Fused two-input q4_tiled matvec (weights read once per pair).
2837#[allow(clippy::too_many_arguments)]
2838fn q4t_matvec2(
2839    bytes: &[u8],
2840    x1: &[f32],
2841    x2: &[f32],
2842    rows: usize,
2843    cols: usize,
2844    o1: &mut [f32],
2845    o2: &mut [f32],
2846    pool: Option<&Pool>,
2847) {
2848    let gpr = cols / GROUP_SIZE;
2849    let p1 = SendMut(o1.as_mut_ptr());
2850    let p2 = SendMut(o2.as_mut_ptr());
2851    if a8w8_enabled() {
2852        let a1 = split_act(x1);
2853        let a2 = split_act(x2);
2854        let run = move |start: usize, end: usize| {
2855            for r in start..end {
2856                let mut v1 = dot_q4t_row_i8(bytes, r, gpr, &a1.xq) * a1.sx;
2857                let mut v2 = dot_q4t_row_i8(bytes, r, gpr, &a2.xq) * a2.sx;
2858                for &(j, xv) in &a1.outliers {
2859                    let (w, s) = q4t_outlier(bytes, r, gpr, j);
2860                    v1 += w * s * xv;
2861                }
2862                for &(j, xv) in &a2.outliers {
2863                    let (w, s) = q4t_outlier(bytes, r, gpr, j);
2864                    v2 += w * s * xv;
2865                }
2866                // SAFETY: disjoint row ranges per worker.
2867                unsafe {
2868                    *p1.at(r) = v1;
2869                    *p2.at(r) = v2;
2870                }
2871            }
2872        };
2873        dispatch_rows(pool, rows, &run);
2874        return;
2875    }
2876    let run = move |start: usize, end: usize| {
2877        for r in start..end {
2878            // SAFETY: disjoint row ranges per worker.
2879            unsafe {
2880                *p1.at(r) = q4t_row_exact(bytes, r, gpr, x1);
2881                *p2.at(r) = q4t_row_exact(bytes, r, gpr, x2);
2882            }
2883        }
2884    };
2885    dispatch_rows(pool, rows, &run);
2886}
2887
2888/// Batched q4_tiled matmat: each row's tiles stream once per microbatch.
2889#[allow(clippy::too_many_arguments)]
2890fn q4t_matmat(
2891    bytes: &[u8],
2892    xs_all: &[f32],
2893    b: usize,
2894    rows: usize,
2895    cols: usize,
2896    out: &mut [f32],
2897    pool: Option<&Pool>,
2898) {
2899    debug_assert_eq!(out.len(), b * rows);
2900    let gpr = cols / GROUP_SIZE;
2901    let out_addr = SendMut(out.as_mut_ptr());
2902    if a8w8_enabled() {
2903        let acts: Vec<SplitAct> =
2904            (0..b).map(|bi| split_act(&xs_all[bi * cols..(bi + 1) * cols])).collect();
2905        let acts = &acts;
2906        #[cfg(target_arch = "x86_64")]
2907        let blocked_ok = avx2_enabled()
2908            && std::env::var("CMF_X86_BLOCKED").map(|v| v != "0").unwrap_or(true);
2909        #[cfg(not(target_arch = "x86_64"))]
2910        let blocked_ok = false;
2911        let run = move |start: usize, end: usize| {
2912            for r in start..end {
2913                let mut bi = 0usize;
2914                #[cfg(target_arch = "x86_64")]
2915                if blocked_ok {
2916                    while bi + 4 <= acts.len() {
2917                        let xs = [
2918                            acts[bi].xq.as_slice(),
2919                            acts[bi + 1].xq.as_slice(),
2920                            acts[bi + 2].xq.as_slice(),
2921                            acts[bi + 3].xq.as_slice(),
2922                        ];
2923                        let d = unsafe { dot_q4t_row_1x4_avx2(bytes, r, gpr, xs) };
2924                        for k in 0..4 {
2925                            let act = &acts[bi + k];
2926                            let mut acc = d[k] * act.sx;
2927                            for &(j, xv) in &act.outliers {
2928                                let (w, sc) = q4t_outlier(bytes, r, gpr, j);
2929                                acc += w * sc * xv;
2930                            }
2931                            // SAFETY: disjoint (bi, r) cells per worker.
2932                            unsafe { *out_addr.at((bi + k) * rows + r) = acc };
2933                        }
2934                        bi += 4;
2935                    }
2936                }
2937                let _ = blocked_ok;
2938                while bi < acts.len() {
2939                    let act = &acts[bi];
2940                    let mut acc = dot_q4t_row_i8(bytes, r, gpr, &act.xq) * act.sx;
2941                    for &(j, xv) in &act.outliers {
2942                        let (w, s) = q4t_outlier(bytes, r, gpr, j);
2943                        acc += w * s * xv;
2944                    }
2945                    // SAFETY: disjoint (bi, r) cells per worker range.
2946                    unsafe { *out_addr.at(bi * rows + r) = acc };
2947                    bi += 1;
2948                }
2949            }
2950        };
2951        dispatch_rows(pool, rows, &run);
2952        return;
2953    }
2954    let run = move |start: usize, end: usize| {
2955        for r in start..end {
2956            for bi in 0..b {
2957                let x = &xs_all[bi * cols..(bi + 1) * cols];
2958                // SAFETY: disjoint (bi, r) cells per worker range.
2959                unsafe { *out_addr.at(bi * rows + r) = q4t_row_exact(bytes, r, gpr, x) };
2960            }
2961        }
2962    };
2963    dispatch_rows(pool, rows, &run);
2964}
2965
2966// ── q1 (dtype 12): binary weights, [f16 scale][4B sign bits] per
2967// 32-group tile. The kernel family mirrors q4_tiled: one sequential
2968// stream of 6-byte tiles, per-tile integer dot × scale, exact outlier
2969// correction (A8W8 contract), exact scalar path under CMF_SDOT=0. ──
2970
2971/// Per-32-group sums of the quantized activation — the ±1 identity's
2972/// shared half: `dot = −2·sdot(mask, x) − gsum[g]`, computed ONCE per
2973/// matvec and reused by every row.
2974fn q1_group_sums(xq: &[i8], gpr: usize) -> Vec<i32> {
2975    (0..gpr)
2976        .map(|gi| {
2977            xq[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE]
2978                .iter()
2979                .map(|&v| v as i32)
2980                .sum()
2981        })
2982        .collect()
2983}
2984
2985/// One q1 row via the A8W8 int8 path — mask-SDOT on ARM (no ±1
2986/// expansion at all), scalar bit loop elsewhere (AVX2 queued with the
2987/// x86 pass).
2988#[inline]
2989#[allow(unreachable_code)]
2990/// AVX2 q1 row via the same ±1 identity as the ARM sdot kernel: the
2991/// sign bits expand to a {0, −1} byte mask through shuffle+cmpeq, the
2992/// masked activation sums through maddubs(1, x&mask), and
2993/// `dot = −(2·masked_sum + Σx_group)` — bit-identical integer math.
2994#[cfg(target_arch = "x86_64")]
2995#[target_feature(enable = "avx2")]
2996unsafe fn dot_q1_row_avx2(
2997    bytes: &[u8],
2998    r: usize,
2999    gpr: usize,
3000    xq: &[i8],
3001    gsum: &[i32],
3002) -> f32 {
3003    // SAFETY: callers uphold the 6B-tile and xq/gsum length contracts.
3004    unsafe {
3005        use core::arch::x86_64::*;
3006        // Byte j of the mask must replicate bits-byte j/8.
3007        let expand = _mm256_setr_epi8(
3008            0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
3009            2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
3010        );
3011        let bitsel = _mm256_setr_epi8(
3012            1, 2, 4, 8, 16, 32, 64, -128, 1, 2, 4, 8, 16, 32, 64, -128,
3013            1, 2, 4, 8, 16, 32, 64, -128, 1, 2, 4, 8, 16, 32, 64, -128,
3014        );
3015        let ones8 = _mm256_set1_epi8(1);
3016        let ones16 = _mm256_set1_epi16(1);
3017        let mut acc = 0f32;
3018        for gi in 0..gpr {
3019            let t = bytes.as_ptr().add((r * gpr + gi) * Q1_TILE);
3020            let s = f16_to_f32(u16::from_le_bytes([*t, *t.add(1)]));
3021            let bits = u32::from_le_bytes([*t.add(2), *t.add(3), *t.add(4), *t.add(5)]);
3022            let bc = _mm256_shuffle_epi8(_mm256_set1_epi32(bits as i32), expand);
3023            let mask = _mm256_cmpeq_epi8(_mm256_and_si256(bc, bitsel), bitsel);
3024            let x = _mm256_loadu_si256(xq.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
3025            let sel = _mm256_and_si256(x, mask);
3026            // Σ of selected i8 lanes: maddubs(1u8, sel_i8) pairs → madd.
3027            let p16 = _mm256_maddubs_epi16(ones8, sel);
3028            let d32 = _mm256_madd_epi16(p16, ones16);
3029            let hi128 = _mm256_extracti128_si256::<1>(d32);
3030            let s128 = _mm_add_epi32(_mm256_castsi256_si128(d32), hi128);
3031            let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
3032            let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
3033            let msum = _mm_cvtsi128_si32(s32);
3034            // The and-select keeps x UN-negated (unlike ARM's −1-mask
3035            // sdot): d = Σ_set − Σ_unset = 2·Σ_set − Σ_all.
3036            let d = 2 * msum - gsum[gi];
3037            acc += d as f32 * s;
3038        }
3039        acc
3040    }
3041}
3042
3043/// The blocked 1×4 flavor: the expanded bit mask serves four activation
3044/// streams per group (mask build once, four select+reduce chains).
3045#[cfg(target_arch = "x86_64")]
3046#[target_feature(enable = "avx2")]
3047unsafe fn dot_q1_row_1x4_avx2(
3048    bytes: &[u8],
3049    r: usize,
3050    gpr: usize,
3051    xs: [&[i8]; 4],
3052    gsums: [&[i32]; 4],
3053) -> [f32; 4] {
3054    // SAFETY: callers uphold the 6B-tile and xq/gsum length contracts.
3055    unsafe {
3056        use core::arch::x86_64::*;
3057        let expand = _mm256_setr_epi8(
3058            0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
3059            2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
3060        );
3061        let bitsel = _mm256_setr_epi8(
3062            1, 2, 4, 8, 16, 32, 64, -128, 1, 2, 4, 8, 16, 32, 64, -128,
3063            1, 2, 4, 8, 16, 32, 64, -128, 1, 2, 4, 8, 16, 32, 64, -128,
3064        );
3065        let ones8 = _mm256_set1_epi8(1);
3066        let ones16 = _mm256_set1_epi16(1);
3067        let mut acc = [0f32; 4];
3068        for gi in 0..gpr {
3069            let t = bytes.as_ptr().add((r * gpr + gi) * Q1_TILE);
3070            let s = f16_to_f32(u16::from_le_bytes([*t, *t.add(1)]));
3071            let bits = u32::from_le_bytes([*t.add(2), *t.add(3), *t.add(4), *t.add(5)]);
3072            let bc = _mm256_shuffle_epi8(_mm256_set1_epi32(bits as i32), expand);
3073            let mask = _mm256_cmpeq_epi8(_mm256_and_si256(bc, bitsel), bitsel);
3074            for (k, xq) in xs.iter().enumerate() {
3075                let x =
3076                    _mm256_loadu_si256(xq.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
3077                let sel = _mm256_and_si256(x, mask);
3078                let p16 = _mm256_maddubs_epi16(ones8, sel);
3079                let d32 = _mm256_madd_epi16(p16, ones16);
3080                let hi128 = _mm256_extracti128_si256::<1>(d32);
3081                let s128 = _mm_add_epi32(_mm256_castsi256_si128(d32), hi128);
3082                let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
3083                let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
3084                let msum = _mm_cvtsi128_si32(s32);
3085                let d = 2 * msum - gsums[k][gi];
3086                acc[k] += d as f32 * s;
3087            }
3088        }
3089        acc
3090    }
3091}
3092
3093#[allow(unreachable_code)]
3094fn dot_q1_row_i8(bytes: &[u8], r: usize, gpr: usize, xq: &[i8], gsum: &[i32]) -> f32 {
3095    #[cfg(target_arch = "aarch64")]
3096    unsafe {
3097        return dot_q1_row_sdot(bytes, r, gpr, xq, gsum);
3098    }
3099    #[cfg(target_arch = "x86_64")]
3100    if avx2_enabled() {
3101        unsafe {
3102            return dot_q1_row_avx2(bytes, r, gpr, xq, gsum);
3103        }
3104    }
3105    let _ = gsum;
3106    let mut acc = 0f32;
3107    for gi in 0..gpr {
3108        let tile = &bytes[(r * gpr + gi) * Q1_TILE..(r * gpr + gi + 1) * Q1_TILE];
3109        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
3110        let mut d = 0i32;
3111        for (j, &b) in tile[2..].iter().enumerate() {
3112            for k in 0..8 {
3113                let w = ((b >> k) & 1) as i32 * 2 - 1;
3114                d += w * xq[gi * GROUP_SIZE + j * 8 + k] as i32;
3115            }
3116        }
3117        acc += d as f32 * s;
3118    }
3119    acc
3120}
3121
3122/// SDOT q1 row via the ±1 identity: the vtst mask (0xFF where the bit
3123/// is set, i.e. −1 as i8) feeds `sdot` DIRECTLY — no expansion to ±1
3124/// lanes at all — and `dot = −(2·sdot(mask, x) + Σx_group)`, with the
3125/// per-group activation sums shared across every row of the matvec.
3126/// Four tiles (128 weights) per iteration: integer dots reduce through
3127/// a vpaddq tree into ONE i32x4 that meets its four scales in a single
3128/// fused f32 multiply-add. Integer math throughout — bit-identical to
3129/// the scalar ±1 reference.
3130#[cfg(target_arch = "aarch64")]
3131#[target_feature(enable = "neon,dotprod")]
3132unsafe fn dot_q1_row_sdot(bytes: &[u8], r: usize, gpr: usize, xq: &[i8], gsum: &[i32]) -> f32 {
3133    // SAFETY: callers uphold slice-length contracts (6B tile per group,
3134    // xq.len() == gpr·GROUP_SIZE, gsum.len() == gpr).
3135    unsafe {
3136        use core::arch::aarch64::*;
3137        use core::arch::asm;
3138        const MASKS: [u8; 16] = [1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128];
3139        let m = vld1q_u8(MASKS.as_ptr());
3140        // One tile's −Σ_set(x) as an UNREDUCED i32x4 (two mask-sdots).
3141        macro_rules! tile_dot {
3142            ($t:expr, $x:expr) => {{
3143                let v0 = vcombine_u8(vdup_n_u8(*$t.add(2)), vdup_n_u8(*$t.add(3)));
3144                let v1 = vcombine_u8(vdup_n_u8(*$t.add(4)), vdup_n_u8(*$t.add(5)));
3145                let w0 = vreinterpretq_s8_u8(vtstq_u8(v0, m));
3146                let w1 = vreinterpretq_s8_u8(vtstq_u8(v1, m));
3147                let x0 = vld1q_s8($x);
3148                let x1 = vld1q_s8($x.add(16));
3149                let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
3150                asm!(
3151                    "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
3152                    "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
3153                    a0 = inout(vreg) a0, a1 = inout(vreg) a1,
3154                    w0 = in(vreg) w0, x0 = in(vreg) x0, w1 = in(vreg) w1, x1 = in(vreg) x1,
3155                    options(pure, nomem, nostack),
3156                );
3157                vaddq_s32(a0, a1)
3158            }};
3159        }
3160        // TBL unpack over PAIR loads: one vld1q covers two 6B tiles
3161        // ([s s b b b b][s s b b b b] + 4B slack), TBL replicates each
3162        // bit-byte across 8 lanes for vtst, and the four scales gather
3163        // through tbl2 into one fcvtl — the 16 ld1r broadcast loads and
3164        // 4 branchy software f16 conversions per 128 weights (the
3165        // measured load-port wall of this kernel) become 2 vector
3166        // loads + 9 table lookups. Integer math order is unchanged —
3167        // bit-identical results (FCVTL is exact on every f16).
3168        const IW00: [u8; 16] = [2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3];
3169        const IW01: [u8; 16] = [4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5];
3170        const IW10: [u8; 16] = [8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9];
3171        const IW11: [u8; 16] = [10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11];
3172        const ISC: [u8; 8] = [0, 1, 6, 7, 16, 17, 22, 23];
3173        let (iw00, iw01) = (vld1q_u8(IW00.as_ptr()), vld1q_u8(IW01.as_ptr()));
3174        let (iw10, iw11) = (vld1q_u8(IW10.as_ptr()), vld1q_u8(IW11.as_ptr()));
3175        let isc = vld1_u8(ISC.as_ptr());
3176        // One tile's −Σ_set(x) from a TBL-unpacked pair load.
3177        macro_rules! tile_dot_tbl {
3178            ($ld:expr, $i0:expr, $i1:expr, $x:expr) => {{
3179                let w0 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8($ld, $i0), m));
3180                let w1 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8($ld, $i1), m));
3181                let x0 = vld1q_s8($x);
3182                let x1 = vld1q_s8($x.add(16));
3183                let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
3184                asm!(
3185                    "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
3186                    "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
3187                    a0 = inout(vreg) a0, a1 = inout(vreg) a1,
3188                    w0 = in(vreg) w0, x0 = in(vreg) x0, w1 = in(vreg) w1, x1 = in(vreg) x1,
3189                    options(pure, nomem, nostack),
3190                );
3191                vaddq_s32(a0, a1)
3192            }};
3193        }
3194        let base = bytes.as_ptr().add(r * gpr * Q1_TILE);
3195        let row_base = r * gpr * Q1_TILE;
3196        let abs_end = bytes.len();
3197        let xp = xq.as_ptr();
3198        let gp = gsum.as_ptr();
3199        let mut accv = vdupq_n_f32(0.0);
3200        let mut gi = 0;
3201        // The second pair load reads 4B past tile gi+3 — stay inside
3202        // the payload slice (only the file's final tiles fall back).
3203        while gi + 4 <= gpr && row_base + (gi + 4) * Q1_TILE + 4 <= abs_end {
3204            let t0 = base.add(gi * Q1_TILE);
3205            let ld_a = vld1q_u8(t0);
3206            let ld_b = vld1q_u8(t0.add(2 * Q1_TILE));
3207            let d0 = tile_dot_tbl!(ld_a, iw00, iw01, xp.add(gi * GROUP_SIZE));
3208            let d1 = tile_dot_tbl!(ld_a, iw10, iw11, xp.add((gi + 1) * GROUP_SIZE));
3209            let d2 = tile_dot_tbl!(ld_b, iw00, iw01, xp.add((gi + 2) * GROUP_SIZE));
3210            let d3 = tile_dot_tbl!(ld_b, iw10, iw11, xp.add((gi + 3) * GROUP_SIZE));
3211            // [−Σ0, −Σ1, −Σ2, −Σ3] → dots = −(2·Σset_neg + gsum)
3212            let neg = vpaddq_s32(vpaddq_s32(d0, d1), vpaddq_s32(d2, d3));
3213            let g = vld1q_s32(gp.add(gi));
3214            let dots = vnegq_s32(vaddq_s32(vshlq_n_s32::<1>(neg), g));
3215            let sc16 = vqtbl2_u8(uint8x16x2_t(ld_a, ld_b), isc);
3216            let scf: float32x4_t;
3217            asm!(
3218                "fcvtl {o:v}.4s, {i:v}.4h",
3219                o = out(vreg) scf, i = in(vreg) sc16,
3220                options(pure, nomem, nostack),
3221            );
3222            accv = vfmaq_f32(accv, vcvtq_f32_s32(dots), scf);
3223            gi += 4;
3224        }
3225        let mut acc = vaddvq_f32(accv);
3226        while gi < gpr {
3227            let t = base.add(gi * Q1_TILE);
3228            let s = f16_to_f32(u16::from_le_bytes([*t, *t.add(1)]));
3229            let d = vaddvq_s32(tile_dot!(t, xp.add(gi * GROUP_SIZE)));
3230            acc += (-(2 * d + *gp.add(gi))) as f32 * s;
3231            gi += 1;
3232        }
3233        acc
3234    }
3235}
3236
3237/// Blocked q1 1×4: one TBL unpack of the tile pair serves FOUR
3238/// activation streams (prefill amortization — the same idea as the
3239/// AVX2 twin; per stream the group order, fma order and tail match the
3240/// single-row kernel exactly, so batch == matvec bit-for-bit).
3241#[cfg(target_arch = "aarch64")]
3242#[target_feature(enable = "neon,dotprod")]
3243unsafe fn dot_q1_row_1x4_sdot(
3244    bytes: &[u8],
3245    r: usize,
3246    gpr: usize,
3247    xs: [&[i8]; 4],
3248    gs: [&[i32]; 4],
3249) -> [f32; 4] {
3250    // SAFETY: same slice-length contracts as `dot_q1_row_sdot`, ×4.
3251    unsafe {
3252        use core::arch::aarch64::*;
3253        use core::arch::asm;
3254        const MASKS: [u8; 16] = [1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128];
3255        const IW00: [u8; 16] = [2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3];
3256        const IW01: [u8; 16] = [4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5];
3257        const IW10: [u8; 16] = [8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9];
3258        const IW11: [u8; 16] = [10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11];
3259        const ISC: [u8; 8] = [0, 1, 6, 7, 16, 17, 22, 23];
3260        let m = vld1q_u8(MASKS.as_ptr());
3261        let (iw00, iw01) = (vld1q_u8(IW00.as_ptr()), vld1q_u8(IW01.as_ptr()));
3262        let (iw10, iw11) = (vld1q_u8(IW10.as_ptr()), vld1q_u8(IW11.as_ptr()));
3263        let isc = vld1_u8(ISC.as_ptr());
3264        macro_rules! sdot2 {
3265            ($w0:expr, $w1:expr, $x:expr) => {{
3266                let x0 = vld1q_s8($x);
3267                let x1 = vld1q_s8($x.add(16));
3268                let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
3269                asm!(
3270                    "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
3271                    "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
3272                    a0 = inout(vreg) a0, a1 = inout(vreg) a1,
3273                    w0 = in(vreg) $w0, x0 = in(vreg) x0, w1 = in(vreg) $w1, x1 = in(vreg) x1,
3274                    options(pure, nomem, nostack),
3275                );
3276                vaddq_s32(a0, a1)
3277            }};
3278        }
3279        let base = bytes.as_ptr().add(r * gpr * Q1_TILE);
3280        let row_base = r * gpr * Q1_TILE;
3281        let abs_end = bytes.len();
3282        let mut accv = [vdupq_n_f32(0.0); 4];
3283        let mut gi = 0;
3284        while gi + 4 <= gpr && row_base + (gi + 4) * Q1_TILE + 4 <= abs_end {
3285            let t0 = base.add(gi * Q1_TILE);
3286            let ld_a = vld1q_u8(t0);
3287            let ld_b = vld1q_u8(t0.add(2 * Q1_TILE));
3288            // Unpack ONCE — eight ±mask vectors serve all four streams.
3289            let w00 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8(ld_a, iw00), m));
3290            let w01 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8(ld_a, iw01), m));
3291            let w10 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8(ld_a, iw10), m));
3292            let w11 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8(ld_a, iw11), m));
3293            let w20 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8(ld_b, iw00), m));
3294            let w21 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8(ld_b, iw01), m));
3295            let w30 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8(ld_b, iw10), m));
3296            let w31 = vreinterpretq_s8_u8(vtstq_u8(vqtbl1q_u8(ld_b, iw11), m));
3297            let sc16 = vqtbl2_u8(uint8x16x2_t(ld_a, ld_b), isc);
3298            let scf: float32x4_t;
3299            asm!(
3300                "fcvtl {o:v}.4s, {i:v}.4h",
3301                o = out(vreg) scf, i = in(vreg) sc16,
3302                options(pure, nomem, nostack),
3303            );
3304            for k in 0..4 {
3305                let xp = xs[k].as_ptr();
3306                let d0 = sdot2!(w00, w01, xp.add(gi * GROUP_SIZE));
3307                let d1 = sdot2!(w10, w11, xp.add((gi + 1) * GROUP_SIZE));
3308                let d2 = sdot2!(w20, w21, xp.add((gi + 2) * GROUP_SIZE));
3309                let d3 = sdot2!(w30, w31, xp.add((gi + 3) * GROUP_SIZE));
3310                let neg = vpaddq_s32(vpaddq_s32(d0, d1), vpaddq_s32(d2, d3));
3311                let g = vld1q_s32(gs[k].as_ptr().add(gi));
3312                let dots = vnegq_s32(vaddq_s32(vshlq_n_s32::<1>(neg), g));
3313                accv[k] = vfmaq_f32(accv[k], vcvtq_f32_s32(dots), scf);
3314            }
3315            gi += 4;
3316        }
3317        let mut acc = [
3318            vaddvq_f32(accv[0]),
3319            vaddvq_f32(accv[1]),
3320            vaddvq_f32(accv[2]),
3321            vaddvq_f32(accv[3]),
3322        ];
3323        while gi < gpr {
3324            let t = base.add(gi * Q1_TILE);
3325            let sc = f16_to_f32(u16::from_le_bytes([*t, *t.add(1)]));
3326            let v0 = vcombine_u8(vdup_n_u8(*t.add(2)), vdup_n_u8(*t.add(3)));
3327            let v1 = vcombine_u8(vdup_n_u8(*t.add(4)), vdup_n_u8(*t.add(5)));
3328            let w0 = vreinterpretq_s8_u8(vtstq_u8(v0, m));
3329            let w1 = vreinterpretq_s8_u8(vtstq_u8(v1, m));
3330            for k in 0..4 {
3331                let d = vaddvq_s32(sdot2!(w0, w1, xs[k].as_ptr().add(gi * GROUP_SIZE)));
3332                acc[k] += (-(2 * d + *gs[k].as_ptr().add(gi))) as f32 * sc;
3333            }
3334            gi += 1;
3335        }
3336        acc
3337    }
3338}
3339
3340/// (weight ±1, scale) of one q1 element — the exact outlier term.
3341#[inline]
3342fn q1_outlier(bytes: &[u8], r: usize, gpr: usize, j: usize) -> (f32, f32) {
3343    let gi = j / GROUP_SIZE;
3344    let k = j % GROUP_SIZE;
3345    let tile = &bytes[(r * gpr + gi) * Q1_TILE..(r * gpr + gi + 1) * Q1_TILE];
3346    let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
3347    let bit = (tile[2 + k / 8] >> (k % 8)) & 1;
3348    ((bit as i32 * 2 - 1) as f32, s)
3349}
3350
3351/// Exact scalar q1 row (CMF_SDOT=0 contract).
3352#[inline]
3353fn q1_row_exact(bytes: &[u8], r: usize, gpr: usize, x: &[f32]) -> f32 {
3354    let mut acc = 0f32;
3355    for gi in 0..gpr {
3356        let tile = &bytes[(r * gpr + gi) * Q1_TILE..(r * gpr + gi + 1) * Q1_TILE];
3357        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
3358        let xg = &x[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
3359        let mut ga = 0f32;
3360        for (j, &b) in tile[2..].iter().enumerate() {
3361            for k in 0..8 {
3362                ga += (((b >> k) & 1) as f32 * 2.0 - 1.0) * xg[j * 8 + k];
3363            }
3364        }
3365        acc += ga * s;
3366    }
3367    acc
3368}
3369
3370/// One q1 row range via A8W8 (the body of `q1_matvec`'s hot loop,
3371/// extracted so multi-matrix jobs drive the same kernel).
3372#[allow(clippy::too_many_arguments)]
3373fn q1_range_a8w8(
3374    bytes: &[u8],
3375    gpr: usize,
3376    act: &SplitAct,
3377    gsum: &[i32],
3378    out: SendMut,
3379    start: usize,
3380    end: usize,
3381) {
3382    for r in start..end {
3383        let mut acc = dot_q1_row_i8(bytes, r, gpr, &act.xq, gsum) * act.sx;
3384        for &(j, xv) in &act.outliers {
3385            let (w, s) = q1_outlier(bytes, r, gpr, j);
3386            acc += w * s * xv;
3387        }
3388        // SAFETY: disjoint row ranges per worker.
3389        unsafe { *out.at(r) = acc };
3390    }
3391}
3392
3393/// Exact-scalar q1 row range (CMF_SDOT=0 contract).
3394fn q1_range_f32(bytes: &[u8], gpr: usize, x: &[f32], out: SendMut, start: usize, end: usize) {
3395    for r in start..end {
3396        // SAFETY: disjoint row ranges per worker.
3397        unsafe { *out.at(r) = q1_row_exact(bytes, r, gpr, x) };
3398    }
3399}
3400
3401/// q1t per-row overlay locator. After the base (`base_len`) come
3402/// `[u32 row_ptr[rows+1]]` then `[(u16 col, f16 val)]` grouped by row (row
3403/// `r`'s entries are `[row_ptr[r], row_ptr[r+1])`). Returns
3404/// `(row_ptr offset, entries offset, present)`.
3405fn q1t_overlay(bytes: &[u8], base_len: usize, rows: usize) -> (usize, usize, bool) {
3406    let entries = base_len + (rows + 1) * 4;
3407    (base_len, entries, entries <= bytes.len())
3408}
3409
3410/// Read `row_ptr[r]` from the overlay's prefix-sum table.
3411#[inline]
3412fn q1t_rowptr(bytes: &[u8], rp_off: usize, r: usize) -> usize {
3413    let o = rp_off + r * 4;
3414    u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]) as usize
3415}
3416
3417/// Byte → the 5 ternary signs it packs `{−1,0,+1}` as f32, precomputed so
3418/// decoding a q1t code is a table load, not the base-3 divide/modulo per
3419/// weight (division is ~20–40× the cost of a load). Built at compile time.
3420const SIGN5: [[f32; 5]; 256] = {
3421    let mut lut = [[0.0f32; 5]; 256];
3422    let pow3 = [1u16, 3, 9, 27, 81];
3423    let mut byte = 0usize;
3424    while byte < 256 {
3425        let mut i = 0usize;
3426        while i < 5 {
3427            let code = (byte as u16 / pow3[i]) % 3;
3428            lut[byte][i] = if code == 1 {
3429                1.0
3430            } else if code == 2 {
3431                -1.0
3432            } else {
3433                0.0
3434            };
3435            i += 1;
3436        }
3437        byte += 1;
3438    }
3439    lut
3440};
3441
3442/// Same table, as i8 signs — the operand for the int8 SDOT base kernel.
3443const SIGN5_I8: [[i8; 5]; 256] = {
3444    let mut lut = [[0i8; 5]; 256];
3445    let pow3 = [1u16, 3, 9, 27, 81];
3446    let mut byte = 0usize;
3447    while byte < 256 {
3448        let mut i = 0usize;
3449        while i < 5 {
3450            let code = (byte as u16 / pow3[i]) % 3;
3451            lut[byte][i] = if code == 1 {
3452                1
3453            } else if code == 2 {
3454                -1
3455            } else {
3456                0
3457            };
3458            i += 1;
3459        }
3460        byte += 1;
3461    }
3462    lut
3463};
3464
3465/// The same 5 i8 signs packed into a u64 (`[s0 s1 s2 s3 s4 0 0 0]`, LE) so the
3466/// group unpack is 7 unaligned u64 stores at offsets 0,5,10,…,30 instead of
3467/// six 5-byte copies + LUT indexing — each store's trailing zeros are fixed by
3468/// the next store, and the last one runs 6 B past the 32nd weight (the unpack
3469/// buffer is padded to 40). This is the decode/prefill hot inner op.
3470const SIGN5_U64: [u64; 256] = {
3471    let mut lut = [0u64; 256];
3472    let pow3 = [1u16, 3, 9, 27, 81];
3473    let mut byte = 0usize;
3474    while byte < 256 {
3475        let mut v = 0u64;
3476        let mut i = 0usize;
3477        while i < 5 {
3478            let code = (byte as u16 / pow3[i]) % 3;
3479            let s: u8 = if code == 1 {
3480                1
3481            } else if code == 2 {
3482                0xFF
3483            } else {
3484                0
3485            };
3486            v |= (s as u64) << (i * 8);
3487            i += 1;
3488        }
3489        lut[byte] = v;
3490        byte += 1;
3491    }
3492    lut
3493};
3494
3495/// Ternary base weight at `(row r, col j)` = `sign(code)·s_group`. Used to add
3496/// back activation-outlier columns, whose `x` was zeroed for the int8 bulk dot
3497/// (`split_act`). At a weight-outlier position the code is 0, so this is 0 and
3498/// the overlay correction owns that column — no double counting.
3499#[inline]
3500fn q1t_base_weight(bytes: &[u8], r: usize, gpr: usize, j: usize) -> f32 {
3501    const TILE: usize = cortiq_core::quant::Q1T_TILE;
3502    let off = (r * gpr + j / GROUP_SIZE) * TILE;
3503    let s = f16_to_f32(u16::from_le_bytes([bytes[off], bytes[off + 1]]));
3504    let within = j % GROUP_SIZE;
3505    SIGN5[bytes[off + 2 + within / 5] as usize][within % 5] * s
3506}
3507
3508/// One 32-group int8 dot via two SDOTs. Bit-exact vs the scalar i8 sum
3509/// (integer accumulation is order-independent).
3510#[cfg(target_arch = "aarch64")]
3511#[target_feature(enable = "neon,dotprod")]
3512#[inline]
3513unsafe fn sdot32_i8(w: *const i8, x: *const i8) -> i32 {
3514    // SAFETY: caller guarantees 32 readable i8 at each pointer.
3515    unsafe {
3516        use core::arch::aarch64::*;
3517        use core::arch::asm;
3518        let w0 = vld1q_s8(w);
3519        let w1 = vld1q_s8(w.add(16));
3520        let x0 = vld1q_s8(x);
3521        let x1 = vld1q_s8(x.add(16));
3522        let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
3523        asm!(
3524            "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
3525            "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
3526            a0 = inout(vreg) a0, a1 = inout(vreg) a1,
3527            w0 = in(vreg) w0, x0 = in(vreg) x0, w1 = in(vreg) w1, x1 = in(vreg) x1,
3528            options(pure, nomem, nostack),
3529        );
3530        vaddvq_s32(vaddq_s32(a0, a1))
3531    }
3532}
3533
3534/// One 32-group int8 dot via AVX2: signed·signed as `maddubs(|w|, sign(x,w))`
3535/// then `madd` and a horizontal reduce (the same idiom as `dot_q4t_row_avx2`).
3536#[cfg(target_arch = "x86_64")]
3537#[target_feature(enable = "avx2")]
3538#[inline]
3539unsafe fn i8dot32_avx2(w: *const i8, x: *const i8) -> i32 {
3540    // SAFETY: caller guarantees 32 readable i8 at each pointer.
3541    unsafe {
3542        use core::arch::x86_64::*;
3543        let wv = _mm256_loadu_si256(w as *const __m256i);
3544        let xv = _mm256_loadu_si256(x as *const __m256i);
3545        let p16 = _mm256_maddubs_epi16(_mm256_abs_epi8(wv), _mm256_sign_epi8(xv, wv));
3546        let d = _mm256_madd_epi16(p16, _mm256_set1_epi16(1));
3547        let hi128 = _mm256_extracti128_si256::<1>(d);
3548        let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
3549        let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
3550        let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
3551        _mm_cvtsi128_si32(s32)
3552    }
3553}
3554
3555/// Unpack one q1t group's base-3 codes into 32 i8 signs via 7 unaligned u64
3556/// stores (see `SIGN5_U64`). `dst` MUST have ≥ 40 bytes: the 7th store writes
3557/// `dst[30..38]`. Stores go in order so each one's trailing zeros are
3558/// overwritten by the next; the final 6 padding bytes are unused by the dot.
3559#[inline]
3560fn q1t_unpack_group_i8(codes: *const u8, dst: &mut [i8]) {
3561    debug_assert!(dst.len() >= 40);
3562    // SAFETY: codes points at 7 readable bytes; dst has ≥ 40 bytes so every
3563    // 8-byte store at offset bi*5 (bi ≤ 6 → ≤ 30) stays in bounds.
3564    unsafe {
3565        let p = dst.as_mut_ptr();
3566        for bi in 0..7 {
3567            core::ptr::write_unaligned(
3568                p.add(bi * 5) as *mut u64,
3569                SIGN5_U64[*codes.add(bi) as usize],
3570            );
3571        }
3572    }
3573}
3574
3575/// One 32-group int8 dot, arch-dispatched (the matmat inner loop, where the
3576/// row's signs are unpacked once and dotted against every batch input).
3577/// Callers are gated by `a8w8_enabled()`, so the target-feature arms are
3578/// reachable; the scalar arm is a non-SIMD-arch fallback.
3579#[inline]
3580fn q1t_i8dot32(w: *const i8, x: *const i8) -> i32 {
3581    #[cfg(target_arch = "aarch64")]
3582    unsafe {
3583        return sdot32_i8(w, x);
3584    }
3585    #[cfg(target_arch = "x86_64")]
3586    unsafe {
3587        return i8dot32_avx2(w, x);
3588    }
3589    #[allow(unreachable_code)]
3590    unsafe {
3591        let mut s = 0i32;
3592        for k in 0..GROUP_SIZE {
3593            s += *w.add(k) as i32 * *x.add(k) as i32;
3594        }
3595        s
3596    }
3597}
3598
3599#[inline]
3600unsafe fn q1t_unpack_reg_u64s(codes: *const u8) -> (u64, u64, u64, u64) {
3601    let (s0, s1, s2, s3, s4, s5, s6) = unsafe {
3602        (
3603            SIGN5_U64[*codes as usize],
3604            SIGN5_U64[*codes.add(1) as usize],
3605            SIGN5_U64[*codes.add(2) as usize],
3606            SIGN5_U64[*codes.add(3) as usize],
3607            SIGN5_U64[*codes.add(4) as usize],
3608            SIGN5_U64[*codes.add(5) as usize],
3609            SIGN5_U64[*codes.add(6) as usize],
3610        )
3611    };
3612
3613    let u0 = s0 | (s1 << 40);
3614    let u1 = (s1 >> 24) | (s2 << 16) | (s3 << 56);
3615    let u2 = (s3 >> 8) | (s4 << 32);
3616    let u3 = (s4 >> 32) | (s5 << 8) | (s6 << 48);
3617
3618    (u0, u1, u2, u3)
3619}
3620
3621/// One q1t row's int8 base dot: `Σ_group s·dot(signs, xq)` (before the shared
3622/// `sx`). Direct register unpacking (zero stack stores/loads, no STLF stalls).
3623/// ARM SDOT.
3624#[cfg(target_arch = "aarch64")]
3625#[target_feature(enable = "neon,dotprod")]
3626unsafe fn q1t_dot_row_sdot(bytes: &[u8], r: usize, gpr: usize, xq: &[i8]) -> f32 {
3627    use core::arch::aarch64::*;
3628    use core::arch::asm;
3629    unsafe {
3630        const TILE: usize = cortiq_core::quant::Q1T_TILE;
3631        let mut acc = 0f32;
3632        let bytes_ptr = bytes.as_ptr();
3633        let xq_ptr = xq.as_ptr();
3634        let row_off = r * gpr * TILE;
3635
3636        let gpr2 = gpr & !1;
3637        let mut gi = 0;
3638        while gi < gpr2 {
3639            let off0 = row_off + gi * TILE;
3640            let off1 = off0 + TILE;
3641            let s0 = f16_to_f32(u16::from_le_bytes([*bytes_ptr.add(off0), *bytes_ptr.add(off0 + 1)]));
3642            let s1 = f16_to_f32(u16::from_le_bytes([*bytes_ptr.add(off1), *bytes_ptr.add(off1 + 1)]));
3643
3644            let (u0_0, u1_0, u2_0, u3_0) = q1t_unpack_reg_u64s(bytes_ptr.add(off0 + 2));
3645            let (u0_1, u1_1, u2_1, u3_1) = q1t_unpack_reg_u64s(bytes_ptr.add(off1 + 2));
3646
3647            let w0_0 = vreinterpretq_s8_u64(vcombine_u64(vcreate_u64(u0_0), vcreate_u64(u1_0)));
3648            let w1_0 = vreinterpretq_s8_u64(vcombine_u64(vcreate_u64(u2_0), vcreate_u64(u3_0)));
3649            let w0_1 = vreinterpretq_s8_u64(vcombine_u64(vcreate_u64(u0_1), vcreate_u64(u1_1)));
3650            let w1_1 = vreinterpretq_s8_u64(vcombine_u64(vcreate_u64(u2_1), vcreate_u64(u3_1)));
3651
3652            let x0_0 = vld1q_s8(xq_ptr.add(gi * GROUP_SIZE));
3653            let x1_0 = vld1q_s8(xq_ptr.add(gi * GROUP_SIZE + 16));
3654            let x0_1 = vld1q_s8(xq_ptr.add((gi + 1) * GROUP_SIZE));
3655            let x1_1 = vld1q_s8(xq_ptr.add((gi + 1) * GROUP_SIZE + 16));
3656
3657            let (mut a0_0, mut a1_0) = (vdupq_n_s32(0), vdupq_n_s32(0));
3658            let (mut a0_1, mut a1_1) = (vdupq_n_s32(0), vdupq_n_s32(0));
3659            asm!(
3660                "sdot {a0_0:v}.4s, {w0_0:v}.16b, {x0_0:v}.16b",
3661                "sdot {a1_0:v}.4s, {w1_0:v}.16b, {x1_0:v}.16b",
3662                "sdot {a0_1:v}.4s, {w0_1:v}.16b, {x0_1:v}.16b",
3663                "sdot {a1_1:v}.4s, {w1_1:v}.16b, {x1_1:v}.16b",
3664                a0_0 = inout(vreg) a0_0, a1_0 = inout(vreg) a1_0,
3665                a0_1 = inout(vreg) a0_1, a1_1 = inout(vreg) a1_1,
3666                w0_0 = in(vreg) w0_0, x0_0 = in(vreg) x0_0, w1_0 = in(vreg) w1_0, x1_0 = in(vreg) x1_0,
3667                w0_1 = in(vreg) w0_1, x0_1 = in(vreg) x0_1, w1_1 = in(vreg) w1_1, x1_1 = in(vreg) x1_1,
3668                options(pure, nomem, nostack),
3669            );
3670            let d0 = vaddvq_s32(vaddq_s32(a0_0, a1_0));
3671            let d1 = vaddvq_s32(vaddq_s32(a0_1, a1_1));
3672            acc += d0 as f32 * s0 + d1 as f32 * s1;
3673            gi += 2;
3674        }
3675
3676        if gi < gpr {
3677            let off = row_off + gi * TILE;
3678            let s = f16_to_f32(u16::from_le_bytes([*bytes_ptr.add(off), *bytes_ptr.add(off + 1)]));
3679            let (u0, u1, u2, u3) = q1t_unpack_reg_u64s(bytes_ptr.add(off + 2));
3680            let w0 = vreinterpretq_s8_u64(vcombine_u64(vcreate_u64(u0), vcreate_u64(u1)));
3681            let w1 = vreinterpretq_s8_u64(vcombine_u64(vcreate_u64(u2), vcreate_u64(u3)));
3682            let x0 = vld1q_s8(xq_ptr.add(gi * GROUP_SIZE));
3683            let x1 = vld1q_s8(xq_ptr.add(gi * GROUP_SIZE + 16));
3684            let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
3685            asm!(
3686                "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
3687                "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
3688                a0 = inout(vreg) a0, a1 = inout(vreg) a1,
3689                w0 = in(vreg) w0, x0 = in(vreg) x0, w1 = in(vreg) w1, x1 = in(vreg) x1,
3690                options(pure, nomem, nostack),
3691            );
3692            let d = vaddvq_s32(vaddq_s32(a0, a1));
3693            acc += d as f32 * s;
3694        }
3695        acc
3696    }
3697}
3698
3699/// x86 AVX2 mirror of `q1t_dot_row_sdot` (maddubs int8 dot per group).
3700#[cfg(target_arch = "x86_64")]
3701#[target_feature(enable = "avx2")]
3702unsafe fn q1t_dot_row_avx2(bytes: &[u8], r: usize, gpr: usize, xq: &[i8]) -> f32 {
3703    use core::arch::x86_64::*;
3704    unsafe {
3705        const TILE: usize = cortiq_core::quant::Q1T_TILE;
3706        let mut acc = 0f32;
3707        let bytes_ptr = bytes.as_ptr();
3708        let xq_ptr = xq.as_ptr();
3709        let row_off = r * gpr * TILE;
3710
3711        let ones = _mm256_set1_epi16(1);
3712        for gi in 0..gpr {
3713            let off = row_off + gi * TILE;
3714            let s = f16_to_f32(u16::from_le_bytes([*bytes_ptr.add(off), *bytes_ptr.add(off + 1)]));
3715            let (u0, u1, u2, u3) = q1t_unpack_reg_u64s(bytes_ptr.add(off + 2));
3716            let wv = _mm256_set_epi64x(u3 as i64, u2 as i64, u1 as i64, u0 as i64);
3717            let xv = _mm256_loadu_si256(xq_ptr.add(gi * GROUP_SIZE) as *const __m256i);
3718            let p16 = _mm256_maddubs_epi16(_mm256_abs_epi8(wv), _mm256_sign_epi8(xv, wv));
3719            let d256 = _mm256_madd_epi16(p16, ones);
3720            let d128 = _mm_add_epi32(_mm256_castsi256_si128(d256), _mm256_extracti128_si256(d256, 1));
3721            let d64 = _mm_add_epi32(d128, _mm_shuffle_epi32(d128, 0xee));
3722            let d32 = _mm_cvtsi128_si32(_mm_add_epi32(d64, _mm_shuffle_epi32(d64, 0x55)));
3723            acc += d32 as f32 * s;
3724        }
3725        acc
3726    }
3727}
3728
3729/// Per-row int8 base dot, dispatched once per row (matvec decode hot path).
3730/// Callers are gated by `a8w8_enabled()`, so the target-feature kernels are
3731/// reachable.
3732#[inline]
3733fn q1t_dot_row_i8(bytes: &[u8], r: usize, gpr: usize, xq: &[i8]) -> f32 {
3734    #[cfg(target_arch = "aarch64")]
3735    unsafe {
3736        return q1t_dot_row_sdot(bytes, r, gpr, xq);
3737    }
3738    #[cfg(target_arch = "x86_64")]
3739    unsafe {
3740        return q1t_dot_row_avx2(bytes, r, gpr, xq);
3741    }
3742    #[allow(unreachable_code)]
3743    {
3744        const TILE: usize = cortiq_core::quant::Q1T_TILE;
3745        let mut acc = 0f32;
3746        let mut sg = [0i8; GROUP_SIZE + 8]; // +8 slack for the u64-store unpack
3747        for gi in 0..gpr {
3748            let off = (r * gpr + gi) * TILE;
3749            let s = f16_to_f32(u16::from_le_bytes([bytes[off], bytes[off + 1]]));
3750            q1t_unpack_group_i8(bytes.as_ptr().wrapping_add(off + 2), &mut sg);
3751            let mut d = 0i32;
3752            for k in 0..GROUP_SIZE {
3753                d += sg[k] as i32 * xq[gi * GROUP_SIZE + k] as i32;
3754            }
3755            acc += d as f32 * s;
3756        }
3757        acc
3758    }
3759}
3760
3761/// Σ over a row's outliers of `value·x[col]` — the correction that adds the
3762/// overlay's exact weights on top of the base dot. INVARIANT: the encoder
3763/// writes ternary code 0 at every outlier position (`quantize_q1t`), so the
3764/// base contributes nothing there and this is a plain `value·x`, not
3765/// `(value − base)·x` — no scattered per-outlier scale read. Row `r`'s entries
3766/// are the contiguous slice `[row_ptr[r], row_ptr[r+1])`, so no binary search.
3767fn q1t_row_outlier_correction(
3768    bytes: &[u8],
3769    r: usize,
3770    rp_off: usize,
3771    entries_off: usize,
3772    has_ov: bool,
3773    x: &[f32],
3774) -> f32 {
3775    if !has_ov {
3776        return 0.0;
3777    }
3778    let (c0, c1) = (q1t_rowptr(bytes, rp_off, r), q1t_rowptr(bytes, rp_off, r + 1));
3779    let mut corr = 0f32;
3780    for p in c0..c1 {
3781        let e = entries_off + p * 4;
3782        let col = u16::from_le_bytes([bytes[e], bytes[e + 1]]) as usize;
3783        let val = f16_to_f32(u16::from_le_bytes([bytes[e + 2], bytes[e + 3]]));
3784        corr += val * x[col];
3785    }
3786    corr
3787}
3788
3789/// Dequantize one q1t row into `buf[..cols]` via the sign LUT (no division),
3790/// then apply the row's outliers (its `[row_ptr[r], row_ptr[r+1])` slice).
3791/// Used by the batched (prefill) path where the decode amortizes over the batch.
3792fn q1t_dequant_row(
3793    bytes: &[u8],
3794    r: usize,
3795    gpr: usize,
3796    rp_off: usize,
3797    entries_off: usize,
3798    has_ov: bool,
3799    buf: &mut [f32],
3800) {
3801    const TILE: usize = cortiq_core::quant::Q1T_TILE;
3802    for g in 0..gpr {
3803        let off = (r * gpr + g) * TILE;
3804        let s = f16_to_f32(u16::from_le_bytes([bytes[off], bytes[off + 1]]));
3805        let codes = &bytes[off + 2..off + TILE];
3806        let bc = g * GROUP_SIZE;
3807        // 6 full bytes (30 codes) + a 7th byte holding the last 2.
3808        for bi in 0..6 {
3809            let lut = &SIGN5[codes[bi] as usize];
3810            let d = &mut buf[bc + bi * 5..bc + bi * 5 + 5];
3811            for i in 0..5 {
3812                d[i] = lut[i] * s;
3813            }
3814        }
3815        let lut = &SIGN5[codes[6] as usize];
3816        buf[bc + 30] = lut[0] * s;
3817        buf[bc + 31] = lut[1] * s;
3818    }
3819    if !has_ov {
3820        return;
3821    }
3822    let (c0, c1) = (q1t_rowptr(bytes, rp_off, r), q1t_rowptr(bytes, rp_off, r + 1));
3823    for p in c0..c1 {
3824        let e = entries_off + p * 4;
3825        let col = u16::from_le_bytes([bytes[e], bytes[e + 1]]) as usize;
3826        buf[col] = f16_to_f32(u16::from_le_bytes([bytes[e + 2], bytes[e + 3]]));
3827    }
3828}
3829
3830/// Add the sparse outlier overlay onto a base dot already in `out` (the GPU
3831/// computes the ternary base; the overlay stays on the CPU — its entries are
3832/// few and its per-row gather doesn't vectorize on the GPU). Row-parallel.
3833fn q1t_add_overlay(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
3834    const TILE: usize = cortiq_core::quant::Q1T_TILE;
3835    let gpr = cols / GROUP_SIZE;
3836    let (rp_off, ent_off, has_ov) = q1t_overlay(bytes, rows * gpr * TILE, rows);
3837    if !has_ov {
3838        return;
3839    }
3840    let out_addr = SendMut(out.as_mut_ptr());
3841    let run = move |start: usize, end: usize| {
3842        for r in start..end {
3843            let corr = q1t_row_outlier_correction(bytes, r, rp_off, ent_off, has_ov, x);
3844            // SAFETY: disjoint rows; add onto the base the GPU already wrote.
3845            unsafe { *out_addr.at(r) += corr };
3846        }
3847    };
3848    dispatch_rows(pool, rows, &run);
3849}
3850
3851/// Q1T row range via the A8W8 int8 path — shared activation split,
3852/// per-row: base SDOT dot + outlier correction + overlay.
3853#[allow(clippy::too_many_arguments)]
3854fn q1t_range_a8w8(
3855    bytes: &[u8],
3856    gpr: usize,
3857    rp_off: usize,
3858    ent_off: usize,
3859    has_ov: bool,
3860    act: &SplitAct,
3861    x: &[f32],
3862    out: SendMut,
3863    start: usize,
3864    end: usize,
3865) {
3866    for r in start..end {
3867        let mut acc = q1t_dot_row_i8(bytes, r, gpr, &act.xq) * act.sx;
3868        for &(j, xv) in &act.outliers {
3869            acc += q1t_base_weight(bytes, r, gpr, j) * xv;
3870        }
3871        acc += q1t_row_outlier_correction(bytes, r, rp_off, ent_off, has_ov, x);
3872        // SAFETY: disjoint row ranges per worker.
3873        unsafe { *out.at(r) = acc };
3874    }
3875}
3876
3877/// Q1T row range via the f32 path (no SDOT) — for matvec_many batched
3878/// dispatch when a8w8 is unavailable.
3879#[allow(clippy::too_many_arguments)]
3880fn q1t_range_f32_batch(
3881    bytes: &[u8],
3882    gpr: usize,
3883    rp_off: usize,
3884    ent_off: usize,
3885    has_ov: bool,
3886    x: &[f32],
3887    out: SendMut,
3888    start: usize,
3889    end: usize,
3890) {
3891    const TILE: usize = cortiq_core::quant::Q1T_TILE;
3892    let mut sg = [0f32; GROUP_SIZE];
3893    for r in start..end {
3894        let mut acc = 0f32;
3895        for g in 0..gpr {
3896            let off = (r * gpr + g) * TILE;
3897            let s = f16_to_f32(u16::from_le_bytes([bytes[off], bytes[off + 1]]));
3898            let codes = &bytes[off + 2..off + TILE];
3899            let xg = &x[g * GROUP_SIZE..g * GROUP_SIZE + GROUP_SIZE];
3900            for bi in 0..6 {
3901                sg[bi * 5..bi * 5 + 5].copy_from_slice(&SIGN5[codes[bi] as usize]);
3902            }
3903            let lut = &SIGN5[codes[6] as usize];
3904            sg[30] = lut[0];
3905            sg[31] = lut[1];
3906            let mut gsum = 0f32;
3907            for k in 0..GROUP_SIZE {
3908                gsum += sg[k] * xg[k];
3909            }
3910            acc += s * gsum;
3911        }
3912        acc += q1t_row_outlier_correction(bytes, r, rp_off, ent_off, has_ov, x);
3913        // SAFETY: disjoint row ranges per worker.
3914        unsafe { *out.at(r) = acc };
3915    }
3916}
3917
3918/// Ternary (q1t) matvec — decode+dot straight from mmap, one group at a time:
3919/// no per-ROW buffer, no division (the sign LUT), and a tiny per-group sign
3920/// buffer so the 32-wide dot vectorizes. This is the decode hot path.
3921fn q1t_matvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
3922    debug_assert_eq!(out.len(), rows);
3923    const TILE: usize = cortiq_core::quant::Q1T_TILE;
3924    let gpr = cols / GROUP_SIZE;
3925    let (rp_off, ent_off, has_ov) = q1t_overlay(bytes, rows * gpr * TILE, rows);
3926    let out_addr = SendMut(out.as_mut_ptr());
3927    // int8 SDOT base dot (ARM dotprod): ~4× the f32 arithmetic. x → i8 once
3928    // (`split_act`), activation outliers added back exactly in f32, weight
3929    // overlay on top. ARM SDOT / x86 AVX2; CMF_SDOT=0 keeps the exact f32 path.
3930    if a8w8_enabled() {
3931        let act = split_act(x);
3932        let act = &act;
3933        let run = move |start: usize, end: usize| {
3934            for r in start..end {
3935                let mut acc = q1t_dot_row_i8(bytes, r, gpr, &act.xq) * act.sx;
3936                for &(j, xv) in &act.outliers {
3937                    acc += q1t_base_weight(bytes, r, gpr, j) * xv;
3938                }
3939                acc += q1t_row_outlier_correction(bytes, r, rp_off, ent_off, has_ov, x);
3940                // SAFETY: disjoint row ranges per worker.
3941                unsafe { *out_addr.at(r) = acc };
3942            }
3943        };
3944        dispatch_rows(pool, rows, &run);
3945        return;
3946    }
3947    let run = move |start: usize, end: usize| {
3948        // Per-group signs, unpacked contiguously so the dot below is a clean
3949        // 32-wide reduction the autovectorizer turns into f32x4 FMAs — the
3950        // 5-values-per-byte base-3 layout won't SIMD in place.
3951        let mut sg = [0f32; GROUP_SIZE];
3952        for r in start..end {
3953            let mut acc = 0f32;
3954            for g in 0..gpr {
3955                let off = (r * gpr + g) * TILE;
3956                let s = f16_to_f32(u16::from_le_bytes([bytes[off], bytes[off + 1]]));
3957                let codes = &bytes[off + 2..off + TILE];
3958                let xg = &x[g * GROUP_SIZE..g * GROUP_SIZE + GROUP_SIZE];
3959                for bi in 0..6 {
3960                    sg[bi * 5..bi * 5 + 5].copy_from_slice(&SIGN5[codes[bi] as usize]);
3961                }
3962                let lut = &SIGN5[codes[6] as usize];
3963                sg[30] = lut[0];
3964                sg[31] = lut[1];
3965                let mut gsum = 0f32;
3966                for k in 0..GROUP_SIZE {
3967                    gsum += sg[k] * xg[k];
3968                }
3969                acc += s * gsum;
3970            }
3971            acc += q1t_row_outlier_correction(bytes, r, rp_off, ent_off, has_ov, x);
3972            unsafe { *out_addr.at(r) = acc };
3973        }
3974    };
3975    dispatch_rows(pool, rows, &run);
3976}
3977
3978/// Ternary (q1t) matmat (prefill) — dequant each row once, dot the whole
3979/// batch against it (amortizes the per-row decode).
3980fn q1t_matmat(
3981    bytes: &[u8],
3982    xs: &[f32],
3983    b: usize,
3984    rows: usize,
3985    cols: usize,
3986    out: &mut [f32],
3987    pool: Option<&Pool>,
3988) {
3989    debug_assert_eq!(out.len(), b * rows);
3990    const TILE: usize = cortiq_core::quant::Q1T_TILE;
3991    let gpr = cols / GROUP_SIZE;
3992    let (rp_off, ent_off, has_ov) = q1t_overlay(bytes, rows * gpr * TILE, rows);
3993    let out_addr = SendMut(out.as_mut_ptr());
3994    // int8 prefill (ARM SDOT / x86 AVX2): quantize the B inputs once, unpack
3995    // each weight row's signs to i8 ONCE, then int8-dot against every input —
3996    // the row sign-decode amortizes over the whole batch. CMF_SDOT=0 → f32.
3997    if a8w8_enabled() {
3998        let acts: Vec<SplitAct> = (0..b)
3999            .map(|bi| split_act(&xs[bi * cols..(bi + 1) * cols]))
4000            .collect();
4001        let acts = &acts;
4002        let run = move |start: usize, end: usize| {
4003            let mut sg = vec![0i8; cols + 8]; // row signs, i8 (+8 unpack slack)
4004            let mut sc = vec![0f32; gpr]; // per-group scales
4005            let mut accs = vec![0f32; b]; // per-batch accumulators, reused per row
4006            for r in start..end {
4007                for g in 0..gpr {
4008                    let off = (r * gpr + g) * TILE;
4009                    sc[g] = f16_to_f32(u16::from_le_bytes([bytes[off], bytes[off + 1]]));
4010                    q1t_unpack_group_i8(bytes.as_ptr().wrapping_add(off + 2), &mut sg[g * GROUP_SIZE..]);
4011                }
4012                for bi in 0..b {
4013                    let act = &acts[bi];
4014                    let mut isum = 0f32;
4015                    for g in 0..gpr {
4016                        let d = q1t_i8dot32(
4017                            sg.as_ptr().wrapping_add(g * GROUP_SIZE),
4018                            act.xq.as_ptr().wrapping_add(g * GROUP_SIZE),
4019                        );
4020                        isum += d as f32 * sc[g];
4021                    }
4022                    let mut acc = isum * act.sx;
4023                    for &(j, xv) in &act.outliers {
4024                        acc += q1t_base_weight(bytes, r, gpr, j) * xv;
4025                    }
4026                    accs[bi] = acc;
4027                }
4028                // Overlay ONCE per row for the whole batch: read each (col, val)
4029                // from mmap a single time (was b× — the re-read dominated prefill)
4030                // and fan it out over the batch via the cached inputs.
4031                if has_ov {
4032                    let (c0, c1) =
4033                        (q1t_rowptr(bytes, rp_off, r), q1t_rowptr(bytes, rp_off, r + 1));
4034                    for p in c0..c1 {
4035                        let e = ent_off + p * 4;
4036                        let col = u16::from_le_bytes([bytes[e], bytes[e + 1]]) as usize;
4037                        let val = f16_to_f32(u16::from_le_bytes([bytes[e + 2], bytes[e + 3]]));
4038                        for bi in 0..b {
4039                            accs[bi] += val * xs[bi * cols + col];
4040                        }
4041                    }
4042                }
4043                for bi in 0..b {
4044                    unsafe { *out_addr.at(bi * rows + r) = accs[bi] };
4045                }
4046            }
4047        };
4048        dispatch_rows(pool, rows, &run);
4049        return;
4050    }
4051    let run = move |start: usize, end: usize| {
4052        let mut buf = vec![0f32; cols];
4053        for r in start..end {
4054            q1t_dequant_row(bytes, r, gpr, rp_off, ent_off, has_ov, &mut buf);
4055            for bi in 0..b {
4056                let xr = &xs[bi * cols..(bi + 1) * cols];
4057                let mut acc = 0f32;
4058                for j in 0..cols {
4059                    acc += buf[j] * xr[j];
4060                }
4061                unsafe { *out_addr.at(bi * rows + r) = acc };
4062            }
4063        }
4064    };
4065    dispatch_rows(pool, rows, &run);
4066}
4067
4068fn q1_matvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
4069    debug_assert_eq!(out.len(), rows);
4070    let gpr = cols / GROUP_SIZE;
4071    let out_addr = SendMut(out.as_mut_ptr());
4072    if a8w8_enabled() {
4073        let act = split_act(x);
4074        let gsum = q1_group_sums(&act.xq, gpr);
4075        let (act, gsum) = (&act, &gsum);
4076        let run =
4077            move |start: usize, end: usize| q1_range_a8w8(bytes, gpr, act, gsum, out_addr, start, end);
4078        dispatch_rows(pool, rows, &run);
4079        return;
4080    }
4081    let run = move |start: usize, end: usize| q1_range_f32(bytes, gpr, x, out_addr, start, end);
4082    dispatch_rows(pool, rows, &run);
4083}
4084
4085/// Fused two-input q1 matvec (weights read once per pair).
4086#[allow(clippy::too_many_arguments)]
4087fn q1_matvec2(
4088    bytes: &[u8],
4089    x1: &[f32],
4090    x2: &[f32],
4091    rows: usize,
4092    cols: usize,
4093    o1: &mut [f32],
4094    o2: &mut [f32],
4095    pool: Option<&Pool>,
4096) {
4097    let gpr = cols / GROUP_SIZE;
4098    let p1 = SendMut(o1.as_mut_ptr());
4099    let p2 = SendMut(o2.as_mut_ptr());
4100    if a8w8_enabled() {
4101        let a1 = split_act(x1);
4102        let a2 = split_act(x2);
4103        let g1 = q1_group_sums(&a1.xq, gpr);
4104        let g2 = q1_group_sums(&a2.xq, gpr);
4105        let (a1, a2, g1, g2) = (&a1, &a2, &g1, &g2);
4106        let run = move |start: usize, end: usize| {
4107            for r in start..end {
4108                let mut v1 = dot_q1_row_i8(bytes, r, gpr, &a1.xq, g1) * a1.sx;
4109                let mut v2 = dot_q1_row_i8(bytes, r, gpr, &a2.xq, g2) * a2.sx;
4110                for &(j, xv) in &a1.outliers {
4111                    let (w, s) = q1_outlier(bytes, r, gpr, j);
4112                    v1 += w * s * xv;
4113                }
4114                for &(j, xv) in &a2.outliers {
4115                    let (w, s) = q1_outlier(bytes, r, gpr, j);
4116                    v2 += w * s * xv;
4117                }
4118                // SAFETY: disjoint row ranges per worker.
4119                unsafe {
4120                    *p1.at(r) = v1;
4121                    *p2.at(r) = v2;
4122                }
4123            }
4124        };
4125        dispatch_rows(pool, rows, &run);
4126        return;
4127    }
4128    let run = move |start: usize, end: usize| {
4129        for r in start..end {
4130            // SAFETY: disjoint row ranges per worker.
4131            unsafe {
4132                *p1.at(r) = q1_row_exact(bytes, r, gpr, x1);
4133                *p2.at(r) = q1_row_exact(bytes, r, gpr, x2);
4134            }
4135        }
4136    };
4137    dispatch_rows(pool, rows, &run);
4138}
4139
4140/// Batched q1 matmat: each row's tiles stream once per microbatch.
4141#[allow(clippy::too_many_arguments)]
4142fn q1_matmat(
4143    bytes: &[u8],
4144    xs_all: &[f32],
4145    b: usize,
4146    rows: usize,
4147    cols: usize,
4148    out: &mut [f32],
4149    pool: Option<&Pool>,
4150) {
4151    debug_assert_eq!(out.len(), b * rows);
4152    let gpr = cols / GROUP_SIZE;
4153    let out_addr = SendMut(out.as_mut_ptr());
4154    if a8w8_enabled() {
4155        let acts: Vec<(SplitAct, Vec<i32>)> = (0..b)
4156            .map(|bi| {
4157                let act = split_act(&xs_all[bi * cols..(bi + 1) * cols]);
4158                let gsum = q1_group_sums(&act.xq, gpr);
4159                (act, gsum)
4160            })
4161            .collect();
4162        let acts = &acts;
4163        #[cfg(target_arch = "x86_64")]
4164        let blocked_ok = avx2_enabled()
4165            && std::env::var("CMF_X86_BLOCKED").map(|v| v != "0").unwrap_or(true);
4166        #[cfg(target_arch = "aarch64")]
4167        let blocked_ok = sdot_enabled()
4168            && std::env::var("CMF_X86_BLOCKED").map(|v| v != "0").unwrap_or(true);
4169        let run = move |start: usize, end: usize| {
4170            for r in start..end {
4171                let mut bi = 0usize;
4172                // Blocked 1×4: the unpacked bit mask serves four
4173                // activation streams per group.
4174                #[cfg(target_arch = "aarch64")]
4175                if blocked_ok {
4176                    while bi + 4 <= acts.len() {
4177                        let xs = [
4178                            acts[bi].0.xq.as_slice(),
4179                            acts[bi + 1].0.xq.as_slice(),
4180                            acts[bi + 2].0.xq.as_slice(),
4181                            acts[bi + 3].0.xq.as_slice(),
4182                        ];
4183                        let gs = [
4184                            acts[bi].1.as_slice(),
4185                            acts[bi + 1].1.as_slice(),
4186                            acts[bi + 2].1.as_slice(),
4187                            acts[bi + 3].1.as_slice(),
4188                        ];
4189                        let d = unsafe { dot_q1_row_1x4_sdot(bytes, r, gpr, xs, gs) };
4190                        for k in 0..4 {
4191                            let (act, _) = &acts[bi + k];
4192                            let mut acc = d[k] * act.sx;
4193                            for &(j, xv) in &act.outliers {
4194                                let (w, sc) = q1_outlier(bytes, r, gpr, j);
4195                                acc += w * sc * xv;
4196                            }
4197                            // SAFETY: disjoint (bi, r) cells per worker.
4198                            unsafe { *out_addr.at((bi + k) * rows + r) = acc };
4199                        }
4200                        bi += 4;
4201                    }
4202                }
4203                #[cfg(target_arch = "x86_64")]
4204                if blocked_ok {
4205                    while bi + 4 <= acts.len() {
4206                        let xs = [
4207                            acts[bi].0.xq.as_slice(),
4208                            acts[bi + 1].0.xq.as_slice(),
4209                            acts[bi + 2].0.xq.as_slice(),
4210                            acts[bi + 3].0.xq.as_slice(),
4211                        ];
4212                        let gs = [
4213                            acts[bi].1.as_slice(),
4214                            acts[bi + 1].1.as_slice(),
4215                            acts[bi + 2].1.as_slice(),
4216                            acts[bi + 3].1.as_slice(),
4217                        ];
4218                        let d = unsafe { dot_q1_row_1x4_avx2(bytes, r, gpr, xs, gs) };
4219                        for k in 0..4 {
4220                            let (act, _) = &acts[bi + k];
4221                            let mut acc = d[k] * act.sx;
4222                            for &(j, xv) in &act.outliers {
4223                                let (w, sc) = q1_outlier(bytes, r, gpr, j);
4224                                acc += w * sc * xv;
4225                            }
4226                            // SAFETY: disjoint (bi, r) cells per worker.
4227                            unsafe { *out_addr.at((bi + k) * rows + r) = acc };
4228                        }
4229                        bi += 4;
4230                    }
4231                }
4232                while bi < acts.len() {
4233                    let (act, gsum) = &acts[bi];
4234                    let mut acc = dot_q1_row_i8(bytes, r, gpr, &act.xq, gsum) * act.sx;
4235                    for &(j, xv) in &act.outliers {
4236                        let (w, s) = q1_outlier(bytes, r, gpr, j);
4237                        acc += w * s * xv;
4238                    }
4239                    // SAFETY: disjoint (bi, r) cells per worker range.
4240                    unsafe { *out_addr.at(bi * rows + r) = acc };
4241                    bi += 1;
4242                }
4243            }
4244        };
4245        dispatch_rows(pool, rows, &run);
4246        return;
4247    }
4248    let run = move |start: usize, end: usize| {
4249        for r in start..end {
4250            for bi in 0..b {
4251                let x = &xs_all[bi * cols..(bi + 1) * cols];
4252                // SAFETY: disjoint (bi, r) cells per worker range.
4253                unsafe { *out_addr.at(bi * rows + r) = q1_row_exact(bytes, r, gpr, x) };
4254            }
4255        }
4256    };
4257    dispatch_rows(pool, rows, &run);
4258}
4259
4260/// Fused q4_block matvec straight from the mapped bytes. SDOT path when
4261/// dotprod is available (port of vmfcore `dot_q4_block_sdot`, measured
4262/// +23% on q4 decode): nibbles → centered i8, int8×int8 `sdot` per
4263/// 32-group, exact outlier correction — the same A8W8 contract as q8.
4264/// `CMF_SDOT=0` keeps the exact scalar path.
4265fn q4matvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
4266    debug_assert_eq!(out.len(), rows);
4267    let (packed, scales) = q4_split(bytes, rows, cols);
4268    let gpr = cols / GROUP_SIZE;
4269    let out_addr = SendMut(out.as_mut_ptr());
4270
4271    if a8w8_enabled() {
4272        let act = split_act(x);
4273        let run = move |start: usize, end: usize| {
4274            q4_range_a8w8(packed, scales, gpr, cols, &act, out_addr, start, end)
4275        };
4276        dispatch_rows(pool, rows, &run);
4277        return;
4278    }
4279
4280    let run = move |start: usize, end: usize| {
4281        q4_range_f32(packed, scales, gpr, x, out_addr, start, end)
4282    };
4283    dispatch_rows(pool, rows, &run);
4284}
4285
4286/// One q4 row via the A8W8 int8 path — SDOT on ARM, AVX2 maddubs on
4287/// x86 (scalar fallback is unreachable: callers gate on a8w8_enabled).
4288#[inline]
4289#[allow(unreachable_code)]
4290/// One UNPACKED q4 row (centered i8 in `buf`) against four activation
4291/// streams: the 32-byte weight chunk and its abs() load once per group,
4292/// the per-group f16 scale decodes once — four maddubs+reduce chains
4293/// instead of four full (load, abs, dot) rounds.
4294#[cfg(target_arch = "x86_64")]
4295#[target_feature(enable = "avx2")]
4296unsafe fn dot_q4b_row_1x4_avx2(
4297    buf: &[u8],
4298    scales: &[u8],
4299    g0: usize,
4300    gpr: usize,
4301    xs: [&[i8]; 4],
4302) -> [f32; 4] {
4303    // SAFETY: callers uphold buffer contracts (buf.len() == gpr·32).
4304    unsafe {
4305        use core::arch::x86_64::*;
4306        let ones = _mm256_set1_epi16(1);
4307        let mut acc = [0f32; 4];
4308        for gi in 0..gpr {
4309            let s = f16_to_f32(u16::from_le_bytes([
4310                scales[(g0 + gi) * 2],
4311                scales[(g0 + gi) * 2 + 1],
4312            ]));
4313            let w = _mm256_loadu_si256(buf.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
4314            let aw = _mm256_abs_epi8(w);
4315            for (k, xq) in xs.iter().enumerate() {
4316                let x =
4317                    _mm256_loadu_si256(xq.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
4318                let p16 = _mm256_maddubs_epi16(aw, _mm256_sign_epi8(x, w));
4319                let d = _mm256_madd_epi16(p16, ones);
4320                let hi128 = _mm256_extracti128_si256::<1>(d);
4321                let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
4322                let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
4323                let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
4324                acc[k] += _mm_cvtsi128_si32(s32) as f32 * s;
4325            }
4326        }
4327        acc
4328    }
4329}
4330
4331/// The vbit flavor of the blocked 1×4: the per-activation A8W8 scale
4332/// folds in PER GROUP as `(d·sx)·s` — bit-matching the single-matvec
4333/// accumulation order (the q4_block flavor applies sx once at the end,
4334/// matching ITS single path; the two conventions are historical and
4335/// each blocked leg must mirror its own).
4336#[cfg(target_arch = "x86_64")]
4337#[target_feature(enable = "avx2")]
4338unsafe fn dot_q4b_row_1x4_sx_avx2(
4339    buf: &[u8],
4340    scales: &[u8],
4341    g0: usize,
4342    gpr: usize,
4343    xs: [&[i8]; 4],
4344    sxs: [f32; 4],
4345) -> [f32; 4] {
4346    // SAFETY: callers uphold buffer contracts (buf.len() == gpr·32).
4347    unsafe {
4348        use core::arch::x86_64::*;
4349        let ones = _mm256_set1_epi16(1);
4350        let mut acc = [0f32; 4];
4351        for gi in 0..gpr {
4352            let s = f16_to_f32(u16::from_le_bytes([
4353                scales[(g0 + gi) * 2],
4354                scales[(g0 + gi) * 2 + 1],
4355            ]));
4356            let w = _mm256_loadu_si256(buf.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
4357            let aw = _mm256_abs_epi8(w);
4358            for (k, xq) in xs.iter().enumerate() {
4359                let x =
4360                    _mm256_loadu_si256(xq.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
4361                let p16 = _mm256_maddubs_epi16(aw, _mm256_sign_epi8(x, w));
4362                let d = _mm256_madd_epi16(p16, ones);
4363                let hi128 = _mm256_extracti128_si256::<1>(d);
4364                let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
4365                let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
4366                let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
4367                acc[k] += (_mm_cvtsi128_si32(s32) as f32 * sxs[k]) * s;
4368            }
4369        }
4370        acc
4371    }
4372}
4373
4374#[allow(unreachable_code)]
4375fn dot_q4_row_i8(packed: &[u8], scales: &[u8], g0: usize, gpr: usize, xq: &[i8]) -> f32 {
4376    #[cfg(target_arch = "aarch64")]
4377    unsafe {
4378        return dot_q4_row_sdot(packed, scales, g0, gpr, xq);
4379    }
4380    #[cfg(target_arch = "x86_64")]
4381    unsafe {
4382        return dot_q4_row_avx2(packed, scales, g0, gpr, xq);
4383    }
4384    let mut acc = 0f32;
4385    for gi in 0..gpr {
4386        let g = g0 + gi;
4387        let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
4388        let mut d = 0i32;
4389        for (k, &b) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
4390            d += ((b & 0x0F) as i32 - 8) * xq[gi * GROUP_SIZE + k * 2] as i32
4391                + (((b >> 4) & 0x0F) as i32 - 8) * xq[gi * GROUP_SIZE + k * 2 + 1] as i32;
4392        }
4393        acc += d as f32 * s;
4394    }
4395    acc
4396}
4397
4398/// Two-activation q4 row via the A8W8 int8 path (see `dot_q4_row_i8`).
4399#[inline]
4400#[allow(unreachable_code)]
4401fn dot_q4_row_i8_2(
4402    packed: &[u8],
4403    scales: &[u8],
4404    g0: usize,
4405    gpr: usize,
4406    xq1: &[i8],
4407    xq2: &[i8],
4408) -> (f32, f32) {
4409    #[cfg(target_arch = "aarch64")]
4410    unsafe {
4411        return dot_q4_row_sdot2(packed, scales, g0, gpr, xq1, xq2);
4412    }
4413    #[cfg(target_arch = "x86_64")]
4414    unsafe {
4415        return dot_q4_row_avx2_2(packed, scales, g0, gpr, xq1, xq2);
4416    }
4417    (
4418        dot_q4_row_i8(packed, scales, g0, gpr, xq1),
4419        dot_q4_row_i8(packed, scales, g0, gpr, xq2),
4420    )
4421}
4422
4423/// One q4 row range via SDOT (kernel body of `q4matvec`, extracted so
4424/// multi-matrix jobs can drive it for several tensors in one dispatch).
4425#[allow(clippy::too_many_arguments)]
4426fn q4_range_a8w8(
4427    packed: &[u8],
4428    scales: &[u8],
4429    gpr: usize,
4430    cols: usize,
4431    act: &SplitAct,
4432    out: SendMut,
4433    start: usize,
4434    end: usize,
4435) {
4436    for r in start..end {
4437        let mut acc = dot_q4_row_i8(packed, scales, r * gpr, gpr, &act.xq) * act.sx;
4438        // xq is zeroed at outlier slots — add the exact terms.
4439        for &(j, xv) in &act.outliers {
4440            let flat = r * cols + j;
4441            let byte = packed[flat / 2];
4442            let nib = if flat & 1 == 0 { byte & 0x0F } else { byte >> 4 };
4443            let s = f16_to_f32(u16::from_le_bytes([
4444                scales[(flat / GROUP_SIZE) * 2],
4445                scales[(flat / GROUP_SIZE) * 2 + 1],
4446            ]));
4447            acc += ((nib as i32 - 8) as f32) * s * xv;
4448        }
4449        // SAFETY: disjoint row ranges per worker.
4450        unsafe { *out.at(r) = acc };
4451    }
4452}
4453
4454/// Two-input q4 row range via the A8W8 int8 path — kernel body of
4455/// `q4matvec2`, extracted for pair multi-matrix jobs.
4456#[allow(clippy::too_many_arguments)]
4457fn q4_range2_a8w8(
4458    packed: &[u8],
4459    scales: &[u8],
4460    gpr: usize,
4461    cols: usize,
4462    a1: &SplitAct,
4463    a2: &SplitAct,
4464    p1: SendMut,
4465    p2: SendMut,
4466    start: usize,
4467    end: usize,
4468) {
4469    for r in start..end {
4470        let (s1, s2) = dot_q4_row_i8_2(packed, scales, r * gpr, gpr, &a1.xq, &a2.xq);
4471        let mut acc1 = s1 * a1.sx;
4472        let mut acc2 = s2 * a2.sx;
4473        // xq is zeroed at outlier slots — add the exact terms.
4474        let fix = |outliers: &[(usize, f32)], acc: &mut f32| {
4475            for &(j, xv) in outliers {
4476                let flat = r * cols + j;
4477                let byte = packed[flat / 2];
4478                let nib = if flat & 1 == 0 { byte & 0x0F } else { byte >> 4 };
4479                let s = f16_to_f32(u16::from_le_bytes([
4480                    scales[(flat / GROUP_SIZE) * 2],
4481                    scales[(flat / GROUP_SIZE) * 2 + 1],
4482                ]));
4483                *acc += ((nib as i32 - 8) as f32) * s * xv;
4484            }
4485        };
4486        fix(&a1.outliers, &mut acc1);
4487        fix(&a2.outliers, &mut acc2);
4488        // SAFETY: disjoint row ranges per worker.
4489        unsafe {
4490            *p1.at(r) = acc1;
4491            *p2.at(r) = acc2;
4492        }
4493    }
4494}
4495
4496/// Exact scalar q4 row range (same extraction, non-SDOT path).
4497fn q4_range_f32(
4498    packed: &[u8],
4499    scales: &[u8],
4500    gpr: usize,
4501    x: &[f32],
4502    out: SendMut,
4503    start: usize,
4504    end: usize,
4505) {
4506    for r in start..end {
4507        let mut acc = 0f32;
4508        for gi in 0..gpr {
4509            let g = r * gpr + gi;
4510            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
4511            let pk = &packed[g * 16..(g + 1) * 16];
4512            let xg = &x[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
4513            let mut ga = 0f32;
4514            for (k, &b) in pk.iter().enumerate() {
4515                ga += ((b & 0x0F) as f32 - 8.0) * xg[k * 2]
4516                    + (((b >> 4) & 0x0F) as f32 - 8.0) * xg[k * 2 + 1];
4517            }
4518            acc += ga * s;
4519        }
4520        // SAFETY: disjoint row ranges per worker.
4521        unsafe { *out.at(r) = acc };
4522    }
4523}
4524
4525/// Fused two-input q4 matvec: nibbles are unpacked ONCE per group and
4526/// dotted against both activations (was: two full matvecs — double
4527/// weight traffic). Per-lane math matches `q4matvec` exactly.
4528#[allow(clippy::too_many_arguments)]
4529fn q4matvec2(
4530    bytes: &[u8],
4531    x1: &[f32],
4532    x2: &[f32],
4533    rows: usize,
4534    cols: usize,
4535    o1: &mut [f32],
4536    o2: &mut [f32],
4537    pool: Option<&Pool>,
4538) {
4539    debug_assert_eq!(o1.len(), rows);
4540    debug_assert_eq!(o2.len(), rows);
4541    let (packed, scales) = q4_split(bytes, rows, cols);
4542    let gpr = cols / GROUP_SIZE;
4543
4544    if a8w8_enabled() {
4545        let a1 = split_act(x1);
4546        let a2 = split_act(x2);
4547        let p1 = SendMut(o1.as_mut_ptr());
4548        let p2 = SendMut(o2.as_mut_ptr());
4549        let run = move |start: usize, end: usize| {
4550            q4_range2_a8w8(packed, scales, gpr, cols, &a1, &a2, p1, p2, start, end)
4551        };
4552        dispatch_rows(pool, rows, &run);
4553        return;
4554    }
4555
4556    let p1 = SendMut(o1.as_mut_ptr());
4557    let p2 = SendMut(o2.as_mut_ptr());
4558    let run = move |start: usize, end: usize| {
4559        q4_range2_f32(packed, scales, gpr, x1, x2, p1, p2, start, end)
4560    };
4561    dispatch_rows(pool, rows, &run);
4562}
4563
4564/// Two-input exact scalar q4 row range (same extraction).
4565#[allow(clippy::too_many_arguments)]
4566fn q4_range2_f32(
4567    packed: &[u8],
4568    scales: &[u8],
4569    gpr: usize,
4570    x1: &[f32],
4571    x2: &[f32],
4572    p1: SendMut,
4573    p2: SendMut,
4574    start: usize,
4575    end: usize,
4576) {
4577    for r in start..end {
4578        let (mut acc1, mut acc2) = (0f32, 0f32);
4579        for gi in 0..gpr {
4580            let g = r * gpr + gi;
4581            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
4582            let pk = &packed[g * 16..(g + 1) * 16];
4583            let x1g = &x1[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
4584            let x2g = &x2[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
4585            let (mut g1, mut g2) = (0f32, 0f32);
4586            for (k, &b) in pk.iter().enumerate() {
4587                let wl = (b & 0x0F) as f32 - 8.0;
4588                let wh = ((b >> 4) & 0x0F) as f32 - 8.0;
4589                g1 += wl * x1g[k * 2] + wh * x1g[k * 2 + 1];
4590                g2 += wl * x2g[k * 2] + wh * x2g[k * 2 + 1];
4591            }
4592            acc1 += g1 * s;
4593            acc2 += g2 * s;
4594        }
4595        // SAFETY: disjoint row ranges per worker.
4596        unsafe {
4597            *p1.at(r) = acc1;
4598            *p2.at(r) = acc2;
4599        }
4600    }
4601}
4602
4603thread_local! {
4604    /// Per-worker decoded-row scratch for the batched q4/vbit kernels
4605    /// (centered i8 for SDOT, f32 for the exact/scalar paths).
4606    static ROW_I8: std::cell::RefCell<Vec<u8>> = const { std::cell::RefCell::new(Vec::new()) };
4607    static ROW_F32: std::cell::RefCell<Vec<f32>> = const { std::cell::RefCell::new(Vec::new()) };
4608}
4609
4610/// Batched q4 matmat: each weight row is unpacked from the mmap ONCE
4611/// and dotted against ALL b activations (prefill used to fall back to b
4612/// full matvecs — b× weight traffic and b× nibble decode). Per-position
4613/// math matches `q4matvec` exactly: same group order, same accumulation.
4614/// `out` is row-major [b, rows] like `qmatmat`.
4615#[allow(clippy::too_many_arguments)]
4616fn q4matmat(
4617    bytes: &[u8],
4618    xs_all: &[f32],
4619    b: usize,
4620    rows: usize,
4621    cols: usize,
4622    out: &mut [f32],
4623    pool: Option<&Pool>,
4624) {
4625    debug_assert_eq!(xs_all.len(), b * cols);
4626    debug_assert_eq!(out.len(), b * rows);
4627    let (packed, scales) = q4_split(bytes, rows, cols);
4628    let gpr = cols / GROUP_SIZE;
4629    let gscale = |g: usize| f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
4630
4631    if a8w8_enabled() {
4632        let acts: Vec<SplitAct> =
4633            (0..b).map(|bi| split_act(&xs_all[bi * cols..(bi + 1) * cols])).collect();
4634        let acts = &acts;
4635        let out_addr = SendMut(out.as_mut_ptr());
4636        let run = move |start: usize, end: usize| {
4637            ROW_I8.with(|rb| {
4638                let mut buf = rb.borrow_mut();
4639                buf.resize(cols, 0);
4640                for r in start..end {
4641                    // Unpack the row's nibbles to centered i8 once
4642                    // (element 2k = low nibble, 2k+1 = high — flat order,
4643                    // same as dot_q4_row_sdot's zip).
4644                    for gi in 0..gpr {
4645                        let g = r * gpr + gi;
4646                        for (k, &bt) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
4647                            buf[gi * GROUP_SIZE + k * 2] = ((bt & 0x0F) as i32 - 8) as i8 as u8;
4648                            buf[gi * GROUP_SIZE + k * 2 + 1] =
4649                                (((bt >> 4) & 0x0F) as i32 - 8) as i8 as u8;
4650                        }
4651                    }
4652                    let mut bi = 0usize;
4653                    #[cfg(target_arch = "x86_64")]
4654                    if avx2_enabled()
4655                        && std::env::var("CMF_X86_BLOCKED").map(|v| v != "0").unwrap_or(true)
4656                    {
4657                        while bi + 4 <= acts.len() {
4658                            let xs = [
4659                                acts[bi].xq.as_slice(),
4660                                acts[bi + 1].xq.as_slice(),
4661                                acts[bi + 2].xq.as_slice(),
4662                                acts[bi + 3].xq.as_slice(),
4663                            ];
4664                            let d = unsafe {
4665                                dot_q4b_row_1x4_avx2(&buf, scales, r * gpr, gpr, xs)
4666                            };
4667                            for k in 0..4 {
4668                                let act = &acts[bi + k];
4669                                let mut acc = d[k] * act.sx;
4670                                for &(j, xv) in &act.outliers {
4671                                    acc += (buf[j] as i8) as f32
4672                                        * gscale((r * cols + j) / GROUP_SIZE)
4673                                        * xv;
4674                                }
4675                                // SAFETY: disjoint (bi, r) cells per worker.
4676                                unsafe { *out_addr.at((bi + k) * rows + r) = acc };
4677                            }
4678                            bi += 4;
4679                        }
4680                    }
4681                    while bi < acts.len() {
4682                        let act = &acts[bi];
4683                        let mut acc = 0f32;
4684                        for gi in 0..gpr {
4685                            let d = dot_i8_i8(
4686                                &buf[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE],
4687                                &act.xq[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE],
4688                            );
4689                            acc += d as f32 * gscale(r * gpr + gi);
4690                        }
4691                        acc *= act.sx;
4692                        // xq is zeroed at outlier slots — exact terms.
4693                        for &(j, xv) in &act.outliers {
4694                            acc += (buf[j] as i8) as f32 * gscale((r * cols + j) / GROUP_SIZE) * xv;
4695                        }
4696                        // SAFETY: disjoint (bi, r) cells per worker row range.
4697                        unsafe { *out_addr.at(bi * rows + r) = acc };
4698                        bi += 1;
4699                    }
4700                }
4701            })
4702        };
4703        dispatch_rows(pool, rows, &run);
4704        return;
4705    }
4706
4707    let out_addr = SendMut(out.as_mut_ptr());
4708    let run = move |start: usize, end: usize| {
4709        ROW_F32.with(|rb| {
4710            let mut buf = rb.borrow_mut();
4711            buf.resize(cols, 0.0);
4712            for r in start..end {
4713                // Decode raw (nib − 8) values once; scales stay per-group
4714                // so the accumulation order matches q4matvec bit-for-bit.
4715                for gi in 0..gpr {
4716                    let g = r * gpr + gi;
4717                    for (k, &bt) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
4718                        buf[gi * GROUP_SIZE + k * 2] = (bt & 0x0F) as f32 - 8.0;
4719                        buf[gi * GROUP_SIZE + k * 2 + 1] = ((bt >> 4) & 0x0F) as f32 - 8.0;
4720                    }
4721                }
4722                for bi in 0..b {
4723                    let x = &xs_all[bi * cols..(bi + 1) * cols];
4724                    let mut acc = 0f32;
4725                    for gi in 0..gpr {
4726                        let mut ga = 0f32;
4727                        // Pairwise (lo + hi) addition, matching
4728                        // q4matvec's `ga += lo·x + hi·x` shape exactly —
4729                        // a flat one-per-element loop rounds differently
4730                        // and broke bit-parity on the scalar (x86) path.
4731                        for k in 0..GROUP_SIZE / 2 {
4732                            let e = gi * GROUP_SIZE + k * 2;
4733                            ga += buf[e] * x[e] + buf[e + 1] * x[e + 1];
4734                        }
4735                        acc += ga * gscale(r * gpr + gi);
4736                    }
4737                    // SAFETY: disjoint (bi, r) cells per worker row range.
4738                    unsafe { *out_addr.at(bi * rows + r) = acc };
4739                }
4740            }
4741        })
4742    };
4743    dispatch_rows(pool, rows, &run);
4744}
4745
4746/// Batched vbit matmat: each variable-bit row is decoded from the mmap
4747/// ONCE for the whole microbatch. Same per-position math as
4748/// `vbitmatvec` (SDOT A8W8 with exact outliers / exact f32 for b=8 rows
4749/// and the scalar path).
4750#[allow(clippy::too_many_arguments)]
4751fn vbitmatmat(
4752    bytes: &[u8],
4753    offsets: &[usize],
4754    xs_all: &[f32],
4755    b: usize,
4756    rows: usize,
4757    cols: usize,
4758    out: &mut [f32],
4759    pool: Option<&Pool>,
4760) {
4761    debug_assert_eq!(xs_all.len(), b * cols);
4762    debug_assert_eq!(out.len(), b * rows);
4763    debug_assert_eq!(offsets.len(), rows + 1);
4764    let ng = cols / GROUP_SIZE;
4765    let bits = &bytes[..rows];
4766    let sc_off = rows;
4767    let gscale = |r: usize, g: usize| {
4768        let so = (r * ng + g) * 2;
4769        f16_to_f32(u16::from_le_bytes([bytes[sc_off + so], bytes[sc_off + so + 1]]))
4770    };
4771
4772    // Decode row r's raw (u − L) values into `dst` (f32, unscaled).
4773    let decode_f32 = |r: usize, dst: &mut [f32]| {
4774        let bw = bits[r] as usize;
4775        let l = ((1i32 << (bw - 1)) - 1) as f32;
4776        let data = &bytes[offsets[r]..offsets[r + 1]];
4777        let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
4778        for d in dst.iter_mut() {
4779            while nbits < bw {
4780                acc = (acc << 8) | data[idx] as u64;
4781                idx += 1;
4782                nbits += 8;
4783            }
4784            let u = ((acc >> (nbits - bw)) & ((1u64 << bw) - 1)) as f32;
4785            nbits -= bw;
4786            *d = u - l;
4787        }
4788    };
4789
4790    if a8w8_enabled() {
4791        let acts: Vec<SplitAct> =
4792            (0..b).map(|bi| split_act(&xs_all[bi * cols..(bi + 1) * cols])).collect();
4793        let acts = &acts;
4794        let out_addr = SendMut(out.as_mut_ptr());
4795        let run = move |start: usize, end: usize| {
4796            for r in start..end {
4797                let bw = bits[r] as usize;
4798                if bw == 8 {
4799                    // u−L reaches 128 → no i8 path; decode once, exact
4800                    // f32 dots for every position (same as vbitmatvec).
4801                    ROW_F32.with(|rb| {
4802                        let mut buf = rb.borrow_mut();
4803                        buf.resize(cols, 0.0);
4804                        decode_f32(r, &mut buf);
4805                        for bi in 0..b {
4806                            let x = &xs_all[bi * cols..(bi + 1) * cols];
4807                            let mut dot = 0f32;
4808                            for g in 0..ng {
4809                                let mut gd = 0f32;
4810                                for k in 0..GROUP_SIZE {
4811                                    gd += buf[g * GROUP_SIZE + k] * x[g * GROUP_SIZE + k];
4812                                }
4813                                dot += gd * gscale(r, g);
4814                            }
4815                            // SAFETY: disjoint (bi, r) cells per worker range.
4816                            unsafe { *out_addr.at(bi * rows + r) = dot };
4817                        }
4818                    });
4819                    continue;
4820                }
4821                let l = (1i32 << (bw - 1)) - 1;
4822                let data = &bytes[offsets[r]..offsets[r + 1]];
4823                ROW_I8.with(|rb| {
4824                    let mut buf = rb.borrow_mut();
4825                    buf.resize(cols, 0);
4826                    #[inline(always)]
4827                    fn fill<const B: usize>(data: &[u8], l: i32, buf: &mut [u8]) {
4828                        for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
4829                            let u = unpack8::<B>(&data[blk * B..]);
4830                            for k in 0..8 {
4831                                chunk[k] = (u[k] - l) as i8 as u8;
4832                            }
4833                        }
4834                    }
4835                    match bw {
4836                        3 => fill::<3>(data, l, &mut buf),
4837                        4 => vbit_fill4(data, &mut buf),
4838                        5 => fill::<5>(data, l, &mut buf),
4839                        6 => fill::<6>(data, l, &mut buf),
4840                        _ => unreachable!("vbit bit-width {bw} (validated at load)"),
4841                    }
4842                    let mut bi = 0usize;
4843                    // The vbit scale table shares q4_block's layout
4844                    // (contiguous f16 per (row·ng + g)), so the same
4845                    // blocked 1×4 kernel serves the decoded row.
4846                    #[cfg(target_arch = "x86_64")]
4847                    if avx2_enabled()
4848                        && std::env::var("CMF_X86_BLOCKED").map(|v| v != "0").unwrap_or(true)
4849                    {
4850                        while bi + 4 <= acts.len() {
4851                            let xs = [
4852                                acts[bi].xq.as_slice(),
4853                                acts[bi + 1].xq.as_slice(),
4854                                acts[bi + 2].xq.as_slice(),
4855                                acts[bi + 3].xq.as_slice(),
4856                            ];
4857                            let sxs = [
4858                                acts[bi].sx,
4859                                acts[bi + 1].sx,
4860                                acts[bi + 2].sx,
4861                                acts[bi + 3].sx,
4862                            ];
4863                            let d = unsafe {
4864                                dot_q4b_row_1x4_sx_avx2(&buf, &bytes[sc_off..], r * ng, ng, xs, sxs)
4865                            };
4866                            for k in 0..4 {
4867                                let act = &acts[bi + k];
4868                                let mut dot = d[k];
4869                                for &(j, xv) in &act.outliers {
4870                                    dot +=
4871                                        (buf[j] as i8) as f32 * gscale(r, j / GROUP_SIZE) * xv;
4872                                }
4873                                // SAFETY: disjoint (bi, r) cells per worker.
4874                                unsafe { *out_addr.at((bi + k) * rows + r) = dot };
4875                            }
4876                            bi += 4;
4877                        }
4878                    }
4879                    while bi < acts.len() {
4880                        let act = &acts[bi];
4881                        let mut dot = 0f32;
4882                        for g in 0..ng {
4883                            let d = dot_i8_i8(
4884                                &buf[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
4885                                &act.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
4886                            ) as f32
4887                                * act.sx;
4888                            dot += d * gscale(r, g);
4889                        }
4890                        for &(j, xv) in &act.outliers {
4891                            dot += (buf[j] as i8) as f32 * gscale(r, j / GROUP_SIZE) * xv;
4892                        }
4893                        // SAFETY: disjoint (bi, r) cells per worker range.
4894                        unsafe { *out_addr.at(bi * rows + r) = dot };
4895                        bi += 1;
4896                    }
4897                });
4898            }
4899        };
4900        dispatch_rows(pool, rows, &run);
4901        return;
4902    }
4903
4904    let out_addr = SendMut(out.as_mut_ptr());
4905    let run = move |start: usize, end: usize| {
4906        ROW_F32.with(|rb| {
4907            let mut buf = rb.borrow_mut();
4908            buf.resize(cols, 0.0);
4909            for r in start..end {
4910                decode_f32(r, &mut buf);
4911                for bi in 0..b {
4912                    let x = &xs_all[bi * cols..(bi + 1) * cols];
4913                    let mut dot = 0f32;
4914                    for g in 0..ng {
4915                        let mut gd = 0f32;
4916                        for k in 0..GROUP_SIZE {
4917                            gd += buf[g * GROUP_SIZE + k] * x[g * GROUP_SIZE + k];
4918                        }
4919                        dot += gd * gscale(r, g);
4920                    }
4921                    // SAFETY: disjoint (bi, r) cells per worker range.
4922                    unsafe { *out_addr.at(bi * rows + r) = dot };
4923                }
4924            }
4925        })
4926    };
4927    dispatch_rows(pool, rows, &run);
4928}
4929
4930/// Build a GPU batch job for a q8-family mapped tensor (primary
4931/// shard): prescaled input + directory coordinates. None → not
4932/// GPU-eligible, caller stays on the CPU.
4933pub(crate) fn gpu_batch_job<'a>(
4934    t: &'a QTensor,
4935    x: &[f32],
4936) -> Option<(std::sync::Arc<CmfModel>, crate::gpu::BatchJob<'a>)> {
4937    match t {
4938        QTensor::Mapped {
4939            model,
4940            idx,
4941            dtype: dt @ (TensorDtype::Q8Row | TensorDtype::Q8_2f),
4942            rows,
4943            cols,
4944            row_scale,
4945            col_field,
4946            ..
4947        } => Some((
4948            model.clone(),
4949            crate::gpu::BatchJob {
4950                idx: *idx,
4951                rows: *rows,
4952                cols: *cols,
4953                row_scale,
4954                xs: prescale(x, col_field, *dt).into_owned(),
4955                q1: false,
4956            },
4957        )),
4958        // q1: raw f32 activations, tile-embedded scales.
4959        QTensor::Mapped {
4960            model,
4961            idx,
4962            dtype: TensorDtype::Q1,
4963            rows,
4964            cols,
4965            ..
4966        } => Some((
4967            model.clone(),
4968            crate::gpu::BatchJob {
4969                idx: *idx,
4970                rows: *rows,
4971                cols: *cols,
4972                row_scale: &[],
4973                xs: x.to_vec(),
4974                q1: true,
4975            },
4976        )),
4977        _ => None,
4978    }
4979}
4980
4981thread_local! {
4982    static PRESCALE_BUF1: std::cell::RefCell<Vec<f32>> = const { std::cell::RefCell::new(Vec::new()) };
4983    static PRESCALE_BUF2: std::cell::RefCell<Vec<f32>> = const { std::cell::RefCell::new(Vec::new()) };
4984}
4985
4986pub(crate) fn prescale<'a>(
4987    x: &'a [f32],
4988    col_field: &[f32],
4989    dtype: TensorDtype,
4990) -> std::borrow::Cow<'a, [f32]> {
4991    if dtype == TensorDtype::Q8_2f {
4992        x.iter().zip(col_field).map(|(a, c)| a * c).collect()
4993    } else {
4994        std::borrow::Cow::Borrowed(x)
4995    }
4996}
4997
4998/// θ col-field fold for q8_2f activations. Borrowed pass-through for
4999/// every other dtype, using thread-local buffers to eliminate per-matvec allocations.
5000pub(crate) fn prescale_with<R, F: FnOnce(&[f32]) -> R>(
5001    x: &[f32],
5002    col_field: &[f32],
5003    dtype: TensorDtype,
5004    buf_id: u8,
5005    f: F,
5006) -> R {
5007    if dtype == TensorDtype::Q8_2f {
5008        if buf_id == 1 {
5009            PRESCALE_BUF1.with(|b| {
5010                let mut buf = b.borrow_mut();
5011                buf.clear();
5012                buf.extend(x.iter().zip(col_field).map(|(a, c)| a * c));
5013                f(&buf)
5014            })
5015        } else {
5016            PRESCALE_BUF2.with(|b| {
5017                let mut buf = b.borrow_mut();
5018                buf.clear();
5019                buf.extend(x.iter().zip(col_field).map(|(a, c)| a * c));
5020                f(&buf)
5021            })
5022        }
5023    } else {
5024        f(x)
5025    }
5026}
5027
5028// ───────────────────── x86-64 AVX2 kernels (roadmap этап 2) ─────────────────────
5029
5030/// AVX2+FMA available? Default ON when the CPU supports both;
5031/// `CMF_AVX2=0` disables (falls back to the autovectorized loops).
5032#[cfg(target_arch = "x86_64")]
5033pub(crate) fn avx2_enabled() -> bool {
5034    use std::sync::OnceLock;
5035    static ON: OnceLock<bool> = OnceLock::new();
5036    *ON.get_or_init(|| {
5037        std::env::var("CMF_AVX2").map(|v| v != "0").unwrap_or(true)
5038            && std::arch::is_x86_feature_detected!("avx2")
5039            && std::arch::is_x86_feature_detected!("fma")
5040    })
5041}
5042
5043/// AVX2 A8W8 allowed? The quantized-activation contract is switched by
5044/// the SAME env as the ARM SDOT path: `CMF_SDOT=0` keeps exact kernels
5045/// (the golden-parity exact gate relies on it) — AVX2 f32 kernels stay
5046/// active either way, they are exact (regrouped sums only).
5047#[cfg(target_arch = "x86_64")]
5048fn avx2_a8w8_enabled() -> bool {
5049    use std::sync::OnceLock;
5050    static ON: OnceLock<bool> = OnceLock::new();
5051    *ON.get_or_init(|| {
5052        avx2_enabled() && std::env::var("CMF_SDOT").map(|v| v != "0").unwrap_or(true)
5053    })
5054}
5055
5056/// A8W8 quantized-activation path available on THIS machine? One
5057/// switch across architectures: ARM dotprod (CMF_SDOT) or x86 AVX2
5058/// (CMF_AVX2 + the same CMF_SDOT exact-contract override).
5059#[inline]
5060pub(crate) fn a8w8_enabled() -> bool {
5061    #[cfg(target_arch = "aarch64")]
5062    {
5063        sdot_enabled()
5064    }
5065    #[cfg(target_arch = "x86_64")]
5066    {
5067        avx2_a8w8_enabled()
5068    }
5069    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
5070    {
5071        false
5072    }
5073}
5074
5075/// int8·int8 dot dispatch: SDOT on ARM; AVX-512 VNNI (vpdpbusd) or AVX2
5076/// maddubs on x86. Callers are gated by `a8w8_enabled()`.
5077#[inline]
5078#[allow(unreachable_code)]
5079fn dot_i8_i8(w: &[u8], xq: &[i8]) -> i32 {
5080    #[cfg(target_arch = "aarch64")]
5081    unsafe {
5082        return dot_i8_sdot(w, xq);
5083    }
5084    #[cfg(target_arch = "x86_64")]
5085    unsafe {
5086        if avx512vnni_enabled() {
5087            return dot_i8_i8_vnni(w, xq);
5088        }
5089        return dot_i8_i8_avx2(w, xq);
5090    }
5091    w.iter().zip(xq).map(|(&a, &b)| (a as i8) as i32 * b as i32).sum()
5092}
5093
5094/// AVX-512 VNNI available? (F+BW+VL+VNNI; `CMF_AVX512=0` falls back to
5095/// AVX2.) VL matters: short 32-byte groups (q4/vbit) ride the 256-bit
5096/// `vpdpbusd` encoding.
5097#[cfg(target_arch = "x86_64")]
5098fn avx512vnni_enabled() -> bool {
5099    use std::sync::OnceLock;
5100    static ON: OnceLock<bool> = OnceLock::new();
5101    *ON.get_or_init(|| {
5102        std::env::var("CMF_AVX512").map(|v| v != "0").unwrap_or(true)
5103            && std::arch::is_x86_feature_detected!("avx512f")
5104            && std::arch::is_x86_feature_detected!("avx512bw")
5105            && std::arch::is_x86_feature_detected!("avx512vl")
5106            && std::arch::is_x86_feature_detected!("avx512vnni")
5107    })
5108}
5109
5110/// int8·int8 via AVX-512 VNNI: `vpdpbusd` fuses the maddubs+madd+add
5111/// triple into one u8×i8 dot-accumulate. AVX-512 has no vpsignb, so the
5112/// |w|·sign(x,w) trick becomes |w| × (x negated where w<0) via a mask
5113/// subtract — w==0 lanes contribute 0 through |w|=0 either way.
5114#[cfg(target_arch = "x86_64")]
5115#[target_feature(enable = "avx2,avx512f,avx512bw,avx512vl,avx512vnni")]
5116unsafe fn dot_i8_i8_vnni(w: &[u8], xq: &[i8]) -> i32 {
5117    // SAFETY: callers uphold slice-length contracts (see call sites).
5118    unsafe {
5119        use core::arch::x86_64::*;
5120        let n = w.len();
5121        let mut j = 0usize;
5122        let mut total: i32;
5123        // 4 independent accumulators: vpdpbusd is its own loop-carried
5124        // dependency (~5-cycle latency) — a single-acc loop runs
5125        // latency-bound and LOSES to the AVX2 maddubs kernel, measured
5126        // on Granite Rapids.
5127        {
5128            #[inline(always)]
5129            unsafe fn step(
5130                w: *const u8,
5131                x: *const i8,
5132                acc: core::arch::x86_64::__m512i,
5133            ) -> core::arch::x86_64::__m512i {
5134                unsafe {
5135                    use core::arch::x86_64::*;
5136                    let wv = _mm512_loadu_si512(w as *const _);
5137                    let xv = _mm512_loadu_si512(x as *const _);
5138                    let aw = _mm512_abs_epi8(wv);
5139                    let neg = _mm512_movepi8_mask(wv);
5140                    let sx = _mm512_mask_sub_epi8(xv, neg, _mm512_setzero_si512(), xv);
5141                    _mm512_dpbusd_epi32(acc, aw, sx)
5142                }
5143            }
5144            let (mut a0, mut a1, mut a2, mut a3) = (
5145                _mm512_setzero_si512(),
5146                _mm512_setzero_si512(),
5147                _mm512_setzero_si512(),
5148                _mm512_setzero_si512(),
5149            );
5150            while j + 256 <= n {
5151                a0 = step(w.as_ptr().add(j), xq.as_ptr().add(j), a0);
5152                a1 = step(w.as_ptr().add(j + 64), xq.as_ptr().add(j + 64), a1);
5153                a2 = step(w.as_ptr().add(j + 128), xq.as_ptr().add(j + 128), a2);
5154                a3 = step(w.as_ptr().add(j + 192), xq.as_ptr().add(j + 192), a3);
5155                j += 256;
5156            }
5157            while j + 64 <= n {
5158                a0 = step(w.as_ptr().add(j), xq.as_ptr().add(j), a0);
5159                j += 64;
5160            }
5161            let s01 = _mm512_add_epi32(a0, a1);
5162            let s23 = _mm512_add_epi32(a2, a3);
5163            total = _mm512_reduce_add_epi32(_mm512_add_epi32(s01, s23));
5164        }
5165        // 32-wide (q4/vbit groups are exactly 32 bytes).
5166        if j + 32 <= n {
5167            let wv = _mm256_loadu_si256(w.as_ptr().add(j) as *const __m256i);
5168            let xv = _mm256_loadu_si256(xq.as_ptr().add(j) as *const __m256i);
5169            let d = _mm256_dpbusd_epi32(
5170                _mm256_setzero_si256(),
5171                _mm256_abs_epi8(wv),
5172                _mm256_sign_epi8(xv, wv),
5173            );
5174            let hi128 = _mm256_extracti128_si256::<1>(d);
5175            let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
5176            let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
5177            let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
5178            total += _mm_cvtsi128_si32(s32);
5179            j += 32;
5180        }
5181        while j < n {
5182            total += (w[j] as i8) as i32 * xq[j] as i32;
5183            j += 1;
5184        }
5185        total
5186    }
5187}
5188
5189/// i8 row · f32 x via AVX2/FMA (x86 mirror of `dot_i8_f32_neon`).
5190#[cfg(target_arch = "x86_64")]
5191#[target_feature(enable = "avx2,fma")]
5192unsafe fn dot_i8_f32_avx2(w: &[u8], x: &[f32]) -> f32 {
5193    // SAFETY: callers uphold slice-length contracts (see call sites).
5194    unsafe {
5195        use core::arch::x86_64::*;
5196        let n = x.len();
5197        let wp = w.as_ptr();
5198        let xp = x.as_ptr();
5199        let (mut a0, mut a1) = (_mm256_setzero_ps(), _mm256_setzero_ps());
5200        let mut j = 0usize;
5201        while j + 16 <= n {
5202            let wb = _mm_loadu_si128(wp.add(j) as *const __m128i);
5203            let lo = _mm256_cvtepi8_epi32(wb);
5204            let hi = _mm256_cvtepi8_epi32(_mm_srli_si128::<8>(wb));
5205            a0 = _mm256_fmadd_ps(_mm256_cvtepi32_ps(lo), _mm256_loadu_ps(xp.add(j)), a0);
5206            a1 = _mm256_fmadd_ps(_mm256_cvtepi32_ps(hi), _mm256_loadu_ps(xp.add(j + 8)), a1);
5207            j += 16;
5208        }
5209        let acc = _mm256_add_ps(a0, a1);
5210        let hi128 = _mm256_extractf128_ps::<1>(acc);
5211        let s128 = _mm_add_ps(_mm256_castps256_ps128(acc), hi128);
5212        let s64 = _mm_add_ps(s128, _mm_movehl_ps(s128, s128));
5213        let s32 = _mm_add_ss(s64, _mm_shuffle_ps::<1>(s64, s64));
5214        let mut sum = _mm_cvtss_f32(s32);
5215        while j < n {
5216            sum += (*wp.add(j) as i8) as f32 * *xp.add(j);
5217            j += 1;
5218        }
5219        sum
5220    }
5221}
5222
5223/// int8(weight)·int8(activation) → i32 via AVX2 maddubs — the x86
5224/// analogue of the SDOT A8W8 path. `maddubs` takes u8×i8, so the
5225/// standard sign trick applies: |w| × sign(x, w) ≡ w × x per lane.
5226/// Pair saturation is safe: |w|≤128, |x|≤127 → 2·128·127 < 32767.
5227#[cfg(target_arch = "x86_64")]
5228#[target_feature(enable = "avx2")]
5229unsafe fn dot_i8_i8_avx2(w: &[u8], xq: &[i8]) -> i32 {
5230    // SAFETY: callers uphold slice-length contracts (see call sites).
5231    unsafe {
5232        use core::arch::x86_64::*;
5233        let n = w.len();
5234        let ones = _mm256_set1_epi16(1);
5235        let mut acc = _mm256_setzero_si256();
5236        let mut j = 0usize;
5237        while j + 32 <= n {
5238            let wv = _mm256_loadu_si256(w.as_ptr().add(j) as *const __m256i);
5239            let xv = _mm256_loadu_si256(xq.as_ptr().add(j) as *const __m256i);
5240            let p16 = _mm256_maddubs_epi16(_mm256_abs_epi8(wv), _mm256_sign_epi8(xv, wv));
5241            acc = _mm256_add_epi32(acc, _mm256_madd_epi16(p16, ones));
5242            j += 32;
5243        }
5244        let hi128 = _mm256_extracti128_si256::<1>(acc);
5245        let s128 = _mm_add_epi32(_mm256_castsi256_si128(acc), hi128);
5246        let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
5247        let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
5248        let mut s = _mm_cvtsi128_si32(s32);
5249        while j < n {
5250            s += (w[j] as i8) as i32 * xq[j] as i32;
5251            j += 1;
5252        }
5253        s
5254    }
5255}
5256
5257/// smmla 2×4: one instruction covers a 2-row × 2-activation × 8-deep
5258/// tile (32 MACs vs sdot's 16) — the weight pair loads once per 8-k
5259/// slice as a combined 2×8 register and meets two activation pairs.
5260#[cfg(target_arch = "aarch64")]
5261#[target_feature(enable = "neon,i8mm")]
5262unsafe fn dot_i8_smmla_2x4(w0: &[u8], w1: &[u8], xs: [&[i8]; 4]) -> [[i32; 4]; 2] {
5263    // SAFETY: callers uphold slice-length contracts.
5264    unsafe {
5265        use core::arch::aarch64::*;
5266        use core::arch::asm;
5267        let n = w0.len();
5268        let w0p = w0.as_ptr() as *const i8;
5269        let w1p = w1.as_ptr() as *const i8;
5270        // acc01 holds [c(r0,x0) c(r0,x1) c(r1,x0) c(r1,x1)]; acc23 the
5271        // same for x2/x3.
5272        let mut acc01 = vdupq_n_s32(0);
5273        let mut acc23 = vdupq_n_s32(0);
5274        let mut i = 0usize;
5275        while i + 8 <= n {
5276            let wa = vcombine_s8(vld1_s8(w0p.add(i)), vld1_s8(w1p.add(i)));
5277            let xb01 =
5278                vcombine_s8(vld1_s8(xs[0].as_ptr().add(i)), vld1_s8(xs[1].as_ptr().add(i)));
5279            let xb23 =
5280                vcombine_s8(vld1_s8(xs[2].as_ptr().add(i)), vld1_s8(xs[3].as_ptr().add(i)));
5281            asm!(
5282                "smmla {a01:v}.4s, {w:v}.16b, {x01:v}.16b",
5283                "smmla {a23:v}.4s, {w:v}.16b, {x23:v}.16b",
5284                a01 = inout(vreg) acc01, a23 = inout(vreg) acc23,
5285                w = in(vreg) wa, x01 = in(vreg) xb01, x23 = in(vreg) xb23,
5286                options(pure, nomem, nostack),
5287            );
5288            i += 8;
5289        }
5290        let mut out = [[0i32; 4]; 2];
5291        let a01: [i32; 4] = core::mem::transmute(acc01);
5292        let a23: [i32; 4] = core::mem::transmute(acc23);
5293        out[0][0] = a01[0];
5294        out[0][1] = a01[1];
5295        out[1][0] = a01[2];
5296        out[1][1] = a01[3];
5297        out[0][2] = a23[0];
5298        out[0][3] = a23[1];
5299        out[1][2] = a23[2];
5300        out[1][3] = a23[3];
5301        if i < n {
5302            for (k, x) in xs.iter().enumerate() {
5303                for j in i..n {
5304                    out[0][k] += (w0[j] as i8) as i32 * x[j] as i32;
5305                    out[1][k] += (w1[j] as i8) as i32 * x[j] as i32;
5306                }
5307            }
5308        }
5309        out
5310    }
5311}
5312
5313/// ARM twin of the x86 blocked prefill GEMM: two weight rows stay in
5314/// registers across four activation streams, eight sdot accumulators.
5315/// (The per-row form re-read each W row once per activation.)
5316#[cfg(target_arch = "aarch64")]
5317#[target_feature(enable = "neon,dotprod")]
5318unsafe fn dot_i8_sdot_2x4(w0: &[u8], w1: &[u8], xs: [&[i8]; 4]) -> [[i32; 4]; 2] {
5319    // SAFETY: callers uphold slice-length contracts.
5320    unsafe {
5321        use core::arch::aarch64::*;
5322        use core::arch::asm;
5323        let n = w0.len();
5324        let w0p = w0.as_ptr() as *const i8;
5325        let w1p = w1.as_ptr() as *const i8;
5326        let mut acc = [[vdupq_n_s32(0); 4]; 2];
5327        let mut i = 0usize;
5328        while i + 16 <= n {
5329            let wv0 = vld1q_s8(w0p.add(i));
5330            let wv1 = vld1q_s8(w1p.add(i));
5331            for (k, x) in xs.iter().enumerate() {
5332                let xv = vld1q_s8(x.as_ptr().add(i));
5333                let (mut a0, mut a1) = (acc[0][k], acc[1][k]);
5334                asm!(
5335                    "sdot {a0:v}.4s, {w0:v}.16b, {x:v}.16b",
5336                    "sdot {a1:v}.4s, {w1:v}.16b, {x:v}.16b",
5337                    a0 = inout(vreg) a0, a1 = inout(vreg) a1,
5338                    w0 = in(vreg) wv0, w1 = in(vreg) wv1, x = in(vreg) xv,
5339                    options(pure, nomem, nostack),
5340                );
5341                acc[0][k] = a0;
5342                acc[1][k] = a1;
5343            }
5344            i += 16;
5345        }
5346        let mut out = [[0i32; 4]; 2];
5347        for r in 0..2 {
5348            for k in 0..4 {
5349                out[r][k] = vaddvq_s32(acc[r][k]);
5350            }
5351        }
5352        if i < n {
5353            for (k, x) in xs.iter().enumerate() {
5354                for j in i..n {
5355                    out[0][k] += (w0[j] as i8) as i32 * x[j] as i32;
5356                    out[1][k] += (w1[j] as i8) as i32 * x[j] as i32;
5357                }
5358            }
5359        }
5360        out
5361    }
5362}
5363
5364/// Blocked 2 weight rows × 4 activations for the prefill GEMM
5365/// (roadmap P0: packed panels + multi-row accumulators). The two rows'
5366/// abs() live in registers across all four activation streams; the
5367/// sign-fixup is recomputed per pair (the price of the maddubs trick).
5368/// Returns raw i8·i8 dots; the caller applies scales and outliers.
5369#[cfg(target_arch = "x86_64")]
5370#[target_feature(enable = "avx2")]
5371unsafe fn dot_i8_i8_avx2_2x4(w0: &[u8], w1: &[u8], xs: [&[i8]; 4]) -> [[i32; 4]; 2] {
5372    // SAFETY: callers uphold slice-length contracts.
5373    unsafe {
5374        use core::arch::x86_64::*;
5375        let n = w0.len();
5376        let ones = _mm256_set1_epi16(1);
5377        let mut acc = [[_mm256_setzero_si256(); 4]; 2];
5378        let mut j = 0usize;
5379        while j + 32 <= n {
5380            let wv0 = _mm256_loadu_si256(w0.as_ptr().add(j) as *const __m256i);
5381            let wv1 = _mm256_loadu_si256(w1.as_ptr().add(j) as *const __m256i);
5382            let aw0 = _mm256_abs_epi8(wv0);
5383            let aw1 = _mm256_abs_epi8(wv1);
5384            for (k, x) in xs.iter().enumerate() {
5385                let xv = _mm256_loadu_si256(x.as_ptr().add(j) as *const __m256i);
5386                let p0 = _mm256_maddubs_epi16(aw0, _mm256_sign_epi8(xv, wv0));
5387                acc[0][k] = _mm256_add_epi32(acc[0][k], _mm256_madd_epi16(p0, ones));
5388                let p1 = _mm256_maddubs_epi16(aw1, _mm256_sign_epi8(xv, wv1));
5389                acc[1][k] = _mm256_add_epi32(acc[1][k], _mm256_madd_epi16(p1, ones));
5390            }
5391            j += 32;
5392        }
5393        let mut out = [[0i32; 4]; 2];
5394        for r in 0..2 {
5395            for k in 0..4 {
5396                let a = acc[r][k];
5397                let hi128 = _mm256_extracti128_si256::<1>(a);
5398                let s128 = _mm_add_epi32(_mm256_castsi256_si128(a), hi128);
5399                let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
5400                let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
5401                out[r][k] = _mm_cvtsi128_si32(s32);
5402            }
5403        }
5404        if j < n {
5405            for (k, x) in xs.iter().enumerate() {
5406                for i in j..n {
5407                    out[0][k] += (w0[i] as i8) as i32 * x[i] as i32;
5408                    out[1][k] += (w1[i] as i8) as i32 * x[i] as i32;
5409                }
5410            }
5411        }
5412        out
5413    }
5414}
5415
5416/// AVX2/VNNI q8 row dot with exact outlier correction (x86 mirror of
5417/// `row_dot_sdot` — same A8W8 contract). With AVX-512 VNNI the row goes
5418/// through the bias trick: Σ(w+128)·x via pure `vpdpbusd` (no per-lane
5419/// sign fixups), corrected by −128·Σx with Σx precomputed per split.
5420#[cfg(target_arch = "x86_64")]
5421#[inline]
5422fn row_dot_avx2(row: &[u8], act: &SplitAct) -> f32 {
5423    let dot = if avx512vnni_enabled() && row.len() >= 64 {
5424        (unsafe { dot_u8p128_i8_vnni(row, &act.xq) }) - 128 * act.xsum
5425    } else {
5426        unsafe { dot_i8_i8_avx2(row, &act.xq) }
5427    };
5428    let mut acc = dot as f32 * act.sx;
5429    for &(j, xv) in &act.outliers {
5430        acc += (row[j] as i8) as f32 * xv;
5431    }
5432    acc
5433}
5434
5435/// Σ (w[i]+128)·x[i] via pure `vpdpbusd` — the caller subtracts
5436/// 128·Σx. Four independent accumulators (dpbusd is ~5-cycle latency;
5437/// a single-acc loop runs latency-bound, measured on Granite Rapids).
5438#[cfg(target_arch = "x86_64")]
5439#[target_feature(enable = "avx2,avx512f,avx512bw,avx512vl,avx512vnni")]
5440unsafe fn dot_u8p128_i8_vnni(w: &[u8], xq: &[i8]) -> i32 {
5441    // SAFETY: callers uphold slice-length contracts (see call sites).
5442    unsafe {
5443        use core::arch::x86_64::*;
5444        let n = w.len();
5445        let flip = _mm512_set1_epi8(-128); // XOR 0x80: i8 w → u8 (w+128)
5446        #[inline(always)]
5447        unsafe fn step(
5448            w: *const u8,
5449            x: *const i8,
5450            flip: core::arch::x86_64::__m512i,
5451            acc: core::arch::x86_64::__m512i,
5452        ) -> core::arch::x86_64::__m512i {
5453            unsafe {
5454                use core::arch::x86_64::*;
5455                let wv = _mm512_xor_si512(_mm512_loadu_si512(w as *const _), flip);
5456                _mm512_dpbusd_epi32(acc, wv, _mm512_loadu_si512(x as *const _))
5457            }
5458        }
5459        let (mut a0, mut a1, mut a2, mut a3) = (
5460            _mm512_setzero_si512(),
5461            _mm512_setzero_si512(),
5462            _mm512_setzero_si512(),
5463            _mm512_setzero_si512(),
5464        );
5465        let mut j = 0usize;
5466        while j + 256 <= n {
5467            a0 = step(w.as_ptr().add(j), xq.as_ptr().add(j), flip, a0);
5468            a1 = step(w.as_ptr().add(j + 64), xq.as_ptr().add(j + 64), flip, a1);
5469            a2 = step(w.as_ptr().add(j + 128), xq.as_ptr().add(j + 128), flip, a2);
5470            a3 = step(w.as_ptr().add(j + 192), xq.as_ptr().add(j + 192), flip, a3);
5471            j += 256;
5472        }
5473        while j + 64 <= n {
5474            a0 = step(w.as_ptr().add(j), xq.as_ptr().add(j), flip, a0);
5475            j += 64;
5476        }
5477        let mut total = _mm512_reduce_add_epi32(_mm512_add_epi32(
5478            _mm512_add_epi32(a0, a1),
5479            _mm512_add_epi32(a2, a3),
5480        ));
5481        // Scalar tail: (w as i8) + 128 ≡ (w as u8) ^ 0x80.
5482        while j < n {
5483            total += ((w[j] ^ 0x80) as i32) * xq[j] as i32;
5484            j += 1;
5485        }
5486        total
5487    }
5488}
5489
5490/// One q4 row via AVX2: nibbles → centered i8 (unpacklo/hi restores the
5491/// writer's flat order, same as the NEON vzip pair), maddubs against
5492/// the pre-quantized activation group, × the group's f16 scale. Pair
5493/// saturation safe: |w|≤8, |x|≤127 → 2·8·127 ≪ 32767. Mirror of
5494/// `dot_q4_row_sdot`.
5495#[cfg(target_arch = "x86_64")]
5496#[target_feature(enable = "avx2")]
5497unsafe fn dot_q4_row_avx2(packed: &[u8], scales: &[u8], g0: usize, gpr: usize, xq: &[i8]) -> f32 {
5498    // SAFETY: callers uphold slice-length contracts (16 packed bytes and
5499    // 2 scale bytes per group; xq.len() == gpr·GROUP_SIZE).
5500    unsafe {
5501        use core::arch::x86_64::*;
5502        let lomask = _mm_set1_epi8(0x0F);
5503        let eight = _mm256_set1_epi8(8);
5504        let ones = _mm256_set1_epi16(1);
5505        let mut acc = 0f32;
5506        for gi in 0..gpr {
5507            let g = g0 + gi;
5508            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
5509            let b = _mm_loadu_si128(packed.as_ptr().add(g * 16) as *const __m128i);
5510            let lo = _mm_and_si128(b, lomask);
5511            let hi = _mm_and_si128(_mm_srli_epi16::<4>(b), lomask);
5512            let w = _mm256_sub_epi8(
5513                _mm256_set_m128i(_mm_unpackhi_epi8(lo, hi), _mm_unpacklo_epi8(lo, hi)),
5514                eight,
5515            );
5516            let x = _mm256_loadu_si256(xq.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
5517            let p16 = _mm256_maddubs_epi16(_mm256_abs_epi8(w), _mm256_sign_epi8(x, w));
5518            let d = _mm256_madd_epi16(p16, ones);
5519            let hi128 = _mm256_extracti128_si256::<1>(d);
5520            let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
5521            let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
5522            let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
5523            acc += _mm_cvtsi128_si32(s32) as f32 * s;
5524        }
5525        acc
5526    }
5527}
5528
5529/// Two-activation q4 row via AVX2: nibbles unpacked ONCE per group,
5530/// both activations dotted against the same centered i8 register.
5531#[cfg(target_arch = "x86_64")]
5532#[target_feature(enable = "avx2")]
5533unsafe fn dot_q4_row_avx2_2(
5534    packed: &[u8],
5535    scales: &[u8],
5536    g0: usize,
5537    gpr: usize,
5538    xq1: &[i8],
5539    xq2: &[i8],
5540) -> (f32, f32) {
5541    // SAFETY: callers uphold slice-length contracts (see dot_q4_row_avx2).
5542    unsafe {
5543        use core::arch::x86_64::*;
5544        let lomask = _mm_set1_epi8(0x0F);
5545        let eight = _mm256_set1_epi8(8);
5546        let ones = _mm256_set1_epi16(1);
5547        let (mut acc1, mut acc2) = (0f32, 0f32);
5548        #[inline(always)]
5549        unsafe fn hsum(d: core::arch::x86_64::__m256i) -> i32 {
5550            unsafe {
5551                use core::arch::x86_64::*;
5552                let hi128 = _mm256_extracti128_si256::<1>(d);
5553                let s128 = _mm_add_epi32(_mm256_castsi256_si128(d), hi128);
5554                let s64 = _mm_add_epi32(s128, _mm_srli_si128::<8>(s128));
5555                let s32 = _mm_add_epi32(s64, _mm_srli_si128::<4>(s64));
5556                _mm_cvtsi128_si32(s32)
5557            }
5558        }
5559        for gi in 0..gpr {
5560            let g = g0 + gi;
5561            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
5562            let b = _mm_loadu_si128(packed.as_ptr().add(g * 16) as *const __m128i);
5563            let lo = _mm_and_si128(b, lomask);
5564            let hi = _mm_and_si128(_mm_srli_epi16::<4>(b), lomask);
5565            let w = _mm256_sub_epi8(
5566                _mm256_set_m128i(_mm_unpackhi_epi8(lo, hi), _mm_unpacklo_epi8(lo, hi)),
5567                eight,
5568            );
5569            let aw = _mm256_abs_epi8(w);
5570            let x1 = _mm256_loadu_si256(xq1.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
5571            let x2 = _mm256_loadu_si256(xq2.as_ptr().add(gi * GROUP_SIZE) as *const __m256i);
5572            let d1 = _mm256_madd_epi16(_mm256_maddubs_epi16(aw, _mm256_sign_epi8(x1, w)), ones);
5573            let d2 = _mm256_madd_epi16(_mm256_maddubs_epi16(aw, _mm256_sign_epi8(x2, w)), ones);
5574            acc1 += hsum(d1) as f32 * s;
5575            acc2 += hsum(d2) as f32 * s;
5576        }
5577        (acc1, acc2)
5578    }
5579}
5580
5581/// One q8 row range via AVX2 (x86 mirror of `q8_range_sdot`).
5582#[cfg(target_arch = "x86_64")]
5583fn q8_range_avx2(
5584    q: &[u8],
5585    row_scale: &[f32],
5586    act: &SplitAct,
5587    cols: usize,
5588    out_addr: SendMut,
5589    start: usize,
5590    end: usize,
5591) {
5592    for o in start..end {
5593        let v = row_dot_avx2(&q[o * cols..(o + 1) * cols], act) * row_scale[o];
5594        // SAFETY: disjoint row ranges per worker.
5595        unsafe { *out_addr.at(o) = v };
5596    }
5597}
5598
5599/// Two-input q8 row range via AVX2 (x86 mirror of `q8_range2_sdot`).
5600#[cfg(target_arch = "x86_64")]
5601#[allow(clippy::too_many_arguments)]
5602fn q8_range2_avx2(
5603    q: &[u8],
5604    row_scale: &[f32],
5605    a1: &SplitAct,
5606    a2: &SplitAct,
5607    cols: usize,
5608    p1: SendMut,
5609    p2: SendMut,
5610    start: usize,
5611    end: usize,
5612) {
5613    for o in start..end {
5614        let row = &q[o * cols..(o + 1) * cols];
5615        // SAFETY: disjoint row ranges per worker.
5616        unsafe {
5617            *p1.at(o) = row_dot_avx2(row, a1) * row_scale[o];
5618            *p2.at(o) = row_dot_avx2(row, a2) * row_scale[o];
5619        }
5620    }
5621}
5622
5623// ───────────────────── A8W8 SDOT path (port of vmfcore, ×1.78 decode) ─────────────────────
5624
5625/// ARMv8.6 i8mm (smmla): 32 int8 MACs per instruction vs sdot's 16 —
5626/// yet MEASURED 2.4× SLOWER than the blocked sdot on Apple silicon
5627/// (108 vs 264 GF/s): the on-the-fly vcombine packing and the two-
5628/// accumulator dependency chain swamp the MAC advantage, and Apple's
5629/// four SIMD pipes already keep sdot fed. OPT-IN (CMF_I8MM=1) for
5630/// field trials on Cortex-A710/X-class parts with two pipes, where the
5631/// balance may differ; a pre-interleaved weight layout (repack infra)
5632/// is the known path if it ever earns its keep.
5633#[cfg(target_arch = "aarch64")]
5634fn i8mm_enabled() -> bool {
5635    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5636    *ON.get_or_init(|| {
5637        std::env::var("CMF_I8MM").map(|v| v == "1").unwrap_or(false)
5638            && std::arch::is_aarch64_feature_detected!("i8mm")
5639    })
5640}
5641
5642/// SDOT enabled? Default ON when the CPU has ARMv8.2 dotprod;
5643/// `CMF_SDOT=0` disables (falls back to i8×f32 NEON).
5644/// (On non-ARM release builds only the test tolerance switch calls it.)
5645#[cfg_attr(not(target_arch = "aarch64"), allow(dead_code))]
5646fn sdot_enabled() -> bool {
5647    use std::sync::OnceLock;
5648    static ON: OnceLock<bool> = OnceLock::new();
5649    *ON.get_or_init(|| {
5650        let want = std::env::var("CMF_SDOT").map(|v| v != "0").unwrap_or(true);
5651        if !want { return false; }
5652        
5653        #[cfg(target_arch = "aarch64")]
5654        {
5655            if std::arch::is_aarch64_feature_detected!("dotprod") {
5656                return true;
5657            }
5658            #[cfg(target_os = "android")]
5659            {
5660                if let Ok(cpuinfo) = std::fs::read_to_string("/proc/cpuinfo") {
5661                    if cpuinfo.lines().any(|l| (l.starts_with("Features") || l.starts_with("features")) && l.contains("asimddp")) {
5662                        return true;
5663                    }
5664                }
5665            }
5666            false
5667        }
5668        #[cfg(not(target_arch = "aarch64"))]
5669        {
5670            false
5671        }
5672    })
5673}
5674
5675/// Two-field activation split (≡ vmfcore `q8_split_prep`): outlier
5676/// channels (>8·rms) are computed exactly in f32; the bulk (outliers
5677/// zeroed → clean absmax) goes through int8 SDOT. Computed ONCE per
5678/// matvec, shared by all rows/workers.
5679struct SplitAct {
5680    xq: Vec<i8>,
5681    sx: f32,
5682    outliers: Vec<(usize, f32)>,
5683    /// Σ xq — the VNNI bias-trick correction (`(w+128)·x` sums need
5684    /// `−128·Σx`); one i32 per split, computed once per matvec.
5685    #[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
5686    xsum: i32,
5687}
5688
5689thread_local! {
5690    /// Recycled xq buffers: split_act runs for every matvec (~200/token)
5691    /// and its hidden-size allocation was steady-state heap churn.
5692    static XQ_FREE: std::cell::RefCell<Vec<Vec<i8>>> =
5693        const { std::cell::RefCell::new(Vec::new()) };
5694}
5695
5696impl Drop for SplitAct {
5697    fn drop(&mut self) {
5698        let buf = std::mem::take(&mut self.xq);
5699        if buf.capacity() > 0 {
5700            XQ_FREE.with(|f| {
5701                let mut f = f.borrow_mut();
5702                if f.len() < 16 {
5703                    f.push(buf);
5704                }
5705            });
5706        }
5707    }
5708}
5709
5710fn split_act(x: &[f32]) -> SplitAct {
5711    let n = x.len();
5712    let rms = (x.iter().map(|&v| (v * v) as f64).sum::<f64>() / n.max(1) as f64).sqrt() as f32;
5713    let thr = 8.0 * rms;
5714    // One pass: collect outliers and the bulk absmax (outliers excluded —
5715    // identical to the old zero-then-fold over a copied buffer, minus the
5716    // full-vector copy).
5717    let mut outliers: Vec<(usize, f32)> = Vec::new();
5718    let mut amax = 0f32;
5719    for (j, &v) in x.iter().enumerate() {
5720        let a = v.abs();
5721        if a > thr {
5722            outliers.push((j, v));
5723        } else if a > amax {
5724            amax = a;
5725        }
5726    }
5727    let sx = if amax > 0.0 { amax / 127.0 } else { 1.0 };
5728    let inv = 1.0 / sx;
5729    let mut xq = XQ_FREE.with(|f| f.borrow_mut().pop()).unwrap_or_default();
5730    xq.clear();
5731    xq.reserve(n);
5732    if outliers.is_empty() {
5733        xq.extend(x.iter().map(|&v| (v * inv).round().clamp(-127.0, 127.0) as i8));
5734    } else {
5735        // Outlier slots quantize to 0 (their exact term is added later).
5736        xq.extend(x.iter().map(|&v| {
5737            if v.abs() > thr {
5738                0
5739            } else {
5740                (v * inv).round().clamp(-127.0, 127.0) as i8
5741            }
5742        }));
5743    }
5744    let xsum = xq.iter().map(|&v| v as i32).sum();
5745    SplitAct { xq, sx, outliers, xsum }
5746}
5747
5748fn split_act_q8_2f(x: &[f32], col: &[f32]) -> SplitAct {
5749    let n = x.len();
5750    let rms = (x.iter().zip(col).map(|(&a, &c)| { let v = a * c; (v * v) as f64 })
5751        .sum::<f64>() / n.max(1) as f64).sqrt() as f32;
5752    let thr = 8.0 * rms;
5753    
5754    let mut outliers = Vec::new();
5755    let mut amax = 0f32;
5756    for (j, (&a, &c)) in x.iter().zip(col).enumerate() {
5757        let v = a * c;
5758        let s = v.abs();
5759        if s > thr {
5760            outliers.push((j, v));
5761        } else if s > amax {
5762            amax = s;
5763        }
5764    }
5765    
5766    let sx = if amax > 0.0 { amax / 127.0 } else { 1.0 };
5767    let inv = 1.0 / sx;
5768    let mut xq = XQ_FREE.with(|f| f.borrow_mut().pop()).unwrap_or_default();
5769    xq.clear();
5770    xq.reserve(n);
5771    if outliers.is_empty() {
5772        xq.extend(x.iter().zip(col).map(|(&a, &c)| ((a * c) * inv).round().clamp(-127.0, 127.0) as i8));
5773    } else {
5774        xq.extend(x.iter().zip(col).map(|(&a, &c)| {
5775            let v = a * c;
5776            if v.abs() > thr { 0 } else { (v * inv).round().clamp(-127.0, 127.0) as i8 }
5777        }));
5778    }
5779    let xsum = xq.iter().map(|&v| v as i32).sum();
5780    SplitAct { xq, sx, outliers, xsum }
5781}
5782
5783/// int8(weight)·int8(activation) → i32 via `sdot` (inline asm — the
5784/// vdotq intrinsic is unstable; port of vmfcore `dot_i8_sdot`).
5785#[cfg(target_arch = "aarch64")]
5786#[target_feature(enable = "neon,dotprod")]
5787unsafe fn dot_i8_sdot(w: &[u8], xq: &[i8]) -> i32 {
5788    // SAFETY: callers uphold slice-length contracts (see call sites).
5789    unsafe {
5790        use core::arch::aarch64::*;
5791        use core::arch::asm;
5792        let wp = w.as_ptr() as *const i8;
5793        let n = w.len();
5794        let (mut a0, mut a1, mut a2, mut a3) =
5795            (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
5796        let mut i = 0;
5797        while i + 64 <= n {
5798            let (w0, x0) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
5799            let (w1, x1) = (vld1q_s8(wp.add(i + 16)), vld1q_s8(xq.as_ptr().add(i + 16)));
5800            let (w2, x2) = (vld1q_s8(wp.add(i + 32)), vld1q_s8(xq.as_ptr().add(i + 32)));
5801            let (w3, x3) = (vld1q_s8(wp.add(i + 48)), vld1q_s8(xq.as_ptr().add(i + 48)));
5802            asm!(
5803                "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
5804                "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
5805                "sdot {a2:v}.4s, {w2:v}.16b, {x2:v}.16b",
5806                "sdot {a3:v}.4s, {w3:v}.16b, {x3:v}.16b",
5807                a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
5808                w0 = in(vreg) w0, x0 = in(vreg) x0, w1 = in(vreg) w1, x1 = in(vreg) x1,
5809                w2 = in(vreg) w2, x2 = in(vreg) x2, w3 = in(vreg) w3, x3 = in(vreg) x3,
5810                options(pure, nomem, nostack),
5811            );
5812            i += 64;
5813        }
5814        while i + 16 <= n {
5815            let (wv, xv) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
5816            asm!("sdot {a:v}.4s, {w:v}.16b, {x:v}.16b",
5817                 a = inout(vreg) a0, w = in(vreg) wv, x = in(vreg) xv, options(pure, nomem, nostack));
5818            i += 16;
5819        }
5820        let mut s = vaddvq_s32(vaddq_s32(vaddq_s32(a0, a1), vaddq_s32(a2, a3)));
5821        while i < n {
5822            s += (*wp.add(i)) as i32 * xq[i] as i32;
5823            i += 1;
5824        }
5825        s
5826}
5827}
5828
5829/// Row-blocked SDOT: 4 output rows per pass — the activation chunk is
5830/// loaded once and reused, 4 independent accumulators hide sdot latency
5831/// (port of vmfcore `dot_i8_sdot_4rows`).
5832#[cfg(target_arch = "aarch64")]
5833#[target_feature(enable = "neon,dotprod")]
5834unsafe fn dot_i8_sdot_4rows(w0: &[u8], w1: &[u8], w2: &[u8], w3: &[u8], xq: &[i8]) -> [i32; 4] {
5835    // SAFETY: callers uphold slice-length contracts (see call sites).
5836    unsafe {
5837        use core::arch::aarch64::*;
5838        use core::arch::asm;
5839        let n = xq.len();
5840        let px = xq.as_ptr();
5841        let (p0, p1, p2, p3) = (
5842            w0.as_ptr() as *const i8,
5843            w1.as_ptr() as *const i8,
5844            w2.as_ptr() as *const i8,
5845            w3.as_ptr() as *const i8,
5846        );
5847        let (mut a0, mut a1, mut a2, mut a3) =
5848            (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
5849        let mut i = 0;
5850        while i + 16 <= n {
5851            let x = vld1q_s8(px.add(i));
5852            let v0 = vld1q_s8(p0.add(i));
5853            let v1 = vld1q_s8(p1.add(i));
5854            let v2 = vld1q_s8(p2.add(i));
5855            let v3 = vld1q_s8(p3.add(i));
5856            asm!(
5857                "sdot {a0:v}.4s, {v0:v}.16b, {x:v}.16b",
5858                "sdot {a1:v}.4s, {v1:v}.16b, {x:v}.16b",
5859                "sdot {a2:v}.4s, {v2:v}.16b, {x:v}.16b",
5860                "sdot {a3:v}.4s, {v3:v}.16b, {x:v}.16b",
5861                a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
5862                v0 = in(vreg) v0, v1 = in(vreg) v1, v2 = in(vreg) v2, v3 = in(vreg) v3, x = in(vreg) x,
5863                options(pure, nomem, nostack),
5864            );
5865            i += 16;
5866        }
5867        let mut r = [vaddvq_s32(a0), vaddvq_s32(a1), vaddvq_s32(a2), vaddvq_s32(a3)];
5868        while i < n {
5869            let xi = *px.add(i) as i32;
5870            r[0] += (*p0.add(i)) as i32 * xi;
5871            r[1] += (*p1.add(i)) as i32 * xi;
5872            r[2] += (*p2.add(i)) as i32 * xi;
5873            r[3] += (*p3.add(i)) as i32 * xi;
5874            i += 1;
5875        }
5876        r
5877}
5878}
5879
5880/// 4 interleaved rows in one pass: the repacked group is [r0[c], r1[c],
5881/// r2[c], r3[c]] per 16-byte chunk, so each iteration reads ONE 64-byte
5882/// line plus the shared activation chunk — a single sequential weight
5883/// stream per worker. Per-row accumulation is the same one-accumulator
5884/// scheme as `dot_i8_sdot_4rows`; integer sums are exact, so outputs
5885/// are bit-identical to the mmap-layout kernel.
5886#[cfg(target_arch = "aarch64")]
5887#[target_feature(enable = "neon,dotprod")]
5888unsafe fn dot_i8_sdot_4rows_il(g: &[u8], xq: &[i8]) -> [i32; 4] {
5889    // SAFETY: callers uphold slice-length contracts (g.len() == 4·n,
5890    // n % 16 == 0 — guaranteed by the repack gate).
5891    unsafe {
5892        use core::arch::aarch64::*;
5893        use core::arch::asm;
5894        let n = xq.len();
5895        let px = xq.as_ptr();
5896        let pg = g.as_ptr() as *const i8;
5897        let (mut a0, mut a1, mut a2, mut a3) =
5898            (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
5899        let mut i = 0;
5900        while i + 16 <= n {
5901            let x = vld1q_s8(px.add(i));
5902            let base = pg.add(4 * i);
5903            let v0 = vld1q_s8(base);
5904            let v1 = vld1q_s8(base.add(16));
5905            let v2 = vld1q_s8(base.add(32));
5906            let v3 = vld1q_s8(base.add(48));
5907            asm!(
5908                "sdot {a0:v}.4s, {v0:v}.16b, {x:v}.16b",
5909                "sdot {a1:v}.4s, {v1:v}.16b, {x:v}.16b",
5910                "sdot {a2:v}.4s, {v2:v}.16b, {x:v}.16b",
5911                "sdot {a3:v}.4s, {v3:v}.16b, {x:v}.16b",
5912                a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
5913                v0 = in(vreg) v0, v1 = in(vreg) v1, v2 = in(vreg) v2, v3 = in(vreg) v3, x = in(vreg) x,
5914                options(pure, nomem, nostack),
5915            );
5916            i += 16;
5917        }
5918        [vaddvq_s32(a0), vaddvq_s32(a1), vaddvq_s32(a2), vaddvq_s32(a3)]
5919    }
5920}
5921
5922/// One q8 row range via SDOT (4-row blocks + tail) — the body of
5923/// `qmatvec`'s hot loop, extracted so multi-matrix jobs can drive the
5924/// SAME kernel for several tensors under one pool dispatch. `rep` — the
5925/// load-time interleaved repack (empty = mmap layout only); rows outside
5926/// full 4-row groups always come from the mmap layout.
5927#[cfg(target_arch = "aarch64")]
5928fn q8_range_sdot(
5929    q: &[u8],
5930    rep: &[u8],
5931    row_scale: &[f32],
5932    act: &SplitAct,
5933    cols: usize,
5934    out_addr: SendMut,
5935    start: usize,
5936    end: usize,
5937) {
5938    let mut o = start;
5939    // Leading rows to the group boundary (repack path only): the pool
5940    // splits row ranges arbitrarily, groups are absolute.
5941    if !rep.is_empty() {
5942        while o < end && o % 4 != 0 {
5943            let v = row_dot_sdot(&q[o * cols..(o + 1) * cols], act) * row_scale[o];
5944            unsafe { *out_addr.at(o) = v };
5945            o += 1;
5946        }
5947    }
5948    while o + 4 <= end {
5949        let r = if rep.is_empty() {
5950            unsafe {
5951                dot_i8_sdot_4rows(
5952                    &q[o * cols..(o + 1) * cols],
5953                    &q[(o + 1) * cols..(o + 2) * cols],
5954                    &q[(o + 2) * cols..(o + 3) * cols],
5955                    &q[(o + 3) * cols..(o + 4) * cols],
5956                    &act.xq,
5957                )
5958            }
5959        } else {
5960            unsafe { dot_i8_sdot_4rows_il(&rep[o * cols..(o + 4) * cols], &act.xq) }
5961        };
5962        for k in 0..4 {
5963            let mut acc = r[k] as f32 * act.sx;
5964            for &(j, xv) in &act.outliers {
5965                acc += (q[(o + k) * cols + j] as i8) as f32 * xv;
5966            }
5967            // SAFETY: disjoint row ranges per worker.
5968            unsafe { *out_addr.at(o + k) = acc * row_scale[o + k] };
5969        }
5970        o += 4;
5971    }
5972    while o < end {
5973        let v = row_dot_sdot(&q[o * cols..(o + 1) * cols], act) * row_scale[o];
5974        unsafe { *out_addr.at(o) = v };
5975        o += 1;
5976    }
5977}
5978
5979/// Two-input q8 row range via SDOT — `qmatvec2`'s hot loop, extracted
5980/// for the fused pair multi-matrix job (`matvec2_many`).
5981#[cfg(target_arch = "aarch64")]
5982#[allow(clippy::too_many_arguments)]
5983fn q8_range2_sdot(
5984    q: &[u8],
5985    row_scale: &[f32],
5986    a1: &SplitAct,
5987    a2: &SplitAct,
5988    cols: usize,
5989    p1: SendMut,
5990    p2: SendMut,
5991    start: usize,
5992    end: usize,
5993) {
5994    for o in start..end {
5995        let row = &q[o * cols..(o + 1) * cols];
5996        // SAFETY: disjoint row ranges per worker.
5997        unsafe {
5998            *p1.at(o) = row_dot_sdot(row, a1) * row_scale[o];
5999            *p2.at(o) = row_dot_sdot(row, a2) * row_scale[o];
6000        }
6001    }
6002}
6003
6004/// Two-input q8 row range, f32 kernel (non-SDOT) — same extraction.
6005#[allow(clippy::too_many_arguments)]
6006fn q8_range2_f32(
6007    q: &[u8],
6008    row_scale: &[f32],
6009    x1: &[f32],
6010    x2: &[f32],
6011    cols: usize,
6012    p1: SendMut,
6013    p2: SendMut,
6014    start: usize,
6015    end: usize,
6016) {
6017    for o in start..end {
6018        let row = &q[o * cols..(o + 1) * cols];
6019        // SAFETY: disjoint row ranges per worker.
6020        unsafe {
6021            *p1.at(o) = dot_i8_f32(row, x1) * row_scale[o];
6022            *p2.at(o) = dot_i8_f32(row, x2) * row_scale[o];
6023        }
6024    }
6025}
6026
6027/// Scalar/NEON-f32 q8 row range (non-SDOT platforms) — same extraction.
6028fn q8_range_f32(
6029    q: &[u8],
6030    row_scale: &[f32],
6031    xs: &[f32],
6032    cols: usize,
6033    out_addr: SendMut,
6034    start: usize,
6035    end: usize,
6036) {
6037    for o in start..end {
6038        let v = dot_i8_f32(&q[o * cols..(o + 1) * cols], xs) * row_scale[o];
6039        // SAFETY: disjoint row ranges per worker.
6040        unsafe { *out_addr.at(o) = v };
6041    }
6042}
6043
6044/// SDOT row dot with exact outlier correction:
6045/// `dot = sdot(w, xq)·sx + Σ_outl w[j]·x[j]` (then × row_scale by caller).
6046#[cfg(target_arch = "aarch64")]
6047#[inline]
6048fn row_dot_sdot(row: &[u8], act: &SplitAct) -> f32 {
6049    let mut acc = unsafe { dot_i8_sdot(row, &act.xq) } as f32 * act.sx;
6050    for &(j, xv) in &act.outliers {
6051        acc += (row[j] as i8) as f32 * xv;
6052    }
6053    acc
6054}
6055
6056/// One q4 row via SDOT: each 32-group's nibbles unpack to centered i8
6057/// (nib−8 ∈ [−8,7]), int8×int8 `sdot` against the pre-quantized
6058/// activation group, × the group's f16 scale. Returns Σ_g dot_g·s_g;
6059/// the caller multiplies by the activation scale and adds the exact
6060/// outlier terms (port of vmfcore `dot_q4_block_sdot`, +23% measured).
6061/// Nibble order matches the writer: element 2k = low nibble, 2k+1 = high
6062/// → zip(lo,hi) restores flat order.
6063#[cfg(target_arch = "aarch64")]
6064#[target_feature(enable = "neon,dotprod")]
6065unsafe fn dot_q4_row_sdot(packed: &[u8], scales: &[u8], g0: usize, gpr: usize, xq: &[i8]) -> f32 {
6066    // SAFETY: callers uphold slice-length contracts (16 packed bytes and
6067    // 2 scale bytes per group; xq.len() == gpr·GROUP_SIZE).
6068    unsafe {
6069        use core::arch::aarch64::*;
6070        use core::arch::asm;
6071        let lomask = vdupq_n_u8(0x0F);
6072        let eight = vdupq_n_s8(8);
6073        let mut acc = 0f32;
6074        for gi in 0..gpr {
6075            let g = g0 + gi;
6076            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
6077            let b = vld1q_u8(packed.as_ptr().add(g * 16));
6078            let lo = vandq_u8(b, lomask);
6079            let hi = vshrq_n_u8::<4>(b);
6080            let e0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(lo, hi)), eight);
6081            let e1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(lo, hi)), eight);
6082            let x0 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE));
6083            let x1 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE + 16));
6084            let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
6085            asm!(
6086                "sdot {a0:v}.4s, {e0:v}.16b, {x0:v}.16b",
6087                "sdot {a1:v}.4s, {e1:v}.16b, {x1:v}.16b",
6088                a0 = inout(vreg) a0, a1 = inout(vreg) a1,
6089                e0 = in(vreg) e0, x0 = in(vreg) x0, e1 = in(vreg) e1, x1 = in(vreg) x1,
6090                options(pure, nomem, nostack),
6091            );
6092            acc += vaddvq_s32(vaddq_s32(a0, a1)) as f32 * s;
6093        }
6094        acc
6095    }
6096}
6097
6098/// Two-activation q4 row via SDOT: the nibble unpack (the expensive
6099/// part) happens ONCE per group; both pre-quantized activations are
6100/// dotted against the same centered i8 registers. Per-lane math matches
6101/// `dot_q4_row_sdot` exactly.
6102#[cfg(target_arch = "aarch64")]
6103#[target_feature(enable = "neon,dotprod")]
6104unsafe fn dot_q4_row_sdot2(
6105    packed: &[u8],
6106    scales: &[u8],
6107    g0: usize,
6108    gpr: usize,
6109    xq1: &[i8],
6110    xq2: &[i8],
6111) -> (f32, f32) {
6112    // SAFETY: callers uphold slice-length contracts (16 packed bytes and
6113    // 2 scale bytes per group; xq*.len() == gpr·GROUP_SIZE).
6114    unsafe {
6115        use core::arch::aarch64::*;
6116        use core::arch::asm;
6117        let lomask = vdupq_n_u8(0x0F);
6118        let eight = vdupq_n_s8(8);
6119        let (mut acc1, mut acc2) = (0f32, 0f32);
6120        for gi in 0..gpr {
6121            let g = g0 + gi;
6122            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
6123            let b = vld1q_u8(packed.as_ptr().add(g * 16));
6124            let lo = vandq_u8(b, lomask);
6125            let hi = vshrq_n_u8::<4>(b);
6126            let e0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(lo, hi)), eight);
6127            let e1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(lo, hi)), eight);
6128            let x10 = vld1q_s8(xq1.as_ptr().add(gi * GROUP_SIZE));
6129            let x11 = vld1q_s8(xq1.as_ptr().add(gi * GROUP_SIZE + 16));
6130            let x20 = vld1q_s8(xq2.as_ptr().add(gi * GROUP_SIZE));
6131            let x21 = vld1q_s8(xq2.as_ptr().add(gi * GROUP_SIZE + 16));
6132            let (mut a0, mut a1, mut b0, mut b1) =
6133                (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
6134            asm!(
6135                "sdot {a0:v}.4s, {e0:v}.16b, {x10:v}.16b",
6136                "sdot {a1:v}.4s, {e1:v}.16b, {x11:v}.16b",
6137                "sdot {b0:v}.4s, {e0:v}.16b, {x20:v}.16b",
6138                "sdot {b1:v}.4s, {e1:v}.16b, {x21:v}.16b",
6139                a0 = inout(vreg) a0, a1 = inout(vreg) a1,
6140                b0 = inout(vreg) b0, b1 = inout(vreg) b1,
6141                e0 = in(vreg) e0, e1 = in(vreg) e1,
6142                x10 = in(vreg) x10, x11 = in(vreg) x11,
6143                x20 = in(vreg) x20, x21 = in(vreg) x21,
6144                options(pure, nomem, nostack),
6145            );
6146            acc1 += vaddvq_s32(vaddq_s32(a0, a1)) as f32 * s;
6147            acc2 += vaddvq_s32(vaddq_s32(b0, b1)) as f32 * s;
6148        }
6149        (acc1, acc2)
6150    }
6151}
6152
6153// ───────────────────── fused int8 kernels ─────────────────────
6154
6155/// `acc += w · row` where the row is centered i8 — NEON widen+fma on
6156/// aarch64, scalar elsewhere. The KV-cache q8 value path rides on this.
6157#[inline]
6158pub(crate) fn axpy_i8_f32(acc: &mut [f32], row: &[i8], w: f32) {
6159    #[cfg(target_arch = "aarch64")]
6160    unsafe {
6161        return axpy_i8_f32_neon(acc, row, w);
6162    }
6163    #[cfg(target_arch = "x86_64")]
6164    if avx2_enabled() {
6165        return unsafe { axpy_i8_f32_avx2(acc, row, w) };
6166    }
6167    #[allow(unreachable_code)]
6168    {
6169        for (a, &b) in acc.iter_mut().zip(row) {
6170            *a += w * b as f32;
6171        }
6172    }
6173}
6174
6175/// i8→f32 axpy via AVX2/FMA (x86 mirror of `axpy_i8_f32_neon`).
6176#[cfg(target_arch = "x86_64")]
6177#[target_feature(enable = "avx2,fma")]
6178unsafe fn axpy_i8_f32_avx2(acc: &mut [f32], row: &[i8], w: f32) {
6179    // SAFETY: callers uphold slice-length contracts (see call sites).
6180    unsafe {
6181        use core::arch::x86_64::*;
6182        let n = acc.len().min(row.len());
6183        let ap = acc.as_mut_ptr();
6184        let rp = row.as_ptr();
6185        let wv = _mm256_set1_ps(w);
6186        let mut j = 0usize;
6187        while j + 16 <= n {
6188            let rb = _mm_loadu_si128(rp.add(j) as *const __m128i);
6189            let lo = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(rb));
6190            let hi = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(_mm_srli_si128::<8>(rb)));
6191            let v0 = _mm256_fmadd_ps(wv, lo, _mm256_loadu_ps(ap.add(j)));
6192            let v1 = _mm256_fmadd_ps(wv, hi, _mm256_loadu_ps(ap.add(j + 8)));
6193            _mm256_storeu_ps(ap.add(j), v0);
6194            _mm256_storeu_ps(ap.add(j + 8), v1);
6195            j += 16;
6196        }
6197        while j < n {
6198            *ap.add(j) += w * (*rp.add(j)) as f32;
6199            j += 1;
6200        }
6201    }
6202}
6203
6204#[cfg(target_arch = "aarch64")]
6205#[target_feature(enable = "neon")]
6206unsafe fn axpy_i8_f32_neon(acc: &mut [f32], row: &[i8], w: f32) {
6207    // SAFETY: callers uphold slice-length contracts (see call sites).
6208    unsafe {
6209        use core::arch::aarch64::*;
6210        let n = acc.len().min(row.len());
6211        let ap = acc.as_mut_ptr();
6212        let rp = row.as_ptr();
6213        let wv = vdupq_n_f32(w);
6214        let mut j = 0usize;
6215        while j + 16 <= n {
6216            let rb = vld1q_s8(rp.add(j));
6217            let lo = vmovl_s8(vget_low_s8(rb));
6218            let hi = vmovl_s8(vget_high_s8(rb));
6219            for (off, half) in [(0, lo), (8, hi)] {
6220                let f0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(half)));
6221                let f1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(half)));
6222                let o = j + off;
6223                vst1q_f32(ap.add(o), vfmaq_f32(vld1q_f32(ap.add(o)), wv, f0));
6224                vst1q_f32(ap.add(o + 4), vfmaq_f32(vld1q_f32(ap.add(o + 4)), wv, f1));
6225            }
6226            j += 16;
6227        }
6228        while j < n {
6229            *ap.add(j) += w * (*rp.add(j)) as f32;
6230            j += 1;
6231        }
6232}
6233}
6234
6235/// i8 row · f32 x. NEON on aarch64 (ported from vmfcore `dot_i8_f32_neon`,
6236/// ≈9× scalar), scalar elsewhere.
6237#[inline]
6238pub(crate) fn dot_i8_f32(w: &[u8], x: &[f32]) -> f32 {
6239    #[cfg(target_arch = "aarch64")]
6240    unsafe {
6241        return dot_i8_f32_neon(w, x);
6242    }
6243    #[cfg(target_arch = "x86_64")]
6244    if avx2_enabled() {
6245        return unsafe { dot_i8_f32_avx2(w, x) };
6246    }
6247    #[allow(unreachable_code)]
6248    {
6249        let mut sum = 0.0f32;
6250        for (j, &b) in w.iter().enumerate() {
6251            sum += (b as i8) as f32 * x[j];
6252        }
6253        sum
6254    }
6255}
6256
6257/// i8 row · (x ⊙ col_field) — the q8_2f row dot with the θ col-field
6258/// folded into the product (no prescaled copy of x). NEON on aarch64,
6259/// scalar elsewhere. Used by the active-neuron path `row_dot`.
6260#[inline]
6261fn dot_i8_col_f32(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
6262    #[cfg(target_arch = "aarch64")]
6263    unsafe {
6264        return dot_i8_col_f32_neon(w, x, col);
6265    }
6266    #[allow(unreachable_code)]
6267    {
6268        let mut sum = 0.0f32;
6269        for (j, &b) in w.iter().enumerate() {
6270            sum += (b as i8) as f32 * x[j] * col[j];
6271        }
6272        sum
6273    }
6274}
6275
6276#[cfg(target_arch = "aarch64")]
6277#[target_feature(enable = "neon")]
6278unsafe fn dot_i8_col_f32_neon(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
6279    // SAFETY: callers uphold slice-length contracts (see call sites).
6280    unsafe {
6281        use core::arch::aarch64::*;
6282        let n = x.len();
6283        let wp = w.as_ptr() as *const i8;
6284        let xp = x.as_ptr();
6285        let cp = col.as_ptr();
6286        let (mut a0, mut a1, mut a2, mut a3) =
6287            (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
6288        let mut j = 0usize;
6289        while j + 16 <= n {
6290            let wb = vld1q_s8(wp.add(j));
6291            let lo = vmovl_s8(vget_low_s8(wb));
6292            let hi = vmovl_s8(vget_high_s8(wb));
6293            let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
6294            let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
6295            let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
6296            let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
6297            a0 = vfmaq_f32(a0, w0, vmulq_f32(vld1q_f32(xp.add(j)), vld1q_f32(cp.add(j))));
6298            a1 = vfmaq_f32(a1, w1, vmulq_f32(vld1q_f32(xp.add(j + 4)), vld1q_f32(cp.add(j + 4))));
6299            a2 = vfmaq_f32(a2, w2, vmulq_f32(vld1q_f32(xp.add(j + 8)), vld1q_f32(cp.add(j + 8))));
6300            a3 = vfmaq_f32(a3, w3, vmulq_f32(vld1q_f32(xp.add(j + 12)), vld1q_f32(cp.add(j + 12))));
6301            j += 16;
6302        }
6303        let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
6304        while j < n {
6305            sum += (*wp.add(j)) as f32 * *xp.add(j) * *cp.add(j);
6306            j += 1;
6307        }
6308        sum
6309}
6310}
6311
6312#[cfg(target_arch = "aarch64")]
6313#[target_feature(enable = "neon")]
6314unsafe fn dot_i8_f32_neon(w: &[u8], x: &[f32]) -> f32 {
6315    // SAFETY: callers uphold slice-length contracts (see call sites).
6316    unsafe {
6317        use core::arch::aarch64::*;
6318        let n = x.len();
6319        let wp = w.as_ptr() as *const i8;
6320        let xp = x.as_ptr();
6321        let (mut a0, mut a1, mut a2, mut a3) =
6322            (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
6323        let mut j = 0usize;
6324        while j + 16 <= n {
6325            let wb = vld1q_s8(wp.add(j));
6326            let lo = vmovl_s8(vget_low_s8(wb));
6327            let hi = vmovl_s8(vget_high_s8(wb));
6328            let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
6329            let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
6330            let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
6331            let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
6332            a0 = vfmaq_f32(a0, w0, vld1q_f32(xp.add(j)));
6333            a1 = vfmaq_f32(a1, w1, vld1q_f32(xp.add(j + 4)));
6334            a2 = vfmaq_f32(a2, w2, vld1q_f32(xp.add(j + 8)));
6335            a3 = vfmaq_f32(a3, w3, vld1q_f32(xp.add(j + 12)));
6336            j += 16;
6337        }
6338        let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
6339        while j < n {
6340            sum += (*wp.add(j)) as f32 * *xp.add(j);
6341            j += 1;
6342        }
6343        sum
6344}
6345}
6346
6347#[allow(clippy::too_many_arguments)]
6348fn qmatvec(
6349    q: &[u8],
6350    rep: &[u8],
6351    row_scale: &[f32],
6352    x: &[f32],
6353    col_field: &[f32],
6354    dtype: TensorDtype,
6355    rows: usize,
6356    cols: usize,
6357    out: &mut [f32],
6358    pool: Option<&Pool>,
6359) {
6360    debug_assert_eq!(out.len(), rows);
6361    #[cfg(not(target_arch = "aarch64"))]
6362    let _ = rep;
6363
6364    #[cfg(target_arch = "aarch64")]
6365    if sdot_enabled() {
6366        let act = if dtype == TensorDtype::Q8_2f {
6367            split_act_q8_2f(x, col_field)
6368        } else {
6369            split_act(x)
6370        };
6371        let out_addr = SendMut(out.as_mut_ptr());
6372        let run_range = |start: usize, end: usize| {
6373            q8_range_sdot(q, rep, row_scale, &act, cols, out_addr, start, end)
6374        };
6375        match pool {
6376            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
6377            _ => run_range(0, rows),
6378        }
6379        return;
6380    }
6381    // x86 A8W8 via AVX2 maddubs — same quantized-activation contract as
6382    // the SDOT path (CMF_AVX2=0 keeps the exact i8×f32 loop).
6383    #[cfg(target_arch = "x86_64")]
6384    if avx2_a8w8_enabled() {
6385        let act = if dtype == TensorDtype::Q8_2f {
6386            split_act_q8_2f(x, col_field)
6387        } else {
6388            split_act(x)
6389        };
6390        let out_addr = SendMut(out.as_mut_ptr());
6391        let run_range =
6392            |start: usize, end: usize| q8_range_avx2(q, row_scale, &act, cols, out_addr, start, end);
6393        match pool {
6394            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
6395            _ => run_range(0, rows),
6396        }
6397        return;
6398    }
6399
6400    prescale_with(x, col_field, dtype, 1, |xs| {
6401        let out_addr = SendMut(out.as_mut_ptr());
6402        let run_range = move |start: usize, end: usize| {
6403            for o in start..end {
6404                let v = dot_i8_f32(&q[o * cols..(o + 1) * cols], xs) * row_scale[o];
6405                // SAFETY: disjoint row ranges per worker.
6406                unsafe { *out_addr.at(o) = v };
6407            }
6408        };
6409        match pool {
6410            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
6411            _ => run_range(0, rows),
6412        }
6413    });
6414}
6415
6416#[allow(clippy::too_many_arguments)]
6417fn qmatvec2(
6418    q: &[u8],
6419    row_scale: &[f32],
6420    x1: &[f32],
6421    x2: &[f32],
6422    col_field: &[f32],
6423    dtype: TensorDtype,
6424    rows: usize,
6425    cols: usize,
6426    o1: &mut [f32],
6427    o2: &mut [f32],
6428    pool: Option<&Pool>,
6429) {
6430    #[cfg(target_arch = "aarch64")]
6431    if sdot_enabled() {
6432        let a1s = if dtype == TensorDtype::Q8_2f { split_act_q8_2f(x1, col_field) } else { split_act(x1) };
6433        let a2s = if dtype == TensorDtype::Q8_2f { split_act_q8_2f(x2, col_field) } else { split_act(x2) };
6434        let p1 = SendMut(o1.as_mut_ptr());
6435        let p2 = SendMut(o2.as_mut_ptr());
6436        let run_range = |start: usize, end: usize| {
6437            q8_range2_sdot(q, row_scale, &a1s, &a2s, cols, p1, p2, start, end)
6438        };
6439        match pool {
6440            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
6441            _ => run_range(0, rows),
6442        }
6443        return;
6444    }
6445    #[cfg(target_arch = "x86_64")]
6446    if avx2_a8w8_enabled() {
6447        let a1s = if dtype == TensorDtype::Q8_2f { split_act_q8_2f(x1, col_field) } else { split_act(x1) };
6448        let a2s = if dtype == TensorDtype::Q8_2f { split_act_q8_2f(x2, col_field) } else { split_act(x2) };
6449        let p1 = SendMut(o1.as_mut_ptr());
6450        let p2 = SendMut(o2.as_mut_ptr());
6451        let run_range = |start: usize, end: usize| {
6452            q8_range2_avx2(q, row_scale, &a1s, &a2s, cols, p1, p2, start, end)
6453        };
6454        match pool {
6455            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
6456            _ => run_range(0, rows),
6457        }
6458        return;
6459    }
6460
6461    prescale_with(x1, col_field, dtype, 1, |x1s| {
6462        prescale_with(x2, col_field, dtype, 2, |x2s| {
6463            let p1 = SendMut(o1.as_mut_ptr());
6464            let p2 = SendMut(o2.as_mut_ptr());
6465            let run_range = move |start: usize, end: usize| {
6466                for o in start..end {
6467                    let row = &q[o * cols..(o + 1) * cols];
6468                    let s1 = dot_i8_f32(row, x1s) * row_scale[o];
6469                    let s2 = dot_i8_f32(row, x2s) * row_scale[o];
6470                    // SAFETY: disjoint row ranges per worker.
6471                    unsafe {
6472                        *p1.at(o) = s1;
6473                        *p2.at(o) = s2;
6474                    }
6475                }
6476            };
6477            match pool {
6478                Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
6479                _ => run_range(0, rows),
6480            }
6481        });
6482    });
6483}
6484
6485#[derive(Clone, Copy)]
6486struct SendMut(*mut f32);
6487unsafe impl Send for SendMut {}
6488unsafe impl Sync for SendMut {}
6489
6490impl SendMut {
6491    #[inline]
6492    fn at(self, i: usize) -> *mut f32 {
6493        unsafe { self.0.add(i) }
6494    }
6495}
6496
6497#[cfg(test)]
6498mod tests {
6499    use super::*;
6500
6501    #[test]
6502    fn f32_matvec_matches_matvec_rows_bitexact() {
6503        let (rows, cols) = (300, 40);
6504        let w: Vec<f32> = (0..rows * cols).map(|i| (i as f32 * 0.017).sin()).collect();
6505        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.05).cos()).collect();
6506        let qt = QTensor::from_f32(w.clone(), rows, cols);
6507
6508        let mut a = vec![0.0f32; rows];
6509        matvec_rows(None, &w, &x, &mut a);
6510        let mut b = vec![0.0f32; rows];
6511        qt.matvec(&x, &mut b, None);
6512        assert_eq!(a, b);
6513    }
6514
6515    #[test]
6516    fn sdot_kernel_exact_on_grid() {
6517        // Activations already on the i8 grid (±1 with amax=1 → sx=1/127,
6518        // xq=±127 dequantizes EXACTLY) → the SDOT path must match the
6519        // exact f32 dot to float rounding. This isolates kernel
6520        // correctness from quantization noise.
6521        eprintln!("sdot_enabled = {}", sdot_enabled());
6522        let (rows, cols) = (9, 80); // odd rows → exercises 4-row + tail
6523        let w: Vec<u8> = (0..rows * cols)
6524            .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
6525            .collect();
6526        let scales: Vec<f32> = (0..rows).map(|o| 0.005 + o as f32 * 0.001).collect();
6527        let x: Vec<f32> = (0..cols)
6528            .map(|i| match i % 3 {
6529                0 => 1.0,
6530                1 => -1.0,
6531                _ => 0.0,
6532            })
6533            .collect();
6534        let mut a = vec![0.0f32; rows];
6535        qmatvec(&w, &[], &scales, &x, &[], TensorDtype::Q8Row, rows, cols, &mut a, None);
6536        for o in 0..rows {
6537            let mut acc = 0.0f32;
6538            for j in 0..cols {
6539                acc += (w[o * cols + j] as i8) as f32 * x[j];
6540            }
6541            let expect = acc * scales[o];
6542            assert!(
6543                (a[o] - expect).abs() < 1e-3 * expect.abs().max(1e-3),
6544                "row {o}: {} vs {expect}",
6545                a[o]
6546            );
6547        }
6548    }
6549
6550    #[test]
6551    fn q1_tbl_fast_path_matches_reference() {
6552        // gpr = 8 exercises the TBL pair-load fast loop, and the LAST
6553        // row's final 4-tile window trips the 4B-overread guard (the
6554        // payload ends exactly at the last tile) — both paths must
6555        // agree with the dequant reference.
6556        let (rows, cols) = (5, 256);
6557        let gpr = cols / GROUP_SIZE;
6558        let mut bytes = Vec::new();
6559        for t in 0..rows * gpr {
6560            let s = 0.007 + (t % 11) as f32 * 0.004;
6561            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
6562            for j in 0..4 {
6563                bytes.push(((t * 53 + j * 89 + 7) % 249) as u8);
6564            }
6565        }
6566        let x: Vec<f32> = (0..cols)
6567            .map(|i| if (i * 5) % 7 < 3 { 1.0 } else { -1.0 })
6568            .collect();
6569        let mut w = vec![0.0f32; rows * cols];
6570        cortiq_core::quant::dequant_q1(&bytes, &mut w);
6571        let mut got = vec![0.0f32; rows];
6572        q1_matvec(&bytes, &x, rows, cols, &mut got, None);
6573        for o in 0..rows {
6574            let expect: f32 = (0..cols).map(|j| w[o * cols + j] * x[j]).sum();
6575            assert!(
6576                (got[o] - expect).abs() < 1e-3 * expect.abs().max(1e-3),
6577                "row {o}: {} vs {expect}",
6578                got[o]
6579            );
6580        }
6581        // Blocked 1×4 batch (b=5: one quad + remainder) must equal the
6582        // single-matvec path bit-for-bit.
6583        let b = 5usize;
6584        let mut xs_all = Vec::new();
6585        for bi in 0..b {
6586            xs_all.extend(x.iter().map(|v| if bi % 2 == 0 { *v } else { -*v }));
6587        }
6588        let mut mm = vec![0.0f32; b * rows];
6589        q1_matmat(&bytes, &xs_all, b, rows, cols, &mut mm, None);
6590        for bi in 0..b {
6591            let mut single = vec![0.0f32; rows];
6592            q1_matvec(&bytes, &xs_all[bi * cols..(bi + 1) * cols], rows, cols, &mut single, None);
6593            assert_eq!(&mm[bi * rows..(bi + 1) * rows], &single[..], "stream {bi}");
6594        }
6595    }
6596
6597    #[test]
6598    fn q1_kernels_match_exact_reference() {
6599        // Synthetic q1 payload: 6-byte tiles [f16 scale][4B bits].
6600        let (rows, cols) = (7, 96);
6601        let gpr = cols / GROUP_SIZE;
6602        let mut bytes = Vec::new();
6603        for t in 0..rows * gpr {
6604            let s = 0.01 + (t % 13) as f32 * 0.003;
6605            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
6606            for j in 0..4 {
6607                bytes.push(((t * 31 + j * 97) % 251) as u8);
6608            }
6609        }
6610        // On-grid activations (±1, amax 1) → the SDOT path is exact.
6611        let x: Vec<f32> = (0..cols)
6612            .map(|i| if i % 3 == 0 { 1.0 } else { -1.0 })
6613            .collect();
6614        // Reference through the core dequant.
6615        let mut w = vec![0.0f32; rows * cols];
6616        cortiq_core::quant::dequant_q1(&bytes, &mut w);
6617        let mut expect = vec![0.0f32; rows];
6618        for o in 0..rows {
6619            expect[o] = (0..cols).map(|j| w[o * cols + j] * x[j]).sum();
6620        }
6621        let mut got = vec![0.0f32; rows];
6622        q1_matvec(&bytes, &x, rows, cols, &mut got, None);
6623        for o in 0..rows {
6624            assert!(
6625                (got[o] - expect[o]).abs() < 1e-3 * expect[o].abs().max(1e-3),
6626                "row {o}: {} vs {}",
6627                got[o],
6628                expect[o]
6629            );
6630        }
6631        // Pair and batch paths agree with the single path.
6632        let x2: Vec<f32> = x.iter().map(|v| -v).collect();
6633        let (mut a1, mut a2) = (vec![0.0f32; rows], vec![0.0f32; rows]);
6634        q1_matvec2(&bytes, &x, &x2, rows, cols, &mut a1, &mut a2, None);
6635        assert_eq!(a1, got);
6636        let mut xs = x.clone();
6637        xs.extend_from_slice(&x2);
6638        let mut mm = vec![0.0f32; 2 * rows];
6639        q1_matmat(&bytes, &xs, 2, rows, cols, &mut mm, None);
6640        assert_eq!(&mm[..rows], got.as_slice());
6641        assert_eq!(&mm[rows..], a2.as_slice());
6642    }
6643
6644    #[test]
6645    fn repack_is_bit_identical() {
6646        // The interleaved-repack kernel must produce EXACTLY the same
6647        // bits as the mmap-layout kernel: integer accumulation is order-
6648        // exact, the f32 epilogue is identical. Odd rows exercise the
6649        // tail; direct range calls exercise unaligned pool splits.
6650        let (rows, cols) = (267, 96); // 66 groups + 3 tail rows, cols % 16 == 0
6651        let w: Vec<u8> = (0..rows * cols)
6652            .map(|i| (((i * 89) % 253) as i32 - 126) as i8 as u8)
6653            .collect();
6654        let scales: Vec<f32> = (0..rows).map(|o| 0.003 + o as f32 * 0.0007).collect();
6655        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.37).sin() * 2.0).collect();
6656        let rep = q8_repack_layout(&w, rows, cols);
6657        // Group interleave round-trips.
6658        for g in 0..rows / 4 {
6659            for c in 0..cols / 16 {
6660                for lane in 0..4 {
6661                    assert_eq!(
6662                        &rep[g * 4 * cols + c * 64 + lane * 16..g * 4 * cols + c * 64 + lane * 16 + 16],
6663                        &w[(g * 4 + lane) * cols + c * 16..(g * 4 + lane) * cols + c * 16 + 16],
6664                    );
6665                }
6666            }
6667        }
6668        let mut a = vec![0.0f32; rows];
6669        qmatvec(&w, &[], &scales, &x, &[], TensorDtype::Q8Row, rows, cols, &mut a, None);
6670        let mut b = vec![0.0f32; rows];
6671        qmatvec(&w, &rep, &scales, &x, &[], TensorDtype::Q8Row, rows, cols, &mut b, None);
6672        assert_eq!(a, b, "full-range repack output diverged");
6673
6674        #[cfg(target_arch = "aarch64")]
6675        if sdot_enabled() {
6676            // Unaligned range split (pool workers get arbitrary bounds).
6677            let act = split_act(&x);
6678            let mut c1 = vec![0.0f32; rows];
6679            let mut c2 = vec![0.0f32; rows];
6680            q8_range_sdot(&w, &[], &scales, &act, cols, SendMut(c1.as_mut_ptr()), 3, rows - 2);
6681            q8_range_sdot(&w, &rep, &scales, &act, cols, SendMut(c2.as_mut_ptr()), 3, rows - 2);
6682            assert_eq!(c1, c2, "unaligned-range repack output diverged");
6683        }
6684    }
6685
6686    #[test]
6687    fn sdot_a8w8_noise_is_bounded() {
6688        // Off-grid activations: A8 quantization noise must stay small in
6689        // relative L2 over the whole output (realistic accuracy contract;
6690        // vmfcore measured argmax-identical decode on real models).
6691        let (rows, cols) = (16, 512);
6692        let w: Vec<u8> = (0..rows * cols)
6693            .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
6694            .collect();
6695        let scales = vec![0.01f32; rows];
6696        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.21).sin()).collect();
6697        let mut a = vec![0.0f32; rows];
6698        qmatvec(&w, &[], &scales, &x, &[], TensorDtype::Q8Row, rows, cols, &mut a, None);
6699        let (mut num, mut den) = (0f64, 0f64);
6700        for o in 0..rows {
6701            let mut acc = 0.0f32;
6702            for j in 0..cols {
6703                acc += (w[o * cols + j] as i8) as f32 * x[j];
6704            }
6705            let expect = acc * scales[o];
6706            num += ((a[o] - expect) as f64).powi(2);
6707            den += (expect as f64).powi(2);
6708        }
6709        let rel = (num / den.max(1e-12)).sqrt();
6710        assert!(rel < 0.05, "A8W8 relative L2 error too high: {rel}");
6711    }
6712
6713    #[test]
6714    fn i8_dot_neon_matches_scalar() {
6715        let n = 100;
6716        let w: Vec<u8> = (0..n).map(|i| ((i * 37 + 11) % 251) as u8).collect();
6717        let x: Vec<f32> = (0..n).map(|i| (i as f32 * 0.13).sin()).collect();
6718        let mut scalar = 0.0f32;
6719        for j in 0..n {
6720            scalar += (w[j] as i8) as f32 * x[j];
6721        }
6722        let fast = dot_i8_f32(&w, &x);
6723        assert!((scalar - fast).abs() < 1e-3 * scalar.abs().max(1.0));
6724    }
6725
6726    /// Fused vbit matvec must match full dequant_vbit + dense matvec.
6727    #[test]
6728    fn vbitmatvec_matches_full_dequant() {
6729        let (rows, cols) = (6, 64);
6730        let ng = cols / GROUP_SIZE;
6731        // Hand-craft: bits per row, f16 scales, packed rows.
6732        let bits: Vec<u8> = vec![3, 4, 5, 6, 8, 4];
6733        let mut bytes = bits.clone();
6734        for g in 0..rows * ng {
6735            let s = 0.02 + 0.001 * g as f32;
6736            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
6737        }
6738        for r in 0..rows {
6739            let b = bits[r] as usize;
6740            let (mut acc, mut nb) = (0u64, 0usize);
6741            let mut rowbytes = Vec::new();
6742            for i in 0..cols {
6743                let v = ((i * 7 + r * 13) % (1 << b)) as u64;
6744                acc = (acc << b) | v;
6745                nb += b;
6746                while nb >= 8 {
6747                    nb -= 8;
6748                    rowbytes.push(((acc >> nb) & 0xFF) as u8);
6749                }
6750            }
6751            if nb > 0 {
6752                rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
6753            }
6754            bytes.extend_from_slice(&rowbytes);
6755        }
6756        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.19).sin()).collect();
6757
6758        let mut reference = vec![0f32; rows * cols];
6759        cortiq_core::quant::dequant_vbit(&bytes, rows, cols, &mut reference).unwrap();
6760        let mut expect = vec![0f32; rows];
6761        for r in 0..rows {
6762            expect[r] = reference[r * cols..(r + 1) * cols]
6763                .iter()
6764                .zip(&x)
6765                .map(|(w, xv)| w * xv)
6766                .sum();
6767        }
6768        let mut got = vec![0f32; rows];
6769        let offsets = vbit_row_offsets(&bytes, rows, cols);
6770        vbitmatvec(&bytes, &offsets, &x, rows, cols, &mut got, None);
6771        // SDOT path quantizes activations to i8 (A8W8): bounded noise,
6772        // same contract as q8 (exact path is pinned by CMF_SDOT=0 in
6773        // the golden-parity gate).
6774        let tol = if a8w8_enabled() { 6e-2 } else { 1e-4 };
6775        let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1e-6);
6776        for r in 0..rows {
6777            assert!(
6778                (got[r] - expect[r]).abs() < tol * scale,
6779                "row {r}: {} vs {}",
6780                got[r],
6781                expect[r]
6782            );
6783        }
6784    }
6785
6786    /// Fused q4 matvec must match the reference full-dequant + dense
6787    /// matvec bit-for-bit in structure (same f32 math, group order).
6788    /// vbit matmat: the blocked 1×4 leg must match the per-row path
6789    /// (paired env toggle; larger shape so both code paths engage).
6790    #[test]
6791    #[cfg(target_arch = "x86_64")]
6792    fn vbit_matmat_blocked_matches_per_row() {
6793        let (rows, cols, b) = (64usize, 128usize, 9usize);
6794        let ng = cols / GROUP_SIZE;
6795        let bits: Vec<u8> = (0..rows).map(|r| [3u8, 4, 5, 6][r % 4]).collect();
6796        let mut bytes = bits.clone();
6797        for g in 0..rows * ng {
6798            let sc = 0.02 + 0.0005 * g as f32;
6799            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(sc).to_le_bytes());
6800        }
6801        for r in 0..rows {
6802            let bw = bits[r] as usize;
6803            let (mut acc, mut nb) = (0u64, 0usize);
6804            let mut rowbytes = Vec::new();
6805            for i in 0..cols {
6806                let v = ((i * 7 + r * 13) % (1 << bw)) as u64;
6807                acc = (acc << bw) | v;
6808                nb += bw;
6809                while nb >= 8 {
6810                    nb -= 8;
6811                    rowbytes.push(((acc >> nb) & 0xFF) as u8);
6812                }
6813            }
6814            if nb > 0 {
6815                rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
6816            }
6817            bytes.extend_from_slice(&rowbytes);
6818        }
6819        let x: Vec<f32> =
6820            (0..b * cols).map(|i| ((i * 13 + 7) % 97) as f32 / 97.0 - 0.5).collect();
6821        let offsets = vbit_row_offsets(&bytes, rows, cols);
6822        let mut y_a = vec![0f32; b * rows];
6823        let mut y_b = vec![0f32; b * rows];
6824        unsafe { std::env::set_var("CMF_X86_BLOCKED", "1") };
6825        vbitmatmat(&bytes, &offsets, &x, b, rows, cols, &mut y_a, None);
6826        unsafe { std::env::set_var("CMF_X86_BLOCKED", "0") };
6827        vbitmatmat(&bytes, &offsets, &x, b, rows, cols, &mut y_b, None);
6828        unsafe { std::env::remove_var("CMF_X86_BLOCKED") };
6829        let max_d =
6830            y_a.iter().zip(&y_b).map(|(p, q)| (p - q).abs()).fold(0.0f32, f32::max);
6831        assert!(max_d < 1e-4, "vbit blocked ≠ per-row: max|Δ| = {max_d}");
6832    }
6833
6834    #[test]
6835    fn q4matvec_matches_full_dequant() {
6836        let (rows, cols) = (8, 64);
6837        let groups = rows * cols / GROUP_SIZE;
6838        // Hand-craft a q4_block blob: nibbles then f16 scales.
6839        let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
6840        for i in 0..groups * 16 {
6841            bytes.push((((i * 7 + 3) % 256) & 0xFF) as u8);
6842        }
6843        for g in 0..groups {
6844            let s = 0.01 + 0.003 * g as f32;
6845            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
6846        }
6847        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.17).sin()).collect();
6848
6849        let mut reference = vec![0.0f32; rows * cols];
6850        cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
6851        let mut expect = vec![0.0f32; rows];
6852        for r in 0..rows {
6853            expect[r] = reference[r * cols..(r + 1) * cols]
6854                .iter()
6855                .zip(&x)
6856                .map(|(w, xv)| w * xv)
6857                .sum();
6858        }
6859
6860        let mut got = vec![0.0f32; rows];
6861        q4matvec(&bytes, &x, rows, cols, &mut got, None);
6862        // SDOT path quantizes activations to i8 (A8W8): bounded noise,
6863        // same contract as q8/vbit (exact path is pinned by CMF_SDOT=0
6864        // in the golden-parity gate).
6865        let tol = if a8w8_enabled() { 6e-2 } else { 1e-4 };
6866        let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
6867        for r in 0..rows {
6868            assert!(
6869                (got[r] - expect[r]).abs() < tol * scale,
6870                "row {r}: {} vs {}",
6871                got[r],
6872                expect[r]
6873            );
6874        }
6875    }
6876
6877    /// Fused two-input vbit matvec must equal two single matvecs exactly
6878    /// (same per-lane accumulation order on both scalar and SDOT paths).
6879    #[test]
6880    fn vbitmatvec2_equals_two_singles() {
6881        let (rows, cols) = (6, 64);
6882        let ng = cols / GROUP_SIZE;
6883        let bits: Vec<u8> = vec![3, 4, 5, 6, 8, 4];
6884        let mut bytes = bits.clone();
6885        for g in 0..rows * ng {
6886            let s = 0.02 + 0.001 * g as f32;
6887            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
6888        }
6889        for r in 0..rows {
6890            let b = bits[r] as usize;
6891            let (mut acc, mut nb) = (0u64, 0usize);
6892            let mut rowbytes = Vec::new();
6893            for i in 0..cols {
6894                let v = ((i * 7 + r * 13) % (1 << b)) as u64;
6895                acc = (acc << b) | v;
6896                nb += b;
6897                while nb >= 8 {
6898                    nb -= 8;
6899                    rowbytes.push(((acc >> nb) & 0xFF) as u8);
6900                }
6901            }
6902            if nb > 0 {
6903                rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
6904            }
6905            bytes.extend_from_slice(&rowbytes);
6906        }
6907        let x1: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.19).sin()).collect();
6908        let x2: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.11).cos()).collect();
6909        let offsets = vbit_row_offsets(&bytes, rows, cols);
6910
6911        let (mut a1, mut a2) = (vec![0f32; rows], vec![0f32; rows]);
6912        vbitmatvec(&bytes, &offsets, &x1, rows, cols, &mut a1, None);
6913        vbitmatvec(&bytes, &offsets, &x2, rows, cols, &mut a2, None);
6914        let (mut b1, mut b2) = (vec![0f32; rows], vec![0f32; rows]);
6915        vbitmatvec2(&bytes, &offsets, &x1, &x2, rows, cols, &mut b1, &mut b2, None);
6916        assert_eq!(a1, b1, "fused vbit lane 1 must be bit-identical");
6917        assert_eq!(a2, b2, "fused vbit lane 2 must be bit-identical");
6918    }
6919
6920    /// Fused two-input q4 matvec must equal two single matvecs exactly.
6921    #[test]
6922    fn q4matvec2_equals_two_singles() {
6923        let (rows, cols) = (8, 128);
6924        let groups = rows * cols / GROUP_SIZE;
6925        let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
6926        for i in 0..groups * 16 {
6927            bytes.push((((i * 7 + 3) % 256) & 0xFF) as u8);
6928        }
6929        for g in 0..groups {
6930            let s = 0.01 + 0.003 * g as f32;
6931            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
6932        }
6933        // Include an outlier channel so the SDOT correction path is
6934        // exercised in the pair kernel too.
6935        let mut x1: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.17).sin()).collect();
6936        x1[9] = 250.0;
6937        let x2: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.23).cos()).collect();
6938
6939        let (mut a1, mut a2) = (vec![0f32; rows], vec![0f32; rows]);
6940        q4matvec(&bytes, &x1, rows, cols, &mut a1, None);
6941        q4matvec(&bytes, &x2, rows, cols, &mut a2, None);
6942        let (mut b1, mut b2) = (vec![0f32; rows], vec![0f32; rows]);
6943        q4matvec2(&bytes, &x1, &x2, rows, cols, &mut b1, &mut b2, None);
6944        assert_eq!(a1, b1, "fused q4 lane 1 must be bit-identical");
6945        assert_eq!(a2, b2, "fused q4 lane 2 must be bit-identical");
6946    }
6947
6948    /// Multi-matrix job must equal separate matvecs exactly — same
6949    /// kernels, only the dispatch is fused.
6950    #[test]
6951    fn matvec_many_equals_separate_matvecs() {
6952        use crate::pool::Pool;
6953        let (r1, r2, cols) = (300, 200, 64);
6954        let mk = |salt: usize, rows: usize| {
6955            QTensor::from_f32(
6956                (0..rows * cols).map(|i| ((i * 7 + salt) % 97) as f32 / 97.0 - 0.5).collect(),
6957                rows,
6958                cols,
6959            )
6960        };
6961        let (a, b) = (mk(1, r1), mk(5, r2));
6962        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.11).sin()).collect();
6963        let pool = Pool::new(3);
6964
6965        let (mut ea, mut eb) = (vec![0f32; r1], vec![0f32; r2]);
6966        a.matvec(&x, &mut ea, Some(&pool));
6967        b.matvec(&x, &mut eb, Some(&pool));
6968        let (mut ga, mut gb) = (vec![0f32; r1], vec![0f32; r2]);
6969        QTensor::matvec_many([&a, &b], &x, [&mut ga, &mut gb], Some(&pool));
6970        assert_eq!(ea, ga, "fused multi-matrix lane 1 must be bit-identical");
6971        assert_eq!(eb, gb, "fused multi-matrix lane 2 must be bit-identical");
6972    }
6973
6974    /// Batched q4/vbit matmat must equal per-position matvec calls
6975    /// exactly (the fallback it replaced) — same kernels, same order.
6976    #[test]
6977    fn batched_matmat_equals_per_position_matvec() {
6978        let (rows, cols, b) = (8, 64, 5);
6979        // q4 blob.
6980        let groups = rows * cols / GROUP_SIZE;
6981        let mut q4 = Vec::new();
6982        for i in 0..groups * 16 {
6983            q4.push((((i * 7 + 3) % 256) & 0xFF) as u8);
6984        }
6985        for g in 0..groups {
6986            q4.extend_from_slice(
6987                &cortiq_core::quant::f32_to_f16(0.01 + 0.003 * g as f32).to_le_bytes(),
6988            );
6989        }
6990        // vbit blob (mixed widths incl. 8).
6991        let ng = cols / GROUP_SIZE;
6992        let bits: Vec<u8> = vec![3, 4, 5, 6, 8, 4, 5, 3];
6993        let mut vb = bits.clone();
6994        for g in 0..rows * ng {
6995            vb.extend_from_slice(
6996                &cortiq_core::quant::f32_to_f16(0.02 + 0.001 * g as f32).to_le_bytes(),
6997            );
6998        }
6999        for r in 0..rows {
7000            let bw = bits[r] as usize;
7001            let (mut acc, mut nb) = (0u64, 0usize);
7002            let mut rowbytes = Vec::new();
7003            for i in 0..cols {
7004                let v = ((i * 7 + r * 13) % (1 << bw)) as u64;
7005                acc = (acc << bw) | v;
7006                nb += bw;
7007                while nb >= 8 {
7008                    nb -= 8;
7009                    rowbytes.push(((acc >> nb) & 0xFF) as u8);
7010                }
7011            }
7012            if nb > 0 {
7013                rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
7014            }
7015            vb.extend_from_slice(&rowbytes);
7016        }
7017        let offsets = vbit_row_offsets(&vb, rows, cols);
7018
7019        let xs: Vec<f32> = (0..b * cols).map(|i| (i as f32 * 0.13).sin()).collect();
7020
7021        // q4: batch vs singles.
7022        let mut got = vec![0f32; b * rows];
7023        q4matmat(&q4, &xs, b, rows, cols, &mut got, None);
7024        for bi in 0..b {
7025            let mut expect = vec![0f32; rows];
7026            q4matvec(&q4, &xs[bi * cols..(bi + 1) * cols], rows, cols, &mut expect, None);
7027            assert_eq!(&got[bi * rows..(bi + 1) * rows], &expect[..], "q4 batch pos {bi}");
7028        }
7029
7030        // vbit: batch vs singles.
7031        let mut got = vec![0f32; b * rows];
7032        vbitmatmat(&vb, &offsets, &xs, b, rows, cols, &mut got, None);
7033        for bi in 0..b {
7034            let mut expect = vec![0f32; rows];
7035            vbitmatvec(
7036                &vb, &offsets, &xs[bi * cols..(bi + 1) * cols], rows, cols, &mut expect, None,
7037            );
7038            assert_eq!(&got[bi * rows..(bi + 1) * rows], &expect[..], "vbit batch pos {bi}");
7039        }
7040    }
7041
7042    /// q4_tiled kernels must produce BIT-identical outputs to the q4
7043    /// split kernels on the same values (same ints, same order — only
7044    /// the byte placement differs).
7045    #[test]
7046    fn q4_tiled_matches_q4_block_bitexact() {
7047        let (rows, cols, b) = (8usize, 128usize, 3usize);
7048        let groups = rows * cols / GROUP_SIZE;
7049        let mut split = Vec::with_capacity(groups * 18);
7050        for i in 0..groups * 16 {
7051            split.push((((i * 7 + 3) % 256) & 0xFF) as u8);
7052        }
7053        for g in 0..groups {
7054            split.extend_from_slice(
7055                &cortiq_core::quant::f32_to_f16(0.01 + 0.003 * g as f32).to_le_bytes(),
7056            );
7057        }
7058        // Re-tile: [scale][nibbles] per group.
7059        let (packed, scales) = split.split_at(groups * 16);
7060        let mut tiled = Vec::with_capacity(groups * Q4_TILE);
7061        for g in 0..groups {
7062            tiled.extend_from_slice(&scales[g * 2..g * 2 + 2]);
7063            tiled.extend_from_slice(&packed[g * 16..(g + 1) * 16]);
7064        }
7065
7066        let mut x1: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.17).sin()).collect();
7067        x1[9] = 250.0; // exercise the outlier path
7068        let x2: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.23).cos()).collect();
7069
7070        let (mut a, mut t) = (vec![0f32; rows], vec![0f32; rows]);
7071        q4matvec(&split, &x1, rows, cols, &mut a, None);
7072        q4t_matvec(&tiled, &x1, rows, cols, &mut t, None);
7073        assert_eq!(a, t, "q4t matvec must match q4 bit-for-bit");
7074
7075        let (mut a1, mut a2) = (vec![0f32; rows], vec![0f32; rows]);
7076        let (mut t1, mut t2) = (vec![0f32; rows], vec![0f32; rows]);
7077        q4matvec2(&split, &x1, &x2, rows, cols, &mut a1, &mut a2, None);
7078        q4t_matvec2(&tiled, &x1, &x2, rows, cols, &mut t1, &mut t2, None);
7079        assert_eq!(a1, t1);
7080        assert_eq!(a2, t2);
7081
7082        let xs: Vec<f32> = (0..b * cols).map(|i| (i as f32 * 0.13).sin()).collect();
7083        let (mut am, mut tm) = (vec![0f32; b * rows], vec![0f32; b * rows]);
7084        q4matmat(&split, &xs, b, rows, cols, &mut am, None);
7085        q4t_matmat(&tiled, &xs, b, rows, cols, &mut tm, None);
7086        assert_eq!(am, tm, "q4t matmat must match q4 bit-for-bit");
7087    }
7088
7089    /// q4 SDOT outlier correction: a single huge activation channel
7090    /// (>8·rms → outlier, zeroed in xq) must still contribute its EXACT
7091    /// term. On-grid bulk (±1/0 → xq dequantizes exactly) isolates the
7092    /// correction from A8W8 noise. cols must exceed 64: at n=64 the
7093    /// 8·rms threshold equals sqrt(v²+rest) ≥ v, so a single outlier
7094    /// can never qualify (8² = n).
7095    #[test]
7096    fn q4matvec_sdot_outlier_exact() {
7097        let (rows, cols) = (4, 128);
7098        let groups = rows * cols / GROUP_SIZE;
7099        let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
7100        for i in 0..groups * 16 {
7101            bytes.push(((i * 11 + 5) % 256) as u8);
7102        }
7103        for g in 0..groups {
7104            let s = 0.02 + 0.002 * g as f32;
7105            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
7106        }
7107        let mut x: Vec<f32> = (0..cols)
7108            .map(|i| match i % 3 {
7109                0 => 1.0,
7110                1 => -1.0,
7111                _ => 0.0,
7112            })
7113            .collect();
7114        x[17] = 300.0; // ≫ 8·rms → outlier channel
7115
7116        let mut reference = vec![0.0f32; rows * cols];
7117        cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
7118        let mut expect = vec![0.0f32; rows];
7119        for r in 0..rows {
7120            expect[r] = reference[r * cols..(r + 1) * cols]
7121                .iter()
7122                .zip(&x)
7123                .map(|(w, xv)| w * xv)
7124                .sum();
7125        }
7126        let mut got = vec![0.0f32; rows];
7127        q4matvec(&bytes, &x, rows, cols, &mut got, None);
7128        let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
7129        for r in 0..rows {
7130            assert!(
7131                (got[r] - expect[r]).abs() < 2e-3 * scale,
7132                "row {r}: {} vs {} (outlier term must be exact)",
7133                got[r],
7134                expect[r]
7135            );
7136        }
7137    }
7138
7139    /// The fused q1t matvec must equal the reference (dequant_q1t → dot),
7140    /// including the ternary zero level and the binary-searched outlier
7141    /// overlay. Guards the mmap kernel that makes a 12B q1t runnable.
7142    #[test]
7143    fn q1t_matvec_matches_reference() {
7144        use cortiq_core::quant::{dequant_q1t, f32_to_f16};
7145        let (rows, cols) = (3usize, 64usize); // gpr = 2
7146        let gpr = cols / GROUP_SIZE;
7147        let scales = [0.5f32, 0.3, 0.7, 0.2, 0.6, 0.15];
7148        // Overlay (must be sorted by flat index): a few spikes across rows.
7149        let outliers: [(u32, f32); 3] = [(5, 9.0), (70, -4.5), (150, 3.25)];
7150        let is_out = |flat: usize| outliers.iter().any(|&(i, _)| i as usize == flat);
7151        let mut bytes = Vec::new();
7152        for r in 0..rows {
7153            for g in 0..gpr {
7154                bytes.extend_from_slice(&f32_to_f16(scales[r * gpr + g]).to_le_bytes());
7155                let mut c = [0u8; 7];
7156                for k in 0..GROUP_SIZE {
7157                    // Encoder invariant: code 0 at outlier positions.
7158                    let code = if is_out(r * cols + g * GROUP_SIZE + k) {
7159                        0
7160                    } else {
7161                        ((k + r * 3 + g) % 3) as u8 // 0,1,2
7162                    };
7163                    cortiq_core::quant::q1t_pack(&mut c, k, code);
7164                }
7165                bytes.extend_from_slice(&c);
7166            }
7167        }
7168        // Per-row overlay: [u32 row_ptr[rows+1]] then [(u16 col, f16 val)] by
7169        // row (outliers are sorted by flat index → already grouped by row).
7170        let mut row_ptr = vec![0u32; rows + 1];
7171        for &(idx, _) in &outliers {
7172            row_ptr[idx as usize / cols + 1] += 1;
7173        }
7174        for r in 0..rows {
7175            row_ptr[r + 1] += row_ptr[r];
7176        }
7177        for &p in &row_ptr {
7178            bytes.extend_from_slice(&p.to_le_bytes());
7179        }
7180        for &(idx, v) in &outliers {
7181            bytes.extend_from_slice(&((idx as usize % cols) as u16).to_le_bytes());
7182            bytes.extend_from_slice(&f32_to_f16(v).to_le_bytes());
7183        }
7184
7185        let mut refw = vec![0f32; rows * cols];
7186        dequant_q1t(&bytes, rows, cols, &mut refw);
7187        // On-grid activations (±1, amax 1) so the int8 SDOT path reconstructs
7188        // x exactly and matches the f32 reference (same trick as the q1 test).
7189        let x: Vec<f32> = (0..cols).map(|j| if j % 3 == 0 { 1.0 } else { -1.0 }).collect();
7190        let mut expect = vec![0f32; rows];
7191        for r in 0..rows {
7192            let mut a = 0.0f32;
7193            for j in 0..cols {
7194                a += refw[r * cols + j] * x[j];
7195            }
7196            expect[r] = a;
7197        }
7198        let tol = |e: f32| 1e-3 * e.abs().max(1e-3);
7199        let mut got = vec![0f32; rows];
7200        q1t_matvec(&bytes, &x, rows, cols, &mut got, None);
7201        for r in 0..rows {
7202            assert!((got[r] - expect[r]).abs() < tol(expect[r]), "row {r}: {} vs {}", got[r], expect[r]);
7203        }
7204        // matmat (b=2, f32 decode path) must agree too.
7205        let x2: Vec<f32> = x.iter().chain(x.iter().map(|v| v)).copied().collect();
7206        let mut gm = vec![0f32; 2 * rows];
7207        q1t_matmat(&bytes, &x2, 2, rows, cols, &mut gm, None);
7208        for r in 0..rows {
7209            assert!((gm[r] - expect[r]).abs() < tol(expect[r]));
7210            assert!((gm[rows + r] - expect[r]).abs() < tol(expect[r]));
7211        }
7212        // The fused-pair dispatch (matvec2) for Q1T routes to two q1t_matvec
7213        // passes — same kernel, so both outputs equal the single-vec result.
7214        // (Dispatch is covered end-to-end by the mixed-dtype bench; here we
7215        // confirm the two-pass composition is output-identical.)
7216        let (mut p1, mut p2) = (vec![0f32; rows], vec![0f32; rows]);
7217        q1t_matvec(&bytes, &x, rows, cols, &mut p1, None);
7218        q1t_matvec(&bytes, &x, rows, cols, &mut p2, None);
7219        for r in 0..rows {
7220            assert!((p1[r] - expect[r]).abs() < tol(expect[r]) && (p2[r] - expect[r]).abs() < tol(expect[r]));
7221        }
7222    }
7223
7224    // Speed A/B: the base-3-division decode (what the packing commit left in
7225    // place) vs the fused sign-LUT matvec. Both single-threaded, same bytes.
7226    //   cargo test -p cortiq-engine q1t_matvec_speed -- --ignored --nocapture
7227    #[test]
7228    #[ignore]
7229    fn q1t_matvec_speed() {
7230        use cortiq_core::quant::{f32_to_f16, q1t_code, q1t_pack, Q1T_TILE};
7231        use std::time::Instant;
7232        let (rows, cols) = (8192usize, 4096usize); // FFN-sized
7233        let gpr = cols / GROUP_SIZE;
7234        let mut bytes = Vec::with_capacity(rows * gpr * Q1T_TILE + 16);
7235        for r in 0..rows {
7236            for g in 0..gpr {
7237                let s = 0.1 + ((r + g) % 7) as f32 * 0.01;
7238                bytes.extend_from_slice(&f32_to_f16(s).to_le_bytes());
7239                let mut c = [0u8; 7];
7240                for k in 0..GROUP_SIZE {
7241                    q1t_pack(&mut c, k, ((k * 7 + r + g) % 3) as u8);
7242                }
7243                bytes.extend_from_slice(&c);
7244            }
7245        }
7246        let (n, stride) = (rows * cols, 40usize); // ~2.5% outliers, per-row overlay
7247        let mut row_ptr = vec![0u32; rows + 1];
7248        let mut idx = 0usize;
7249        while idx < n {
7250            row_ptr[idx / cols + 1] += 1;
7251            idx += stride;
7252        }
7253        for r in 0..rows {
7254            row_ptr[r + 1] += row_ptr[r];
7255        }
7256        for &p in &row_ptr {
7257            bytes.extend_from_slice(&p.to_le_bytes());
7258        }
7259        let mut idx = 0usize;
7260        while idx < n {
7261            bytes.extend_from_slice(&((idx % cols) as u16).to_le_bytes());
7262            bytes.extend_from_slice(&f32_to_f16((idx % 13) as f32 * 0.1 - 0.6).to_le_bytes());
7263            idx += stride;
7264        }
7265        // On-grid ±1 so the fast path's int8 SDOT is exact vs the f32 "slow"
7266        // reference (the A/B is a timing check; values must still agree).
7267        let x: Vec<f32> = (0..cols).map(|j| if j % 3 == 0 { 1.0 } else { -1.0 }).collect();
7268        let (rp_off, ent_off, has_ov) = q1t_overlay(&bytes, rows * gpr * Q1T_TILE, rows);
7269
7270        // "before": base-3 division decode into a buffer, then dot.
7271        let slow = |out: &mut [f32]| {
7272            let mut buf = vec![0f32; cols];
7273            for r in 0..rows {
7274                for g in 0..gpr {
7275                    let off = (r * gpr + g) * Q1T_TILE;
7276                    let s = f16_to_f32(u16::from_le_bytes([bytes[off], bytes[off + 1]]));
7277                    let codes = &bytes[off + 2..off + Q1T_TILE];
7278                    for k in 0..GROUP_SIZE {
7279                        buf[g * GROUP_SIZE + k] = match q1t_code(codes, k) {
7280                            1 => s,
7281                            2 => -s,
7282                            _ => 0.0,
7283                        };
7284                    }
7285                }
7286                out[r] = q1t_row_outlier_correction(&bytes, r, rp_off, ent_off, has_ov, &x)
7287                    + (0..cols).map(|j| buf[j] * x[j]).sum::<f32>();
7288            }
7289        };
7290        let iters = 5;
7291        let mut a = vec![0f32; rows];
7292        slow(&mut a); // warm
7293        let t = Instant::now();
7294        for _ in 0..iters {
7295            slow(&mut a);
7296        }
7297        let slow_ms = t.elapsed().as_secs_f64() * 1e3 / iters as f64;
7298
7299        let mut b = vec![0f32; rows];
7300        q1t_matvec(&bytes, &x, rows, cols, &mut b, None); // warm
7301        let t = Instant::now();
7302        for _ in 0..iters {
7303            q1t_matvec(&bytes, &x, rows, cols, &mut b, None);
7304        }
7305        let fast_ms = t.elapsed().as_secs_f64() * 1e3 / iters as f64;
7306
7307        for r in 0..rows {
7308            assert!((a[r] - b[r]).abs() < 1e-2, "mismatch row {r}");
7309        }
7310        println!(
7311            "q1t matvec {rows}x{cols} (1 thread): div-decode {slow_ms:.2} ms  fused-LUT {fast_ms:.2} ms  => {:.2}x",
7312            slow_ms / fast_ms
7313        );
7314    }
7315}