Skip to main content

ferrox_cuda/
mul_mm.rs

1//! `mul_mm`: a batched quantized GEMM for CUDA -- the CUDA C source and
2//! the per-quant-kind dispatch table.
3//!
4//! # UNRUN ON HARDWARE
5//!
6//! **No kernel in this module has ever executed on a GPU.** There is no
7//! NVIDIA hardware in the environment it was written in, and this repo's
8//! standing rule (`docs/plans/roadmap.md`, "CUDA stays at *must
9//! compile*") is that a CUDA claim is worth nothing until someone
10//! measures it. Nothing here may be described as a measured capability
11//! in `docs/FEATURES.md` or `docs/MODELS.md` until
12//! `cargo test -p ferrox-cuda --features cuda -- --ignored` has been run
13//! on a real device and the result written down.
14//!
15//! What *is* established, and how:
16//!
17//! 1. **It is a port, not a design.** The arithmetic comes from
18//!    `ferrox-metal`'s `mul_mm_sg_impl` (`crates/ferrox-metal/src/gpu.rs`),
19//!    which is at parity with llama.cpp on Metal and has goldens. The
20//!    per-kind unpack functions in [`crate::mul_mm_kinds`] are
21//!    line-for-line transcriptions of that file's `Q8_0Dequant` /
22//!    `Q4_0Dequant` / `Q5_0Dequant` / `Q5KDequant` / `Q6KDequant`
23//!    functors, which are themselves llama's `dequantize_q8_0` /
24//!    `dequantize_q4_0` / `dequantize_q5_0` / `dequantize_q5_K` /
25//!    `dequantize_q6_K`. The three codebook rows come from llama's
26//!    `ggml/src/ggml-cuda/dequantize.cuh` directly (`:408`, `:424`,
27//!    `:439`), because Metal has no MXFP4 and its IQ4_XS functor uses a
28//!    different sub-block partition.
29//! 2. **It has a scalar twin.** [`crate::mul_mm_ref`] emulates this
30//!    kernel on the CPU -- same tiling, same clamping, same index
31//!    arithmetic, same accumulation order -- and its tests, which run in
32//!    the default (no-GPU) build, hold it against an independent
33//!    dequantize-then-GEMM built on `ferrox_quant`. A transcription
34//!    error in the index math or the unpack shows up there.
35//! 3. **Geometry cannot drift.** Every tile constant in the emitted CUDA
36//!    is `#define`d from the Rust constants in this module, which are
37//!    the same constants the twin uses. The kernel and its twin cannot
38//!    disagree about a tile size; they can only disagree about the ~10
39//!    lines of unpack expression, which is what (1) and (2) cover.
40//!
41//! 4. **The emitted C itself was executed.**
42//!    `tools/mul_mm_host_check/run.sh` compiles each generated `.cu`
43//!    against a keyword-and-barrier shim and runs it on the host CPU --
44//!    one real thread per CUDA thread, a counting barrier for
45//!    `__syncthreads()`, one block at a time so `__shared__` behaves --
46//!    then compares it to the twin. Result on 2026-09-01, macOS/clang,
47//!    both kinds then in the table, three shapes (exact tiles, partial
48//!    on both axes, narrow batch): **zero mismatches, bit for bit**,
49//!    including 1,458 positions where a degenerate f16 scale made both
50//!    sides NaN together. Deleting one term of the CUDA-only index
51//!    arithmetic makes it fail, so it is a check and not a formality.
52//!
53//!    Re-run on 2026-09-05 over all six kinds: **zero mismatches**
54//!    again, 40,932 compared positions. That run was the first for the
55//!    K-quants and for Q5_0 -- the tool took `n_cols` from a fixed list
56//!    that is not a whole Q4_K super-block, so from the day the
57//!    K-quants landed it panicked on the first one and checked
58//!    NOTHING. A tool that cannot fire is worse than no tool; `n_cols`
59//!    is now rounded up per kind. Sabotaging Q5_0's `qh` shift
60//!    (`12` for `16`) makes it report 6,096 mismatches.
61//!
62//!    Re-run on 2026-09-09 over all ELEVEN kinds -- the three codebook
63//!    formats, Q2_K and Q3_K included: **zero mismatches**, 75,042
64//!    compared positions.
65//!    That run was also the tool's first since the inner loop started
66//!    reading its operands as `float4` -- the host shim had no such
67//!    type, so every kind failed to compile and `set -e` aborted the
68//!    script. It was never green rather than green and blind, but the
69//!    effect on coverage was the same. Sabotaging IQ4_XS's `scales_h`
70//!    shift (`2 * il` for `2 * ib`) in the CUDA only makes it report
71//!    4,000 mismatches out of 4,096, and inverting Q3_K's `hmask` bias
72//!    test the same way reports 3,968.
73//!
74//! What none of that covers, and what only hardware can settle: that
75//! NVRTC accepts the source (clang and NVRTC are different front ends),
76//! that the barrier placement survives a real warp scheduler, that the
77//! launch configuration is valid on the target device, and what any of
78//! it costs. A real GPU also contracts `acc += a * b` into an FMA, so
79//! on-device results will be *close to* rather than equal to the twin's;
80//! the hardware test compares with a relative tolerance for that reason.
81//!
82//! # Why this shape
83//!
84//! `ferrox-cuda` had no matrix-matrix product of any kind, so a batched
85//! prefill decomposed into one matvec per position
86//! (`crates/ferrox-core/src/weight_matrix.rs`, the CUDA batch arm). This
87//! is the naive tiled `mul_mm` half of
88//! `docs/plans/llama-cpp-gap-inventory.md` ยง2.7; the `dp4a` integer path
89//! (llama's `mmq.cu`) is explicitly *not* attempted here.
90//!
91//! Unlike Metal's version this uses no matrix-fragment intrinsics
92//! (`wmma`/`mma`): the K-loop is a plain fp32 FMA over shared-memory
93//! tiles. That costs the constant factor and buys a kernel whose
94//! arithmetic a CPU twin can reproduce exactly, which is the only kind
95//! of correctness available without a device.
96
97/// Rows of the weight matrix per threadblock tile.
98pub const BM: usize = 64;
99/// Tokens (batch entries) per threadblock tile.
100pub const BN: usize = 128;
101/// K-elements consumed per tile step. Must be a multiple of [`SUB`], and
102/// every real quantized row length is a multiple of 32, so a K-loop that
103/// steps 32 never straddles a partial block.
104pub const BK: usize = 32;
105/// Rows of the output micro-tile each thread owns.
106pub const TM: usize = 4;
107/// Columns of the output micro-tile each thread owns.
108pub const TN: usize = 8;
109/// Threads per block. `BM/TM * BN/TN` -- one thread per micro-tile.
110pub const THREADS: usize = (BM / TM) * (BN / TN);
111/// Elements produced by one call to the per-kind unpack function. This
112/// is llama's (and `ferrox-metal`'s) sub-block granularity: `il` selects
113/// which 16 consecutive elements of a super-block to decode.
114pub const SUB: usize = 16;
115
116// Compile-time geometry gates. These are the invariants the kernel's
117// `tx`/`ty` decomposition and its A-tile loader assume; a retune that
118// broke one would otherwise produce a kernel that launches and is
119// wrong, so they fail the build rather than a test.
120const _: () = assert!(THREADS == (BM / TM) * (BN / TN));
121const _: () = assert!(BM.is_multiple_of(TM) && BN.is_multiple_of(TN));
122const _: () = assert!(
123    BK.is_multiple_of(SUB),
124    "the K-tile must be whole sub-blocks"
125);
126const _: () = assert!(
127    BM * (BK / SUB) <= THREADS,
128    "the A-tile loader uses a prefix of the block"
129);
130const _: () = assert!(THREADS <= 1024, "CUDA caps a block at 1024 threads");
131// The inner loop reads its micro-tile operands as `float4`, which is
132// what keeps a warp's 32 lanes off eight shared-memory banks. That
133// needs each thread's slice to start 16-byte aligned: the micro-tile
134// widths must be multiples of four, and so must the shared rows they
135// index into, or the fourth lane of a row starts mid-vector.
136const _: () = assert!(
137    TM.is_multiple_of(4) && TN.is_multiple_of(4),
138    "the inner loop loads float4 from shared memory"
139);
140const _: () = assert!(
141    BM.is_multiple_of(4) && BN.is_multiple_of(4),
142    "a shared row must keep the next row 16-byte aligned"
143);
144
145/// One quantized weight format the GEMM can consume.
146///
147/// Adding a format is a row in [`KINDS`] plus a `dequant_src` snippet --
148/// never a second copy of the GEMM body. That is the seam
149/// `ferrox-metal` proved: its `mul_mm_sg_impl` is one templated body
150/// with seven `Dequant` functors, written that way *because* the
151/// previous copy-per-format generation is how
152/// `gqa_prefill_fa_vec_d256` ended up handling half a head.
153#[derive(Debug, Clone, Copy)]
154pub struct MulMmKind {
155    /// GGUF quant name, for error messages.
156    pub name: &'static str,
157    /// NVRTC module cache key. Must be unique per kind.
158    pub module_name: &'static str,
159    /// `__global__` entry point name inside that module.
160    pub fn_name: &'static str,
161    /// On-disk stride of one super-block.
162    pub block_bytes: usize,
163    /// Elements one super-block decodes to.
164    pub block_elems: usize,
165    /// CUDA C defining
166    /// `void ferrox_dequant_sub(const unsigned char* xb, int il, float* reg)`,
167    /// writing `SUB` floats: the elements at `[SUB*il, SUB*il + SUB)`
168    /// of the super-block at `xb`, in ascending element order.
169    ///
170    /// **The contract is that signature and nothing narrower.** It was
171    /// once informally "multiply the stored code by a scale and add a
172    /// bias", which is true of every affine format and true of no
173    /// codebook one: IQ4_NL, IQ4_XS and MXFP4 read a 4-bit code and
174    /// *index a table* with it. Those kinds fill [`Self::codebook`] and
175    /// look the value up; nothing else about the seam changes, which is
176    /// the point of the seam.
177    pub dequant_src: &'static str,
178    /// The 16-entry table `dequant_src` indexes, for the formats whose
179    /// stored code is an index rather than a magnitude. `None` for the
180    /// affine kinds, which need no table.
181    ///
182    /// [`kernel_src`] emits this as a `__constant__` array ahead of
183    /// `dequant_src`, so it is visible to every thread of the block
184    /// without being reloaded per element.
185    pub codebook: Option<Codebook>,
186    /// The scalar twin of `dequant_src`: the same arithmetic in Rust,
187    /// on the host, in the same order. It sits in this struct rather
188    /// than in a parallel table so a kind cannot be added without one --
189    /// the untestable half and the testable half are the same row.
190    pub dequant_twin: fn(xb: &[u8], il: usize, reg: &mut [f32; SUB]),
191}
192
193/// The 16-entry value table a codebook format's 4-bit code indexes.
194///
195/// One slice serves both halves of the kernel: [`kernel_src`] formats
196/// [`Self::values`] into the emitted `__constant__` array, and the Rust
197/// `dequant_twin` beside it indexes the same `values`. There is no
198/// second copy of the numbers to drift, which matters more here than
199/// for an affine format -- a codebook is 16 arbitrary constants that no
200/// arithmetic can re-derive, so a single transposed pair would decode
201/// every tensor slightly wrong and nothing would look obviously broken.
202#[derive(Debug, Clone, Copy, PartialEq)]
203pub struct Codebook {
204    /// The `__constant__` array's identifier in the emitted CUDA C.
205    /// Must be what `dequant_src` spells.
206    pub c_name: &'static str,
207    /// The values, code 0 first.
208    pub values: &'static [f32; 16],
209}
210
211/// Emits one codebook as a `__constant__` array.
212///
213/// `{:?}` on an `f32` round-trips in Rust, and every value in every
214/// table here is exactly representable, so the emitted literal names the
215/// same float the twin uses. `-0.0` survives it, which MXFP4 needs: its
216/// code 8 is negative zero and printing it as `0` would be a different
217/// number.
218fn codebook_src(cb: &Codebook) -> String {
219    let values: Vec<String> = cb.values.iter().map(|v| format!("{v:?}f")).collect();
220    format!(
221        "\n__constant__ float {}[16] = {{{}}};\n",
222        cb.c_name,
223        values.join(", ")
224    )
225}
226
227impl MulMmKind {
228    /// 16-value sub-blocks per super-block -- llama's `nl` template
229    /// argument (2 for the 32-element legacy formats, 16 for the
230    /// 256-element K-quants).
231    pub const fn nl(&self) -> usize {
232        self.block_elems / SUB
233    }
234}
235
236// The per-kind rows live in `mul_mm_kinds`, one module per format
237// family, because this file is the GEMM and they are the formats. They
238// are re-exported here unchanged: `mul_mm::Q4_K` is the path every
239// caller and every test already uses, and a refactor that moves a
240// definition should not move a public name.
241pub use crate::mul_mm_kinds::kquant::K_SCALE_MIN_SRC;
242pub use crate::mul_mm_kinds::{
243    IQ4_NL, IQ4_XS, MXFP4, Q2_K, Q3_K, Q4_0, Q4_K, Q5_0, Q5_K, Q6_K, Q8_0,
244};
245
246/// Scalar twin of the CUDA `ferrox_f16_to_f32` in `F16_SRC`: the same
247/// bit surgery, including the `exp == 31` NaN/Inf arm. `ldexpf(m, e)`
248/// with an exact power of two is a multiply, so this is bit-identical
249/// rather than merely close.
250///
251/// Held against `half::f16` by a test, which is what makes it a twin of
252/// something and not a second guess.
253pub fn f16_to_f32(bits: u16) -> f32 {
254    let sign = (bits >> 15) & 0x1;
255    let exp = u32::from((bits >> 10) & 0x1F);
256    let mant = u32::from(bits & 0x3FF);
257    let scale = if exp == 0 {
258        (mant as f32) * 2f32.powi(-24)
259    } else if exp == 31 {
260        if mant != 0 {
261            f32::from_bits(0x7fc0_0000)
262        } else {
263            f32::from_bits(0x7f80_0000)
264        }
265    } else {
266        ((mant | 0x400) as f32) * 2f32.powi(exp as i32 - 25)
267    };
268    if sign != 0 {
269        -scale
270    } else {
271        scale
272    }
273}
274
275/// The dispatch table. A caller looks up by GGUF quant name; a new
276/// format is one row here.
277pub const KINDS: &[MulMmKind] = &[
278    Q8_0, Q4_0, Q5_0, Q2_K, Q3_K, Q4_K, Q5_K, Q6_K, IQ4_NL, IQ4_XS, MXFP4,
279];
280
281/// Looks up a kind by its GGUF quant name (`"Q4_0"`, `"Q8_0"`).
282/// `None` means this GEMM does not implement that format -- the caller
283/// must fall back and say so, never compute something else.
284pub fn kind_by_name(name: &str) -> Option<&'static MulMmKind> {
285    KINDS.iter().find(|k| k.name == name)
286}
287
288/// f16 -> f32 by explicit bit surgery, shared with the matvec kernels in
289/// `gpu.rs`. NVRTC has `__half` available but only with the CUDA headers
290/// on the include path, which this crate deliberately does not require.
291const F16_SRC: &str = r#"
292__device__ __forceinline__ float ferrox_f16_to_f32(unsigned short bits) {
293    unsigned int sign = (bits >> 15) & 0x1u;
294    unsigned int exp = (bits >> 10) & 0x1Fu;
295    unsigned int mant = bits & 0x3FFu;
296    float scale;
297    if (exp == 0) {
298        scale = ldexpf((float)mant, -24);
299    } else if (exp == 31) {
300        scale = mant ? __int_as_float(0x7fc00000) : __int_as_float(0x7f800000);
301    } else {
302        scale = ldexpf((float)(mant | 0x400), (int)exp - 25);
303    }
304    return sign ? -scale : scale;
305}
306"#;
307
308/// The GEMM body, identical for every quant kind.
309///
310/// `src0` is the quantized weight matrix, `n_rows` rows of `row_bytes`
311/// each. `src1` is `batch` activation rows of `n_cols` f32. `dst` is
312/// written as `dst[token * n_rows + row]`, which is the layout
313/// `WeightMatrix::apply_batch` already returns.
314///
315/// Every `FX_*` name is `#define`d by [`kernel_src`] from this module's
316/// Rust constants, so the emitted kernel and [`crate::mul_mm_ref`]
317/// cannot disagree about geometry.
318///
319/// The out-of-range row clamp is llama's trick, kept from
320/// `mul_mm_sg_impl`: a lane whose row does not exist re-reads the last
321/// valid row rather than branching, so the load loop stays uniform, and
322/// its result is thrown away by the bounds check at the store. Reading
323/// a real row also means the dequant never touches unmapped bytes.
324const BODY_SRC: &str = r#"
325extern "C" __global__ void FX_FN_NAME(
326    const unsigned char* __restrict__ src0,
327    const float* __restrict__ src1,
328    float* __restrict__ dst,
329    int n_rows,
330    int n_cols,
331    int batch,
332    int row_bytes
333) {
334    __shared__ float sa[FX_BK][FX_BM];
335    __shared__ float sb[FX_BK][FX_BN];
336
337    const int r0 = blockIdx.y * FX_BM;
338    const int r1 = blockIdx.x * FX_BN;
339    const int tid = threadIdx.x;
340
341    // Micro-tile owner: `tx` walks rows, `ty` walks tokens.
342    const int tx = tid % (FX_BM / FX_TM);
343    const int ty = tid / (FX_BM / FX_TM);
344
345    float acc[FX_TN][FX_TM];
346#pragma unroll
347    for (int n = 0; n < FX_TN; n++) {
348#pragma unroll
349        for (int m = 0; m < FX_TM; m++) {
350            acc[n][m] = 0.0f;
351        }
352    }
353
354    for (int k0 = 0; k0 < n_cols; k0 += FX_BK) {
355        // Guards the previous iteration's reads of sa/sb.
356        __syncthreads();
357
358        // A-tile: one thread decodes one FX_SUB-element sub-block, so
359        // FX_BM * (FX_BK / FX_SUB) threads cover the tile. Stored
360        // k-major so the K-loop below reads one row of sa per step.
361        if (tid < FX_BM * (FX_BK / FX_SUB)) {
362            const int lr = tid / (FX_BK / FX_SUB);
363            const int ils = tid % (FX_BK / FX_SUB);
364            int row = r0 + lr;
365            if (row >= n_rows) {
366                row = n_rows - 1;
367            }
368            const unsigned char* rp =
369                src0 + (size_t)row * (size_t)row_bytes;
370            const int sub = (k0 / FX_SUB) + ils;
371            float reg[FX_SUB];
372            ferrox_dequant_sub(
373                rp + (size_t)(sub / FX_NL) * (size_t)FX_BLOCK_BYTES,
374                sub % FX_NL,
375                reg);
376#pragma unroll
377            for (int i = 0; i < FX_SUB; i++) {
378                sa[FX_SUB * ils + i][lr] = reg[i];
379            }
380        }
381
382        // B-tile: consecutive threads read consecutive k of one token.
383        // Tokens past the end are zero-filled rather than skipped, so
384        // the K-loop needs no per-token predicate.
385        for (int idx = tid; idx < FX_BK * FX_BN; idx += FX_THREADS) {
386            const int j = idx / FX_BK;
387            const int kk = idx % FX_BK;
388            const int col = r1 + j;
389            sb[kk][j] = (col < batch)
390                ? src1[(size_t)col * (size_t)n_cols + (size_t)(k0 + kk)]
391                : 0.0f;
392        }
393
394        __syncthreads();
395
396#pragma unroll
397        for (int kk = 0; kk < FX_BK; kk++) {
398            float a[FX_TM];
399            float b[FX_TN];
400            // One 16-byte load per four operands, not four 4-byte ones.
401            // A warp's 32 lanes take 16 distinct `tx`, so the scalar
402            // form had them striding four floats apart across eight
403            // banks -- a four-way conflict on the hottest load in the
404            // kernel. As `float4` the same 16 lanes read 256 contiguous
405            // bytes, which the hardware serves without conflict.
406#pragma unroll
407            for (int m = 0; m < FX_TM; m += 4) {
408                const float4 v = *(const float4*)&sa[kk][tx * FX_TM + m];
409                a[m + 0] = v.x;
410                a[m + 1] = v.y;
411                a[m + 2] = v.z;
412                a[m + 3] = v.w;
413            }
414#pragma unroll
415            for (int n = 0; n < FX_TN; n += 4) {
416                const float4 v = *(const float4*)&sb[kk][ty * FX_TN + n];
417                b[n + 0] = v.x;
418                b[n + 1] = v.y;
419                b[n + 2] = v.z;
420                b[n + 3] = v.w;
421            }
422#pragma unroll
423            for (int n = 0; n < FX_TN; n++) {
424#pragma unroll
425                for (int m = 0; m < FX_TM; m++) {
426                    acc[n][m] += a[m] * b[n];
427                }
428            }
429        }
430    }
431
432    for (int n = 0; n < FX_TN; n++) {
433        const int col = r1 + ty * FX_TN + n;
434        if (col >= batch) {
435            continue;
436        }
437        for (int m = 0; m < FX_TM; m++) {
438            const int row = r0 + tx * FX_TM + m;
439            if (row < n_rows) {
440                dst[(size_t)col * (size_t)n_rows + (size_t)row] = acc[n][m];
441            }
442        }
443    }
444}
445"#;
446
447/// Emits the complete CUDA C translation unit for one quant kind.
448///
449/// Deterministic and side-effect free, which is what lets the tests in
450/// [`crate::mul_mm_ref`] assert against it without a device.
451pub fn kernel_src(kind: &MulMmKind) -> String {
452    let defines = format!(
453        "#define FX_BM {}\n\
454         #define FX_BN {}\n\
455         #define FX_BK {}\n\
456         #define FX_TM {}\n\
457         #define FX_TN {}\n\
458         #define FX_THREADS {}\n\
459         #define FX_SUB {}\n\
460         #define FX_NL {}\n\
461         #define FX_BLOCK_BYTES {}\n",
462        BM,
463        BN,
464        BK,
465        TM,
466        TN,
467        THREADS,
468        SUB,
469        kind.nl(),
470        kind.block_bytes,
471    );
472    let body = BODY_SRC.replace("FX_FN_NAME", kind.fn_name);
473    // The codebook comes from the kind's own row, so this is not a
474    // second table that has to agree with `KINDS` -- it is `KINDS`.
475    let codebook = kind.codebook.as_ref().map(codebook_src).unwrap_or_default();
476    // Q4_K and Q5_K share llama's 6-bit scale/min unpack. It is emitted
477    // for every kind rather than conditionally: an unused `__device__`
478    // helper costs nothing after NVRTC's dead-code pass, and a
479    // per-kind include list is one more table that has to agree with
480    // another one.
481    format!(
482        "{defines}{F16_SRC}{K_SCALE_MIN_SRC}{codebook}{}{body}",
483        kind.dequant_src
484    )
485}
486
487/// Why a `mul_mm` dispatch was refused. Named rather than silent: a
488/// shape this kernel cannot do must fall back to a path that can, and
489/// the caller has to be able to say which.
490#[derive(Debug, Clone, PartialEq, Eq)]
491pub enum MulMmUnsupported {
492    /// `n_cols` is not a multiple of the K-tile. Every real GGUF row
493    /// length is a multiple of 32, so this means a synthetic shape.
494    ColsNotTileAligned { n_cols: usize, tile: usize },
495    /// `n_cols` is not a whole number of super-blocks for this kind.
496    ColsNotBlockAligned {
497        n_cols: usize,
498        block_elems: usize,
499        kind: &'static str,
500    },
501    /// `row_bytes` does not match `n_cols` worth of super-blocks.
502    RowBytesMismatch {
503        row_bytes: usize,
504        expected: usize,
505        kind: &'static str,
506    },
507    /// The weight buffer is not `n_rows * row_bytes`.
508    WeightsTooSmall { got: usize, want: usize },
509    /// The activation buffer is not `batch * n_cols`.
510    ActivationsTooSmall { got: usize, want: usize },
511    /// A zero-sized dispatch. Not an error the caller must handle
512    /// specially, but not something to launch a grid for either.
513    EmptyShape,
514}
515
516impl std::fmt::Display for MulMmUnsupported {
517    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518        match self {
519            Self::ColsNotTileAligned { n_cols, tile } => {
520                write!(f, "mul_mm: n_cols {n_cols} is not a multiple of the K-tile {tile}")
521            }
522            Self::ColsNotBlockAligned {
523                n_cols,
524                block_elems,
525                kind,
526            } => write!(
527                f,
528                "mul_mm: n_cols {n_cols} is not a whole number of {kind} blocks ({block_elems} elems)"
529            ),
530            Self::RowBytesMismatch {
531                row_bytes,
532                expected,
533                kind,
534            } => write!(
535                f,
536                "mul_mm: row_bytes {row_bytes} does not match {expected} for {kind} at this n_cols"
537            ),
538            Self::WeightsTooSmall { got, want } => {
539                write!(f, "mul_mm: weight buffer is {got} bytes, needs {want}")
540            }
541            Self::ActivationsTooSmall { got, want } => {
542                write!(f, "mul_mm: activation buffer is {got} floats, needs {want}")
543            }
544            Self::EmptyShape => write!(f, "mul_mm: empty shape"),
545        }
546    }
547}
548
549impl std::error::Error for MulMmUnsupported {}
550
551/// The shape checks the kernel's index arithmetic assumes, in one place
552/// so the launch path and the scalar twin cannot check different things.
553pub fn validate_shape(
554    kind: &MulMmKind,
555    weights_len: usize,
556    x_len: usize,
557    n_rows: usize,
558    n_cols: usize,
559    batch: usize,
560    row_bytes: usize,
561) -> Result<(), MulMmUnsupported> {
562    if n_rows == 0 || n_cols == 0 || batch == 0 {
563        return Err(MulMmUnsupported::EmptyShape);
564    }
565    if !n_cols.is_multiple_of(BK) {
566        return Err(MulMmUnsupported::ColsNotTileAligned { n_cols, tile: BK });
567    }
568    if !n_cols.is_multiple_of(kind.block_elems) {
569        return Err(MulMmUnsupported::ColsNotBlockAligned {
570            n_cols,
571            block_elems: kind.block_elems,
572            kind: kind.name,
573        });
574    }
575    let expected_row_bytes = (n_cols / kind.block_elems) * kind.block_bytes;
576    if row_bytes != expected_row_bytes {
577        return Err(MulMmUnsupported::RowBytesMismatch {
578            row_bytes,
579            expected: expected_row_bytes,
580            kind: kind.name,
581        });
582    }
583    let want_weights = n_rows * row_bytes;
584    if weights_len < want_weights {
585        return Err(MulMmUnsupported::WeightsTooSmall {
586            got: weights_len,
587            want: want_weights,
588        });
589    }
590    let want_x = batch * n_cols;
591    if x_len < want_x {
592        return Err(MulMmUnsupported::ActivationsTooSmall {
593            got: x_len,
594            want: want_x,
595        });
596    }
597    Ok(())
598}
599
600/// Whether a batched dispatch of this shape is worth a GEMM at all.
601///
602/// One token is a matvec, and `gpu.rs`'s matvec kernels are the arm that
603/// has actually run on hardware; sending a single row through the tile
604/// would waste every output column but one. The caller should
605/// keep using `apply_gpu` below this threshold.
606pub fn worth_a_gemm(batch: usize) -> bool {
607    // `.max(2)` so this stays "never a single token" even if the tile
608    // width is retuned downward.
609    batch >= (BN / 4).max(2)
610}
611
612/// Grid dimensions for a dispatch, shared by the launch path and the
613/// twin's block loop so they enumerate exactly the same tiles.
614pub fn grid_dims(n_rows: usize, batch: usize) -> (usize, usize) {
615    (batch.div_ceil(BN), n_rows.div_ceil(BM))
616}
617
618#[cfg(test)]
619mod dequant_twin_tests {
620    use super::*;
621
622    /// Every twin, against the CPU dequant this project already holds
623    /// against llama.cpp.
624    ///
625    /// This is the only oracle available without a GPU, and it is a real
626    /// one: `ferrox_quant::dequant_*` decodes a whole super-block, so
627    /// sub-block `il` of the kernel must equal elements `[16*il,
628    /// 16*il+16)` of it. A transcription that mixes up llama's three
629    /// different uses of `il` produces plausible numbers from the wrong
630    /// offsets, and that is exactly what this catches.
631    ///
632    /// The case list is checked to COVER [`KINDS`]: a kind added to the
633    /// table with no `ferrox_quant` dequant beside it gets a kernel, a
634    /// twin, and no evidence that either decodes the format. The scale
635    /// pinning comes from [`crate::mul_mm_ref::fixtures`], the one
636    /// place that knows where each format keeps its scales.
637    #[test]
638    fn every_dequant_twin_matches_the_cpu_dequant() {
639        type Dequant = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
640        let cases: &[(&MulMmKind, Dequant)] = &[
641            (&Q8_0, ferrox_quant::dequant_q8_0),
642            (&Q4_0, ferrox_quant::dequant_q4_0),
643            (&Q5_0, ferrox_quant::dequant_q5_0),
644            (&Q4_K, ferrox_quant::dequant_q4_k),
645            (&Q5_K, ferrox_quant::dequant_q5_k),
646            (&Q2_K, ferrox_quant::dequant_q2_k),
647            (&Q3_K, ferrox_quant::dequant_q3_k),
648            (&Q6_K, ferrox_quant::dequant_q6_k),
649            (&IQ4_NL, ferrox_quant::dequant_iq4_nl),
650            (&IQ4_XS, ferrox_quant::dequant_iq4_xs),
651            (&MXFP4, ferrox_quant::dequant_mxfp4_gguf),
652        ];
653
654        for k in KINDS {
655            assert!(
656                cases.iter().any(|(c, _)| c.name == k.name),
657                "{}: in KINDS with no dequant-twin case",
658                k.name
659            );
660        }
661
662        for (k, dequant) in cases {
663            for seed in [1u32, 7, 12345] {
664                let block = crate::mul_mm_ref::fixtures::block(k, seed);
665                let want = dequant(&block).expect("cpu dequant");
666                assert_eq!(want.len(), k.block_elems, "{} block size", k.name);
667
668                for il in 0..k.nl() {
669                    let mut reg = [0f32; SUB];
670                    (k.dequant_twin)(&block, il, &mut reg);
671                    for (j, got) in reg.iter().enumerate() {
672                        let expect = want[SUB * il + j];
673                        let tol = expect.abs().max(1.0) * 1e-5;
674                        assert!(
675                            (got - expect).abs() <= tol,
676                            "{} seed {seed} sub-block {il} element {j}: \
677                             kernel twin {got} vs cpu dequant {expect}",
678                            k.name
679                        );
680                    }
681                }
682            }
683        }
684    }
685
686    /// The block geometry each kind declares has to be the geometry the
687    /// format actually has, or the GEMM walks the row with the wrong
688    /// stride and every number after the first block is garbage.
689    ///
690    /// Driven by [`KINDS`] and answered from `ferrox_quant`'s
691    /// constants, so neither half is a hand-written list: a row added
692    /// to the table with no `ferrox_quant` geometry beside it fails
693    /// here, and a row whose geometry is a literal that drifted from
694    /// the format fails here too.
695    ///
696    /// `nl` is part of that geometry and is NOT the same for every row.
697    /// It falls out of `block_elems / SUB`, so the assertion is that
698    /// `block_elems` is a whole number of sub-blocks rather than a
699    /// restated 2 or 16 -- the K-quants are 256-element super-blocks
700    /// (`nl` 16) and the legacy and codebook 32-element kinds are `nl`
701    /// 2, and inheriting the wrong one asks the kernel for sub-blocks
702    /// 2..16 of a block that has two.
703    #[test]
704    fn declared_block_geometry_is_the_gguf_geometry() {
705        let geometry: &[(&str, usize, usize)] = &[
706            (
707                "Q8_0",
708                ferrox_quant::Q8_0_BLOCK_BYTES,
709                ferrox_quant::Q8_0_BLOCK_ELEMS,
710            ),
711            (
712                "Q4_0",
713                ferrox_quant::Q4_0_BLOCK_BYTES,
714                ferrox_quant::Q4_0_BLOCK_ELEMS,
715            ),
716            (
717                "Q5_0",
718                ferrox_quant::Q5_0_BLOCK_BYTES,
719                ferrox_quant::Q5_0_BLOCK_ELEMS,
720            ),
721            (
722                "Q4_K",
723                ferrox_quant::Q4_K_BLOCK_BYTES,
724                ferrox_quant::Q4_K_BLOCK_ELEMS,
725            ),
726            (
727                "Q5_K",
728                ferrox_quant::Q5_K_BLOCK_BYTES,
729                ferrox_quant::Q5_K_BLOCK_ELEMS,
730            ),
731            (
732                "Q2_K",
733                ferrox_quant::Q2_K_BLOCK_BYTES,
734                ferrox_quant::Q2_K_BLOCK_ELEMS,
735            ),
736            (
737                "Q3_K",
738                ferrox_quant::Q3_K_BLOCK_BYTES,
739                ferrox_quant::Q3_K_BLOCK_ELEMS,
740            ),
741            (
742                "Q6_K",
743                ferrox_quant::Q6_K_BLOCK_BYTES,
744                ferrox_quant::Q6_K_BLOCK_ELEMS,
745            ),
746            (
747                "IQ4_NL",
748                ferrox_quant::IQ4_NL_BLOCK_BYTES,
749                ferrox_quant::IQ4_NL_BLOCK_ELEMS,
750            ),
751            (
752                "IQ4_XS",
753                ferrox_quant::IQ4_XS_BLOCK_BYTES,
754                ferrox_quant::IQ4_XS_BLOCK_ELEMS,
755            ),
756            (
757                "MXFP4",
758                ferrox_quant::MXFP4_GGUF_BLOCK_BYTES,
759                ferrox_quant::MXFP4_GGUF_BLOCK_ELEMS,
760            ),
761        ];
762
763        for k in KINDS {
764            let (_, bytes_, elems) = geometry
765                .iter()
766                .find(|(name, _, _)| *name == k.name)
767                .unwrap_or_else(|| panic!("{}: in KINDS with no ferrox_quant geometry", k.name));
768            assert_eq!(k.block_bytes, *bytes_, "{} block_bytes", k.name);
769            assert_eq!(k.block_elems, *elems, "{} block_elems", k.name);
770            assert_eq!(
771                k.block_elems,
772                k.nl() * SUB,
773                "{}: nl() must partition the super-block into {SUB}-element sub-blocks",
774                k.name
775            );
776            assert_eq!(
777                kind_by_name(k.name).map(|f| f.name),
778                Some(k.name),
779                "{}: does not resolve by its own name",
780                k.name
781            );
782        }
783
784        // Two kinds must not collide in the process-wide NVRTC module
785        // cache, and two must not share an entry point.
786        for (i, a) in KINDS.iter().enumerate() {
787            for b in &KINDS[i + 1..] {
788                assert_ne!(a.module_name, b.module_name, "{} vs {}", a.name, b.name);
789                assert_ne!(a.fn_name, b.fn_name, "{} vs {}", a.name, b.name);
790            }
791        }
792
793        // A kind with no kernel must not resolve. Resolving would send
794        // a GEMM to a module that cannot compile, and the caller would
795        // have no way to fall back honestly.
796        for absent in ["Q4_1", "Q5_1", "Q8_1", "IQ1_S", "IQ2_XXS", "IQ3_S"] {
797            assert!(
798                kind_by_name(absent).is_none(),
799                "{absent} resolved to a mul_mm kernel that does not exist"
800            );
801        }
802    }
803
804    /// Every codebook row's `dequant_src` has to actually spell the
805    /// `__constant__` array its [`Codebook`] declares, and the emitted
806    /// array has to carry that row's values -- parsed back out of the
807    /// text rather than compared to a second copy of the formatting.
808    ///
809    /// This is what makes "one slice serves both halves" true rather
810    /// than intended: the numbers are read out of the generated CUDA C
811    /// and held against the slice the Rust twin indexes.
812    ///
813    /// Sabotage: change one entry of `KVALUES_IQ4NL` and this names the
814    /// index; rename `c_name` without renaming it in `dequant_src` and
815    /// the first assertion fires.
816    #[test]
817    fn an_emitted_codebook_is_the_slice_the_twin_indexes() {
818        let mut seen = 0usize;
819        for k in KINDS {
820            let Some(cb) = k.codebook else {
821                assert!(
822                    !kernel_src(k).contains("__constant__"),
823                    "{}: no codebook declared but one is emitted",
824                    k.name
825                );
826                continue;
827            };
828            seen += 1;
829            let src = kernel_src(k);
830            assert!(
831                k.dequant_src.contains(cb.c_name),
832                "{}: dequant_src never indexes {}",
833                k.name,
834                cb.c_name
835            );
836            let decl = format!("__constant__ float {}[16] = {{", cb.c_name);
837            let at = src
838                .find(&decl)
839                .unwrap_or_else(|| panic!("{}: {} is not emitted", k.name, cb.c_name));
840            let body = &src[at + decl.len()..];
841            let body = &body[..body.find('}').expect("unterminated codebook")];
842            let got: Vec<f32> = body
843                .split(',')
844                .map(|t| {
845                    t.trim()
846                        .trim_end_matches('f')
847                        .parse::<f32>()
848                        .unwrap_or_else(|e| panic!("{}: {t:?}: {e}", k.name))
849                })
850                .collect();
851            assert_eq!(got.len(), 16, "{}: codebook is not 16 entries", k.name);
852            for (i, (g, w)) in got.iter().zip(cb.values.iter()).enumerate() {
853                // Bit comparison, not `==`: MXFP4's code 8 is negative
854                // zero, and `-0.0 == 0.0` would let it through.
855                assert_eq!(
856                    g.to_bits(),
857                    w.to_bits(),
858                    "{}: codebook entry {i}: emitted {g}, twin indexes {w}",
859                    k.name
860                );
861            }
862        }
863        assert!(seen >= 3, "the codebook kinds stopped declaring codebooks");
864    }
865}