Skip to main content

needle_core/
cq.rs

1//! Cactus-Quants (CQ) weights — the quantisation scheme Needle v2 `.cact`
2//! blobs use. Bit-for-bit compatible with `needle/model/export.py::_cq_unpack`.
3//!
4//! # Scheme
5//!
6//! A logical `[out, in]` matrix is stored pre-transposed, so one output row is
7//! contiguous along the reduction axis. Each row is cut into groups of `group`
8//! (128 in the shipped checkpoint, `in` zero-padded up to a multiple of it).
9//! Per group the encoder rotates by a normalised Walsh–Hadamard matrix,
10//! records the group's L2 norm in FP16, and quantises the unit vector against a
11//! shared Lloyd-Max codebook. Reconstruction is
12//!
13//! ```text
14//! w_g = (codebook[idx_g] * norm_g) @ H        H = Walsh(group) / sqrt(group)
15//! ```
16//!
17//! Widths are per tensor: 2, 3 or 4 bits against a header codebook, or ternary
18//! (record width 5, three analytic levels). `needle2.cact` ships
19//! `embedding=4,mhc=4,default=2` — 141 tensors at 2 bits, 4 at 4 bits.
20//!
21//! # Why the weights are never reconstructed
22//!
23//! `H` is symmetric, so for an activation slice `x_g`
24//!
25//! ```text
26//! dot(x_g, w_g) = dot(x_g, u_g @ H) = dot(H @ x_g, u_g)      u_g = codebook[idx_g] * norm_g
27//! ```
28//!
29//! The rotation moves off the weights and onto the activations. Reconstructing
30//! weights costs one Hadamard per `(row, group)` — `out * in_pad` work per
31//! matvec. Rotating the input costs one Hadamard per group, `in_pad` work for
32//! the *whole* matrix, and it is shared by every output row. At the shipped
33//! `512x512` that is 512 transforms against 4.
34//!
35//! What is left in the inner loop is a dot product of codebook-decoded bytes
36//! against a pre-rotated activation, with one FP16 scale per group. Weights stay
37//! packed in cache for the entire decode.
38//!
39//! Because several projections in a layer consume the same activation with the
40//! same geometry (`q/k/v/gate` all read the pre-norm output), the rotation is
41//! exposed separately as [`CqWeight::prepare_input`] so it can be paid once and
42//! reused — see [`CqWeight::matvec_prepared`].
43//!
44//! # Padding
45//!
46//! Upstream truncates the reconstruction to `w[:, :in_feat]`. Zero-padding the
47//! activation to `in_padded` reproduces that exactly: the padded lanes multiply
48//! against zeros, so the identity above holds without a correction term.
49
50use crate::hadamard::fwht_normalized;
51use crate::math;
52use alloc::vec;
53use alloc::vec::Vec;
54
55/// Directory record width that marks a ternary tensor. Not a real bit count —
56/// ternary stores four signed 2-bit crumbs per byte.
57pub const TERNARY_RECORD_BITS: u8 = 5;
58
59/// The ternary Lloyd-Max centroid, before the `1/sqrt(group)` scaling.
60/// `quantize._TERNARY_CB` is `{-c, 0, +c}` for this `c`.
61pub const TERNARY_CENTROID: f32 = 1.2240064;
62
63/// Widths present in the header codebook, in the order they are concatenated:
64/// `cb2 | cb3 | cb4`, lengths 4 + 8 + 16 = 28.
65pub const CODEBOOK_WIDTHS: [u8; 3] = [2, 3, 4];
66
67/// Total length of a well-formed header codebook.
68pub const CODEBOOK_LEN: usize = 4 + 8 + 16;
69
70/// Largest quantisation group accepted. Bounds the stack buffer
71/// [`CqWeight::matmul_rows_prepared`] decodes a group into. The shipped
72/// checkpoint uses 128.
73pub const MAX_GROUP: usize = 1024;
74
75/// Errors from constructing a [`CqWeight`].
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum CqError {
78    /// Record `bits` is not 2, 3, 4 or [`TERNARY_RECORD_BITS`].
79    UnsupportedBits(u8),
80    /// `group` is zero, not a multiple of 8, not a power of two, or above
81    /// [`MAX_GROUP`].
82    BadGroup(usize),
83    /// The header codebook is not [`CODEBOOK_LEN`] long.
84    BadCodebookLen(usize),
85    /// The blob is shorter than the packed indices plus FP16 norms require.
86    ShortBlob { need: usize, got: usize },
87    /// `out_feat` or `in_feat` is zero.
88    EmptyShape,
89}
90
91/// Slice out one width's levels from a concatenated `cb2|cb3|cb4` header
92/// codebook. The stored values already carry the `1/sqrt(group)` factor.
93pub fn codebook_slice(codebook: &[f32], bits: u8) -> Result<&[f32], CqError> {
94    if codebook.len() != CODEBOOK_LEN {
95        return Err(CqError::BadCodebookLen(codebook.len()));
96    }
97    match bits {
98        2 => Ok(&codebook[0..4]),
99        3 => Ok(&codebook[4..12]),
100        4 => Ok(&codebook[12..28]),
101        other => Err(CqError::UnsupportedBits(other)),
102    }
103}
104
105/// Bytes one packed row occupies. Mirrors `export._packed_row_bytes`.
106#[inline]
107pub fn packed_row_bytes(in_padded: usize, bits: u8) -> usize {
108    let effective = if bits == TERNARY_RECORD_BITS {
109        2
110    } else {
111        bits as usize
112    };
113    in_padded * effective / 8
114}
115
116/// A Cactus-Quants weight matrix, logically `[out_feat, in_feat]`.
117pub struct CqWeight {
118    /// Packed indices, `out_feat` rows of `row_bytes`.
119    packed: Vec<u8>,
120    /// Per-group L2 norms, `[out_feat * num_groups]`, decoded from FP16.
121    norms: Vec<f32>,
122    /// Byte-decode table: `256 * per_byte` values. Empty when `per_byte == 0`.
123    lut: Vec<f32>,
124    /// Codebook indices packed per byte: 4 at 2 bits and ternary, 2 at 4 bits,
125    /// 0 for 3 bits (no byte alignment — the generic bit reader handles it).
126    per_byte: usize,
127    row_bytes: usize,
128    num_groups: usize,
129    pub out_feat: usize,
130    /// Logical (unpadded) reduction width.
131    pub in_feat: usize,
132    /// Reduction width rounded up to a multiple of `group`.
133    pub in_padded: usize,
134    pub group: usize,
135    /// Record width as stored in the directory: 2, 3, 4, or 5 for ternary.
136    pub bits: u8,
137    /// This tensor's levels, already scaled by `1/sqrt(group)`.
138    levels: Vec<f32>,
139}
140
141impl CqWeight {
142    /// Parse a CQ tensor from its blob: packed indices followed by FP16 norms.
143    ///
144    /// `codebook` is the concatenated `cb2|cb3|cb4` block from the `.cact`
145    /// header; it is ignored for ternary, whose levels are analytic.
146    pub fn from_blob(
147        blob: &[u8],
148        out_feat: usize,
149        in_feat: usize,
150        group: usize,
151        bits: u8,
152        codebook: &[f32],
153    ) -> Result<Self, CqError> {
154        if out_feat == 0 || in_feat == 0 {
155            return Err(CqError::EmptyShape);
156        }
157        if group == 0 || !group.is_multiple_of(8) || !group.is_power_of_two() {
158            // Power-of-two: the group is rotated by a Walsh–Hadamard matrix.
159            // Multiple of 8: keeps every group byte-aligned inside a packed row.
160            return Err(CqError::BadGroup(group));
161        }
162        let levels: Vec<f32> = if bits == TERNARY_RECORD_BITS {
163            let s = TERNARY_CENTROID / math::sqrt(group as f32);
164            vec![-s, 0.0, s]
165        } else {
166            codebook_slice(codebook, bits)?.to_vec()
167        };
168
169        let in_padded = in_feat.div_ceil(group) * group;
170        let num_groups = in_padded / group;
171        let row_bytes = packed_row_bytes(in_padded, bits);
172        let n_packed = out_feat * row_bytes;
173        let n_norm_bytes = out_feat * num_groups * 2;
174        if blob.len() < n_packed + n_norm_bytes {
175            return Err(CqError::ShortBlob {
176                need: n_packed + n_norm_bytes,
177                got: blob.len(),
178            });
179        }
180
181        let packed = blob[..n_packed].to_vec();
182        let mut norms = Vec::with_capacity(out_feat * num_groups);
183        for c in blob[n_packed..n_packed + n_norm_bytes].chunks_exact(2) {
184            norms.push(math::f16_to_f32(u16::from_le_bytes([c[0], c[1]])));
185        }
186
187        let (per_byte, lut) = build_lut(bits, &levels);
188
189        Ok(Self {
190            packed,
191            norms,
192            lut,
193            per_byte,
194            row_bytes,
195            num_groups,
196            out_feat,
197            in_feat,
198            in_padded,
199            group,
200            bits,
201            levels,
202        })
203    }
204
205    /// Bytes of packed weight held (excluding norms) — for size accounting.
206    pub fn packed_bytes(&self) -> usize {
207        self.packed.len()
208    }
209
210    /// Effective bits per weight, counting the FP16 group norms.
211    pub fn effective_bits(&self) -> f32 {
212        let total_bits = (self.packed.len() + self.norms.len() * 2) * 8;
213        total_bits as f32 / (self.out_feat * self.in_padded) as f32
214    }
215
216    /// Scratch length [`prepare_input`] needs.
217    #[inline]
218    pub fn prepared_len(&self) -> usize {
219        self.in_padded
220    }
221
222    /// Rotate an activation into the domain the packed weights live in.
223    ///
224    /// `x` is `in_feat` long; `xh` must be [`prepared_len`] long and is fully
225    /// overwritten. Reusable across every [`CqWeight`] with the same `in_feat`
226    /// and `group`.
227    ///
228    /// [`prepared_len`]: CqWeight::prepared_len
229    pub fn prepare_input(&self, x: &[f32], xh: &mut [f32]) {
230        debug_assert_eq!(x.len(), self.in_feat);
231        debug_assert_eq!(xh.len(), self.in_padded);
232        xh[..self.in_feat].copy_from_slice(x);
233        // The padded tail must be zero: it stands in for the columns upstream
234        // truncates off the reconstruction.
235        xh[self.in_feat..].fill(0.0);
236        for g in xh.chunks_exact_mut(self.group) {
237            fwht_normalized(g);
238        }
239    }
240
241    /// `y = W · x`, allocating the rotation scratch.
242    ///
243    /// Prefer [`prepare_input`] + [`matvec_prepared`] when several weights share
244    /// one activation — a transformer layer's `q/k/v/gate` projections all do.
245    ///
246    /// [`prepare_input`]: CqWeight::prepare_input
247    /// [`matvec_prepared`]: CqWeight::matvec_prepared
248    pub fn matvec(&self, x: &[f32], y: &mut [f32]) {
249        let mut xh = vec![0.0f32; self.in_padded];
250        self.prepare_input(x, &mut xh);
251        self.matvec_prepared(&xh, y);
252    }
253
254    /// `y = W · x` given an activation already rotated by [`prepare_input`].
255    ///
256    /// [`prepare_input`]: CqWeight::prepare_input
257    pub fn matvec_prepared(&self, xh: &[f32], y: &mut [f32]) {
258        self.matvec_rows_prepared(xh, 0, y)
259    }
260
261    /// `y = W[row_start .. row_start + y.len()] · x`, on a prepared activation.
262    ///
263    /// The mHC projections are stored as one tall matrix per kind, with a
264    /// four-row band per layer (sixteen for `phi_res`), so a full matvec would
265    /// compute 27x more rows than a layer needs.
266    pub fn matvec_rows_prepared(&self, xh: &[f32], row_start: usize, y: &mut [f32]) {
267        #[cfg(feature = "parallel")]
268        if self.should_parallelise(y.len()) {
269            return self.matvec_rows_par(xh, row_start, y);
270        }
271        self.matvec_rows_serial(xh, row_start, y)
272    }
273
274    /// Split the output rows across the rayon pool.
275    ///
276    /// Each row's dot product is computed exactly as the serial path computes it,
277    /// so this is bit-identical — only which core runs it changes.
278    #[cfg(feature = "parallel")]
279    fn matvec_rows_par(&self, xh: &[f32], row_start: usize, y: &mut [f32]) {
280        use rayon::prelude::*;
281        let rows = y.len();
282        let chunk = rows
283            .div_ceil(rayon::current_num_threads())
284            .max(MIN_PAR_ROWS);
285        y.par_chunks_mut(chunk).enumerate().for_each(|(ci, part)| {
286            self.matvec_rows_serial(xh, row_start + ci * chunk, part);
287        });
288    }
289
290    fn matvec_rows_serial(&self, xh: &[f32], row_start: usize, y: &mut [f32]) {
291        debug_assert_eq!(xh.len(), self.in_padded);
292        debug_assert!(row_start + y.len() <= self.out_feat);
293        match self.per_byte {
294            4 => self.matvec_lut::<4>(xh, row_start, y),
295            2 => self.matvec_lut::<2>(xh, row_start, y),
296            _ => self.matvec_generic(xh, row_start, y),
297        }
298    }
299
300    /// Byte-LUT decode with a **single** running accumulator — the shape this
301    /// kernel had before the accumulator was split into [`ACC_LANES`] lanes.
302    ///
303    /// Exposed only under the `bench-internals` feature, so the effect of that
304    /// change can be measured against its own predecessor inside one benchmark
305    /// run. Not for production use.
306    #[cfg(feature = "bench-internals")]
307    pub fn matvec_prepared_single_acc(&self, xh: &[f32], y: &mut [f32]) {
308        debug_assert_eq!(xh.len(), self.in_padded);
309        debug_assert_eq!(y.len(), self.out_feat);
310        // `P` must be a const generic here, exactly as in the optimised path:
311        // with a runtime width the inner loop cannot unroll and the arm would
312        // measure the loss of specialisation rather than the loss of lanes.
313        match self.per_byte {
314            4 => self.single_acc::<4>(xh, y),
315            2 => self.single_acc::<2>(xh, y),
316            _ => self.matvec_prepared_reference(xh, y),
317        }
318    }
319
320    #[cfg(feature = "bench-internals")]
321    fn single_acc<const P: usize>(&self, xh: &[f32], y: &mut [f32]) {
322        let bytes_per_group = self.group / P;
323        let lut = &self.lut[..256 * P];
324        for o in 0..self.out_feat {
325            let row = &self.packed[o * self.row_bytes..(o + 1) * self.row_bytes];
326            let row_norms = &self.norms[o * self.num_groups..(o + 1) * self.num_groups];
327            let mut total = 0.0f32;
328            for (g, &norm) in row_norms.iter().enumerate() {
329                let gbytes = &row[g * bytes_per_group..(g + 1) * bytes_per_group];
330                let gx = &xh[g * self.group..(g + 1) * self.group];
331                let mut acc = 0.0f32;
332                for (bi, &byte) in gbytes.iter().enumerate() {
333                    let base = byte as usize * P;
334                    let vals: &[f32; P] = lut[base..base + P].try_into().unwrap();
335                    let xs: &[f32; P] = gx[bi * P..bi * P + P].try_into().unwrap();
336                    for k in 0..P {
337                        acc += vals[k] * xs[k];
338                    }
339                }
340                total += norm * acc;
341            }
342            y[o] = total;
343        }
344    }
345
346    /// Deliberately simple reference for the packed dot product.
347    ///
348    /// One running scalar accumulator, no lane splitting, no byte LUT — decode
349    /// each index and multiply. Kept public and tested against
350    /// [`matvec_prepared`] so the optimised paths have an obviously-correct
351    /// counterpart, and so the effect of the optimisation can be measured
352    /// against it under identical conditions rather than against a number
353    /// recorded at a different time.
354    ///
355    /// Not for production use: it is several times slower.
356    ///
357    /// [`matvec_prepared`]: CqWeight::matvec_prepared
358    pub fn matvec_prepared_reference(&self, xh: &[f32], y: &mut [f32]) {
359        debug_assert_eq!(xh.len(), self.in_padded);
360        debug_assert_eq!(y.len(), self.out_feat);
361        let bits = if self.bits == TERNARY_RECORD_BITS {
362            2
363        } else {
364            self.bits as usize
365        };
366        let mask = (1u32 << bits) - 1;
367        for (o, slot) in y.iter_mut().enumerate() {
368            let row = &self.packed[o * self.row_bytes..(o + 1) * self.row_bytes];
369            let mut total = 0.0f32;
370            for g in 0..self.num_groups {
371                let norm = self.norms[o * self.num_groups + g];
372                let mut acc = 0.0f32;
373                for k in 0..self.group {
374                    let code = read_bits(row, (g * self.group + k) * bits, bits, mask);
375                    let idx = if self.bits == TERNARY_RECORD_BITS {
376                        match code {
377                            3 => 0,
378                            0 => 1,
379                            1 => 2,
380                            _ => 1,
381                        }
382                    } else {
383                        code as usize
384                    };
385                    acc += self.levels[idx] * xh[g * self.group + k];
386                }
387                total += norm * acc;
388            }
389            *slot = total;
390        }
391    }
392
393    /// `y[b * rows + r] = W[row_start + r] · xh[b]`, over `batch` prepared
394    /// activations laid out back to back at [`prepared_len`] stride.
395    ///
396    /// This is where batching pays. A matvec streams the whole packed weight set
397    /// once per position — 11 MB for the shipped model, far past any core's
398    /// private cache, so every position re-reads it from further out. Here each
399    /// group is decoded **once** and applied to all `batch` activations, so the
400    /// weight traffic is paid once for the batch and the inner loop is pure FMA
401    /// over values already in registers.
402    ///
403    /// Bit-identical to `batch` calls to [`matvec_rows_prepared`], not merely
404    /// close: the group norm is applied after the group dot, and the dot is
405    /// summed in the same lane order, so no reassociation is introduced.
406    ///
407    /// `acc` is scratch, at least `batch` long.
408    ///
409    /// [`prepared_len`]: CqWeight::prepared_len
410    /// [`matvec_rows_prepared`]: CqWeight::matvec_rows_prepared
411    pub fn matmul_rows_prepared(
412        &self,
413        xh: &[f32],
414        batch: usize,
415        row_start: usize,
416        rows: usize,
417        y: &mut [f32],
418        acc: &mut [f32],
419    ) {
420        #[cfg(feature = "parallel")]
421        if self.matmul_work(rows, batch) >= PAR_MIN_MACS {
422            // Two ways to split, chosen by shape. Row bands are preferred: each
423            // task decodes its own weights once and the activation is shared.
424            // But the mHC projections have only 4 to 16 rows — far too few to
425            // band — while carrying a whole chunk's worth of positions, so those
426            // split by *batch* instead. Splitting by batch means each task
427            // re-decodes the weights, which is only cheap because the matrix is
428            // small; that is exactly the case where it applies.
429            if rows >= 2 * MIN_PAR_ROWS {
430                return self.matmul_rows_par(xh, batch, row_start, rows, y);
431            }
432            if batch >= 2 * MIN_PAR_BATCH {
433                return self.matmul_batch_par(xh, batch, row_start, rows, y);
434            }
435        }
436        self.matmul_rows_serial(xh, batch, row_start, rows, y, acc)
437    }
438
439    /// Whether a matvec is big enough that splitting it beats the dispatch cost.
440    ///
441    /// A `512x512` matvec is ~30 µs over 512 rows; split eight ways each task
442    /// holds ~4 µs of work, against a rayon dispatch of the same order — so small
443    /// matvecs must stay serial. The only matvec that clears the bar is the tied
444    /// LM head, at 8192 rows.
445    #[cfg(feature = "parallel")]
446    #[inline]
447    fn should_parallelise(&self, rows: usize) -> bool {
448        rows >= 2 * MIN_PAR_ROWS && rows.saturating_mul(self.in_padded) >= PAR_MIN_MACS
449    }
450
451    /// Split the output rows across the pool.
452    ///
453    /// The caller's `y` is batch-major (`y[b * rows + r]`), so a row band is
454    /// strided rather than contiguous and `par_chunks_mut` cannot express it.
455    /// Each task therefore fills a private band and copies it out; the copy is
456    /// `batch * band` floats against `batch * band * in_padded` multiply-adds, so
457    /// well under a tenth of a percent of the work.
458    #[cfg(feature = "parallel")]
459    fn matmul_rows_par(
460        &self,
461        xh: &[f32],
462        batch: usize,
463        row_start: usize,
464        rows: usize,
465        y: &mut [f32],
466    ) {
467        use rayon::prelude::*;
468
469        /// Raw pointer to the output, so disjoint row bands can be written in
470        /// parallel. Safe because the bands below never overlap.
471        struct Out(*mut f32);
472        // SAFETY: every task writes only `y[b * rows + band]` for its own band,
473        // and the bands partition `0..rows`, so no two tasks alias.
474        unsafe impl Send for Out {}
475        unsafe impl Sync for Out {}
476        impl Out {
477            /// Read the pointer through a method so the closure captures the
478            /// wrapper rather than the bare `*mut f32` field — edition-2021
479            /// disjoint capture would otherwise take the field, which is not
480            /// `Sync`, and the `unsafe impl` above would not apply.
481            #[inline]
482            fn ptr(&self) -> *mut f32 {
483                self.0
484            }
485        }
486
487        let out = Out(y.as_mut_ptr());
488        let chunk = rows
489            .div_ceil(rayon::current_num_threads())
490            .max(MIN_PAR_ROWS);
491        let bands: Vec<usize> = (0..rows).step_by(chunk).collect();
492
493        bands.into_par_iter().for_each(|r0| {
494            let band = chunk.min(rows - r0);
495            let mut local = vec![0.0f32; batch * band];
496            let mut acc = vec![0.0f32; batch];
497            self.matmul_rows_serial(xh, batch, row_start + r0, band, &mut local, &mut acc);
498            for b in 0..batch {
499                // SAFETY: disjoint per band, and `b * rows + r0 + band <= y.len()`.
500                unsafe {
501                    core::ptr::copy_nonoverlapping(
502                        local.as_ptr().add(b * band),
503                        out.ptr().add(b * rows + r0),
504                        band,
505                    );
506                }
507            }
508        });
509    }
510
511    /// Split by batch instead of by row.
512    ///
513    /// `y` is batch-major, so a batch range is contiguous and this needs no
514    /// unsafe — unlike the row-band split. Each task handles a slice of the
515    /// prepared activations and the matching slice of the output.
516    #[cfg(feature = "parallel")]
517    fn matmul_batch_par(
518        &self,
519        xh: &[f32],
520        batch: usize,
521        row_start: usize,
522        rows: usize,
523        y: &mut [f32],
524    ) {
525        use rayon::prelude::*;
526        let stride = self.in_padded;
527        let band = batch
528            .div_ceil(rayon::current_num_threads())
529            .max(MIN_PAR_BATCH);
530        y[..batch * rows]
531            .par_chunks_mut(band * rows)
532            .enumerate()
533            .for_each(|(ci, part)| {
534                let b0 = ci * band;
535                let n = part.len() / rows;
536                let mut acc = vec![0.0f32; n];
537                self.matmul_rows_serial(
538                    &xh[b0 * stride..(b0 + n) * stride],
539                    n,
540                    row_start,
541                    rows,
542                    part,
543                    &mut acc,
544                );
545            });
546    }
547
548    /// Multiply-accumulate count for a batched call.
549    #[cfg(feature = "parallel")]
550    #[inline]
551    fn matmul_work(&self, rows: usize, batch: usize) -> usize {
552        rows.saturating_mul(batch).saturating_mul(self.in_padded)
553    }
554
555    #[allow(clippy::too_many_arguments)]
556    fn matmul_rows_serial(
557        &self,
558        xh: &[f32],
559        batch: usize,
560        row_start: usize,
561        rows: usize,
562        y: &mut [f32],
563        acc: &mut [f32],
564    ) {
565        // Dispatched exactly as `matvec_lut` is. Both must take the same branch
566        // on a given machine, or `matmul_rows_prepared` would stop being
567        // bit-identical to repeated `matvec_rows_prepared` — with FMA enabled
568        // LLVM contracts `a*b + c` into one instruction, which rounds differently
569        // from a separate multiply and add.
570        #[cfg(all(target_arch = "x86_64", feature = "simd"))]
571        if crate::quant::has_avx2() {
572            // Safety: has_avx2() confirmed AVX2 and FMA via CPUID.
573            return unsafe { self.matmul_rows_prepared_avx2(xh, batch, row_start, rows, y, acc) };
574        }
575        self.matmul_rows_prepared_impl(xh, batch, row_start, rows, y, acc)
576    }
577
578    /// AVX2/FMA build of [`matmul_rows_prepared_impl`].
579    ///
580    /// # Safety
581    /// The caller must have confirmed AVX2 and FMA support.
582    #[cfg(all(target_arch = "x86_64", feature = "simd"))]
583    #[target_feature(enable = "avx2,fma")]
584    unsafe fn matmul_rows_prepared_avx2(
585        &self,
586        xh: &[f32],
587        batch: usize,
588        row_start: usize,
589        rows: usize,
590        y: &mut [f32],
591        acc: &mut [f32],
592    ) {
593        self.matmul_rows_prepared_impl(xh, batch, row_start, rows, y, acc)
594    }
595
596    #[inline(always)]
597    fn matmul_rows_prepared_impl(
598        &self,
599        xh: &[f32],
600        batch: usize,
601        row_start: usize,
602        rows: usize,
603        y: &mut [f32],
604        acc: &mut [f32],
605    ) {
606        debug_assert_eq!(xh.len(), batch * self.in_padded);
607        debug_assert_eq!(y.len(), batch * rows);
608        debug_assert!(acc.len() >= batch);
609        debug_assert!(row_start + rows <= self.out_feat);
610        debug_assert!(self.group <= MAX_GROUP);
611
612        let stride = self.in_padded;
613        // One group's dequantised levels, unscaled, reused across the batch.
614        let mut buf = [0.0f32; MAX_GROUP];
615        let ug = &mut buf[..self.group];
616        let lanewise = self.per_byte != 0;
617
618        for r in 0..rows {
619            let o = row_start + r;
620            let row = &self.packed[o * self.row_bytes..(o + 1) * self.row_bytes];
621            let row_norms = &self.norms[o * self.num_groups..(o + 1) * self.num_groups];
622            acc[..batch].fill(0.0);
623
624            for (g, &norm) in row_norms.iter().enumerate() {
625                // norm = 1.0 leaves the levels unscaled and rounds nothing.
626                self.decode_group(row, g, 1.0, ug);
627                let base = g * self.group;
628                for b in 0..batch {
629                    let gx = &xh[b * stride + base..b * stride + base + self.group];
630                    // Match the summation order of the matvec path this width
631                    // uses, so the two agree bit for bit.
632                    let s = if lanewise {
633                        group_dot_lanes(ug, gx)
634                    } else {
635                        group_dot_serial(ug, gx)
636                    };
637                    acc[b] += norm * s;
638                }
639            }
640            for b in 0..batch {
641                y[b * rows + r] = acc[b];
642            }
643        }
644    }
645
646    /// Byte-at-a-time path: one LUT entry yields `P` decoded levels.
647    ///
648    /// The accumulator is split into [`ACC_LANES`] independent lanes rather than
649    /// one running scalar. A single accumulator makes the loop latency-bound —
650    /// every FMA waits on the previous — which caps throughput at one element per
651    /// FMA latency regardless of issue width. Independent lanes expose
652    /// instruction-level parallelism and let the autovectoriser fold the body
653    /// into vector FMAs.
654    ///
655    /// The lane count is fixed rather than tied to `P`, so a 4-bit tensor (two
656    /// levels per byte) gets the same parallelism as a 2-bit one by consuming
657    /// `ACC_LANES / P` bytes per iteration.
658    fn matvec_lut<const P: usize>(&self, xh: &[f32], row_start: usize, y: &mut [f32]) {
659        // Baseline x86_64 guarantees only SSE2, and this crate deliberately does
660        // not build with `target-cpu=native`, so without a runtime check the
661        // eight-lane loop compiles to pairs of 4-wide SSE ops. Recompiling the
662        // *same source* under `avx2,fma` and selecting it by CPUID gets the
663        // 8-wide form into a portable binary. No hand-written intrinsics:
664        // measured on aarch64, LLVM's autovectorisation of this loop beats a
665        // hand-written 4-accumulator NEON version (8.1 against 7.1 Gelem/s), so
666        // there is no reason to expect intrinsics to help on x86 either.
667        #[cfg(all(target_arch = "x86_64", feature = "simd"))]
668        if crate::quant::has_avx2() {
669            // Safety: has_avx2() confirmed AVX2 and FMA via CPUID.
670            return unsafe { self.matvec_lut_avx2::<P>(xh, row_start, y) };
671        }
672        self.matvec_lut_impl::<P>(xh, row_start, y)
673    }
674
675    /// AVX2/FMA build of [`matvec_lut_impl`]. Same source, wider vectors.
676    ///
677    /// # Safety
678    /// The caller must have confirmed AVX2 and FMA support.
679    #[cfg(all(target_arch = "x86_64", feature = "simd"))]
680    #[target_feature(enable = "avx2,fma")]
681    unsafe fn matvec_lut_avx2<const P: usize>(&self, xh: &[f32], row_start: usize, y: &mut [f32]) {
682        self.matvec_lut_impl::<P>(xh, row_start, y)
683    }
684
685    #[inline(always)]
686    fn matvec_lut_impl<const P: usize>(&self, xh: &[f32], row_start: usize, y: &mut [f32]) {
687        let bytes_per_group = self.group / P;
688        let lut = &self.lut[..256 * P];
689        for (yi, o) in (row_start..row_start + y.len()).enumerate() {
690            let row = &self.packed[o * self.row_bytes..(o + 1) * self.row_bytes];
691            let row_norms = &self.norms[o * self.num_groups..(o + 1) * self.num_groups];
692            let mut total = 0.0f32;
693            for (g, &norm) in row_norms.iter().enumerate() {
694                let gbytes = &row[g * bytes_per_group..(g + 1) * bytes_per_group];
695                let gx = &xh[g * self.group..(g + 1) * self.group];
696                total += norm * dot_group::<P>(lut, gbytes, gx);
697            }
698            y[yi] = total;
699        }
700    }
701
702    /// Bit-at-a-time path for widths that do not divide a byte (3 bits).
703    fn matvec_generic(&self, xh: &[f32], row_start: usize, y: &mut [f32]) {
704        let bits = self.bits as usize;
705        let mask = (1u32 << bits) - 1;
706        for (yi, o) in (row_start..row_start + y.len()).enumerate() {
707            let row = &self.packed[o * self.row_bytes..(o + 1) * self.row_bytes];
708            let row_norms = &self.norms[o * self.num_groups..(o + 1) * self.num_groups];
709            let mut total = 0.0f32;
710            for (g, &norm) in row_norms.iter().enumerate() {
711                let gx = &xh[g * self.group..(g + 1) * self.group];
712                let mut acc = 0.0f32;
713                for (k, &x) in gx.iter().enumerate() {
714                    let bit = (g * self.group + k) * bits;
715                    let idx = read_bits(row, bit, bits, mask);
716                    acc += self.levels[idx as usize] * x;
717                }
718                total += norm * acc;
719            }
720            y[yi] = total;
721        }
722    }
723
724    /// Reconstruct one output row into `out` (`in_feat` long).
725    ///
726    /// Used where a row is genuinely needed as a vector rather than as a dot
727    /// product — token embedding lookup, and parity tests.
728    pub fn dequantize_row(&self, o: usize, out: &mut [f32]) {
729        debug_assert!(o < self.out_feat);
730        debug_assert_eq!(out.len(), self.in_feat);
731        let row = &self.packed[o * self.row_bytes..(o + 1) * self.row_bytes];
732        let row_norms = &self.norms[o * self.num_groups..(o + 1) * self.num_groups];
733        let mut buf = vec![0.0f32; self.group];
734        for (g, &norm) in row_norms.iter().enumerate() {
735            self.decode_group(row, g, norm, &mut buf);
736            fwht_normalized(&mut buf);
737            let base = g * self.group;
738            for k in 0..self.group {
739                if base + k < self.in_feat {
740                    out[base + k] = buf[k];
741                }
742            }
743        }
744    }
745
746    /// Reconstruct the whole matrix, row-major `[out_feat, in_feat]`.
747    pub fn dequantize_to(&self, out: &mut [f32]) {
748        debug_assert_eq!(out.len(), self.out_feat * self.in_feat);
749        for o in 0..self.out_feat {
750            self.dequantize_row(o, &mut out[o * self.in_feat..(o + 1) * self.in_feat]);
751        }
752    }
753
754    /// Fill `buf` (`group` long) with `codebook[idx] * norm` for one group,
755    /// i.e. `u_g` before the Hadamard rotation.
756    fn decode_group(&self, row: &[u8], g: usize, norm: f32, buf: &mut [f32]) {
757        if self.per_byte != 0 {
758            let p = self.per_byte;
759            let bytes_per_group = self.group / p;
760            let gbytes = &row[g * bytes_per_group..(g + 1) * bytes_per_group];
761            for (bi, &byte) in gbytes.iter().enumerate() {
762                for k in 0..p {
763                    buf[bi * p + k] = self.lut[byte as usize * p + k] * norm;
764                }
765            }
766        } else {
767            let bits = self.bits as usize;
768            let mask = (1u32 << bits) - 1;
769            for (k, slot) in buf.iter_mut().enumerate() {
770                let idx = read_bits(row, (g * self.group + k) * bits, bits, mask);
771                *slot = self.levels[idx as usize] * norm;
772            }
773        }
774    }
775}
776
777/// Smallest row band handed to one thread. Below this the per-task overhead
778/// dominates whatever the total size is.
779#[cfg(feature = "parallel")]
780const MIN_PAR_ROWS: usize = 64;
781
782/// Smallest batch band handed to one thread, for the batch-split path.
783#[cfg(feature = "parallel")]
784const MIN_PAR_BATCH: usize = 8;
785
786/// Multiply-accumulate count below which a call stays on one thread.
787#[cfg(feature = "parallel")]
788const PAR_MIN_MACS: usize = 1 << 20;
789
790/// Accumulator lanes in the packed dot product.
791///
792/// Eight independent f32 lanes: two 128-bit vector FMAs per iteration on NEON or
793/// SSE, one 256-bit on AVX, and enough separate dependency chains to cover FMA
794/// latency on all of them. Must be a multiple of the largest `P` (4).
795pub const ACC_LANES: usize = 8;
796
797/// Dot one quantisation group against a pre-rotated activation slice.
798///
799/// `P` levels are packed per byte, so each iteration consumes `ACC_LANES / P`
800/// bytes. `P` and `ACC_LANES` are both compile-time, so the inner loops unroll
801/// fully and the index arithmetic folds away.
802#[inline(always)]
803fn dot_group<const P: usize>(lut: &[f32], gbytes: &[u8], gx: &[f32]) -> f32 {
804    debug_assert_eq!(ACC_LANES % P, 0);
805    let per_iter = ACC_LANES / P;
806    let mut lanes = [0.0f32; ACC_LANES];
807
808    let full = gbytes.len() - gbytes.len() % per_iter;
809    let mut bi = 0;
810    while bi < full {
811        // The decoded levels are staged in `vals` before the FMA loop on
812        // purpose. Multiplying straight out of the LUT looks leaner but measured
813        // 18% slower on aarch64: gathering into a contiguous array first is what
814        // lets the autovectoriser emit whole-vector loads and FMAs instead of
815        // per-`P` fragments.
816        let mut vals = [0.0f32; ACC_LANES];
817        for t in 0..per_iter {
818            let base = gbytes[bi + t] as usize * P;
819            let entry: &[f32; P] = lut[base..base + P].try_into().unwrap();
820            vals[t * P..(t + 1) * P].copy_from_slice(entry);
821        }
822        let xs: &[f32; ACC_LANES] = gx[bi * P..bi * P + ACC_LANES].try_into().unwrap();
823        for k in 0..ACC_LANES {
824            lanes[k] += vals[k] * xs[k];
825        }
826        bi += per_iter;
827    }
828
829    let mut acc: f32 = lanes.iter().sum();
830    // Tail, for a group smaller than one full iteration.
831    for b in bi..gbytes.len() {
832        let base = gbytes[b] as usize * P;
833        for k in 0..P {
834            acc += lut[base + k] * gx[b * P + k];
835        }
836    }
837    acc
838}
839
840/// Dot a decoded group against an activation slice using [`ACC_LANES`] lanes,
841/// in the same order [`dot_group`] accumulates. Used by the batched matmul so it
842/// matches the LUT matvec path bit for bit.
843#[inline(always)]
844fn group_dot_lanes(u: &[f32], x: &[f32]) -> f32 {
845    debug_assert_eq!(u.len(), x.len());
846    let chunks = u.len() / ACC_LANES;
847    let mut lanes = [0.0f32; ACC_LANES];
848    for c in 0..chunks {
849        let uu: &[f32; ACC_LANES] = u[c * ACC_LANES..(c + 1) * ACC_LANES].try_into().unwrap();
850        let xx: &[f32; ACC_LANES] = x[c * ACC_LANES..(c + 1) * ACC_LANES].try_into().unwrap();
851        for k in 0..ACC_LANES {
852            lanes[k] += uu[k] * xx[k];
853        }
854    }
855    let mut s: f32 = lanes.iter().sum();
856    for k in chunks * ACC_LANES..u.len() {
857        s += u[k] * x[k];
858    }
859    s
860}
861
862/// Sequential dot, matching the single-accumulator order `matvec_generic` uses
863/// for widths that do not divide a byte.
864#[inline(always)]
865fn group_dot_serial(u: &[f32], x: &[f32]) -> f32 {
866    let mut s = 0.0f32;
867    for (a, b) in u.iter().zip(x.iter()) {
868        s += a * b;
869    }
870    s
871}
872
873/// Read a `bits`-wide LSB-first field starting at bit offset `bit`.
874///
875/// Upstream packs in chunks of 8 indices OR'd into one word and emitted as
876/// `bits` little-endian bytes, which is equivalently one continuous LSB-first
877/// bitstream per row — index `k` occupies bits `[k*bits, (k+1)*bits)`.
878#[inline]
879fn read_bits(row: &[u8], bit: usize, bits: usize, mask: u32) -> u32 {
880    let byte = bit / 8;
881    let shift = bit % 8;
882    // A field is at most 4 bits wide, so it spans at most two bytes.
883    let lo = row[byte] as u32;
884    let hi = if shift + bits > 8 {
885        row[byte + 1] as u32
886    } else {
887        0
888    };
889    ((lo | (hi << 8)) >> shift) & mask
890}
891
892/// Build the byte-decode table for widths that divide a byte.
893///
894/// Returns `(indices_per_byte, table)` where `table[b*p .. b*p+p]` holds the
895/// levels for the `p` indices packed into byte `b`, LSB first. `(0, empty)`
896/// means no table — the caller must use the bit reader.
897fn build_lut(bits: u8, levels: &[f32]) -> (usize, Vec<f32>) {
898    // Ternary packs four signed 2-bit crumbs per byte, codes 3, 0, 1 for trit
899    // indices 0, 1, 2 — so sign-extending a crumb yields -1, 0, +1 directly.
900    let ternary = bits == TERNARY_RECORD_BITS;
901    let width = if ternary { 2 } else { bits as usize };
902    let per_byte = match width {
903        2 => 4,
904        4 => 2,
905        _ => return (0, Vec::new()), // 3 bits: no byte alignment
906    };
907    let mask = (1u32 << width) - 1;
908
909    let mut lut = vec![0.0f32; 256 * per_byte];
910    for byte in 0..256usize {
911        for k in 0..per_byte {
912            let code = ((byte as u32) >> (k * width)) & mask;
913            let idx = if ternary {
914                // Mirrors `export._unpack_ternary_crumbs`. Code 2 cannot be
915                // produced by the encoder; map it to the zero level.
916                match code {
917                    3 => 0,
918                    0 => 1,
919                    1 => 2,
920                    _ => 1,
921                }
922            } else {
923                code as usize
924            };
925            lut[byte * per_byte + k] = levels[idx];
926        }
927    }
928    (per_byte, lut)
929}
930
931#[cfg(test)]
932mod tests {
933    use super::*;
934    use crate::hadamard::fwht_normalized;
935
936    /// Concatenated cb2|cb3|cb4 with the shape of a real header codebook.
937    /// Values are arbitrary but distinct so a wrong slice or index shows up.
938    fn fake_codebook(group: usize) -> Vec<f32> {
939        let s = 1.0 / math::sqrt(group as f32);
940        let mut cb = Vec::with_capacity(CODEBOOK_LEN);
941        for &bits in &CODEBOOK_WIDTHS {
942            let levels = 1usize << bits;
943            for i in 0..levels {
944                // Monotone increasing and symmetric-ish, like Lloyd-Max output.
945                let v = (i as f32 - (levels as f32 - 1.0) / 2.0) / (levels as f32 / 2.0);
946                cb.push(v * s);
947            }
948        }
949        assert_eq!(cb.len(), CODEBOOK_LEN);
950        cb
951    }
952
953    /// Pack indices LSB-first, exactly as `export._pack_lsb` does.
954    fn pack_lsb(idx: &[u8], out: usize, in_pad: usize, bits: usize) -> Vec<u8> {
955        let row_bytes = in_pad * bits / 8;
956        let mut packed = vec![0u8; out * row_bytes];
957        for o in 0..out {
958            for k in 0..in_pad {
959                let v = idx[o * in_pad + k] as u32;
960                let bit = k * bits;
961                for b in 0..bits {
962                    if (v >> b) & 1 == 1 {
963                        let abs = bit + b;
964                        packed[o * row_bytes + abs / 8] |= 1 << (abs % 8);
965                    }
966                }
967            }
968        }
969        packed
970    }
971
972    fn f32_to_f16(v: f32) -> u16 {
973        let x = v.to_bits();
974        let sign = ((x >> 31) as u16) << 15;
975        let exp = ((x >> 23) & 0xFF) as i32 - 127;
976        let mant = x & 0x7F_FFFF;
977        if exp < -14 {
978            return sign;
979        }
980        if exp > 15 {
981            return sign | 0x7C00;
982        }
983        sign | (((exp + 15) as u16) << 10) | ((mant >> 13) as u16)
984    }
985
986    struct Case {
987        w: CqWeight,
988        /// The reference reconstruction, `[out, in_feat]`, built the way
989        /// `export._cq_unpack` builds it: decode, scale, then rotate.
990        dense: Vec<f32>,
991    }
992
993    fn build_case(out: usize, in_feat: usize, group: usize, bits: u8) -> Case {
994        let cb = fake_codebook(group);
995        let levels: Vec<f32> = if bits == TERNARY_RECORD_BITS {
996            let s = TERNARY_CENTROID / math::sqrt(group as f32);
997            vec![-s, 0.0, s]
998        } else {
999            codebook_slice(&cb, bits).unwrap().to_vec()
1000        };
1001        let n_levels = levels.len();
1002
1003        let in_pad = in_feat.div_ceil(group) * group;
1004        let num_groups = in_pad / group;
1005
1006        // Deterministic indices that exercise every level.
1007        let idx: Vec<u8> = (0..out * in_pad)
1008            .map(|i| ((i * 7 + i / 13) % n_levels) as u8)
1009            .collect();
1010        let norms: Vec<f32> = (0..out * num_groups)
1011            .map(|i| 0.25 + (i % 11) as f32 * 0.125)
1012            .collect();
1013
1014        let packed = if bits == TERNARY_RECORD_BITS {
1015            // trit 0,1,2 -> crumb 3,0,1
1016            let crumbs: Vec<u8> = idx
1017                .iter()
1018                .map(|&t| if t == 0 { 3 } else { t - 1 })
1019                .collect();
1020            pack_lsb(&crumbs, out, in_pad, 2)
1021        } else {
1022            pack_lsb(&idx, out, in_pad, bits as usize)
1023        };
1024
1025        let mut blob = packed;
1026        for &n in &norms {
1027            blob.extend_from_slice(&f32_to_f16(n).to_le_bytes());
1028        }
1029
1030        // Reference dense reconstruction.
1031        let mut dense = vec![0.0f32; out * in_feat];
1032        for o in 0..out {
1033            for g in 0..num_groups {
1034                let mut u: Vec<f32> = (0..group)
1035                    .map(|k| levels[idx[o * in_pad + g * group + k] as usize])
1036                    .collect();
1037                let norm = math::f16_to_f32(f32_to_f16(norms[o * num_groups + g]));
1038                for v in u.iter_mut() {
1039                    *v *= norm;
1040                }
1041                fwht_normalized(&mut u);
1042                for (k, &val) in u.iter().enumerate() {
1043                    let col = g * group + k;
1044                    if col < in_feat {
1045                        dense[o * in_feat + col] = val;
1046                    }
1047                }
1048            }
1049        }
1050
1051        let w = CqWeight::from_blob(&blob, out, in_feat, group, bits, &cb).unwrap();
1052        Case { w, dense }
1053    }
1054
1055    const WIDTHS: [u8; 4] = [2, 3, 4, TERNARY_RECORD_BITS];
1056
1057    #[test]
1058    fn dequantize_matches_reference_reconstruction() {
1059        for &bits in &WIDTHS {
1060            for &(out, in_feat, group) in &[(5usize, 16usize, 16usize), (3, 40, 8), (7, 128, 128)] {
1061                let c = build_case(out, in_feat, group, bits);
1062                let mut got = vec![0.0f32; out * in_feat];
1063                c.w.dequantize_to(&mut got);
1064                for (i, (&g_i, &want)) in got.iter().zip(c.dense.iter()).enumerate() {
1065                    assert!(
1066                        (g_i - want).abs() < 1e-5,
1067                        "bits={bits} shape={out}x{in_feat} g={group} i={i}: {g_i} vs {want}"
1068                    );
1069                }
1070            }
1071        }
1072    }
1073
1074    /// The whole point of the module: the activation-side rotation must give the
1075    /// same answer as multiplying by the reconstructed weights.
1076    #[test]
1077    fn matvec_matches_dense_multiply() {
1078        for &bits in &WIDTHS {
1079            for &(out, in_feat, group) in &[
1080                (5usize, 16usize, 16usize),
1081                (3, 40, 8),
1082                (9, 128, 128),
1083                (4, 200, 64),
1084            ] {
1085                let c = build_case(out, in_feat, group, bits);
1086                let x: Vec<f32> = (0..in_feat)
1087                    .map(|i| (i as f32 * 0.31).sin() * 1.7 + 0.05 * i as f32)
1088                    .collect();
1089
1090                let mut got = vec![0.0f32; out];
1091                c.w.matvec(&x, &mut got);
1092
1093                for (o, &g_o) in got.iter().enumerate() {
1094                    let want: f32 = (0..in_feat).map(|j| c.dense[o * in_feat + j] * x[j]).sum();
1095                    let tol = 1e-4 * want.abs().max(1.0);
1096                    assert!(
1097                        (g_o - want).abs() < tol,
1098                        "bits={bits} shape={out}x{in_feat} g={group} o={o}: {g_o} vs {want}"
1099                    );
1100                }
1101            }
1102        }
1103    }
1104
1105    /// Padded columns must contribute nothing — upstream truncates them away.
1106    #[test]
1107    fn padding_does_not_leak_into_matvec() {
1108        // in_feat 40 with group 32 pads to 64; 24 columns are dropped upstream.
1109        let c = build_case(6, 40, 32, 2);
1110        let x: Vec<f32> = (0..40).map(|i| 1.0 + i as f32).collect();
1111        let mut got = vec![0.0f32; 6];
1112        c.w.matvec(&x, &mut got);
1113        for (o, &g_o) in got.iter().enumerate() {
1114            let want: f32 = (0..40).map(|j| c.dense[o * 40 + j] * x[j]).sum();
1115            assert!((g_o - want).abs() < 1e-3 * want.abs().max(1.0));
1116        }
1117    }
1118
1119    /// A row band must equal the same rows of a full matvec.
1120    #[test]
1121    fn row_range_matches_full_matvec() {
1122        for &bits in &WIDTHS {
1123            let c = build_case(24, 128, 128, bits);
1124            let x: Vec<f32> = (0..128).map(|i| (i as f32 * 0.19).sin()).collect();
1125            let mut full = vec![0.0f32; 24];
1126            c.w.matvec(&x, &mut full);
1127
1128            let mut xh = vec![0.0f32; c.w.prepared_len()];
1129            c.w.prepare_input(&x, &mut xh);
1130            for &(start, len) in &[(0usize, 4usize), (4, 4), (8, 16), (20, 4), (23, 1)] {
1131                let mut band = vec![0.0f32; len];
1132                c.w.matvec_rows_prepared(&xh, start, &mut band);
1133                assert_eq!(
1134                    &band[..],
1135                    &full[start..start + len],
1136                    "bits={bits} band {start}+{len}"
1137                );
1138            }
1139        }
1140    }
1141
1142    /// The batched matmul must agree with `batch` separate matvecs, exactly:
1143    /// same values in the same order, only the loop nesting differs.
1144    /// The optimised matvec must agree with the simple reference. Different
1145    /// accumulation order, so this is a tolerance check bounded by the
1146    /// activation's L1 norm rather than by the result.
1147    #[test]
1148    fn matvec_matches_simple_reference() {
1149        for &bits in &WIDTHS {
1150            for &(out, in_feat, group) in &[
1151                (16usize, 128usize, 128usize),
1152                (5, 40, 8),
1153                (7, 200, 64),
1154                (9, 512, 128),
1155            ] {
1156                let c = build_case(out, in_feat, group, bits);
1157                let x: Vec<f32> = (0..in_feat)
1158                    .map(|i| (i as f32 * 0.29).sin() * 2.0)
1159                    .collect();
1160                let mut xh = vec![0.0f32; c.w.prepared_len()];
1161                c.w.prepare_input(&x, &mut xh);
1162
1163                let mut fast = vec![0.0f32; out];
1164                let mut slow = vec![0.0f32; out];
1165                c.w.matvec_prepared(&xh, &mut fast);
1166                c.w.matvec_prepared_reference(&xh, &mut slow);
1167
1168                let l1: f32 = xh.iter().map(|v| v.abs()).sum();
1169                for o in 0..out {
1170                    assert!(
1171                        (fast[o] - slow[o]).abs() <= 1e-5 * l1.max(1.0),
1172                        "bits={bits} {out}x{in_feat} g={group} o={o}: {} vs {}",
1173                        fast[o],
1174                        slow[o]
1175                    );
1176                }
1177            }
1178        }
1179    }
1180
1181    #[test]
1182    fn matmul_matches_repeated_matvec() {
1183        for &bits in &WIDTHS {
1184            for &(out, in_feat, group) in &[
1185                (24usize, 128usize, 128usize),
1186                (9, 40, 8),
1187                (5, 200, 64),
1188                (16, 512, 128),
1189            ] {
1190                let c = build_case(out, in_feat, group, bits);
1191                for &batch in &[1usize, 2, 7, 16] {
1192                    // Distinct activation per batch row.
1193                    let mut xh = vec![0.0f32; batch * c.w.prepared_len()];
1194                    let mut xs = Vec::new();
1195                    for b in 0..batch {
1196                        let x: Vec<f32> = (0..in_feat)
1197                            .map(|i| ((i + b * 13) as f32 * 0.23).sin() * 1.3)
1198                            .collect();
1199                        c.w.prepare_input(
1200                            &x,
1201                            &mut xh[b * c.w.prepared_len()..(b + 1) * c.w.prepared_len()],
1202                        );
1203                        xs.push(x);
1204                    }
1205
1206                    for &(row_start, rows) in &[(0usize, out), (0, 1), (1, 3.min(out - 1))] {
1207                        if row_start + rows > out || rows == 0 {
1208                            continue;
1209                        }
1210                        let mut got = vec![0.0f32; batch * rows];
1211                        let mut acc = vec![0.0f32; batch];
1212                        c.w.matmul_rows_prepared(&xh, batch, row_start, rows, &mut got, &mut acc);
1213
1214                        for b in 0..batch {
1215                            let mut want = vec![0.0f32; rows];
1216                            c.w.matvec_rows_prepared(
1217                                &xh[b * c.w.prepared_len()..(b + 1) * c.w.prepared_len()],
1218                                row_start,
1219                                &mut want,
1220                            );
1221                            for r in 0..rows {
1222                                assert_eq!(
1223                                    got[b * rows + r],
1224                                    want[r],
1225                                    "bits={bits} {out}x{in_feat} g={group} batch={batch} \
1226                                     rows={row_start}+{rows} b={b} r={r}"
1227                                );
1228                            }
1229                        }
1230                    }
1231                }
1232            }
1233        }
1234    }
1235
1236    #[test]
1237    fn prepared_input_is_reusable_across_weights() {
1238        let a = build_case(4, 128, 128, 2);
1239        let b = build_case(6, 128, 128, 4);
1240        let x: Vec<f32> = (0..128).map(|i| (i as f32 * 0.07).cos()).collect();
1241
1242        let mut xh = vec![0.0f32; a.w.prepared_len()];
1243        a.w.prepare_input(&x, &mut xh);
1244
1245        let mut ya = vec![0.0f32; 4];
1246        let mut yb = vec![0.0f32; 6];
1247        a.w.matvec_prepared(&xh, &mut ya);
1248        b.w.matvec_prepared(&xh, &mut yb);
1249
1250        let mut ya_ref = vec![0.0f32; 4];
1251        let mut yb_ref = vec![0.0f32; 6];
1252        a.w.matvec(&x, &mut ya_ref);
1253        b.w.matvec(&x, &mut yb_ref);
1254        assert_eq!(ya, ya_ref);
1255        assert_eq!(yb, yb_ref);
1256    }
1257
1258    #[test]
1259    fn row_bytes_match_shipped_tensor_sizes() {
1260        // Directory records from the real needle2.cact.
1261        // (out, in, bits, group) -> total blob bytes
1262        for &(out, in_f, bits, group, want) in &[
1263            (8192usize, 512usize, 4u8, 128usize, 2_162_688usize), // embedding
1264            (512, 512, 2, 128, 69_632),                           // q_proj
1265            (256, 512, 2, 128, 34_816),                           // k_proj
1266            (108, 2048, 4, 128, 114_048),                         // mhc_phi_pre
1267            (432, 2048, 4, 128, 456_192),                         // mhc_phi_res
1268            (32768, 128, 2, 128, 1_114_112),                      // engram tables
1269        ] {
1270            let in_pad = in_f.div_ceil(group) * group;
1271            let got = out * packed_row_bytes(in_pad, bits) + out * (in_pad / group) * 2;
1272            assert_eq!(got, want, "{out}x{in_f} @ {bits}b/{group}");
1273        }
1274    }
1275
1276    #[test]
1277    fn rejects_bad_geometry() {
1278        let cb = fake_codebook(128);
1279        let blob = vec![0u8; 1 << 16];
1280        // CqWeight holds bulk buffers and is deliberately not Debug/PartialEq,
1281        // so compare the error side only.
1282        let err = |b: &[u8], out, in_f, group, bits, cb: &[f32]| {
1283            CqWeight::from_blob(b, out, in_f, group, bits, cb).err()
1284        };
1285        assert_eq!(
1286            err(&blob, 4, 128, 128, 7, &cb),
1287            Some(CqError::UnsupportedBits(7))
1288        );
1289        assert_eq!(err(&blob, 4, 128, 96, 2, &cb), Some(CqError::BadGroup(96)));
1290        assert_eq!(
1291            err(&blob, 4, 128, 100, 2, &cb),
1292            Some(CqError::BadGroup(100))
1293        );
1294        assert_eq!(err(&blob, 0, 128, 128, 2, &cb), Some(CqError::EmptyShape));
1295        assert_eq!(err(&blob, 4, 0, 128, 2, &cb), Some(CqError::EmptyShape));
1296        assert!(matches!(
1297            err(&[0u8; 4], 512, 512, 128, 2, &cb),
1298            Some(CqError::ShortBlob { .. })
1299        ));
1300        assert_eq!(
1301            err(&blob, 4, 128, 128, 2, &cb[..10]),
1302            Some(CqError::BadCodebookLen(10))
1303        );
1304    }
1305}
1306
1307#[cfg(test)]
1308mod dispatch_tests {
1309    // This crate is `no_std`; the test harness links std, so pull it in for the
1310    // feature-detection macro and for printing.
1311    extern crate std;
1312
1313    /// On x86_64 with AVX2 present, the wide path must actually be taken.
1314    ///
1315    /// Without this the AVX2 build could be dead code and every differential test
1316    /// would still pass, having only ever exercised the SSE2 fallback. This is
1317    /// the only assertion that the dispatch itself happens; development for this
1318    /// crate is on aarch64, where there is nothing to dispatch, so it is CI on
1319    /// x86_64 runners that gives the claim teeth.
1320    #[test]
1321    #[cfg(all(target_arch = "x86_64", feature = "simd"))]
1322    fn avx2_path_is_actually_selected_when_supported() {
1323        let cpuid_says = self::std::is_x86_feature_detected!("avx2");
1324        let we_say = crate::quant::has_avx2();
1325        assert_eq!(
1326            we_say, cpuid_says,
1327            "has_avx2() disagrees with std's detection; the dispatch would pick the wrong kernel"
1328        );
1329        if cpuid_says {
1330            assert!(
1331                we_say,
1332                "AVX2 is present but the wide kernel would not be selected"
1333            );
1334            self::std::eprintln!("AVX2 dispatch active: the wide packed-dot kernel is in use");
1335        } else {
1336            self::std::eprintln!("no AVX2 on this host; the portable kernel is in use");
1337        }
1338    }
1339    // There is deliberately no aarch64 counterpart: NEON is part of the ARMv8-A
1340    // baseline, so there is no runtime choice to verify. `matvec_matches_simple_reference`
1341    // covers the kernel's correctness on whatever LLVM emits there.
1342}