pub const BM: usize = 64;
pub const BN: usize = 32;
pub const BK: usize = 32;
pub const TM: usize = 4;
pub const TN: usize = 2;
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");
#[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 dequant_twin: fn(xb: &[u8], il: usize, reg: &mut [f32; SUB]),
}
impl MulMmKind {
pub const fn nl(&self) -> usize {
self.block_elems / SUB
}
}
pub const Q8_0: MulMmKind = MulMmKind {
name: "Q8_0",
module_name: "ferrox_mul_mm_q8_0",
fn_name: "q8_0_mul_mm",
block_bytes: 34,
block_elems: 32,
dequant_src: r#"
__device__ __forceinline__ void ferrox_dequant_sub(
const unsigned char* xb, int il, float* reg
) {
const float d = ferrox_f16_to_f32(
(unsigned short)xb[0] | ((unsigned short)xb[1] << 8));
const signed char* qs = (const signed char*)(xb + 2) + 16 * il;
#pragma unroll
for (int i = 0; i < 16; i++) {
reg[i] = (float)qs[i] * d;
}
}
"#,
dequant_twin: dequant_sub_q8_0,
};
fn dequant_sub_q8_0(xb: &[u8], il: usize, reg: &mut [f32; SUB]) {
let d = f16_to_f32(u16::from(xb[0]) | (u16::from(xb[1]) << 8));
let qs = &xb[2 + SUB * il..2 + SUB * il + SUB];
for (r, q) in reg.iter_mut().zip(qs.iter()) {
*r = f32::from(*q as i8) * d;
}
}
pub const Q4_0: MulMmKind = MulMmKind {
name: "Q4_0",
module_name: "ferrox_mul_mm_q4_0",
fn_name: "q4_0_mul_mm",
block_bytes: 18,
block_elems: 32,
dequant_src: r#"
__device__ __forceinline__ void ferrox_dequant_sub(
const unsigned char* xb, int il, float* reg
) {
const float d = ferrox_f16_to_f32(
(unsigned short)xb[0] | ((unsigned short)xb[1] << 8));
const unsigned char* qs = xb + 2;
const float d1 = il ? d / 16.0f : d;
const float d2 = d1 / 256.0f;
const float md = -8.0f * d;
const unsigned short mask0 = il ? 0x00F0 : 0x000F;
const unsigned short mask1 = (unsigned short)(mask0 << 8);
#pragma unroll
for (int i = 0; i < 8; i++) {
const unsigned short w =
(unsigned short)qs[2 * i] | ((unsigned short)qs[2 * i + 1] << 8);
reg[2 * i + 0] = d1 * (float)(w & mask0) + md;
reg[2 * i + 1] = d2 * (float)(w & mask1) + md;
}
}
"#,
dequant_twin: dequant_sub_q4_0,
};
fn dequant_sub_q4_0(xb: &[u8], il: usize, reg: &mut [f32; SUB]) {
let d = f16_to_f32(u16::from(xb[0]) | (u16::from(xb[1]) << 8));
let qs = &xb[2..2 + 16];
let d1 = if il != 0 { d / 16.0 } else { d };
let d2 = d1 / 256.0;
let md = -8.0 * d;
let mask0: u16 = if il != 0 { 0x00F0 } else { 0x000F };
let mask1: u16 = mask0 << 8;
for i in 0..8 {
let w = u16::from(qs[2 * i]) | (u16::from(qs[2 * i + 1]) << 8);
reg[2 * i] = d1 * f32::from(w & mask0) + md;
reg[2 * i + 1] = d2 * f32::from(w & mask1) + md;
}
}
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];
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];
#pragma unroll
for (int m = 0; m < FX_TM; m++) {
a[m] = sa[kk][tx * FX_TM + m];
}
#pragma unroll
for (int n = 0; n < FX_TN; n++) {
b[n] = sb[kk][ty * FX_TN + n];
}
#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);
format!("{defines}{F16_SRC}{}{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))
}