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};
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    },
37}
38
39impl QTensor {
40    pub fn from_f32(data: Vec<f32>, rows: usize, cols: usize) -> Self {
41        debug_assert_eq!(data.len(), rows * cols);
42        Self::F32 { data, rows, cols }
43    }
44
45    /// Wrap a directory tensor without dequantizing the payload.
46    /// Falls back to dequantized f32 for dtypes without a fused kernel.
47    pub fn from_model(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
48        let idx = model
49            .tensors
50            .iter()
51            .position(|t| t.name == name)
52            .ok_or_else(|| format!("tensor '{name}' not found in CMF directory"))?;
53        let entry = &model.tensors[idx];
54        if entry.shape.len() != 2 {
55            return Err(format!("QTensor::from_model needs 2-D, got '{name}'"));
56        }
57        let (rows, cols) = (entry.shape[0], entry.shape[1]);
58        let bytes = model.entry_bytes(entry);
59
60        match entry.dtype {
61            TensorDtype::Q8Row | TensorDtype::Q8_2f => {
62                let n = rows * cols;
63                let scales_off = n;
64                let row_scale: Vec<f32> = (0..rows)
65                    .map(|o| {
66                        f16_to_f32(u16::from_le_bytes([
67                            bytes[scales_off + o * 2],
68                            bytes[scales_off + o * 2 + 1],
69                        ]))
70                    })
71                    .collect();
72                let col_field: Vec<f32> = if entry.dtype == TensorDtype::Q8_2f {
73                    let col_off = n + rows * 2;
74                    (0..cols)
75                        .map(|i| {
76                            f16_to_f32(u16::from_le_bytes([
77                                bytes[col_off + i * 2],
78                                bytes[col_off + i * 2 + 1],
79                            ]))
80                        })
81                        .collect()
82                } else {
83                    Vec::new()
84                };
85                Ok(Self::Mapped {
86                    model: model.clone(),
87                    idx,
88                    dtype: entry.dtype,
89                    rows,
90                    cols,
91                    row_scale,
92                    col_field,
93                })
94            }
95            // vbit: fused kernel unpacks variable-bit rows from mmap.
96            TensorDtype::Vbit if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
97                model: model.clone(),
98                idx,
99                dtype: entry.dtype,
100                rows,
101                cols,
102                row_scale: Vec::new(),
103                col_field: Vec::new(),
104            }),
105            // q4_block: fused kernel reads nibbles straight from mmap —
106            // a 14B q4 file no longer explodes into ×8 f32 RAM.
107            TensorDtype::Q4Block if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
108                model: model.clone(),
109                idx,
110                dtype: entry.dtype,
111                rows,
112                cols,
113                row_scale: Vec::new(),
114                col_field: Vec::new(),
115            }),
116            // No fused kernel yet → dequantize once (correct, more RAM).
117            _ => {
118                let mut data = vec![0.0f32; rows * cols];
119                cortiq_core::quant::dequant_tensor(entry, bytes, &mut data)?;
120                Ok(Self::from_f32(data, rows, cols))
121            }
122        }
123    }
124
125    pub fn rows(&self) -> usize {
126        match self {
127            Self::F32 { rows, .. } | Self::Mapped { rows, .. } => *rows,
128        }
129    }
130
131    pub fn cols(&self) -> usize {
132        match self {
133            Self::F32 { cols, .. } | Self::Mapped { cols, .. } => *cols,
134        }
135    }
136
137    /// Dense f32 view — only for owned tensors. Masked/sparse execution
138    /// paths require it; quantized weights don't support masks yet.
139    pub fn as_f32(&self) -> Option<&[f32]> {
140        match self {
141            Self::F32 { data, .. } => Some(data),
142            Self::Mapped { .. } => None,
143        }
144    }
145
146    fn quant_bytes(&self) -> &[u8] {
147        match self {
148            Self::Mapped { model, idx, .. } => model.entry_bytes(&model.tensors[*idx]),
149            Self::F32 { .. } => unreachable!("quant_bytes on F32"),
150        }
151    }
152
153    /// Dequantize one row into `dst` (embedding lookup).
154    pub fn row_f32(&self, r: usize, dst: &mut [f32]) {
155        let cols = self.cols();
156        debug_assert_eq!(dst.len(), cols);
157        match self {
158            Self::F32 { data, .. } => dst.copy_from_slice(&data[r * cols..(r + 1) * cols]),
159            Self::Mapped {
160                dtype,
161                row_scale,
162                col_field,
163                ..
164            } => {
165                if *dtype == TensorDtype::Q4Block {
166                    let (packed, scales) = q4_split(self.quant_bytes(), self.rows(), cols);
167                    let gpr = cols / GROUP_SIZE;
168                    for gi in 0..gpr {
169                        let g = r * gpr + gi;
170                        let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
171                        for (k, &b) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
172                            dst[gi * GROUP_SIZE + k * 2] = ((b & 0x0F) as f32 - 8.0) * s;
173                            dst[gi * GROUP_SIZE + k * 2 + 1] = (((b >> 4) & 0x0F) as f32 - 8.0) * s;
174                        }
175                    }
176                    return;
177                }
178                if *dtype == TensorDtype::Vbit {
179                    let bytes = self.quant_bytes();
180                    let rows = self.rows();
181                    let ng = cols / GROUP_SIZE;
182                    let bits = &bytes[..rows];
183                    let sc_off = rows;
184                    let mut off = sc_off + rows * ng * 2;
185                    for rr in 0..r {
186                        off += (cols * bits[rr] as usize + 7) / 8;
187                    }
188                    let b = bits[r] as usize;
189                    let l = ((1usize << (b - 1)) - 1) as f32;
190                    let data = &bytes[off..];
191                    let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
192                    for (i, d) in dst.iter_mut().enumerate() {
193                        while nbits < b {
194                            acc = (acc << 8) | data[idx] as u64;
195                            idx += 1;
196                            nbits += 8;
197                        }
198                        let u = ((acc >> (nbits - b)) & ((1u64 << b) - 1)) as f32;
199                        nbits -= b;
200                        let so = (r * ng + i / GROUP_SIZE) * 2;
201                        let sv = f16_to_f32(u16::from_le_bytes([
202                            bytes[sc_off + so],
203                            bytes[sc_off + so + 1],
204                        ]));
205                        *d = (u - l) * sv;
206                    }
207                    return;
208                }
209                let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
210                let s = row_scale[r];
211                match dtype {
212                    TensorDtype::Q8Row => {
213                        for (d, &b) in dst.iter_mut().zip(q) {
214                            *d = (b as i8) as f32 * s;
215                        }
216                    }
217                    TensorDtype::Q8_2f => {
218                        for (i, (d, &b)) in dst.iter_mut().zip(q).enumerate() {
219                            *d = (b as i8) as f32 * s * col_field[i];
220                        }
221                    }
222                    _ => unreachable!(),
223                }
224            }
225        }
226    }
227
228    /// Can this tensor's columns be read cheaply (for sparse down_proj)?
229    /// True for F32/Q8Row/Q8_2f (per-row scale, direct strided access);
230    /// false for group-packed q4/vbit (column access would unpack whole
231    /// groups — sparse execution falls back to f32 for those).
232    pub fn sparse_col_ok(&self) -> bool {
233        match self {
234            Self::F32 { .. } => true,
235            Self::Mapped { dtype, .. } => {
236                matches!(dtype, TensorDtype::Q8Row | TensorDtype::Q8_2f)
237            }
238        }
239    }
240
241    /// down_proj [hidden, inter]: accumulate `w · col(c)` into `out`
242    /// [hidden] — reads ONLY column `c` (one neuron) from the mmap,
243    /// no full-matrix dequant. `out[k] += w · down[k, c]`.
244    pub fn add_col_scaled(&self, c: usize, w: f32, out: &mut [f32]) {
245        let inter = self.cols();
246        let hidden = self.rows();
247        debug_assert_eq!(out.len(), hidden);
248        match self {
249            Self::F32 { data, .. } => {
250                for (k, o) in out.iter_mut().enumerate() {
251                    *o += w * data[k * inter + c];
252                }
253            }
254            Self::Mapped {
255                dtype, row_scale, col_field, ..
256            } => {
257                let q = self.quant_bytes();
258                let colf = if *dtype == TensorDtype::Q8_2f {
259                    col_field[c]
260                } else {
261                    1.0
262                };
263                let wc = w * colf;
264                for (k, o) in out.iter_mut().enumerate() {
265                    let b = q[k * inter + c] as i8 as f32;
266                    *o += wc * b * row_scale[k];
267                }
268            }
269        }
270    }
271
272    /// Dot of row `r` with `x` (gate/up active-neuron path). Reads only
273    /// row `r` from the mmap — no full dequant. q4/vbit dequant the row
274    /// into `scratch` first (rare for active-FFN weights).
275    pub fn row_dot(&self, r: usize, x: &[f32], scratch: &mut [f32]) -> f32 {
276        let cols = self.cols();
277        match self {
278            Self::F32 { data, .. } => {
279                let row = &data[r * cols..(r + 1) * cols];
280                row.iter().zip(x).map(|(w, v)| w * v).sum()
281            }
282            Self::Mapped { dtype, row_scale, col_field, .. } => match dtype {
283                TensorDtype::Q8Row => {
284                    let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
285                    dot_i8_f32(q, x) * row_scale[r]
286                }
287                TensorDtype::Q8_2f => {
288                    let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
289                    dot_i8_col_f32(q, x, col_field) * row_scale[r]
290                }
291                _ => {
292                    self.row_f32(r, scratch);
293                    scratch.iter().zip(x).map(|(w, v)| w * v).sum()
294                }
295            },
296        }
297    }
298
299    /// `out = W · x` (row-major). F32 delegates to the historical
300    /// bit-exact path; Mapped runs the fused int8 kernel.
301    pub fn matvec(&self, x: &[f32], out: &mut [f32], pool: Option<&Pool>) {
302        match self {
303            Self::F32 { data, .. } => matvec_rows(pool, data, x, out),
304            Self::Mapped {
305                model,
306                idx,
307                dtype,
308                rows,
309                cols,
310                row_scale,
311                col_field,
312            } => {
313                let _ = (model, idx);
314                if *dtype == TensorDtype::Q4Block {
315                    q4matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
316                    return;
317                }
318                if *dtype == TensorDtype::Vbit {
319                    vbitmatvec(self.quant_bytes(), x, *rows, *cols, out, pool);
320                    return;
321                }
322                let xs = prescale(x, col_field, *dtype);
323                // D5: large q8 matrices (lm_head-class) — hybrid
324                // CPU∥GPU: split the rows, both sides compute
325                // SIMULTANEOUSLY (same math, shared prescale).
326                // GPU share: CMF_GPU_SPLIT (0..1, default 0.5).
327                if *rows >= crate::gpu::min_rows()
328                    && matches!(dtype, TensorDtype::Q8Row | TensorDtype::Q8_2f)
329                    && std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0").unwrap_or(true)
330                    && crate::gpu::enabled_here()
331                {
332                    let frac = std::env::var("CMF_GPU_SPLIT")
333                        .ok()
334                        .and_then(|v| v.parse::<f32>().ok())
335                        .unwrap_or(0.5)
336                        .clamp(0.0, 1.0);
337                    let cpu_rows = ((*rows as f32) * (1.0 - frac)) as usize;
338                    let (out_cpu, out_gpu) = out.split_at_mut(cpu_rows);
339                    let bytes = self.quant_bytes();
340                    let ok = std::thread::scope(|sc| {
341                        let g = sc.spawn(|| {
342                            crate::gpu::q8_matvec_range(
343                                model,
344                                *idx,
345                                cpu_rows,
346                                &row_scale[cpu_rows..],
347                                &xs,
348                                *rows - cpu_rows,
349                                *cols,
350                                out_gpu,
351                            )
352                        });
353                        if cpu_rows > 0 {
354                            qmatvec(
355                                &bytes[..cpu_rows * *cols],
356                                &row_scale[..cpu_rows],
357                                &xs,
358                                cpu_rows,
359                                *cols,
360                                out_cpu,
361                                pool,
362                            );
363                        }
364                        g.join().unwrap_or(false)
365                    });
366                    if ok {
367                        return;
368                    }
369                    // GPU failed — CPU finishes its half.
370                    qmatvec(
371                        &bytes[cpu_rows * *cols..(*rows) * *cols],
372                        &row_scale[cpu_rows..],
373                        &xs,
374                        *rows - cpu_rows,
375                        *cols,
376                        out_gpu,
377                        pool,
378                    );
379                    return;
380                }
381                qmatvec(self.quant_bytes(), row_scale, &xs, *rows, *cols, out, pool);
382            }
383        }
384    }
385
386    /// Fused two-input matvec (MTP verify pair): weights streamed once.
387    pub fn matvec2(&self, x1: &[f32], x2: &[f32], o1: &mut [f32], o2: &mut [f32], pool: Option<&Pool>) {
388        match self {
389            Self::F32 { data, .. } => matvec_rows2(pool, data, x1, x2, o1, o2),
390            Self::Mapped {
391                dtype,
392                rows,
393                cols,
394                row_scale,
395                col_field,
396                ..
397            } => {
398                if *dtype == TensorDtype::Q4Block {
399                    q4matvec(self.quant_bytes(), x1, *rows, *cols, o1, pool);
400                    q4matvec(self.quant_bytes(), x2, *rows, *cols, o2, pool);
401                    return;
402                }
403                if *dtype == TensorDtype::Vbit {
404                    vbitmatvec(self.quant_bytes(), x1, *rows, *cols, o1, pool);
405                    vbitmatvec(self.quant_bytes(), x2, *rows, *cols, o2, pool);
406                    return;
407                }
408                let x1s = prescale(x1, col_field, *dtype);
409                let x2s = prescale(x2, col_field, *dtype);
410                qmatvec2(self.quant_bytes(), row_scale, &x1s, &x2s, *rows, *cols, o1, o2, pool);
411            }
412        }
413    }
414}
415
416impl QTensor {
417    /// Batched matvec (prefill-GEMM): xs — row-major [b, cols],
418    /// out — row-major [b, rows]. Element-wise semantics are IDENTICAL
419    /// to b matvec calls (same dot kernels in the same order); the win —
420    /// the weight row streams from DRAM once per batch, not b times.
421    pub fn matmat(&self, xs_all: &[f32], b: usize, out: &mut [f32], pool: Option<&Pool>) {
422        let cols = self.cols();
423        let rows = self.rows();
424        debug_assert_eq!(xs_all.len(), b * cols);
425        debug_assert_eq!(out.len(), b * rows);
426        match self {
427            Self::F32 { data, .. } => {
428                let out_addr = SendMut(out.as_mut_ptr());
429                let run = |start: usize, end: usize| {
430                    for o in start..end {
431                        let row = &data[o * cols..(o + 1) * cols];
432                        for bi in 0..b {
433                            let x = &xs_all[bi * cols..(bi + 1) * cols];
434                            let mut acc = 0f32;
435                            for j in 0..cols {
436                                acc += row[j] * x[j];
437                            }
438                            unsafe { *out_addr.at(bi * rows + o) = acc };
439                        }
440                    }
441                };
442                dispatch_rows(pool, rows, &run);
443            }
444            Self::Mapped {
445                dtype,
446                row_scale,
447                col_field,
448                ..
449            } => {
450                if matches!(dtype, TensorDtype::Q4Block | TensorDtype::Vbit) {
451                    // No batch kernel — honest element-wise fallback.
452                    for bi in 0..b {
453                        let x = &xs_all[bi * cols..(bi + 1) * cols];
454                        let (head, tail) = out[bi * rows..].split_at_mut(rows);
455                        let _ = tail;
456                        self.matvec(x, head, pool);
457                    }
458                    return;
459                }
460                let pre: Vec<std::borrow::Cow<'_, [f32]>> = (0..b)
461                    .map(|bi| {
462                        prescale(&xs_all[bi * cols..(bi + 1) * cols], col_field, *dtype)
463                    })
464                    .collect();
465                // D5: large prefill-batch GEMMs — on the GPU (threshold by
466                // work volume: submission carries b×rows×cols MACs).
467                if b >= 8
468                    && b * rows * cols >= 128_000_000
469                    && crate::gpu::enabled_here()
470                {
471                    if let Self::Mapped { model, idx, .. } = self {
472                        let flat: Vec<f32> =
473                            pre.iter().flat_map(|v| v.iter().copied()).collect();
474                        if crate::gpu::q8_matmat(
475                            model, *idx, row_scale, &flat, b, rows, cols, out)
476                        {
477                            return;
478                        }
479                    }
480                }
481                let q = self.quant_bytes();
482                qmatmat(q, row_scale, &pre, rows, cols, out, pool);
483            }
484        }
485    }
486}
487
488/// Batched q8 kernel: same math as qmatvec, the row makes a single
489/// pass from memory for the whole batch.
490fn qmatmat(
491    q: &[u8],
492    row_scale: &[f32],
493    pre: &[std::borrow::Cow<'_, [f32]>],
494    rows: usize,
495    cols: usize,
496    out: &mut [f32],
497    pool: Option<&Pool>,
498) {
499    let b = pre.len();
500    debug_assert_eq!(out.len(), b * rows);
501    #[cfg(target_arch = "aarch64")]
502    if sdot_enabled() {
503        let acts: Vec<SplitAct> = pre.iter().map(|x| split_act(x)).collect();
504        let out_addr = SendMut(out.as_mut_ptr());
505        let run = |start: usize, end: usize| {
506            for o in start..end {
507                let row = &q[o * cols..(o + 1) * cols];
508                for (bi, act) in acts.iter().enumerate() {
509                    let v = row_dot_sdot(row, act) * row_scale[o];
510                    unsafe { *out_addr.at(bi * rows + o) = v };
511                }
512            }
513        };
514        dispatch_rows(pool, rows, &run);
515        return;
516    }
517    let out_addr = SendMut(out.as_mut_ptr());
518    let run = |start: usize, end: usize| {
519        for o in start..end {
520            let row = &q[o * cols..(o + 1) * cols];
521            for (bi, x) in pre.iter().enumerate() {
522                let mut acc = 0f32;
523                for j in 0..cols {
524                    acc += (row[j] as i8) as f32 * x[j];
525                }
526                unsafe { *out_addr.at(bi * rows + o) = acc * row_scale[o] };
527            }
528        }
529    };
530    dispatch_rows(pool, rows, &run);
531}
532
533/// Split rows across pool workers (shared qmatvec pattern). Self-balancing
534/// — see `Pool::run_rows` for why a static 1/n split is wrong here.
535fn dispatch_rows(pool: Option<&Pool>, rows: usize, run: &(dyn Fn(usize, usize) + Sync)) {
536    match pool {
537        Some(pool) if rows >= 256 => pool.run_rows(rows, run),
538        _ => run(0, rows),
539    }
540}
541
542/// Split a q4_block blob into (packed nibbles, f16 group scales).
543fn q4_split(bytes: &[u8], rows: usize, cols: usize) -> (&[u8], &[u8]) {
544    let groups = rows * cols / GROUP_SIZE;
545    bytes.split_at(groups * 16)
546}
547
548/// Unpack 8 MSB-first B-bit values from exactly B bytes (fixed shifts —
549/// no serial bit-buffer, auto-vectorizable). Every 32-value group starts
550/// byte-aligned (32·B/8 is integral for B∈3..8), so groups decompose
551/// into 4 such blocks.
552#[inline(always)]
553fn unpack8<const B: usize>(data: &[u8]) -> [i32; 8] {
554    let mut acc = 0u64;
555    for i in 0..B {
556        acc = (acc << 8) | data[i] as u64;
557    }
558    let mask = (1u64 << B) - 1;
559    let mut out = [0i32; 8];
560    for (k, o) in out.iter_mut().enumerate() {
561        *o = ((acc >> ((7 - k) * B)) & mask) as i32;
562    }
563    out
564}
565
566/// Fused vbit matvec straight from the mapped bytes (spec §3, P13
567/// FIG.3): [u8 bits: rows][f16 scales: rows·cols/32][bit-packed rows,
568/// MSB-first, byte-padded]. Row data offsets are a prefix sum over
569/// bits — computed once per call, then rows split across the pool.
570fn vbitmatvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
571    debug_assert_eq!(out.len(), rows);
572    let ng = cols / GROUP_SIZE;
573    let bits = &bytes[..rows];
574    let sc_off = rows;
575    let data_off = sc_off + rows * ng * 2;
576    let mut offsets = Vec::with_capacity(rows + 1);
577    let mut off = data_off;
578    for r in 0..rows {
579        offsets.push(off);
580        off += (cols * bits[r] as usize + 7) / 8;
581    }
582    offsets.push(off);
583
584    let offsets = &offsets;
585
586    // SDOT path: unpack the row to centered i8 once, then per-group
587    // int8 dot against the quantized activations — same A8W8 contract
588    // as q8 (bounded noise; CMF_SDOT=0 keeps the exact scalar path).
589    #[cfg(target_arch = "aarch64")]
590    if sdot_enabled() {
591        let act = split_act(x);
592        let act = &act;
593        let row_dot = move |r: usize| -> f32 {
594            let b = bits[r] as usize;
595            let l = ((1i32 << (b - 1)) - 1) as i32;
596            let mask = (1u64 << b) - 1;
597            let data = &bytes[offsets[r]..offsets[r + 1]];
598            if b == 8 {
599                // u−L reaches 128 → does not fit i8; exact f32 path.
600                let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
601                let mut dot = 0f32;
602                for g in 0..ng {
603                    let so = (r * ng + g) * 2;
604                    let sgf = f16_to_f32(u16::from_le_bytes([
605                        bytes[sc_off + so],
606                        bytes[sc_off + so + 1],
607                    ]));
608                    let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
609                    let mut gd = 0f32;
610                    for &xv in xg.iter() {
611                        if nbits < 8 {
612                            acc = (acc << 8) | data[idx] as u64;
613                            idx += 1;
614                            nbits += 8;
615                        }
616                        let u = ((acc >> (nbits - 8)) & 0xFF) as i32;
617                        nbits -= 8;
618                        gd += (u - l) as f32 * xv;
619                    }
620                    dot += gd * sgf;
621                }
622                return dot;
623            }
624            // Per-worker scratch: this closure runs for every row of the
625            // tensor (lm_head ≈ 150k rows/token) — a heap allocation per
626            // row was measurable pure overhead.
627            thread_local! {
628                static VBIT_SCRATCH: std::cell::RefCell<Vec<u8>> =
629                    const { std::cell::RefCell::new(Vec::new()) };
630            }
631            #[inline(always)]
632            fn fill<const B: usize>(data: &[u8], l: i32, buf: &mut [u8]) {
633                for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
634                    let u = unpack8::<B>(&data[blk * B..]);
635                    for k in 0..8 {
636                        chunk[k] = (u[k] - l) as i8 as u8;
637                    }
638                }
639            }
640            let _ = mask;
641            VBIT_SCRATCH.with(|scratch| {
642                let mut buf = scratch.borrow_mut();
643                buf.resize(cols, 0);
644                match b {
645                    3 => fill::<3>(data, l, &mut buf),
646                    4 => fill::<4>(data, l, &mut buf),
647                    5 => fill::<5>(data, l, &mut buf),
648                    6 => fill::<6>(data, l, &mut buf),
649                    _ => unreachable!(),
650                }
651                let mut dot = 0f32;
652                for g in 0..ng {
653                    let so = (r * ng + g) * 2;
654                    let s = f16_to_f32(u16::from_le_bytes([
655                        bytes[sc_off + so],
656                        bytes[sc_off + so + 1],
657                    ]));
658                    let d = unsafe {
659                        dot_i8_sdot(
660                            &buf[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
661                            &act.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
662                        )
663                    } as f32
664                        * act.sx;
665                    dot += d * s;
666                }
667                for &(j, xv) in &act.outliers {
668                    let so = (r * ng + j / GROUP_SIZE) * 2;
669                    let s = f16_to_f32(u16::from_le_bytes([
670                        bytes[sc_off + so],
671                        bytes[sc_off + so + 1],
672                    ]));
673                    // xq is zeroed at outlier slots — add the exact term.
674                    dot += (buf[j] as i8) as f32 * s * xv;
675                }
676                dot
677            })
678        };
679        match pool {
680            Some(pool) if rows >= 256 => {
681                let out_addr = SendMut(out.as_mut_ptr());
682                pool.run(&move |widx, n| {
683                    let chunk = rows.div_ceil(n);
684                    let start = widx * chunk;
685                    let end = (start + chunk).min(rows);
686                    for r in start..end {
687                        // SAFETY: disjoint row ranges per worker.
688                        unsafe { *out_addr.at(r) = row_dot(r) };
689                    }
690                });
691            }
692            _ => {
693                for (r, dst) in out.iter_mut().enumerate() {
694                    *dst = row_dot(r);
695                }
696            }
697        }
698        return;
699    }
700
701    // Per-bit-width specialized inner loops: the compiler unrolls the
702    // constant shifts (the generic bit-buffer loop was branch-bound —
703    // 5.6 vs 13.2 tok/s q4 on the 0.8B).
704    #[inline(always)]
705    fn dot_row<const B: usize>(
706        data: &[u8],
707        bytes: &[u8],
708        sc_off: usize,
709        r: usize,
710        ng: usize,
711        x: &[f32],
712    ) -> f32 {
713        let l = ((1i32 << (B - 1)) - 1) as f32;
714        let gbytes = GROUP_SIZE * B / 8;
715        let mut dot = 0f32;
716        for g in 0..ng {
717            let so = (r * ng + g) * 2;
718            let s = f16_to_f32(u16::from_le_bytes([bytes[sc_off + so], bytes[sc_off + so + 1]]));
719            let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
720            let gd0 = &data[g * gbytes..(g + 1) * gbytes];
721            let mut gd = 0f32;
722            for blk in 0..GROUP_SIZE / 8 {
723                let u = unpack8::<B>(&gd0[blk * B..]);
724                let xb = &xg[blk * 8..blk * 8 + 8];
725                for k in 0..8 {
726                    gd += (u[k] as f32 - l) * xb[k];
727                }
728            }
729            dot += gd * s;
730        }
731        dot
732    }
733    let row_dot = move |r: usize| -> f32 {
734        let data = &bytes[offsets[r]..offsets[r + 1]];
735        match bits[r] {
736            3 => dot_row::<3>(data, bytes, sc_off, r, ng, x),
737            4 => dot_row::<4>(data, bytes, sc_off, r, ng, x),
738            5 => dot_row::<5>(data, bytes, sc_off, r, ng, x),
739            6 => dot_row::<6>(data, bytes, sc_off, r, ng, x),
740            8 => dot_row::<8>(data, bytes, sc_off, r, ng, x),
741            b => unreachable!("vbit bit-width {b} (validated at load)"),
742        }
743    };
744    match pool {
745        Some(pool) if rows >= 256 => {
746            let out_addr = SendMut(out.as_mut_ptr());
747            pool.run(&move |widx, n| {
748                let chunk = rows.div_ceil(n);
749                let start = widx * chunk;
750                let end = (start + chunk).min(rows);
751                for r in start..end {
752                    // SAFETY: disjoint row ranges per worker.
753                    unsafe { *out_addr.at(r) = row_dot(r) };
754                }
755            });
756        }
757        _ => {
758            for (r, dst) in out.iter_mut().enumerate() {
759                *dst = row_dot(r);
760            }
761        }
762    }
763}
764
765/// Fused q4_block matvec straight from the mapped bytes. SDOT path when
766/// dotprod is available (port of vmfcore `dot_q4_block_sdot`, measured
767/// +23% on q4 decode): nibbles → centered i8, int8×int8 `sdot` per
768/// 32-group, exact outlier correction — the same A8W8 contract as q8.
769/// `CMF_SDOT=0` keeps the exact scalar path.
770fn q4matvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
771    debug_assert_eq!(out.len(), rows);
772    let (packed, scales) = q4_split(bytes, rows, cols);
773    let gpr = cols / GROUP_SIZE;
774
775    #[cfg(target_arch = "aarch64")]
776    if sdot_enabled() {
777        let act = split_act(x);
778        let act = &act;
779        let row_dot = move |r: usize| -> f32 {
780            let mut acc =
781                unsafe { dot_q4_row_sdot(packed, scales, r * gpr, gpr, &act.xq) } * act.sx;
782            // xq is zeroed at outlier slots — add the exact terms.
783            for &(j, xv) in &act.outliers {
784                let flat = r * cols + j;
785                let byte = packed[flat / 2];
786                let nib = if flat & 1 == 0 { byte & 0x0F } else { byte >> 4 };
787                let s = f16_to_f32(u16::from_le_bytes([
788                    scales[(flat / GROUP_SIZE) * 2],
789                    scales[(flat / GROUP_SIZE) * 2 + 1],
790                ]));
791                acc += ((nib as i32 - 8) as f32) * s * xv;
792            }
793            acc
794        };
795        match pool {
796            Some(pool) if rows >= 256 => {
797                let out_addr = SendMut(out.as_mut_ptr());
798                pool.run(&move |widx, n| {
799                    let chunk = rows.div_ceil(n);
800                    let start = widx * chunk;
801                    let end = (start + chunk).min(rows);
802                    for r in start..end {
803                        // SAFETY: disjoint row ranges per worker.
804                        unsafe { *out_addr.at(r) = row_dot(r) };
805                    }
806                });
807            }
808            _ => {
809                for (r, dst) in out.iter_mut().enumerate() {
810                    *dst = row_dot(r);
811                }
812            }
813        }
814        return;
815    }
816
817    let row_dot = |r: usize| -> f32 {
818        let mut acc = 0f32;
819        for gi in 0..gpr {
820            let g = r * gpr + gi;
821            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
822            let pk = &packed[g * 16..(g + 1) * 16];
823            let xg = &x[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
824            let mut ga = 0f32;
825            for (k, &b) in pk.iter().enumerate() {
826                ga += ((b & 0x0F) as f32 - 8.0) * xg[k * 2]
827                    + (((b >> 4) & 0x0F) as f32 - 8.0) * xg[k * 2 + 1];
828            }
829            acc += ga * s;
830        }
831        acc
832    };
833    match pool {
834        Some(pool) if rows >= 256 => {
835            let out_addr = SendMut(out.as_mut_ptr());
836            pool.run(&move |widx, n| {
837                let chunk = rows.div_ceil(n);
838                let start = widx * chunk;
839                let end = (start + chunk).min(rows);
840                for r in start..end {
841                    // SAFETY: disjoint row ranges per worker.
842                    unsafe { *out_addr.at(r) = row_dot(r) };
843                }
844            });
845        }
846        _ => {
847            for (r, dst) in out.iter_mut().enumerate() {
848                *dst = row_dot(r);
849            }
850        }
851    }
852}
853
854/// θ col-field fold for q8_2f activations. Borrowed pass-through for
855/// every other dtype — the old unconditional `x.to_vec()` was a pure
856/// per-matvec allocation on the q8_row hot path.
857pub(crate) fn prescale<'a>(
858    x: &'a [f32],
859    col_field: &[f32],
860    dtype: TensorDtype,
861) -> std::borrow::Cow<'a, [f32]> {
862    if dtype == TensorDtype::Q8_2f {
863        x.iter().zip(col_field).map(|(a, c)| a * c).collect()
864    } else {
865        std::borrow::Cow::Borrowed(x)
866    }
867}
868
869// ───────────────────── A8W8 SDOT path (port of vmfcore, ×1.78 decode) ─────────────────────
870
871/// SDOT enabled? Default ON when the CPU has ARMv8.2 dotprod;
872/// `CMF_SDOT=0` disables (falls back to i8×f32 NEON).
873fn sdot_enabled() -> bool {
874    use std::sync::OnceLock;
875    static ON: OnceLock<bool> = OnceLock::new();
876    *ON.get_or_init(|| {
877        let want = std::env::var("CMF_SDOT").map(|v| v != "0").unwrap_or(true);
878        #[cfg(target_arch = "aarch64")]
879        {
880            want && std::arch::is_aarch64_feature_detected!("dotprod")
881        }
882        #[cfg(not(target_arch = "aarch64"))]
883        {
884            let _ = want;
885            false
886        }
887    })
888}
889
890/// Two-field activation split (≡ vmfcore `q8_split_prep`): outlier
891/// channels (>8·rms) are computed exactly in f32; the bulk (outliers
892/// zeroed → clean absmax) goes through int8 SDOT. Computed ONCE per
893/// matvec, shared by all rows/workers.
894struct SplitAct {
895    xq: Vec<i8>,
896    sx: f32,
897    outliers: Vec<(usize, f32)>,
898}
899
900fn split_act(x: &[f32]) -> SplitAct {
901    let n = x.len();
902    let rms = (x.iter().map(|&v| (v * v) as f64).sum::<f64>() / n.max(1) as f64).sqrt() as f32;
903    let thr = 8.0 * rms;
904    let outliers: Vec<(usize, f32)> = (0..n)
905        .filter(|&j| x[j].abs() > thr)
906        .map(|j| (j, x[j]))
907        .collect();
908    let mut xb = x.to_vec();
909    for &(j, _) in &outliers {
910        xb[j] = 0.0;
911    }
912    let amax = xb.iter().fold(0f32, |m, &v| m.max(v.abs()));
913    let sx = if amax > 0.0 { amax / 127.0 } else { 1.0 };
914    let inv = 1.0 / sx;
915    let xq = xb
916        .iter()
917        .map(|&v| (v * inv).round().clamp(-127.0, 127.0) as i8)
918        .collect();
919    SplitAct { xq, sx, outliers }
920}
921
922/// int8(weight)·int8(activation) → i32 via `sdot` (inline asm — the
923/// vdotq intrinsic is unstable; port of vmfcore `dot_i8_sdot`).
924#[cfg(target_arch = "aarch64")]
925#[target_feature(enable = "neon,dotprod")]
926unsafe fn dot_i8_sdot(w: &[u8], xq: &[i8]) -> i32 {
927    // SAFETY: callers uphold slice-length contracts (see call sites).
928    unsafe {
929        use core::arch::aarch64::*;
930        use core::arch::asm;
931        let wp = w.as_ptr() as *const i8;
932        let n = w.len();
933        let (mut a0, mut a1, mut a2, mut a3) =
934            (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
935        let mut i = 0;
936        while i + 64 <= n {
937            let (w0, x0) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
938            let (w1, x1) = (vld1q_s8(wp.add(i + 16)), vld1q_s8(xq.as_ptr().add(i + 16)));
939            let (w2, x2) = (vld1q_s8(wp.add(i + 32)), vld1q_s8(xq.as_ptr().add(i + 32)));
940            let (w3, x3) = (vld1q_s8(wp.add(i + 48)), vld1q_s8(xq.as_ptr().add(i + 48)));
941            asm!(
942                "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
943                "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
944                "sdot {a2:v}.4s, {w2:v}.16b, {x2:v}.16b",
945                "sdot {a3:v}.4s, {w3:v}.16b, {x3:v}.16b",
946                a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
947                w0 = in(vreg) w0, x0 = in(vreg) x0, w1 = in(vreg) w1, x1 = in(vreg) x1,
948                w2 = in(vreg) w2, x2 = in(vreg) x2, w3 = in(vreg) w3, x3 = in(vreg) x3,
949                options(pure, nomem, nostack),
950            );
951            i += 64;
952        }
953        while i + 16 <= n {
954            let (wv, xv) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
955            asm!("sdot {a:v}.4s, {w:v}.16b, {x:v}.16b",
956                 a = inout(vreg) a0, w = in(vreg) wv, x = in(vreg) xv, options(pure, nomem, nostack));
957            i += 16;
958        }
959        let mut s = vaddvq_s32(vaddq_s32(vaddq_s32(a0, a1), vaddq_s32(a2, a3)));
960        while i < n {
961            s += (*wp.add(i)) as i32 * xq[i] as i32;
962            i += 1;
963        }
964        s
965}
966}
967
968/// Row-blocked SDOT: 4 output rows per pass — the activation chunk is
969/// loaded once and reused, 4 independent accumulators hide sdot latency
970/// (port of vmfcore `dot_i8_sdot_4rows`).
971#[cfg(target_arch = "aarch64")]
972#[target_feature(enable = "neon,dotprod")]
973unsafe fn dot_i8_sdot_4rows(w0: &[u8], w1: &[u8], w2: &[u8], w3: &[u8], xq: &[i8]) -> [i32; 4] {
974    // SAFETY: callers uphold slice-length contracts (see call sites).
975    unsafe {
976        use core::arch::aarch64::*;
977        use core::arch::asm;
978        let n = xq.len();
979        let px = xq.as_ptr();
980        let (p0, p1, p2, p3) = (
981            w0.as_ptr() as *const i8,
982            w1.as_ptr() as *const i8,
983            w2.as_ptr() as *const i8,
984            w3.as_ptr() as *const i8,
985        );
986        let (mut a0, mut a1, mut a2, mut a3) =
987            (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
988        let mut i = 0;
989        while i + 16 <= n {
990            let x = vld1q_s8(px.add(i));
991            let v0 = vld1q_s8(p0.add(i));
992            let v1 = vld1q_s8(p1.add(i));
993            let v2 = vld1q_s8(p2.add(i));
994            let v3 = vld1q_s8(p3.add(i));
995            asm!(
996                "sdot {a0:v}.4s, {v0:v}.16b, {x:v}.16b",
997                "sdot {a1:v}.4s, {v1:v}.16b, {x:v}.16b",
998                "sdot {a2:v}.4s, {v2:v}.16b, {x:v}.16b",
999                "sdot {a3:v}.4s, {v3:v}.16b, {x:v}.16b",
1000                a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
1001                v0 = in(vreg) v0, v1 = in(vreg) v1, v2 = in(vreg) v2, v3 = in(vreg) v3, x = in(vreg) x,
1002                options(pure, nomem, nostack),
1003            );
1004            i += 16;
1005        }
1006        let mut r = [vaddvq_s32(a0), vaddvq_s32(a1), vaddvq_s32(a2), vaddvq_s32(a3)];
1007        while i < n {
1008            let xi = *px.add(i) as i32;
1009            r[0] += (*p0.add(i)) as i32 * xi;
1010            r[1] += (*p1.add(i)) as i32 * xi;
1011            r[2] += (*p2.add(i)) as i32 * xi;
1012            r[3] += (*p3.add(i)) as i32 * xi;
1013            i += 1;
1014        }
1015        r
1016}
1017}
1018
1019/// SDOT row dot with exact outlier correction:
1020/// `dot = sdot(w, xq)·sx + Σ_outl w[j]·x[j]` (then × row_scale by caller).
1021#[cfg(target_arch = "aarch64")]
1022#[inline]
1023fn row_dot_sdot(row: &[u8], act: &SplitAct) -> f32 {
1024    let mut acc = unsafe { dot_i8_sdot(row, &act.xq) } as f32 * act.sx;
1025    for &(j, xv) in &act.outliers {
1026        acc += (row[j] as i8) as f32 * xv;
1027    }
1028    acc
1029}
1030
1031/// One q4 row via SDOT: each 32-group's nibbles unpack to centered i8
1032/// (nib−8 ∈ [−8,7]), int8×int8 `sdot` against the pre-quantized
1033/// activation group, × the group's f16 scale. Returns Σ_g dot_g·s_g;
1034/// the caller multiplies by the activation scale and adds the exact
1035/// outlier terms (port of vmfcore `dot_q4_block_sdot`, +23% measured).
1036/// Nibble order matches the writer: element 2k = low nibble, 2k+1 = high
1037/// → zip(lo,hi) restores flat order.
1038#[cfg(target_arch = "aarch64")]
1039#[target_feature(enable = "neon,dotprod")]
1040unsafe fn dot_q4_row_sdot(packed: &[u8], scales: &[u8], g0: usize, gpr: usize, xq: &[i8]) -> f32 {
1041    // SAFETY: callers uphold slice-length contracts (16 packed bytes and
1042    // 2 scale bytes per group; xq.len() == gpr·GROUP_SIZE).
1043    unsafe {
1044        use core::arch::aarch64::*;
1045        use core::arch::asm;
1046        let lomask = vdupq_n_u8(0x0F);
1047        let eight = vdupq_n_s8(8);
1048        let mut acc = 0f32;
1049        for gi in 0..gpr {
1050            let g = g0 + gi;
1051            let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
1052            let b = vld1q_u8(packed.as_ptr().add(g * 16));
1053            let lo = vandq_u8(b, lomask);
1054            let hi = vshrq_n_u8::<4>(b);
1055            let e0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(lo, hi)), eight);
1056            let e1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(lo, hi)), eight);
1057            let x0 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE));
1058            let x1 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE + 16));
1059            let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
1060            asm!(
1061                "sdot {a0:v}.4s, {e0:v}.16b, {x0:v}.16b",
1062                "sdot {a1:v}.4s, {e1:v}.16b, {x1:v}.16b",
1063                a0 = inout(vreg) a0, a1 = inout(vreg) a1,
1064                e0 = in(vreg) e0, x0 = in(vreg) x0, e1 = in(vreg) e1, x1 = in(vreg) x1,
1065                options(pure, nomem, nostack),
1066            );
1067            acc += vaddvq_s32(vaddq_s32(a0, a1)) as f32 * s;
1068        }
1069        acc
1070    }
1071}
1072
1073// ───────────────────── fused int8 kernels ─────────────────────
1074
1075/// `acc += w · row` where the row is centered i8 — NEON widen+fma on
1076/// aarch64, scalar elsewhere. The KV-cache q8 value path rides on this.
1077#[inline]
1078pub(crate) fn axpy_i8_f32(acc: &mut [f32], row: &[i8], w: f32) {
1079    #[cfg(target_arch = "aarch64")]
1080    unsafe {
1081        return axpy_i8_f32_neon(acc, row, w);
1082    }
1083    #[allow(unreachable_code)]
1084    {
1085        for (a, &b) in acc.iter_mut().zip(row) {
1086            *a += w * b as f32;
1087        }
1088    }
1089}
1090
1091#[cfg(target_arch = "aarch64")]
1092#[target_feature(enable = "neon")]
1093unsafe fn axpy_i8_f32_neon(acc: &mut [f32], row: &[i8], w: f32) {
1094    // SAFETY: callers uphold slice-length contracts (see call sites).
1095    unsafe {
1096        use core::arch::aarch64::*;
1097        let n = acc.len().min(row.len());
1098        let ap = acc.as_mut_ptr();
1099        let rp = row.as_ptr();
1100        let wv = vdupq_n_f32(w);
1101        let mut j = 0usize;
1102        while j + 16 <= n {
1103            let rb = vld1q_s8(rp.add(j));
1104            let lo = vmovl_s8(vget_low_s8(rb));
1105            let hi = vmovl_s8(vget_high_s8(rb));
1106            for (off, half) in [(0, lo), (8, hi)] {
1107                let f0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(half)));
1108                let f1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(half)));
1109                let o = j + off;
1110                vst1q_f32(ap.add(o), vfmaq_f32(vld1q_f32(ap.add(o)), wv, f0));
1111                vst1q_f32(ap.add(o + 4), vfmaq_f32(vld1q_f32(ap.add(o + 4)), wv, f1));
1112            }
1113            j += 16;
1114        }
1115        while j < n {
1116            *ap.add(j) += w * (*rp.add(j)) as f32;
1117            j += 1;
1118        }
1119}
1120}
1121
1122/// i8 row · f32 x. NEON on aarch64 (ported from vmfcore `dot_i8_f32_neon`,
1123/// ≈9× scalar), scalar elsewhere.
1124#[inline]
1125pub(crate) fn dot_i8_f32(w: &[u8], x: &[f32]) -> f32 {
1126    #[cfg(target_arch = "aarch64")]
1127    unsafe {
1128        return dot_i8_f32_neon(w, x);
1129    }
1130    #[allow(unreachable_code)]
1131    {
1132        let mut sum = 0.0f32;
1133        for (j, &b) in w.iter().enumerate() {
1134            sum += (b as i8) as f32 * x[j];
1135        }
1136        sum
1137    }
1138}
1139
1140/// i8 row · (x ⊙ col_field) — the q8_2f row dot with the θ col-field
1141/// folded into the product (no prescaled copy of x). NEON on aarch64,
1142/// scalar elsewhere. Used by the active-neuron path `row_dot`.
1143#[inline]
1144fn dot_i8_col_f32(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
1145    #[cfg(target_arch = "aarch64")]
1146    unsafe {
1147        return dot_i8_col_f32_neon(w, x, col);
1148    }
1149    #[allow(unreachable_code)]
1150    {
1151        let mut sum = 0.0f32;
1152        for (j, &b) in w.iter().enumerate() {
1153            sum += (b as i8) as f32 * x[j] * col[j];
1154        }
1155        sum
1156    }
1157}
1158
1159#[cfg(target_arch = "aarch64")]
1160#[target_feature(enable = "neon")]
1161unsafe fn dot_i8_col_f32_neon(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
1162    // SAFETY: callers uphold slice-length contracts (see call sites).
1163    unsafe {
1164        use core::arch::aarch64::*;
1165        let n = x.len();
1166        let wp = w.as_ptr() as *const i8;
1167        let xp = x.as_ptr();
1168        let cp = col.as_ptr();
1169        let (mut a0, mut a1, mut a2, mut a3) =
1170            (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1171        let mut j = 0usize;
1172        while j + 16 <= n {
1173            let wb = vld1q_s8(wp.add(j));
1174            let lo = vmovl_s8(vget_low_s8(wb));
1175            let hi = vmovl_s8(vget_high_s8(wb));
1176            let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
1177            let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
1178            let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
1179            let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
1180            a0 = vfmaq_f32(a0, w0, vmulq_f32(vld1q_f32(xp.add(j)), vld1q_f32(cp.add(j))));
1181            a1 = vfmaq_f32(a1, w1, vmulq_f32(vld1q_f32(xp.add(j + 4)), vld1q_f32(cp.add(j + 4))));
1182            a2 = vfmaq_f32(a2, w2, vmulq_f32(vld1q_f32(xp.add(j + 8)), vld1q_f32(cp.add(j + 8))));
1183            a3 = vfmaq_f32(a3, w3, vmulq_f32(vld1q_f32(xp.add(j + 12)), vld1q_f32(cp.add(j + 12))));
1184            j += 16;
1185        }
1186        let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
1187        while j < n {
1188            sum += (*wp.add(j)) as f32 * *xp.add(j) * *cp.add(j);
1189            j += 1;
1190        }
1191        sum
1192}
1193}
1194
1195#[cfg(target_arch = "aarch64")]
1196#[target_feature(enable = "neon")]
1197unsafe fn dot_i8_f32_neon(w: &[u8], x: &[f32]) -> f32 {
1198    // SAFETY: callers uphold slice-length contracts (see call sites).
1199    unsafe {
1200        use core::arch::aarch64::*;
1201        let n = x.len();
1202        let wp = w.as_ptr() as *const i8;
1203        let xp = x.as_ptr();
1204        let (mut a0, mut a1, mut a2, mut a3) =
1205            (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1206        let mut j = 0usize;
1207        while j + 16 <= n {
1208            let wb = vld1q_s8(wp.add(j));
1209            let lo = vmovl_s8(vget_low_s8(wb));
1210            let hi = vmovl_s8(vget_high_s8(wb));
1211            let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
1212            let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
1213            let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
1214            let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
1215            a0 = vfmaq_f32(a0, w0, vld1q_f32(xp.add(j)));
1216            a1 = vfmaq_f32(a1, w1, vld1q_f32(xp.add(j + 4)));
1217            a2 = vfmaq_f32(a2, w2, vld1q_f32(xp.add(j + 8)));
1218            a3 = vfmaq_f32(a3, w3, vld1q_f32(xp.add(j + 12)));
1219            j += 16;
1220        }
1221        let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
1222        while j < n {
1223            sum += (*wp.add(j)) as f32 * *xp.add(j);
1224            j += 1;
1225        }
1226        sum
1227}
1228}
1229
1230#[allow(clippy::too_many_arguments)]
1231fn qmatvec(
1232    q: &[u8],
1233    row_scale: &[f32],
1234    xs: &[f32],
1235    rows: usize,
1236    cols: usize,
1237    out: &mut [f32],
1238    pool: Option<&Pool>,
1239) {
1240    debug_assert_eq!(out.len(), rows);
1241
1242    #[cfg(target_arch = "aarch64")]
1243    if sdot_enabled() {
1244        let act = split_act(xs);
1245        let out_addr = SendMut(out.as_mut_ptr());
1246        let run_range = |start: usize, end: usize| {
1247            let mut o = start;
1248            while o + 4 <= end {
1249                let r = unsafe {
1250                    dot_i8_sdot_4rows(
1251                        &q[o * cols..(o + 1) * cols],
1252                        &q[(o + 1) * cols..(o + 2) * cols],
1253                        &q[(o + 2) * cols..(o + 3) * cols],
1254                        &q[(o + 3) * cols..(o + 4) * cols],
1255                        &act.xq,
1256                    )
1257                };
1258                for k in 0..4 {
1259                    let mut acc = r[k] as f32 * act.sx;
1260                    for &(j, xv) in &act.outliers {
1261                        acc += (q[(o + k) * cols + j] as i8) as f32 * xv;
1262                    }
1263                    // SAFETY: disjoint row ranges per worker.
1264                    unsafe { *out_addr.at(o + k) = acc * row_scale[o + k] };
1265                }
1266                o += 4;
1267            }
1268            while o < end {
1269                let v = row_dot_sdot(&q[o * cols..(o + 1) * cols], &act) * row_scale[o];
1270                unsafe { *out_addr.at(o) = v };
1271                o += 1;
1272            }
1273        };
1274        match pool {
1275            Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
1276            _ => run_range(0, rows),
1277        }
1278        return;
1279    }
1280
1281    let row_dot = |o: usize| -> f32 { dot_i8_f32(&q[o * cols..(o + 1) * cols], xs) * row_scale[o] };
1282    match pool {
1283        Some(pool) if rows >= 256 => {
1284            let out_addr = SendMut(out.as_mut_ptr());
1285            pool.run(&move |widx, n| {
1286                let chunk = rows.div_ceil(n);
1287                let start = widx * chunk;
1288                let end = (start + chunk).min(rows);
1289                for o in start..end {
1290                    // SAFETY: disjoint row ranges per worker.
1291                    unsafe { *out_addr.at(o) = row_dot(o) };
1292                }
1293            });
1294        }
1295        _ => {
1296            for (o, dst) in out.iter_mut().enumerate() {
1297                *dst = row_dot(o);
1298            }
1299        }
1300    }
1301}
1302
1303#[allow(clippy::too_many_arguments)]
1304fn qmatvec2(
1305    q: &[u8],
1306    row_scale: &[f32],
1307    x1: &[f32],
1308    x2: &[f32],
1309    rows: usize,
1310    cols: usize,
1311    o1: &mut [f32],
1312    o2: &mut [f32],
1313    pool: Option<&Pool>,
1314) {
1315    #[cfg(target_arch = "aarch64")]
1316    if sdot_enabled() {
1317        let a1s = split_act(x1);
1318        let a2s = split_act(x2);
1319        let p1 = SendMut(o1.as_mut_ptr());
1320        let p2 = SendMut(o2.as_mut_ptr());
1321        let run_range = |start: usize, end: usize| {
1322            for o in start..end {
1323                let row = &q[o * cols..(o + 1) * cols];
1324                // SAFETY: disjoint row ranges per worker.
1325                unsafe {
1326                    *p1.at(o) = row_dot_sdot(row, &a1s) * row_scale[o];
1327                    *p2.at(o) = row_dot_sdot(row, &a2s) * row_scale[o];
1328                }
1329            }
1330        };
1331        match pool {
1332            Some(pool) if rows >= 256 => {
1333                pool.run(&|widx, n| {
1334                    let chunk = rows.div_ceil(n);
1335                    let start = widx * chunk;
1336                    run_range(start, (start + chunk).min(rows));
1337                });
1338            }
1339            _ => run_range(0, rows),
1340        }
1341        return;
1342    }
1343
1344    let row_dots = |o: usize| -> (f32, f32) {
1345        let row = &q[o * cols..(o + 1) * cols];
1346        (dot_i8_f32(row, x1) * row_scale[o], dot_i8_f32(row, x2) * row_scale[o])
1347    };
1348    match pool {
1349        Some(pool) if rows >= 256 => {
1350            let p1 = SendMut(o1.as_mut_ptr());
1351            let p2 = SendMut(o2.as_mut_ptr());
1352            pool.run(&move |widx, n| {
1353                let chunk = rows.div_ceil(n);
1354                let start = widx * chunk;
1355                let end = (start + chunk).min(rows);
1356                for o in start..end {
1357                    let (s1, s2) = row_dots(o);
1358                    // SAFETY: disjoint row ranges per worker.
1359                    unsafe {
1360                        *p1.at(o) = s1;
1361                        *p2.at(o) = s2;
1362                    }
1363                }
1364            });
1365        }
1366        _ => {
1367            for o in 0..rows {
1368                let (s1, s2) = row_dots(o);
1369                o1[o] = s1;
1370                o2[o] = s2;
1371            }
1372        }
1373    }
1374}
1375
1376#[derive(Clone, Copy)]
1377struct SendMut(*mut f32);
1378unsafe impl Send for SendMut {}
1379unsafe impl Sync for SendMut {}
1380
1381impl SendMut {
1382    #[inline]
1383    fn at(self, i: usize) -> *mut f32 {
1384        unsafe { self.0.add(i) }
1385    }
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390    use super::*;
1391
1392    #[test]
1393    fn f32_matvec_matches_matvec_rows_bitexact() {
1394        let (rows, cols) = (300, 40);
1395        let w: Vec<f32> = (0..rows * cols).map(|i| (i as f32 * 0.017).sin()).collect();
1396        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.05).cos()).collect();
1397        let qt = QTensor::from_f32(w.clone(), rows, cols);
1398
1399        let mut a = vec![0.0f32; rows];
1400        matvec_rows(None, &w, &x, &mut a);
1401        let mut b = vec![0.0f32; rows];
1402        qt.matvec(&x, &mut b, None);
1403        assert_eq!(a, b);
1404    }
1405
1406    #[test]
1407    fn sdot_kernel_exact_on_grid() {
1408        // Activations already on the i8 grid (±1 with amax=1 → sx=1/127,
1409        // xq=±127 dequantizes EXACTLY) → the SDOT path must match the
1410        // exact f32 dot to float rounding. This isolates kernel
1411        // correctness from quantization noise.
1412        eprintln!("sdot_enabled = {}", sdot_enabled());
1413        let (rows, cols) = (9, 80); // odd rows → exercises 4-row + tail
1414        let w: Vec<u8> = (0..rows * cols)
1415            .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
1416            .collect();
1417        let scales: Vec<f32> = (0..rows).map(|o| 0.005 + o as f32 * 0.001).collect();
1418        let x: Vec<f32> = (0..cols)
1419            .map(|i| match i % 3 {
1420                0 => 1.0,
1421                1 => -1.0,
1422                _ => 0.0,
1423            })
1424            .collect();
1425        let mut a = vec![0.0f32; rows];
1426        qmatvec(&w, &scales, &x, rows, cols, &mut a, None);
1427        for o in 0..rows {
1428            let mut acc = 0.0f32;
1429            for j in 0..cols {
1430                acc += (w[o * cols + j] as i8) as f32 * x[j];
1431            }
1432            let expect = acc * scales[o];
1433            assert!(
1434                (a[o] - expect).abs() < 1e-3 * expect.abs().max(1e-3),
1435                "row {o}: {} vs {expect}",
1436                a[o]
1437            );
1438        }
1439    }
1440
1441    #[test]
1442    fn sdot_a8w8_noise_is_bounded() {
1443        // Off-grid activations: A8 quantization noise must stay small in
1444        // relative L2 over the whole output (realistic accuracy contract;
1445        // vmfcore measured argmax-identical decode on real models).
1446        let (rows, cols) = (16, 512);
1447        let w: Vec<u8> = (0..rows * cols)
1448            .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
1449            .collect();
1450        let scales = vec![0.01f32; rows];
1451        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.21).sin()).collect();
1452        let mut a = vec![0.0f32; rows];
1453        qmatvec(&w, &scales, &x, rows, cols, &mut a, None);
1454        let (mut num, mut den) = (0f64, 0f64);
1455        for o in 0..rows {
1456            let mut acc = 0.0f32;
1457            for j in 0..cols {
1458                acc += (w[o * cols + j] as i8) as f32 * x[j];
1459            }
1460            let expect = acc * scales[o];
1461            num += ((a[o] - expect) as f64).powi(2);
1462            den += (expect as f64).powi(2);
1463        }
1464        let rel = (num / den.max(1e-12)).sqrt();
1465        assert!(rel < 0.05, "A8W8 relative L2 error too high: {rel}");
1466    }
1467
1468    #[test]
1469    fn i8_dot_neon_matches_scalar() {
1470        let n = 100;
1471        let w: Vec<u8> = (0..n).map(|i| ((i * 37 + 11) % 251) as u8).collect();
1472        let x: Vec<f32> = (0..n).map(|i| (i as f32 * 0.13).sin()).collect();
1473        let mut scalar = 0.0f32;
1474        for j in 0..n {
1475            scalar += (w[j] as i8) as f32 * x[j];
1476        }
1477        let fast = dot_i8_f32(&w, &x);
1478        assert!((scalar - fast).abs() < 1e-3 * scalar.abs().max(1.0));
1479    }
1480
1481    /// Fused vbit matvec must match full dequant_vbit + dense matvec.
1482    #[test]
1483    fn vbitmatvec_matches_full_dequant() {
1484        let (rows, cols) = (6, 64);
1485        let ng = cols / GROUP_SIZE;
1486        // Hand-craft: bits per row, f16 scales, packed rows.
1487        let bits: Vec<u8> = vec![3, 4, 5, 6, 8, 4];
1488        let mut bytes = bits.clone();
1489        for g in 0..rows * ng {
1490            let s = 0.02 + 0.001 * g as f32;
1491            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
1492        }
1493        for r in 0..rows {
1494            let b = bits[r] as usize;
1495            let (mut acc, mut nb) = (0u64, 0usize);
1496            let mut rowbytes = Vec::new();
1497            for i in 0..cols {
1498                let v = ((i * 7 + r * 13) % (1 << b)) as u64;
1499                acc = (acc << b) | v;
1500                nb += b;
1501                while nb >= 8 {
1502                    nb -= 8;
1503                    rowbytes.push(((acc >> nb) & 0xFF) as u8);
1504                }
1505            }
1506            if nb > 0 {
1507                rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
1508            }
1509            bytes.extend_from_slice(&rowbytes);
1510        }
1511        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.19).sin()).collect();
1512
1513        let mut reference = vec![0f32; rows * cols];
1514        cortiq_core::quant::dequant_vbit(&bytes, rows, cols, &mut reference).unwrap();
1515        let mut expect = vec![0f32; rows];
1516        for r in 0..rows {
1517            expect[r] = reference[r * cols..(r + 1) * cols]
1518                .iter()
1519                .zip(&x)
1520                .map(|(w, xv)| w * xv)
1521                .sum();
1522        }
1523        let mut got = vec![0f32; rows];
1524        vbitmatvec(&bytes, &x, rows, cols, &mut got, None);
1525        // SDOT path quantizes activations to i8 (A8W8): bounded noise,
1526        // same contract as q8 (exact path is pinned by CMF_SDOT=0 in
1527        // the golden-parity gate).
1528        let tol = if sdot_enabled() { 6e-2 } else { 1e-4 };
1529        let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1e-6);
1530        for r in 0..rows {
1531            assert!(
1532                (got[r] - expect[r]).abs() < tol * scale,
1533                "row {r}: {} vs {}",
1534                got[r],
1535                expect[r]
1536            );
1537        }
1538    }
1539
1540    /// Fused q4 matvec must match the reference full-dequant + dense
1541    /// matvec bit-for-bit in structure (same f32 math, group order).
1542    #[test]
1543    fn q4matvec_matches_full_dequant() {
1544        let (rows, cols) = (8, 64);
1545        let groups = rows * cols / GROUP_SIZE;
1546        // Hand-craft a q4_block blob: nibbles then f16 scales.
1547        let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
1548        for i in 0..groups * 16 {
1549            bytes.push((((i * 7 + 3) % 256) & 0xFF) as u8);
1550        }
1551        for g in 0..groups {
1552            let s = 0.01 + 0.003 * g as f32;
1553            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
1554        }
1555        let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.17).sin()).collect();
1556
1557        let mut reference = vec![0.0f32; rows * cols];
1558        cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
1559        let mut expect = vec![0.0f32; rows];
1560        for r in 0..rows {
1561            expect[r] = reference[r * cols..(r + 1) * cols]
1562                .iter()
1563                .zip(&x)
1564                .map(|(w, xv)| w * xv)
1565                .sum();
1566        }
1567
1568        let mut got = vec![0.0f32; rows];
1569        q4matvec(&bytes, &x, rows, cols, &mut got, None);
1570        // SDOT path quantizes activations to i8 (A8W8): bounded noise,
1571        // same contract as q8/vbit (exact path is pinned by CMF_SDOT=0
1572        // in the golden-parity gate).
1573        let tol = if sdot_enabled() { 6e-2 } else { 1e-4 };
1574        let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
1575        for r in 0..rows {
1576            assert!(
1577                (got[r] - expect[r]).abs() < tol * scale,
1578                "row {r}: {} vs {}",
1579                got[r],
1580                expect[r]
1581            );
1582        }
1583    }
1584
1585    /// q4 SDOT outlier correction: a single huge activation channel
1586    /// (>8·rms → outlier, zeroed in xq) must still contribute its EXACT
1587    /// term. On-grid bulk (±1/0 → xq dequantizes exactly) isolates the
1588    /// correction from A8W8 noise. cols must exceed 64: at n=64 the
1589    /// 8·rms threshold equals sqrt(v²+rest) ≥ v, so a single outlier
1590    /// can never qualify (8² = n).
1591    #[test]
1592    fn q4matvec_sdot_outlier_exact() {
1593        let (rows, cols) = (4, 128);
1594        let groups = rows * cols / GROUP_SIZE;
1595        let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
1596        for i in 0..groups * 16 {
1597            bytes.push(((i * 11 + 5) % 256) as u8);
1598        }
1599        for g in 0..groups {
1600            let s = 0.02 + 0.002 * g as f32;
1601            bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
1602        }
1603        let mut x: Vec<f32> = (0..cols)
1604            .map(|i| match i % 3 {
1605                0 => 1.0,
1606                1 => -1.0,
1607                _ => 0.0,
1608            })
1609            .collect();
1610        x[17] = 300.0; // ≫ 8·rms → outlier channel
1611
1612        let mut reference = vec![0.0f32; rows * cols];
1613        cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
1614        let mut expect = vec![0.0f32; rows];
1615        for r in 0..rows {
1616            expect[r] = reference[r * cols..(r + 1) * cols]
1617                .iter()
1618                .zip(&x)
1619                .map(|(w, xv)| w * xv)
1620                .sum();
1621        }
1622        let mut got = vec![0.0f32; rows];
1623        q4matvec(&bytes, &x, rows, cols, &mut got, None);
1624        let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
1625        for r in 0..rows {
1626            assert!(
1627                (got[r] - expect[r]).abs() < 2e-3 * scale,
1628                "row {r}: {} vs {} (outlier term must be exact)",
1629                got[r],
1630                expect[r]
1631            );
1632        }
1633    }
1634}