use crate::mul_mm::{
grid_dims, validate_shape, MulMmKind, MulMmUnsupported, BK, BM, BN, SUB, THREADS, TM, TN,
};
pub fn mul_mm_reference(
kind: &MulMmKind,
weights: &[u8],
x_batch: &[f32],
n_rows: usize,
n_cols: usize,
batch: usize,
row_bytes: usize,
) -> Result<Vec<f32>, MulMmUnsupported> {
validate_shape(
kind,
weights.len(),
x_batch.len(),
n_rows,
n_cols,
batch,
row_bytes,
)?;
let mut dst = vec![0f32; batch * n_rows];
let (grid_x, grid_y) = grid_dims(n_rows, batch);
for by in 0..grid_y {
for bx in 0..grid_x {
emulate_block(
kind, weights, x_batch, &mut dst, n_rows, n_cols, batch, row_bytes, bx, by,
);
}
}
Ok(dst)
}
#[allow(clippy::too_many_arguments)] fn emulate_block(
kind: &MulMmKind,
src0: &[u8],
src1: &[f32],
dst: &mut [f32],
n_rows: usize,
n_cols: usize,
batch: usize,
row_bytes: usize,
bx: usize,
by: usize,
) {
let nl = kind.nl();
let r0 = by * BM;
let r1 = bx * BN;
let mut sa = vec![0f32; BK * BM];
let mut sb = vec![0f32; BK * BN];
let mut acc = vec![0f32; THREADS * TN * TM];
let mut k0 = 0usize;
while k0 < n_cols {
for tid in 0..THREADS {
if tid < BM * (BK / SUB) {
let lr = tid / (BK / SUB);
let ils = tid % (BK / SUB);
let mut row = r0 + lr;
if row >= n_rows {
row = n_rows - 1;
}
let rp = &src0[row * row_bytes..(row + 1) * row_bytes];
let sub = (k0 / SUB) + ils;
let xb = &rp[(sub / nl) * kind.block_bytes..];
let mut reg = [0f32; SUB];
(kind.dequant_twin)(xb, sub % nl, &mut reg);
for (i, v) in reg.iter().enumerate() {
sa[(SUB * ils + i) * BM + lr] = *v;
}
}
let mut idx = tid;
while idx < BK * BN {
let j = idx / BK;
let kk = idx % BK;
let col = r1 + j;
sb[kk * BN + j] = if col < batch {
src1[col * n_cols + k0 + kk]
} else {
0.0
};
idx += THREADS;
}
}
for tid in 0..THREADS {
let tx = tid % (BM / TM);
let ty = tid / (BM / TM);
let acc = &mut acc[tid * TN * TM..(tid + 1) * TN * TM];
for kk in 0..BK {
let mut a = [0f32; TM];
let mut b = [0f32; TN];
for (m, slot) in a.iter_mut().enumerate() {
*slot = sa[kk * BM + tx * TM + m];
}
for (n, slot) in b.iter_mut().enumerate() {
*slot = sb[kk * BN + ty * TN + n];
}
for n in 0..TN {
for m in 0..TM {
acc[n * TM + m] += a[m] * b[n];
}
}
}
}
k0 += BK;
}
for tid in 0..THREADS {
let tx = tid % (BM / TM);
let ty = tid / (BM / TM);
let acc = &acc[tid * TN * TM..(tid + 1) * TN * TM];
for n in 0..TN {
let col = r1 + ty * TN + n;
if col >= batch {
continue;
}
for m in 0..TM {
let row = r0 + tx * TM + m;
if row < n_rows {
dst[col * n_rows + row] = acc[n * TM + m];
}
}
}
}
}
#[cfg(test)]
pub(crate) mod fixtures {
use crate::mul_mm::MulMmKind;
pub(crate) fn pseudo_bytes(seed: u32, len: usize) -> Vec<u8> {
let mut state = seed.wrapping_mul(2654435761).wrapping_add(1);
(0..len)
.map(|_| {
state = state.wrapping_mul(1103515245).wrapping_add(12345);
(state >> 16) as u8
})
.collect()
}
fn finite_f16(block: &mut [u8], at: usize) {
let bits = u16::from(block[at]) | (u16::from(block[at + 1]) << 8);
let bits = (bits & 0x83FF) | (11 << 10);
block[at] = bits as u8;
block[at + 1] = (bits >> 8) as u8;
}
pub(crate) fn pin_finite_scales(kind: &MulMmKind, block: &mut [u8]) {
match kind.name {
"Q8_0" | "Q4_0" | "Q5_0" | "IQ4_NL" | "IQ4_XS" => finite_f16(block, 0),
"Q4_K" | "Q5_K" => {
finite_f16(block, 0);
finite_f16(block, 2);
}
"Q6_K" => finite_f16(block, 208),
"Q2_K" => {
finite_f16(block, 80);
finite_f16(block, 82);
}
"Q3_K" => finite_f16(block, 108),
"MXFP4" => block[0] = 123 + (block[0] & 7),
other => panic!("{other}: no scale-pinning rule; add one beside the KINDS row"),
}
}
pub(crate) fn block(kind: &MulMmKind, seed: u32) -> Vec<u8> {
let mut b = pseudo_bytes(seed, kind.block_bytes);
pin_finite_scales(kind, &mut b);
b
}
pub(crate) fn weights(kind: &MulMmKind, n_rows: usize, n_cols: usize, seed: u32) -> Vec<u8> {
assert!(
n_cols.is_multiple_of(kind.block_elems),
"{}: {n_cols} columns is not a whole number of blocks",
kind.name
);
let blocks = (n_cols / kind.block_elems) * n_rows;
let mut out = Vec::with_capacity(blocks * kind.block_bytes);
for b in 0..blocks {
out.extend_from_slice(&block(kind, seed.wrapping_add(b as u32 * 7919)));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mul_mm::{f16_to_f32, kernel_src, KINDS, Q4_0, Q8_0};
fn pseudo_bytes(seed: u32, len: usize) -> Vec<u8> {
let mut state = seed.wrapping_mul(2654435761).wrapping_add(1);
(0..len)
.map(|_| {
state = state.wrapping_mul(1103515245).wrapping_add(12345);
(state >> 16) as u8
})
.collect()
}
fn q8_0_matrix(n_rows: usize, n_cols: usize) -> Vec<u8> {
let mut out = Vec::new();
for r in 0..n_rows {
let row: Vec<f32> = (0..n_cols)
.map(|i| (((r * n_cols + i) as f32) * 0.037).sin())
.collect();
out.extend(ferrox_quant::quantize_q8_0(&row));
}
out
}
fn q4_0_matrix(n_rows: usize, n_cols: usize) -> Vec<u8> {
let blocks = n_cols / 32;
let mut out = Vec::new();
for r in 0..n_rows {
for b in 0..blocks {
let scale = half::f16::from_f32(0.05 + ((r * blocks + b) % 13) as f32 * 0.01);
out.extend_from_slice(&scale.to_le_bytes());
let nibbles = pseudo_bytes((r * blocks + b) as u32 + 7, 16);
out.extend_from_slice(&nibbles);
}
}
out
}
fn activations(batch: usize, n_cols: usize) -> Vec<f32> {
(0..batch * n_cols)
.map(|i| ((i as f32) * 0.019).cos())
.collect()
}
fn independent_gemm(
dequant_row: impl Fn(&[u8]) -> Vec<f32>,
weights: &[u8],
x: &[f32],
n_rows: usize,
n_cols: usize,
batch: usize,
row_bytes: usize,
) -> Vec<f32> {
let mut out = vec![0f32; batch * n_rows];
for r in 0..n_rows {
let w = dequant_row(&weights[r * row_bytes..(r + 1) * row_bytes]);
assert_eq!(w.len(), n_cols);
for t in 0..batch {
let xr = &x[t * n_cols..(t + 1) * n_cols];
let mut acc = 0f32;
for k in 0..n_cols {
acc += w[k] * xr[k];
}
out[t * n_rows + r] = acc;
}
}
out
}
fn assert_close(got: &[f32], want: &[f32], tol: f32, what: &str) {
assert_eq!(got.len(), want.len(), "{what}: length");
for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
let scale = w.abs().max(1.0);
assert!(
(g - w).abs() <= tol * scale,
"{what}: element {i}: twin={g} reference={w}"
);
}
}
#[test]
fn f16_twin_matches_half_crate_over_every_bit_pattern() {
for bits in 0u32..=0xFFFF {
let bits = bits as u16;
let want = half::f16::from_bits(bits).to_f32();
let got = f16_to_f32(bits);
if want.is_nan() {
assert!(got.is_nan(), "bits {bits:#06x}: want NaN, got {got}");
} else {
assert_eq!(
got.to_bits(),
want.to_bits(),
"bits {bits:#06x}: got {got}, want {want}"
);
}
}
}
#[test]
fn q8_0_sub_block_twin_reconstructs_the_block_in_order() {
let row: Vec<f32> = (0..64).map(|i| ((i as f32) * 0.31).sin()).collect();
let bytes = ferrox_quant::quantize_q8_0(&row);
let want = ferrox_quant::dequant_q8_0(&bytes).unwrap();
for (blk, chunk) in bytes.chunks(Q8_0.block_bytes).enumerate() {
for il in 0..Q8_0.nl() {
let mut reg = [0f32; SUB];
(Q8_0.dequant_twin)(chunk, il, &mut reg);
for (i, got) in reg.iter().enumerate() {
let want = want[blk * Q8_0.block_elems + il * SUB + i];
assert_eq!(*got, want, "block {blk} il {il} elem {i}");
}
}
}
}
#[test]
fn q4_0_sub_block_twin_reconstructs_the_block_in_order() {
let bytes = q4_0_matrix(1, 64);
let want = ferrox_quant::dequant_q4_0(&bytes).unwrap();
for (blk, chunk) in bytes.chunks(Q4_0.block_bytes).enumerate() {
for il in 0..Q4_0.nl() {
let mut reg = [0f32; SUB];
(Q4_0.dequant_twin)(chunk, il, &mut reg);
for (i, got) in reg.iter().enumerate() {
let want = want[blk * Q4_0.block_elems + il * SUB + i];
assert!(
(got - want).abs() <= 1e-6 * want.abs().max(1.0),
"block {blk} il {il} elem {i}: twin={got} reference={want}"
);
}
}
}
}
#[test]
fn q8_0_gemm_twin_matches_independent_reference_on_exact_tiles() {
let (n_rows, n_cols, batch) = (BM * 2, 128, BN);
let row_bytes = (n_cols / 32) * Q8_0.block_bytes;
let weights = q8_0_matrix(n_rows, n_cols);
let x = activations(batch, n_cols);
let got = mul_mm_reference(&Q8_0, &weights, &x, n_rows, n_cols, batch, row_bytes).unwrap();
let want = independent_gemm(
|r| ferrox_quant::dequant_q8_0(r).unwrap(),
&weights,
&x,
n_rows,
n_cols,
batch,
row_bytes,
);
assert_close(&got, &want, 1e-5, "q8_0 exact tiles");
}
#[test]
fn q8_0_gemm_twin_matches_independent_reference_on_partial_tiles() {
let (n_rows, n_cols, batch) = (BM + 7, 96, BN + 5);
let row_bytes = (n_cols / 32) * Q8_0.block_bytes;
let weights = q8_0_matrix(n_rows, n_cols);
let x = activations(batch, n_cols);
let got = mul_mm_reference(&Q8_0, &weights, &x, n_rows, n_cols, batch, row_bytes).unwrap();
let want = independent_gemm(
|r| ferrox_quant::dequant_q8_0(r).unwrap(),
&weights,
&x,
n_rows,
n_cols,
batch,
row_bytes,
);
assert_close(&got, &want, 1e-5, "q8_0 partial tiles");
}
#[test]
fn q8_0_gemm_twin_matches_independent_reference_at_batch_one() {
let (n_rows, n_cols, batch) = (37, 64, 1);
let row_bytes = (n_cols / 32) * Q8_0.block_bytes;
let weights = q8_0_matrix(n_rows, n_cols);
let x = activations(batch, n_cols);
let got = mul_mm_reference(&Q8_0, &weights, &x, n_rows, n_cols, batch, row_bytes).unwrap();
let want = independent_gemm(
|r| ferrox_quant::dequant_q8_0(r).unwrap(),
&weights,
&x,
n_rows,
n_cols,
batch,
row_bytes,
);
assert_close(&got, &want, 1e-5, "q8_0 batch 1");
}
#[test]
fn q4_0_gemm_twin_matches_independent_reference_on_partial_tiles() {
let (n_rows, n_cols, batch) = (BM + 3, 96, BN + 9);
let row_bytes = (n_cols / 32) * Q4_0.block_bytes;
let weights = q4_0_matrix(n_rows, n_cols);
let x = activations(batch, n_cols);
let got = mul_mm_reference(&Q4_0, &weights, &x, n_rows, n_cols, batch, row_bytes).unwrap();
let want = independent_gemm(
|r| ferrox_quant::dequant_q4_0(r).unwrap(),
&weights,
&x,
n_rows,
n_cols,
batch,
row_bytes,
);
assert_close(&got, &want, 1e-5, "q4_0 partial tiles");
}
#[test]
fn every_kind_gemm_twin_matches_the_independent_reference() {
type Dequant = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
let dequants: &[(&str, Dequant)] = &[
("Q8_0", ferrox_quant::dequant_q8_0),
("Q4_0", ferrox_quant::dequant_q4_0),
("Q5_0", ferrox_quant::dequant_q5_0),
("Q4_K", ferrox_quant::dequant_q4_k),
("Q5_K", ferrox_quant::dequant_q5_k),
("Q2_K", ferrox_quant::dequant_q2_k),
("Q3_K", ferrox_quant::dequant_q3_k),
("Q6_K", ferrox_quant::dequant_q6_k),
("IQ4_NL", ferrox_quant::dequant_iq4_nl),
("IQ4_XS", ferrox_quant::dequant_iq4_xs),
("MXFP4", ferrox_quant::dequant_mxfp4_gguf),
];
for k in KINDS {
let (_, dequant) = dequants
.iter()
.find(|(name, _)| *name == k.name)
.unwrap_or_else(|| panic!("{}: in KINDS with no ferrox_quant dequant", k.name));
for (n_rows, cols, batch) in [(BM * 2, 128usize, BN), (BM + 7, 96, BN + 9), (37, 64, 3)]
{
let n_cols = cols.next_multiple_of(k.block_elems);
let row_bytes = (n_cols / k.block_elems) * k.block_bytes;
let weights = fixtures::weights(k, n_rows, n_cols, 4242);
let x = activations(batch, n_cols);
let got =
mul_mm_reference(k, &weights, &x, n_rows, n_cols, batch, row_bytes).unwrap();
let want = independent_gemm(
|r| dequant(r).unwrap(),
&weights,
&x,
n_rows,
n_cols,
batch,
row_bytes,
);
assert_close(
&got,
&want,
1e-5,
&format!("{} {n_rows}x{n_cols}x{batch}", k.name),
);
}
}
}
#[test]
fn twin_output_depends_on_every_part_of_the_weight_matrix() {
let (n_rows, n_cols, batch) = (BM + 7, 96, 3);
let row_bytes = (n_cols / 32) * Q8_0.block_bytes;
let base = q8_0_matrix(n_rows, n_cols);
let x = activations(batch, n_cols);
let want = mul_mm_reference(&Q8_0, &base, &x, n_rows, n_cols, batch, row_bytes).unwrap();
let mut poked = base.clone();
let last = poked.len() - 1;
poked[last] = poked[last].wrapping_add(64);
let got = mul_mm_reference(&Q8_0, &poked, &x, n_rows, n_cols, batch, row_bytes).unwrap();
assert!(
got.iter().zip(want.iter()).any(|(a, b)| a != b),
"poking the last weight byte changed nothing -- the twin is not reading it"
);
}
#[test]
fn shape_validation_names_what_it_refuses() {
let err = mul_mm_reference(&Q8_0, &[], &[], 4, 48, 1, 51).unwrap_err();
assert!(
matches!(err, MulMmUnsupported::ColsNotTileAligned { .. }),
"got {err:?}"
);
let err = mul_mm_reference(&Q8_0, &[0; 999], &[0.0; 64], 4, 64, 1, 33).unwrap_err();
assert!(
matches!(err, MulMmUnsupported::RowBytesMismatch { .. }),
"got {err:?}"
);
let err = mul_mm_reference(&Q8_0, &[0; 68], &[0.0; 64], 4, 64, 1, 68).unwrap_err();
assert!(
matches!(err, MulMmUnsupported::WeightsTooSmall { .. }),
"got {err:?}"
);
let err = mul_mm_reference(&Q8_0, &[0; 272], &[0.0; 64], 4, 64, 2, 68).unwrap_err();
assert!(
matches!(err, MulMmUnsupported::ActivationsTooSmall { .. }),
"got {err:?}"
);
assert!(mul_mm_reference(&Q8_0, &[], &[], 0, 64, 1, 68).is_err());
}
#[test]
fn emitted_source_defines_the_entry_point_and_the_geometry() {
for k in KINDS {
let src = kernel_src(k);
assert!(
src.contains(&format!("__global__ void {}(", k.fn_name)),
"{}: emitted source does not define {}",
k.name,
k.fn_name
);
assert!(
src.contains("void ferrox_dequant_sub("),
"{}: no unpack function",
k.name
);
assert!(
src.contains("float ferrox_f16_to_f32("),
"{}: no f16 helper",
k.name
);
assert!(
src.contains(&format!("#define FX_BLOCK_BYTES {}\n", k.block_bytes)),
"{}: block geometry not defined from the Rust constant",
k.name
);
assert!(
src.contains(&format!("#define FX_NL {}\n", k.nl())),
"{}: sub-block count not defined from the Rust constant",
k.name
);
for (name, value) in [
("FX_BM", BM),
("FX_BN", BN),
("FX_BK", BK),
("FX_TM", TM),
("FX_TN", TN),
("FX_THREADS", THREADS),
("FX_SUB", SUB),
] {
assert!(
src.contains(&format!("#define {name} {value}\n")),
"{}: {name} is not emitted as the Rust constant {value}",
k.name
);
}
assert!(
src.contains("const float4 v = *(const float4*)&sa[kk]")
&& src.contains("const float4 v = *(const float4*)&sb[kk]"),
"{}: the inner loop no longer loads its operands as float4",
k.name
);
assert!(
src.contains(&format!("#define FX_THREADS {THREADS}\n")),
"{}: thread count not defined from the Rust constant",
k.name
);
assert!(
!src.contains("FX_FN_NAME"),
"{}: unsubstituted name",
k.name
);
}
}
#[test]
fn single_token_dispatches_stay_on_the_matvec_path() {
use crate::mul_mm::worth_a_gemm;
assert!(!worth_a_gemm(0));
assert!(!worth_a_gemm(1), "one token is a matvec at any tile width");
let threshold = (1..=4 * BN)
.find(|b| worth_a_gemm(*b))
.expect("some batch is worth a GEMM");
assert!(threshold >= 2, "never a single token");
assert!(
threshold <= BN,
"a full tile of tokens must be worth a GEMM, threshold {threshold} > BN {BN}"
);
for b in 1..4 * BN {
assert_eq!(
worth_a_gemm(b),
b >= threshold,
"batch {b} disagrees with threshold {threshold}"
);
}
}
#[test]
fn the_grid_covers_a_partial_tile_on_both_axes() {
assert_eq!(grid_dims(BM, BN), (1, 1));
assert_eq!(grid_dims(BM + 1, BN + 1), (2, 2));
assert_eq!(grid_dims(BM * 3, BN * 2 + 1), (3, 3));
}
}