use crate::buffer::MlxBuffer;
use crate::device::MlxDevice;
use crate::encoder::{as_bytes, CommandEncoder, KernelArg};
use crate::error::{MlxError, Result};
use crate::kernel_registry::KernelRegistry;
use crate::ops::mul_mv_ext::MulMvExtGpuArgs;
use crate::ops::quantized_matmul_ggml::GgmlType;
const FC_NSG: usize = 2300;
const FC_NE00: usize = 2304;
const TILE: u32 = 8;
const NT_MAX: u32 = 4;
const SMEM_MAX: u64 = 16384;
const MIN_THREADGROUPS: u64 = 128;
const TILE_BYTES: u64 = 8 * 8 * 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum MulMvMmaKind {
Generic,
Block,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct MulMvMmaTiling {
pub(super) kind: MulMvMmaKind,
pub(super) nsg: u32,
pub(super) nt: u32,
}
pub(super) fn kind(ggml_type: GgmlType) -> Option<MulMvMmaKind> {
match ggml_type {
GgmlType::Q8_0 => Some(MulMvMmaKind::Block),
GgmlType::Q4_K | GgmlType::Q6_K | GgmlType::Q5_0 => Some(MulMvMmaKind::Generic),
_ => None,
}
}
pub(super) fn k_step(kind: MulMvMmaKind) -> u32 {
match kind {
MulMvMmaKind::Block => 32,
MulMvMmaKind::Generic => 64,
}
}
fn halve_to_limit(mut n: u32, limit: u64) -> u32 {
while n > 1 && u64::from(n) > limit {
n /= 2;
}
n
}
pub(super) fn tiling(ggml_type: GgmlType, n: u32, k: u32) -> Option<MulMvMmaTiling> {
let kind = kind(ggml_type)?;
let n_steps = u64::from(k / k_step(kind));
let nsg0 = if n <= 64 {
32
} else if n <= 6144 {
16
} else {
8
};
let nsg = halve_to_limit(nsg0, n_steps);
let nt_smem = SMEM_MAX / (u64::from(nsg) * TILE_BYTES);
let nt_rows = u64::from(n) / (MIN_THREADGROUPS * u64::from(TILE));
let nt = halve_to_limit(NT_MAX, nt_smem.min(nt_rows));
Some(MulMvMmaTiling { kind, nsg, nt })
}
fn kernel_name(ggml_type: GgmlType, nt: u32) -> Result<&'static str> {
Ok(match (ggml_type, nt) {
(GgmlType::Q4_K, 1) => "kernel_mul_mv_mma_q4_K_f32_nt1_rt1",
(GgmlType::Q4_K, 2) => "kernel_mul_mv_mma_q4_K_f32_nt2_rt1",
(GgmlType::Q4_K, 4) => "kernel_mul_mv_mma_q4_K_f32_nt4_rt1",
(GgmlType::Q6_K, 1) => "kernel_mul_mv_mma_q6_K_f32_nt1_rt1",
(GgmlType::Q6_K, 2) => "kernel_mul_mv_mma_q6_K_f32_nt2_rt1",
(GgmlType::Q6_K, 4) => "kernel_mul_mv_mma_q6_K_f32_nt4_rt1",
(GgmlType::Q5_0, 1) => "kernel_mul_mv_mma_q5_0_f32_nt1_rt1",
(GgmlType::Q5_0, 2) => "kernel_mul_mv_mma_q5_0_f32_nt2_rt1",
(GgmlType::Q5_0, 4) => "kernel_mul_mv_mma_q5_0_f32_nt4_rt1",
(GgmlType::Q8_0, 1) => "kernel_mul_mv_mma_q8_0_f32_nt1_rt1",
(GgmlType::Q8_0, 2) => "kernel_mul_mv_mma_q8_0_f32_nt2_rt1",
(GgmlType::Q8_0, 4) => "kernel_mul_mv_mma_q8_0_f32_nt4_rt1",
(other, nt) => {
return Err(MlxError::InvalidArgument(format!(
"mul_mv_mma: no kernel for {other:?} at nt {nt}"
)))
}
})
}
pub(super) struct MulMvMmaParams {
pub(super) m: u32,
pub(super) n: u32,
pub(super) k: u32,
pub(super) ggml_type: GgmlType,
}
pub(super) fn mul_mv_mma_dispatch(
encoder: &mut CommandEncoder,
registry: &mut KernelRegistry,
device: &MlxDevice,
weight: &MlxBuffer,
input: &MlxBuffer,
output: &MlxBuffer,
params: &MulMvMmaParams,
) -> Result<()> {
let plan = tiling(params.ggml_type, params.n, params.k).ok_or_else(|| {
MlxError::InvalidArgument(format!(
"mul_mv_mma: unsupported type {:?}",
params.ggml_type
))
})?;
let name = kernel_name(params.ggml_type, plan.nt)?;
let ne00_fc = match plan.kind {
MulMvMmaKind::Block => params.k as i32,
MulMvMmaKind::Generic => 0,
};
let mut nsg = plan.nsg;
let pipeline = loop {
let pipeline = registry
.get_pipeline_with_constants(
name,
device.metal_device(),
&[],
&[(FC_NSG, nsg as i32), (FC_NE00, ne00_fc)],
)?
.clone();
if nsg > 1 && pipeline.max_total_threads_per_threadgroup() < u64::from(nsg) * 32 {
nsg /= 2;
continue;
}
break pipeline;
};
let smem = u64::from(nsg) * u64::from(plan.nt) * TILE_BYTES;
let block_qk = params.ggml_type.block_values();
let nb00 = u64::from(params.ggml_type.block_bytes());
let nb01 = u64::from(params.k / block_qk) * nb00;
let nb02 = nb01 * u64::from(params.n);
let nb11 = u64::from(params.k) * 4;
let nb12 = nb11 * u64::from(params.m);
let args = MulMvExtGpuArgs {
ne00: params.k as i32,
ne01: params.n as i32,
ne02: 1,
_pad0: 0,
nb00,
nb01,
nb02,
nb03: nb02,
ne10: params.k as i32,
ne11: params.m as i32,
ne12: 1,
_pad1: 0,
nb10: 4,
nb11,
nb12,
nb13: nb12,
ne0: params.n as i32,
ne1: params.m as i32,
r2: 1,
r3: 1,
_pad2: 0,
};
let rows_per_group = u64::from(TILE * plan.nt);
encoder.dispatch_tracked_threadgroups_with_args_and_shared(
&pipeline,
&[
(0, KernelArg::Bytes(as_bytes(&args))),
(1, KernelArg::Buffer(weight)),
(2, KernelArg::Buffer(input)),
(3, KernelArg::Buffer(output)),
],
&[(0, smem)],
&[weight, input],
&[output],
crate::MTLSize::new(u64::from(params.n).div_ceil(rows_per_group), 1, 1),
crate::MTLSize::new(32, u64::from(nsg), 1),
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn plan(ggml_type: GgmlType, n: u32, k: u32) -> (MulMvMmaKind, u32, u32) {
let t = tiling(ggml_type, n, k).expect("admitted type");
(t.kind, t.nsg, t.nt)
}
#[test]
fn gemma_shapes_match_the_reference_tiling() {
use MulMvMmaKind::{Block, Generic};
for (n, k, nsg, nt) in [
(4096, 2816, 16, 4),
(8192, 2816, 8, 4),
(2048, 2816, 16, 2),
(1024, 2816, 16, 1),
(2816, 4096, 16, 2),
(2816, 8192, 16, 2),
(2112, 2816, 16, 2),
] {
assert_eq!(plan(GgmlType::Q4_K, n, k), (Generic, nsg, nt), "Q4_K {n}x{k}");
}
assert_eq!(plan(GgmlType::Q6_K, 262_144, 2816), (Generic, 8, 4));
assert_eq!(plan(GgmlType::Q6_K, 2048, 2816), (Generic, 16, 2));
assert_eq!(plan(GgmlType::Q5_0, 2816, 2112), (Generic, 16, 2));
assert_eq!(plan(GgmlType::Q8_0, 2816, 2112), (Block, 16, 2));
assert_eq!(plan(GgmlType::Q4_K, 37, 512), (Generic, 8, 1));
assert_eq!(plan(GgmlType::Q8_0, 37, 320), (Block, 8, 1));
assert!(tiling(GgmlType::Q5_K, 4096, 2816).is_none());
}
}