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}