pub const BM: usize = 64;
pub const BN: usize = 128;
pub const BK: usize = 32;
pub const TM: usize = 4;
pub const TN: usize = 8;
pub const THREADS: usize = (BM / TM) * (BN / TN);
pub const SUB: usize = 16;
const _: () = assert!(THREADS == (BM / TM) * (BN / TN));
const _: () = assert!(BM.is_multiple_of(TM) && BN.is_multiple_of(TN));
const _: () = assert!(
BK.is_multiple_of(SUB),
"the K-tile must be whole sub-blocks"
);
const _: () = assert!(
BM * (BK / SUB) <= THREADS,
"the A-tile loader uses a prefix of the block"
);
const _: () = assert!(THREADS <= 1024, "CUDA caps a block at 1024 threads");
const _: () = assert!(
TM.is_multiple_of(4) && TN.is_multiple_of(4),
"the inner loop loads float4 from shared memory"
);
const _: () = assert!(
BM.is_multiple_of(4) && BN.is_multiple_of(4),
"a shared row must keep the next row 16-byte aligned"
);
#[derive(Debug, Clone, Copy)]
pub struct MulMmKind {
pub name: &'static str,
pub module_name: &'static str,
pub fn_name: &'static str,
pub block_bytes: usize,
pub block_elems: usize,
pub dequant_src: &'static str,
pub codebook: Option<Codebook>,
pub dequant_twin: fn(xb: &[u8], il: usize, reg: &mut [f32; SUB]),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Codebook {
pub c_name: &'static str,
pub values: &'static [f32; 16],
}
fn codebook_src(cb: &Codebook) -> String {
let values: Vec<String> = cb.values.iter().map(|v| format!("{v:?}f")).collect();
format!(
"\n__constant__ float {}[16] = {{{}}};\n",
cb.c_name,
values.join(", ")
)
}
impl MulMmKind {
pub const fn nl(&self) -> usize {
self.block_elems / SUB
}
}
pub use crate::mul_mm_kinds::kquant::K_SCALE_MIN_SRC;
pub use crate::mul_mm_kinds::{
IQ4_NL, IQ4_XS, MXFP4, Q2_K, Q3_K, Q4_0, Q4_K, Q5_0, Q5_K, Q6_K, Q8_0,
};
pub fn f16_to_f32(bits: u16) -> f32 {
let sign = (bits >> 15) & 0x1;
let exp = u32::from((bits >> 10) & 0x1F);
let mant = u32::from(bits & 0x3FF);
let scale = if exp == 0 {
(mant as f32) * 2f32.powi(-24)
} else if exp == 31 {
if mant != 0 {
f32::from_bits(0x7fc0_0000)
} else {
f32::from_bits(0x7f80_0000)
}
} else {
((mant | 0x400) as f32) * 2f32.powi(exp as i32 - 25)
};
if sign != 0 {
-scale
} else {
scale
}
}
pub const KINDS: &[MulMmKind] = &[
Q8_0, Q4_0, Q5_0, Q2_K, Q3_K, Q4_K, Q5_K, Q6_K, IQ4_NL, IQ4_XS, MXFP4,
];
pub fn kind_by_name(name: &str) -> Option<&'static MulMmKind> {
KINDS.iter().find(|k| k.name == name)
}
const F16_SRC: &str = r#"
__device__ __forceinline__ float ferrox_f16_to_f32(unsigned short bits) {
unsigned int sign = (bits >> 15) & 0x1u;
unsigned int exp = (bits >> 10) & 0x1Fu;
unsigned int mant = bits & 0x3FFu;
float scale;
if (exp == 0) {
scale = ldexpf((float)mant, -24);
} else if (exp == 31) {
scale = mant ? __int_as_float(0x7fc00000) : __int_as_float(0x7f800000);
} else {
scale = ldexpf((float)(mant | 0x400), (int)exp - 25);
}
return sign ? -scale : scale;
}
"#;
const BODY_SRC: &str = r#"
extern "C" __global__ void FX_FN_NAME(
const unsigned char* __restrict__ src0,
const float* __restrict__ src1,
float* __restrict__ dst,
int n_rows,
int n_cols,
int batch,
int row_bytes
) {
__shared__ float sa[FX_BK][FX_BM];
__shared__ float sb[FX_BK][FX_BN];
const int r0 = blockIdx.y * FX_BM;
const int r1 = blockIdx.x * FX_BN;
const int tid = threadIdx.x;
// Micro-tile owner: `tx` walks rows, `ty` walks tokens.
const int tx = tid % (FX_BM / FX_TM);
const int ty = tid / (FX_BM / FX_TM);
float acc[FX_TN][FX_TM];
#pragma unroll
for (int n = 0; n < FX_TN; n++) {
#pragma unroll
for (int m = 0; m < FX_TM; m++) {
acc[n][m] = 0.0f;
}
}
for (int k0 = 0; k0 < n_cols; k0 += FX_BK) {
// Guards the previous iteration's reads of sa/sb.
__syncthreads();
// A-tile: one thread decodes one FX_SUB-element sub-block, so
// FX_BM * (FX_BK / FX_SUB) threads cover the tile. Stored
// k-major so the K-loop below reads one row of sa per step.
if (tid < FX_BM * (FX_BK / FX_SUB)) {
const int lr = tid / (FX_BK / FX_SUB);
const int ils = tid % (FX_BK / FX_SUB);
int row = r0 + lr;
if (row >= n_rows) {
row = n_rows - 1;
}
const unsigned char* rp =
src0 + (size_t)row * (size_t)row_bytes;
const int sub = (k0 / FX_SUB) + ils;
float reg[FX_SUB];
ferrox_dequant_sub(
rp + (size_t)(sub / FX_NL) * (size_t)FX_BLOCK_BYTES,
sub % FX_NL,
reg);
#pragma unroll
for (int i = 0; i < FX_SUB; i++) {
sa[FX_SUB * ils + i][lr] = reg[i];
}
}
// B-tile: consecutive threads read consecutive k of one token.
// Tokens past the end are zero-filled rather than skipped, so
// the K-loop needs no per-token predicate.
for (int idx = tid; idx < FX_BK * FX_BN; idx += FX_THREADS) {
const int j = idx / FX_BK;
const int kk = idx % FX_BK;
const int col = r1 + j;
sb[kk][j] = (col < batch)
? src1[(size_t)col * (size_t)n_cols + (size_t)(k0 + kk)]
: 0.0f;
}
__syncthreads();
#pragma unroll
for (int kk = 0; kk < FX_BK; kk++) {
float a[FX_TM];
float b[FX_TN];
// One 16-byte load per four operands, not four 4-byte ones.
// A warp's 32 lanes take 16 distinct `tx`, so the scalar
// form had them striding four floats apart across eight
// banks -- a four-way conflict on the hottest load in the
// kernel. As `float4` the same 16 lanes read 256 contiguous
// bytes, which the hardware serves without conflict.
#pragma unroll
for (int m = 0; m < FX_TM; m += 4) {
const float4 v = *(const float4*)&sa[kk][tx * FX_TM + m];
a[m + 0] = v.x;
a[m + 1] = v.y;
a[m + 2] = v.z;
a[m + 3] = v.w;
}
#pragma unroll
for (int n = 0; n < FX_TN; n += 4) {
const float4 v = *(const float4*)&sb[kk][ty * FX_TN + n];
b[n + 0] = v.x;
b[n + 1] = v.y;
b[n + 2] = v.z;
b[n + 3] = v.w;
}
#pragma unroll
for (int n = 0; n < FX_TN; n++) {
#pragma unroll
for (int m = 0; m < FX_TM; m++) {
acc[n][m] += a[m] * b[n];
}
}
}
}
for (int n = 0; n < FX_TN; n++) {
const int col = r1 + ty * FX_TN + n;
if (col >= batch) {
continue;
}
for (int m = 0; m < FX_TM; m++) {
const int row = r0 + tx * FX_TM + m;
if (row < n_rows) {
dst[(size_t)col * (size_t)n_rows + (size_t)row] = acc[n][m];
}
}
}
}
"#;
pub fn kernel_src(kind: &MulMmKind) -> String {
let defines = format!(
"#define FX_BM {}\n\
#define FX_BN {}\n\
#define FX_BK {}\n\
#define FX_TM {}\n\
#define FX_TN {}\n\
#define FX_THREADS {}\n\
#define FX_SUB {}\n\
#define FX_NL {}\n\
#define FX_BLOCK_BYTES {}\n",
BM,
BN,
BK,
TM,
TN,
THREADS,
SUB,
kind.nl(),
kind.block_bytes,
);
let body = BODY_SRC.replace("FX_FN_NAME", kind.fn_name);
let codebook = kind.codebook.as_ref().map(codebook_src).unwrap_or_default();
format!(
"{defines}{F16_SRC}{K_SCALE_MIN_SRC}{codebook}{}{body}",
kind.dequant_src
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MulMmUnsupported {
ColsNotTileAligned { n_cols: usize, tile: usize },
ColsNotBlockAligned {
n_cols: usize,
block_elems: usize,
kind: &'static str,
},
RowBytesMismatch {
row_bytes: usize,
expected: usize,
kind: &'static str,
},
WeightsTooSmall { got: usize, want: usize },
ActivationsTooSmall { got: usize, want: usize },
EmptyShape,
}
impl std::fmt::Display for MulMmUnsupported {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ColsNotTileAligned { n_cols, tile } => {
write!(f, "mul_mm: n_cols {n_cols} is not a multiple of the K-tile {tile}")
}
Self::ColsNotBlockAligned {
n_cols,
block_elems,
kind,
} => write!(
f,
"mul_mm: n_cols {n_cols} is not a whole number of {kind} blocks ({block_elems} elems)"
),
Self::RowBytesMismatch {
row_bytes,
expected,
kind,
} => write!(
f,
"mul_mm: row_bytes {row_bytes} does not match {expected} for {kind} at this n_cols"
),
Self::WeightsTooSmall { got, want } => {
write!(f, "mul_mm: weight buffer is {got} bytes, needs {want}")
}
Self::ActivationsTooSmall { got, want } => {
write!(f, "mul_mm: activation buffer is {got} floats, needs {want}")
}
Self::EmptyShape => write!(f, "mul_mm: empty shape"),
}
}
}
impl std::error::Error for MulMmUnsupported {}
pub fn validate_shape(
kind: &MulMmKind,
weights_len: usize,
x_len: usize,
n_rows: usize,
n_cols: usize,
batch: usize,
row_bytes: usize,
) -> Result<(), MulMmUnsupported> {
if n_rows == 0 || n_cols == 0 || batch == 0 {
return Err(MulMmUnsupported::EmptyShape);
}
if !n_cols.is_multiple_of(BK) {
return Err(MulMmUnsupported::ColsNotTileAligned { n_cols, tile: BK });
}
if !n_cols.is_multiple_of(kind.block_elems) {
return Err(MulMmUnsupported::ColsNotBlockAligned {
n_cols,
block_elems: kind.block_elems,
kind: kind.name,
});
}
let expected_row_bytes = (n_cols / kind.block_elems) * kind.block_bytes;
if row_bytes != expected_row_bytes {
return Err(MulMmUnsupported::RowBytesMismatch {
row_bytes,
expected: expected_row_bytes,
kind: kind.name,
});
}
let want_weights = n_rows * row_bytes;
if weights_len < want_weights {
return Err(MulMmUnsupported::WeightsTooSmall {
got: weights_len,
want: want_weights,
});
}
let want_x = batch * n_cols;
if x_len < want_x {
return Err(MulMmUnsupported::ActivationsTooSmall {
got: x_len,
want: want_x,
});
}
Ok(())
}
pub fn worth_a_gemm(batch: usize) -> bool {
batch >= (BN / 4).max(2)
}
pub fn grid_dims(n_rows: usize, batch: usize) -> (usize, usize) {
(batch.div_ceil(BN), n_rows.div_ceil(BM))
}
#[cfg(test)]
mod dequant_twin_tests {
use super::*;
#[test]
fn every_dequant_twin_matches_the_cpu_dequant() {
type Dequant = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
let cases: &[(&MulMmKind, 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 {
assert!(
cases.iter().any(|(c, _)| c.name == k.name),
"{}: in KINDS with no dequant-twin case",
k.name
);
}
for (k, dequant) in cases {
for seed in [1u32, 7, 12345] {
let block = crate::mul_mm_ref::fixtures::block(k, seed);
let want = dequant(&block).expect("cpu dequant");
assert_eq!(want.len(), k.block_elems, "{} block size", k.name);
for il in 0..k.nl() {
let mut reg = [0f32; SUB];
(k.dequant_twin)(&block, il, &mut reg);
for (j, got) in reg.iter().enumerate() {
let expect = want[SUB * il + j];
let tol = expect.abs().max(1.0) * 1e-5;
assert!(
(got - expect).abs() <= tol,
"{} seed {seed} sub-block {il} element {j}: \
kernel twin {got} vs cpu dequant {expect}",
k.name
);
}
}
}
}
}
#[test]
fn declared_block_geometry_is_the_gguf_geometry() {
let geometry: &[(&str, usize, usize)] = &[
(
"Q8_0",
ferrox_quant::Q8_0_BLOCK_BYTES,
ferrox_quant::Q8_0_BLOCK_ELEMS,
),
(
"Q4_0",
ferrox_quant::Q4_0_BLOCK_BYTES,
ferrox_quant::Q4_0_BLOCK_ELEMS,
),
(
"Q5_0",
ferrox_quant::Q5_0_BLOCK_BYTES,
ferrox_quant::Q5_0_BLOCK_ELEMS,
),
(
"Q4_K",
ferrox_quant::Q4_K_BLOCK_BYTES,
ferrox_quant::Q4_K_BLOCK_ELEMS,
),
(
"Q5_K",
ferrox_quant::Q5_K_BLOCK_BYTES,
ferrox_quant::Q5_K_BLOCK_ELEMS,
),
(
"Q2_K",
ferrox_quant::Q2_K_BLOCK_BYTES,
ferrox_quant::Q2_K_BLOCK_ELEMS,
),
(
"Q3_K",
ferrox_quant::Q3_K_BLOCK_BYTES,
ferrox_quant::Q3_K_BLOCK_ELEMS,
),
(
"Q6_K",
ferrox_quant::Q6_K_BLOCK_BYTES,
ferrox_quant::Q6_K_BLOCK_ELEMS,
),
(
"IQ4_NL",
ferrox_quant::IQ4_NL_BLOCK_BYTES,
ferrox_quant::IQ4_NL_BLOCK_ELEMS,
),
(
"IQ4_XS",
ferrox_quant::IQ4_XS_BLOCK_BYTES,
ferrox_quant::IQ4_XS_BLOCK_ELEMS,
),
(
"MXFP4",
ferrox_quant::MXFP4_GGUF_BLOCK_BYTES,
ferrox_quant::MXFP4_GGUF_BLOCK_ELEMS,
),
];
for k in KINDS {
let (_, bytes_, elems) = geometry
.iter()
.find(|(name, _, _)| *name == k.name)
.unwrap_or_else(|| panic!("{}: in KINDS with no ferrox_quant geometry", k.name));
assert_eq!(k.block_bytes, *bytes_, "{} block_bytes", k.name);
assert_eq!(k.block_elems, *elems, "{} block_elems", k.name);
assert_eq!(
k.block_elems,
k.nl() * SUB,
"{}: nl() must partition the super-block into {SUB}-element sub-blocks",
k.name
);
assert_eq!(
kind_by_name(k.name).map(|f| f.name),
Some(k.name),
"{}: does not resolve by its own name",
k.name
);
}
for (i, a) in KINDS.iter().enumerate() {
for b in &KINDS[i + 1..] {
assert_ne!(a.module_name, b.module_name, "{} vs {}", a.name, b.name);
assert_ne!(a.fn_name, b.fn_name, "{} vs {}", a.name, b.name);
}
}
for absent in ["Q4_1", "Q5_1", "Q8_1", "IQ1_S", "IQ2_XXS", "IQ3_S"] {
assert!(
kind_by_name(absent).is_none(),
"{absent} resolved to a mul_mm kernel that does not exist"
);
}
}
#[test]
fn an_emitted_codebook_is_the_slice_the_twin_indexes() {
let mut seen = 0usize;
for k in KINDS {
let Some(cb) = k.codebook else {
assert!(
!kernel_src(k).contains("__constant__"),
"{}: no codebook declared but one is emitted",
k.name
);
continue;
};
seen += 1;
let src = kernel_src(k);
assert!(
k.dequant_src.contains(cb.c_name),
"{}: dequant_src never indexes {}",
k.name,
cb.c_name
);
let decl = format!("__constant__ float {}[16] = {{", cb.c_name);
let at = src
.find(&decl)
.unwrap_or_else(|| panic!("{}: {} is not emitted", k.name, cb.c_name));
let body = &src[at + decl.len()..];
let body = &body[..body.find('}').expect("unterminated codebook")];
let got: Vec<f32> = body
.split(',')
.map(|t| {
t.trim()
.trim_end_matches('f')
.parse::<f32>()
.unwrap_or_else(|e| panic!("{}: {t:?}: {e}", k.name))
})
.collect();
assert_eq!(got.len(), 16, "{}: codebook is not 16 entries", k.name);
for (i, (g, w)) in got.iter().zip(cb.values.iter()).enumerate() {
assert_eq!(
g.to_bits(),
w.to_bits(),
"{}: codebook entry {i}: emitted {g}, twin indexes {w}",
k.name
);
}
}
assert!(seen >= 3, "the codebook kinds stopped declaring codebooks");
}
}