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