use std::sync::Arc;
use objc2::runtime::ProtocolObject;
use objc2_metal::MTLComputePipelineState;
use crate::dispatch::{dispatch_1d, dispatch_2d_tg, set_f32, set_gpu_buf, set_u32, Binder};
use crate::runtime::{mtl_size, GpuRuntime};
use crate::tensor::GpuBuffer;
fn capacity_of<T>(buf: &GpuBuffer) -> usize {
buf.nbytes() / std::mem::size_of::<T>()
}
fn elems(rows: u32, dim: u32, what: &str) -> Result<usize, String> {
(rows as usize)
.checked_mul(dim as usize)
.ok_or_else(|| format!("{what}: rows {rows} x dim {dim} overflows usize"))
}
fn require<T>(buf: &GpuBuffer, need: usize, what: &str) -> Result<(), String> {
let have = capacity_of::<T>(buf);
if have < need {
return Err(format!(
"{what}: buffer holds {have} elements, kernel reads/writes {need}"
));
}
Ok(())
}
pub fn rms_norm_f32(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
weight: &GpuBuffer,
out: &GpuBuffer,
rows: u32,
dim: u32,
eps: f32,
) -> Result<(), String> {
rms_norm_f32_with_scalars(rt, x, weight, out, rows, dim, |bnd| {
set_u32(bnd, rows, 3);
set_u32(bnd, dim, 4);
set_f32(bnd, eps, 5);
})
}
pub fn rms_norm_f32_with_scalars(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
weight: &GpuBuffer,
out: &GpuBuffer,
rows: u32,
dim: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let n = elems(rows, dim, "rms_norm_f32")?;
require::<f32>(x, n, "rms_norm_f32 x")?;
require::<f32>(weight, dim as usize, "rms_norm_f32 weight")?;
require::<f32>(out, n, "rms_norm_f32 out")?;
if rows == 0 {
return Ok(());
}
let p = rt.pipeline("rms_norm_f32")?;
let tptg = reduce_tptg(p.maxTotalThreadsPerThreadgroup(), dim as usize);
dispatch_tg_1d(rt, &p, rows as usize, tptg, None, |bnd| {
set_gpu_buf(bnd, x, 0);
set_gpu_buf(bnd, weight, 1);
set_gpu_buf(bnd, out, 2);
scalars(bnd);
})
}
pub fn rms_norm_bf16(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
weight: &GpuBuffer,
out: &GpuBuffer,
rows: u32,
dim: u32,
eps: f32,
) -> Result<(), String> {
rms_norm_bf16_with_scalars(rt, x, weight, out, rows, dim, |bnd| {
set_u32(bnd, rows, 3);
set_u32(bnd, dim, 4);
set_f32(bnd, eps, 5);
})
}
pub fn rms_norm_bf16_with_scalars(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
weight: &GpuBuffer,
out: &GpuBuffer,
rows: u32,
dim: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let n = elems(rows, dim, "rms_norm_bf16")?;
require::<f32>(x, n, "rms_norm_bf16 x")?;
require::<f32>(weight, dim as usize, "rms_norm_bf16 weight")?;
require::<u16>(out, n, "rms_norm_bf16 out")?;
if rows == 0 {
return Ok(());
}
let p = rt.pipeline("rms_norm_bf16")?;
let tptg = reduce_tptg(p.maxTotalThreadsPerThreadgroup(), dim as usize);
dispatch_tg_1d(rt, &p, rows as usize, tptg, None, |bnd| {
set_gpu_buf(bnd, x, 0);
set_gpu_buf(bnd, weight, 1);
set_gpu_buf(bnd, out, 2);
scalars(bnd);
})
}
#[allow(clippy::too_many_arguments)]
pub fn rms_norm_residual_add_f32(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
weight: &GpuBuffer,
resid: &GpuBuffer,
rows: u32,
dim: u32,
eps: f32,
layer_scale: f32,
) -> Result<(), String> {
rms_norm_residual_add_f32_with_scalars(rt, x, weight, resid, rows, dim, |bnd| {
set_u32(bnd, rows, 3);
set_u32(bnd, dim, 4);
set_f32(bnd, eps, 5);
set_f32(bnd, layer_scale, 6);
})
}
pub fn rms_norm_residual_add_f32_with_scalars(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
weight: &GpuBuffer,
resid: &GpuBuffer,
rows: u32,
dim: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let n = elems(rows, dim, "rms_norm_residual_add_f32")?;
require::<f32>(x, n, "rms_norm_residual_add_f32 x")?;
require::<f32>(weight, dim as usize, "rms_norm_residual_add_f32 weight")?;
require::<f32>(resid, n, "rms_norm_residual_add_f32 resid")?;
if rows == 0 {
return Ok(());
}
let p = rt.pipeline("rms_norm_residual_add_f32")?;
let tptg = reduce_tptg(p.maxTotalThreadsPerThreadgroup(), dim as usize);
dispatch_tg_1d(rt, &p, rows as usize, tptg, None, |bnd| {
set_gpu_buf(bnd, x, 0);
set_gpu_buf(bnd, weight, 1);
set_gpu_buf(bnd, resid, 2);
scalars(bnd);
})
}
pub fn mlp_silu(
rt: &Arc<GpuRuntime>,
gate: &GpuBuffer,
up: &GpuBuffer,
out: &GpuBuffer,
n: u32,
) -> Result<(), String> {
mlp_silu_with_scalars(rt, gate, up, out, n, |bnd| set_u32(bnd, n, 3))
}
pub fn mlp_silu_with_scalars(
rt: &Arc<GpuRuntime>,
gate: &GpuBuffer,
up: &GpuBuffer,
out: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let n_us = n as usize;
require::<f32>(gate, n_us, "mlp_silu gate")?;
require::<f32>(up, n_us, "mlp_silu up")?;
require::<f32>(out, n_us, "mlp_silu out")?;
let p = rt.pipeline("mlp_silu")?;
dispatch_1d(rt, &p, n_us, |bnd| {
set_gpu_buf(bnd, gate, 0);
set_gpu_buf(bnd, up, 1);
set_gpu_buf(bnd, out, 2);
scalars(bnd);
})
}
pub fn mlp_gelu_tanh(
rt: &Arc<GpuRuntime>,
gate: &GpuBuffer,
up: &GpuBuffer,
out: &GpuBuffer,
n: u32,
) -> Result<(), String> {
mlp_gelu_tanh_with_scalars(rt, gate, up, out, n, |bnd| set_u32(bnd, n, 3))
}
pub fn mlp_gelu_tanh_with_scalars(
rt: &Arc<GpuRuntime>,
gate: &GpuBuffer,
up: &GpuBuffer,
out: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let n_us = n as usize;
require::<f32>(gate, n_us, "mlp_gelu_tanh gate")?;
require::<f32>(up, n_us, "mlp_gelu_tanh up")?;
require::<f32>(out, n_us, "mlp_gelu_tanh out")?;
let p = rt.pipeline("mlp_gelu_tanh")?;
dispatch_1d(rt, &p, n_us, |bnd| {
set_gpu_buf(bnd, gate, 0);
set_gpu_buf(bnd, up, 1);
set_gpu_buf(bnd, out, 2);
scalars(bnd);
})
}
pub fn mlp_gelu_tanh_bf16(
rt: &Arc<GpuRuntime>,
gate: &GpuBuffer,
up: &GpuBuffer,
out: &GpuBuffer,
n: u32,
) -> Result<(), String> {
mlp_gelu_tanh_bf16_with_scalars(rt, gate, up, out, n, |bnd| set_u32(bnd, n, 3))
}
pub fn mlp_gelu_tanh_bf16_with_scalars(
rt: &Arc<GpuRuntime>,
gate: &GpuBuffer,
up: &GpuBuffer,
out: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let n_us = n as usize;
require::<f32>(gate, n_us, "mlp_gelu_tanh_bf16 gate")?;
require::<f32>(up, n_us, "mlp_gelu_tanh_bf16 up")?;
require::<u16>(out, n_us, "mlp_gelu_tanh_bf16 out")?;
let p = rt.pipeline("mlp_gelu_tanh_bf16")?;
dispatch_1d(rt, &p, n_us, |bnd| {
set_gpu_buf(bnd, gate, 0);
set_gpu_buf(bnd, up, 1);
set_gpu_buf(bnd, out, 2);
scalars(bnd);
})
}
pub fn scale_f32_inplace(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
scale: f32,
n: u32,
) -> Result<(), String> {
scale_f32_inplace_with_scalars(rt, x, n, |bnd| {
set_f32(bnd, scale, 1);
set_u32(bnd, n, 2);
})
}
pub fn scale_f32_inplace_with_scalars(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
require::<f32>(x, n as usize, "scale_f32_inplace x")?;
let p = rt.pipeline("scale_f32_inplace")?;
dispatch_1d(rt, &p, n as usize, |bnd| {
set_gpu_buf(bnd, x, 0);
scalars(bnd);
})
}
#[allow(clippy::too_many_arguments)]
pub fn gemv_q8(
rt: &Arc<GpuRuntime>,
packed: &GpuBuffer,
scales: &GpuBuffer,
zeros: &GpuBuffer,
x: &GpuBuffer,
y: &GpuBuffer,
rows: u32,
cols: u32,
group_size: u32,
) -> Result<(), String> {
gemv_q8_with_scalars(
rt,
packed,
scales,
zeros,
x,
y,
rows,
cols,
group_size,
|bnd| {
set_u32(bnd, rows, 5);
set_u32(bnd, cols, 6);
set_u32(bnd, group_size, 7);
},
)
}
#[allow(clippy::too_many_arguments)]
pub fn gemv_q8_with_scalars(
rt: &Arc<GpuRuntime>,
packed: &GpuBuffer,
scales: &GpuBuffer,
zeros: &GpuBuffer,
x: &GpuBuffer,
y: &GpuBuffer,
rows: u32,
cols: u32,
group_size: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
if group_size == 0 {
return Err("gemv_q8: group_size must be non-zero".into());
}
if cols % group_size != 0 {
return Err(format!(
"gemv_q8: cols {cols} is not a multiple of group_size {group_size}; \
the kernel would silently drop the ragged tail group"
));
}
let weights = elems(rows, cols, "gemv_q8")?;
let groups = elems(rows, cols / group_size, "gemv_q8 groups")?;
require::<i8>(packed, weights, "gemv_q8 packed")?;
require::<f32>(scales, groups, "gemv_q8 scales")?;
require::<f32>(zeros, groups, "gemv_q8 zeros")?;
require::<f32>(x, cols as usize, "gemv_q8 x")?;
require::<f32>(y, rows as usize, "gemv_q8 y")?;
if rows == 0 {
return Ok(());
}
let p = rt.pipeline("gemv_q8")?;
dispatch_tg_1d(
rt,
&p,
simd_gemv_threadgroups(rows),
SIMD_TPTG,
None,
|bnd| {
set_gpu_buf(bnd, packed, 0);
set_gpu_buf(bnd, scales, 1);
set_gpu_buf(bnd, zeros, 2);
set_gpu_buf(bnd, x, 3);
set_gpu_buf(bnd, y, 4);
scalars(bnd);
},
)
}
pub fn kv_store_timestep(
rt: &Arc<GpuRuntime>,
src: &GpuBuffer,
dst: &GpuBuffer,
dst_offset: &GpuBuffer,
n: u32,
) -> Result<(), String> {
kv_store_timestep_with_scalars(rt, src, dst, dst_offset, n, |bnd| set_u32(bnd, n, 2))
}
pub fn kv_store_timestep_with_scalars(
rt: &Arc<GpuRuntime>,
src: &GpuBuffer,
dst: &GpuBuffer,
dst_offset: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
require::<f32>(src, n as usize, "kv_store_timestep src")?;
require::<u32>(dst_offset, 1, "kv_store_timestep dst_offset")?;
require::<f32>(dst, n as usize, "kv_store_timestep dst")?;
let p = rt.pipeline("kv_store_timestep")?;
dispatch_1d(rt, &p, n as usize, |bnd| {
set_gpu_buf(bnd, src, 0);
set_gpu_buf(bnd, dst, 1);
scalars(bnd);
set_gpu_buf(bnd, dst_offset, 3);
})
}
pub fn kv_store_timestep_pair(
rt: &Arc<GpuRuntime>,
src_k: &GpuBuffer,
src_v: &GpuBuffer,
dst_k: &GpuBuffer,
dst_v: &GpuBuffer,
dst_offset: &GpuBuffer,
n: u32,
) -> Result<(), String> {
kv_store_timestep_pair_with_scalars(rt, src_k, src_v, dst_k, dst_v, dst_offset, n, |bnd| {
set_u32(bnd, n, 4)
})
}
#[allow(clippy::too_many_arguments)]
pub fn kv_store_timestep_pair_with_scalars(
rt: &Arc<GpuRuntime>,
src_k: &GpuBuffer,
src_v: &GpuBuffer,
dst_k: &GpuBuffer,
dst_v: &GpuBuffer,
dst_offset: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let n_us = n as usize;
require::<f32>(src_k, n_us, "kv_store_timestep_pair src_k")?;
require::<f32>(src_v, n_us, "kv_store_timestep_pair src_v")?;
require::<f32>(dst_k, n_us, "kv_store_timestep_pair dst_k")?;
require::<f32>(dst_v, n_us, "kv_store_timestep_pair dst_v")?;
require::<u32>(dst_offset, 1, "kv_store_timestep_pair dst_offset")?;
let p = rt.pipeline("kv_store_timestep_pair")?;
dispatch_1d(rt, &p, n_us, |bnd| {
set_gpu_buf(bnd, src_k, 0);
set_gpu_buf(bnd, src_v, 1);
set_gpu_buf(bnd, dst_k, 2);
set_gpu_buf(bnd, dst_v, 3);
scalars(bnd);
set_gpu_buf(bnd, dst_offset, 5);
})
}
pub fn kv_ring_densify(
rt: &Arc<GpuRuntime>,
src: &GpuBuffer,
dst: &GpuBuffer,
filled: &GpuBuffer,
start: &GpuBuffer,
n_slot: u32,
capacity: u32,
) -> Result<(), String> {
kv_ring_densify_with_scalars(rt, src, dst, filled, start, n_slot, capacity, |bnd| {
set_u32(bnd, n_slot, 2);
set_u32(bnd, capacity, 3);
})
}
#[allow(clippy::too_many_arguments)]
pub fn kv_ring_densify_with_scalars(
rt: &Arc<GpuRuntime>,
src: &GpuBuffer,
dst: &GpuBuffer,
filled: &GpuBuffer,
start: &GpuBuffer,
n_slot: u32,
capacity: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
if capacity == 0 {
return Err("kv_ring_densify: capacity must be non-zero (kernel takes % capacity)".into());
}
let ring = elems(capacity, n_slot, "kv_ring_densify")?;
require::<f32>(src, ring, "kv_ring_densify src")?;
require::<f32>(dst, ring, "kv_ring_densify dst")?;
require::<u32>(filled, 1, "kv_ring_densify filled")?;
require::<u32>(start, 1, "kv_ring_densify start")?;
let p = rt.pipeline("kv_ring_densify")?;
dispatch_1d(rt, &p, ring, |bnd| {
set_gpu_buf(bnd, src, 0);
set_gpu_buf(bnd, dst, 1);
scalars(bnd);
set_gpu_buf(bnd, filled, 4);
set_gpu_buf(bnd, start, 5);
})
}
fn dispatch_tg_1d(
rt: &Arc<GpuRuntime>,
pipeline: &ProtocolObject<dyn MTLComputePipelineState>,
groups: usize,
threads_per_tg: usize,
tg_memory: Option<(usize, usize)>,
encode: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
if groups == 0 || threads_per_tg == 0 {
return Ok(());
}
rt.with_binder(|bnd| {
bnd.set_pipeline(pipeline);
encode(bnd);
if let Some((index, bytes)) = tg_memory {
bnd.set_threadgroup_memory(index, bytes);
}
bnd.dispatch(mtl_size(groups, 1, 1), mtl_size(threads_per_tg, 1, 1));
Ok(())
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AttnHeadDim {
D128,
D256,
}
impl AttnHeadDim {
fn dim(self) -> u32 {
match self {
Self::D128 => 128,
Self::D256 => 256,
}
}
fn entry(self) -> &'static str {
match self {
Self::D128 => "flash_attn_swa_h128",
Self::D256 => "flash_attn_swa_h256",
}
}
fn br(self) -> usize {
8
}
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_swa(
rt: &Arc<GpuRuntime>,
head_dim: AttnHeadDim,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
dims: AttnDims,
) -> Result<(), String> {
flash_attn_swa_with_scalars(
rt,
head_dim,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
|bnd| {
set_u32(bnd, dims.batch, 4);
set_u32(bnd, dims.tq, 5);
set_u32(bnd, dims.heads, 7);
set_u32(bnd, dims.heads_kv, 8);
set_u32(bnd, dims.window, 9);
set_f32(bnd, dims.scale, 10);
},
)
}
#[derive(Clone, Copy, Debug)]
pub struct AttnDims {
pub batch: u32,
pub tq: u32,
pub heads: u32,
pub heads_kv: u32,
pub window: u32,
pub scale: f32,
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_swa_with_scalars(
rt: &Arc<GpuRuntime>,
head_dim: AttnHeadDim,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
dims: AttnDims,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let d = head_dim.dim();
validate_attn_dims(&dims, d, q, o, "flash_attn_swa", false)?;
require::<u32>(tkv, 1, "flash_attn_swa tkv")?;
require::<u32>(q_pos_offset, 1, "flash_attn_swa q_pos_offset")?;
require::<u32>(kv_pos_offset, 1, "flash_attn_swa kv_pos_offset")?;
require::<f32>(
k,
elems(dims.batch * dims.heads_kv, d, "flash_attn_swa k")?,
"flash_attn_swa k",
)?;
require::<f32>(
v,
elems(dims.batch * dims.heads_kv, d, "flash_attn_swa v")?,
"flash_attn_swa v",
)?;
let p = rt.pipeline(head_dim.entry())?;
let groups_x = (dims.tq as usize).div_ceil(head_dim.br());
let groups_y = (dims.batch as usize).saturating_mul(dims.heads as usize);
dispatch_2d_tg(rt, &p, groups_x, groups_y, 32, |bnd| {
set_gpu_buf(bnd, q, 0);
set_gpu_buf(bnd, k, 1);
set_gpu_buf(bnd, v, 2);
set_gpu_buf(bnd, o, 3);
set_gpu_buf(bnd, tkv, 6);
scalars(bnd);
set_gpu_buf(bnd, q_pos_offset, 11);
set_gpu_buf(bnd, kv_pos_offset, 12);
})
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_global_h512(
rt: &Arc<GpuRuntime>,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
dims: AttnDims,
out_bf16: bool,
) -> Result<(), String> {
flash_attn_global_h512_with_scalars(
rt,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
out_bf16,
|bnd| {
set_u32(bnd, dims.batch, 4);
set_u32(bnd, dims.tq, 5);
set_u32(bnd, dims.heads, 7);
set_u32(bnd, dims.heads_kv, 8);
set_f32(bnd, dims.scale, 9);
set_u32(bnd, u32::from(out_bf16), 12);
},
)
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_global_h512_with_scalars(
rt: &Arc<GpuRuntime>,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
dims: AttnDims,
out_bf16: bool,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
const D: u32 = 512;
const BR: usize = 4;
validate_attn_dims(&dims, D, q, o, "flash_attn_global_h512", out_bf16)?;
require::<u32>(tkv, 1, "flash_attn_global_h512 tkv")?;
require::<u32>(q_pos_offset, 1, "flash_attn_global_h512 q_pos_offset")?;
require::<u32>(kv_pos_offset, 1, "flash_attn_global_h512 kv_pos_offset")?;
require::<f32>(
k,
elems(dims.batch * dims.heads_kv, D, "k")?,
"flash_attn_global_h512 k",
)?;
require::<f32>(
v,
elems(dims.batch * dims.heads_kv, D, "v")?,
"flash_attn_global_h512 v",
)?;
let p = rt.pipeline("flash_attn_global_h512")?;
let groups_x = (dims.tq as usize).div_ceil(BR);
let groups_y = (dims.batch as usize).saturating_mul(dims.heads as usize);
dispatch_2d_tg(rt, &p, groups_x, groups_y, 32, |bnd| {
set_gpu_buf(bnd, q, 0);
set_gpu_buf(bnd, k, 1);
set_gpu_buf(bnd, v, 2);
set_gpu_buf(bnd, o, 3);
set_gpu_buf(bnd, tkv, 6);
set_gpu_buf(bnd, q_pos_offset, 10);
set_gpu_buf(bnd, kv_pos_offset, 11);
scalars(bnd);
})
}
fn validate_attn_dims(
dims: &AttnDims,
d: u32,
q: &GpuBuffer,
o: &GpuBuffer,
what: &str,
out_bf16: bool,
) -> Result<(), String> {
if dims.heads_kv == 0 {
return Err(format!("{what}: heads_kv must be non-zero"));
}
if dims.heads % dims.heads_kv != 0 {
return Err(format!(
"{what}: heads {} is not a multiple of heads_kv {} — grouped-query \
attention maps a whole number of query heads onto each kv head",
dims.heads, dims.heads_kv
));
}
if !dims.scale.is_finite() {
return Err(format!("{what}: scale must be finite, got {}", dims.scale));
}
let n = elems(dims.batch * dims.tq * dims.heads, d, what)?;
require::<f32>(q, n, &format!("{what} q"))?;
if out_bf16 {
require::<u16>(o, n, &format!("{what} o (bf16)"))?;
} else {
require::<f32>(o, n, &format!("{what} o"))?;
}
Ok(())
}
#[derive(Clone, Copy, Debug)]
pub struct QkvRopeDims {
pub t: u32,
pub heads_q: u32,
pub heads_kv: u32,
pub head_dim: u32,
pub rotary_dim: u32,
pub theta: f32,
pub eps: f32,
}
impl QkvRopeDims {
fn head_count(&self, q_only: bool) -> Result<usize, String> {
let q = (self.t as usize).checked_mul(self.heads_q as usize);
let kv = (self.t as usize).checked_mul(self.heads_kv as usize);
match (q, kv) {
(Some(q), Some(_)) if q_only => Ok(q),
(Some(q), Some(kv)) => q
.checked_add(2 * kv)
.ok_or_else(|| "qkv_rope: head count overflows usize".to_string()),
_ => Err("qkv_rope: head count overflows usize".to_string()),
}
}
fn validate(&self, what: &str) -> Result<(), String> {
if self.head_dim == 0 {
return Err(format!("{what}: head_dim must be non-zero"));
}
if self.rotary_dim > self.head_dim {
return Err(format!(
"{what}: rotary_dim {} exceeds head_dim {} — RoPE would rotate \
past the end of each head",
self.rotary_dim, self.head_dim
));
}
if self.rotary_dim % 2 != 0 {
return Err(format!(
"{what}: rotary_dim {} is odd; RoPE rotates (even, odd) pairs",
self.rotary_dim
));
}
if !self.theta.is_finite() || self.theta <= 0.0 {
return Err(format!("{what}: theta must be finite and positive"));
}
if !self.eps.is_finite() || self.eps < 0.0 {
return Err(format!("{what}: eps must be finite and non-negative"));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QkvRopeVariant {
PosConst,
PosBuffer,
PosBufferKvStore,
}
impl QkvRopeVariant {
fn entry(self) -> &'static str {
match self {
Self::PosConst => "rms_qkv_rope",
Self::PosBuffer => "rms_qkv_rope_posbuf",
Self::PosBufferKvStore => "rms_qkv_rope_kv_store",
}
}
}
#[derive(Clone, Copy)]
pub struct KvStoreTarget<'a> {
pub dst_k: &'a GpuBuffer,
pub dst_v: &'a GpuBuffer,
pub dst_offset: &'a GpuBuffer,
}
#[allow(clippy::too_many_arguments)]
pub fn rms_qkv_rope(
rt: &Arc<GpuRuntime>,
variant: QkvRopeVariant,
qkv: QkvBuffers<'_>,
dims: QkvRopeDims,
pos_offset: u32,
pos_offset_buf: Option<&GpuBuffer>,
kv_store: Option<KvStoreTarget<'_>>,
q_only: bool,
) -> Result<(), String> {
rms_qkv_rope_with_scalars(
rt,
variant,
qkv,
dims,
pos_offset_buf,
kv_store,
q_only,
|bnd| {
set_u32(bnd, dims.t, 6);
set_u32(bnd, dims.heads_q, 7);
set_u32(bnd, dims.heads_kv, 8);
set_u32(bnd, dims.head_dim, 9);
set_u32(bnd, dims.rotary_dim, 10);
if variant == QkvRopeVariant::PosConst {
set_u32(bnd, pos_offset, 11);
}
set_f32(bnd, dims.theta, 12);
set_f32(bnd, dims.eps, 13);
},
)
}
#[derive(Clone, Copy)]
pub struct QkvBuffers<'a> {
pub q: &'a GpuBuffer,
pub k: &'a GpuBuffer,
pub v: &'a GpuBuffer,
pub q_weight: &'a GpuBuffer,
pub k_weight: &'a GpuBuffer,
pub v_weight: &'a GpuBuffer,
}
#[allow(clippy::too_many_arguments)]
pub fn rms_qkv_rope_with_scalars(
rt: &Arc<GpuRuntime>,
variant: QkvRopeVariant,
qkv: QkvBuffers<'_>,
dims: QkvRopeDims,
pos_offset_buf: Option<&GpuBuffer>,
kv_store: Option<KvStoreTarget<'_>>,
q_only: bool,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
dims.validate("rms_qkv_rope")?;
match (variant, pos_offset_buf) {
(QkvRopeVariant::PosConst, Some(_)) => {
return Err("rms_qkv_rope: PosConst takes a constant offset, not a buffer".into())
}
(QkvRopeVariant::PosConst, None) => {}
(_, None) => return Err("rms_qkv_rope: PosBuffer variants require pos_offset_buf".into()),
(_, Some(b)) => require::<u32>(b, 1, "rms_qkv_rope pos_offset_buf")?,
}
match (variant, kv_store.is_some()) {
(QkvRopeVariant::PosBufferKvStore, false) => {
return Err("rms_qkv_rope: PosBufferKvStore requires a KvStoreTarget".into())
}
(QkvRopeVariant::PosBufferKvStore, true) | (_, false) => {}
(_, true) => return Err("rms_qkv_rope: only PosBufferKvStore writes the KV cache".into()),
}
let d = dims.head_dim as usize;
let q_heads = (dims.t as usize).saturating_mul(dims.heads_q as usize);
let kv_heads = (dims.t as usize).saturating_mul(dims.heads_kv as usize);
require::<f32>(qkv.q, q_heads * d, "rms_qkv_rope q")?;
require::<f32>(qkv.q_weight, d, "rms_qkv_rope q_weight")?;
if !q_only {
require::<f32>(qkv.k, kv_heads * d, "rms_qkv_rope k")?;
require::<f32>(qkv.v, kv_heads * d, "rms_qkv_rope v")?;
require::<f32>(qkv.k_weight, d, "rms_qkv_rope k_weight")?;
require::<f32>(qkv.v_weight, d, "rms_qkv_rope v_weight")?;
}
if let Some(t) = &kv_store {
require::<f32>(t.dst_k, kv_heads * d, "rms_qkv_rope dst_k")?;
require::<f32>(t.dst_v, kv_heads * d, "rms_qkv_rope dst_v")?;
require::<u32>(t.dst_offset, 1, "rms_qkv_rope kv_dst_offset")?;
}
let n = dims.head_count(q_only)?;
let p = rt.pipeline(variant.entry())?;
dispatch_1d(rt, &p, n, |bnd| {
set_gpu_buf(bnd, qkv.q, 0);
set_gpu_buf(bnd, qkv.k, 1);
set_gpu_buf(bnd, qkv.v, 2);
set_gpu_buf(bnd, qkv.q_weight, 3);
set_gpu_buf(bnd, qkv.k_weight, 4);
set_gpu_buf(bnd, qkv.v_weight, 5);
scalars(bnd);
if let Some(b) = pos_offset_buf {
set_gpu_buf(bnd, b, 11);
}
if let Some(t) = kv_store {
set_gpu_buf(bnd, t.dst_k, 14);
set_gpu_buf(bnd, t.dst_v, 15);
set_gpu_buf(bnd, t.dst_offset, 16);
}
})
}
fn reduction_tptg(max_threads: usize, want: usize, tg_array_len: usize) -> usize {
let cap = max_threads.min(tg_array_len).min(want).max(1);
1usize << (usize::BITS - 1 - cap.leading_zeros()) as usize
}
pub fn softcap_logits(
rt: &Arc<GpuRuntime>,
logits: &GpuBuffer,
softcap: &GpuBuffer,
n: u32,
) -> Result<(), String> {
softcap_logits_with_scalars(rt, logits, softcap, n, |bnd| set_u32(bnd, n, 2))
}
pub fn softcap_logits_with_scalars(
rt: &Arc<GpuRuntime>,
logits: &GpuBuffer,
softcap: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
require::<f32>(logits, n as usize, "softcap_logits logits")?;
require::<f32>(softcap, 1, "softcap_logits softcap")?;
let p = rt.pipeline("softcap_logits")?;
dispatch_1d(rt, &p, n as usize, |bnd| {
set_gpu_buf(bnd, logits, 0);
set_gpu_buf(bnd, softcap, 1);
scalars(bnd);
})
}
#[allow(clippy::too_many_arguments)]
pub fn argmax_f32_pass(
rt: &Arc<GpuRuntime>,
logits: &GpuBuffer,
out_idx: &GpuBuffer,
out_val: &GpuBuffer,
idx_in: Option<&GpuBuffer>,
softcap: &GpuBuffer,
n: u32,
) -> Result<(), String> {
let has = u32::from(idx_in.is_some());
argmax_f32_pass_with_scalars(rt, logits, out_idx, out_val, idx_in, softcap, n, |bnd| {
set_u32(bnd, n, 3);
set_u32(bnd, has, 5);
})
}
pub fn argmax_pass_groups(n: u32) -> usize {
(n as usize).div_ceil(ARGMAX_TG)
}
const ARGMAX_TG: usize = 256;
#[allow(clippy::too_many_arguments)]
pub fn argmax_f32_pass_with_scalars(
rt: &Arc<GpuRuntime>,
logits: &GpuBuffer,
out_idx: &GpuBuffer,
out_val: &GpuBuffer,
idx_in: Option<&GpuBuffer>,
softcap: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
if n == 0 {
return Err("argmax_f32_pass: n must be non-zero".into());
}
let groups = argmax_pass_groups(n);
require::<f32>(logits, n as usize, "argmax_f32_pass logits")?;
require::<u32>(out_idx, groups, "argmax_f32_pass out_idx")?;
require::<f32>(out_val, groups, "argmax_f32_pass out_val")?;
require::<f32>(softcap, 1, "argmax_f32_pass softcap")?;
if let Some(b) = idx_in {
require::<u32>(b, n as usize, "argmax_f32_pass idx_in")?;
}
let p = rt.pipeline("argmax_f32")?;
let placeholder;
let idx_buf = match idx_in {
Some(b) => b,
None => {
placeholder = rt.alloc_buffer(4)?;
&placeholder
}
};
dispatch_tg_1d(rt, &p, groups, ARGMAX_TG, None, |bnd| {
set_gpu_buf(bnd, logits, 0);
set_gpu_buf(bnd, out_idx, 1);
set_gpu_buf(bnd, out_val, 2);
scalars(bnd);
set_gpu_buf(bnd, idx_buf, 4);
set_gpu_buf(bnd, softcap, 6);
})
}
pub fn softcap_sample(
rt: &Arc<GpuRuntime>,
logits: &GpuBuffer,
out_token: &GpuBuffer,
softcap: &GpuBuffer,
n: u32,
) -> Result<(), String> {
softcap_sample_with_scalars(rt, logits, out_token, softcap, n, |bnd| set_u32(bnd, n, 3))
}
pub fn softcap_sample_with_scalars(
rt: &Arc<GpuRuntime>,
logits: &GpuBuffer,
out_token: &GpuBuffer,
softcap: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
if n == 0 {
return Err("softcap_sample: n must be non-zero".into());
}
require::<f32>(logits, n as usize, "softcap_sample logits")?;
require::<u32>(out_token, 1, "softcap_sample out_token")?;
require::<f32>(softcap, 1, "softcap_sample softcap")?;
let p = rt.pipeline("softcap_sample")?;
let max_threads = p.maxTotalThreadsPerThreadgroup();
let tptg = reduction_tptg(max_threads, (n as usize).next_power_of_two(), 256);
if (n as usize) > tptg {
return Err(format!(
"softcap_sample: n = {n} exceeds the {tptg}-lane threadgroup this \
kernel reduces over; logits past lane {tptg} would be ignored. \
Use softcap_argmax_one_pass for a full vocabulary."
));
}
dispatch_tg_1d(rt, &p, 1, tptg, None, |bnd| {
set_gpu_buf(bnd, logits, 0);
set_gpu_buf(bnd, out_token, 1);
set_gpu_buf(bnd, softcap, 2);
scalars(bnd);
})
}
pub fn softcap_argmax_one_pass(
rt: &Arc<GpuRuntime>,
logits: &GpuBuffer,
out_token: &GpuBuffer,
softcap: &GpuBuffer,
n: u32,
) -> Result<(), String> {
softcap_argmax_one_pass_with_scalars(rt, logits, out_token, softcap, n, |bnd| {
set_u32(bnd, n, 3)
})
}
pub fn softcap_argmax_one_pass_with_scalars(
rt: &Arc<GpuRuntime>,
logits: &GpuBuffer,
out_token: &GpuBuffer,
softcap: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
if n == 0 {
return Err("softcap_argmax_one_pass: n must be non-zero".into());
}
require::<f32>(logits, n as usize, "softcap_argmax_one_pass logits")?;
require::<u32>(out_token, 1, "softcap_argmax_one_pass out_token")?;
require::<f32>(softcap, 1, "softcap_argmax_one_pass softcap")?;
let p = rt.pipeline("softcap_argmax_one_pass")?;
let tptg = reduction_tptg(p.maxTotalThreadsPerThreadgroup(), 1024, 1024);
dispatch_tg_1d(rt, &p, 1, tptg, None, |bnd| {
set_gpu_buf(bnd, logits, 0);
set_gpu_buf(bnd, out_token, 1);
set_gpu_buf(bnd, softcap, 2);
scalars(bnd);
})
}
#[derive(Clone, Copy, Debug)]
pub struct QuantShape {
pub rows: u32,
pub cols: u32,
pub group_size: u32,
}
impl QuantShape {
pub fn groups(&self) -> Result<usize, String> {
self.validate("QuantShape")?;
elems(self.rows, self.cols / self.group_size, "QuantShape groups")
}
fn validate(&self, what: &str) -> Result<(), String> {
if self.group_size == 0 {
return Err(format!("{what}: group_size must be non-zero"));
}
if self.cols % self.group_size != 0 {
return Err(format!(
"{what}: cols {} is not a multiple of group_size {}; the kernels \
compute `cols / group_size` with integer division and would \
silently drop the ragged tail group",
self.cols, self.group_size
));
}
Ok(())
}
}
#[derive(Clone, Copy)]
pub struct Q4Bank<'a> {
pub packed: &'a GpuBuffer,
pub scales: &'a GpuBuffer,
pub zeros: &'a GpuBuffer,
}
impl Q4Bank<'_> {
fn validate(&self, shape: &QuantShape, what: &str) -> Result<(), String> {
shape.validate(what)?;
let groups = shape.groups()?;
let weights = elems(shape.rows, shape.cols, what)?;
require::<u8>(self.packed, weights.div_ceil(2), &format!("{what} packed"))?;
require::<f32>(self.scales, groups, &format!("{what} scales"))?;
require::<f32>(self.zeros, groups, &format!("{what} zeros"))?;
Ok(())
}
}
#[derive(Clone, Copy)]
pub struct Q4MlxBank<'a> {
pub packed: &'a GpuBuffer,
pub scales_biases: &'a GpuBuffer,
}
impl Q4MlxBank<'_> {
fn validate(&self, shape: &QuantShape, what: &str) -> Result<(), String> {
shape.validate(what)?;
let groups = shape.groups()?;
let weights = elems(shape.rows, shape.cols, what)?;
require::<u8>(self.packed, weights.div_ceil(2), &format!("{what} packed"))?;
require::<u32>(
self.scales_biases,
groups,
&format!("{what} scales_biases (bfloat2 per group)"),
)?;
Ok(())
}
}
pub fn gemv_q4(
rt: &Arc<GpuRuntime>,
bank: Q4Bank<'_>,
x: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
tiled: bool,
) -> Result<(), String> {
gemv_q4_with_scalars(rt, bank, x, y, shape, tiled, |bnd| {
set_u32(bnd, shape.rows, 5);
set_u32(bnd, shape.cols, 6);
set_u32(bnd, shape.group_size, 7);
})
}
pub fn gemv_q4_with_scalars(
rt: &Arc<GpuRuntime>,
bank: Q4Bank<'_>,
x: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
tiled: bool,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let entry = if tiled { "gemv_q4_tiled" } else { "gemv_q4" };
bank.validate(&shape, entry)?;
require::<f32>(x, shape.cols as usize, &format!("{entry} x"))?;
require::<f32>(y, shape.rows as usize, &format!("{entry} y"))?;
if shape.rows == 0 {
return Ok(());
}
let p = rt.pipeline(entry)?;
let (groups, tptg, tg_mem) = if tiled {
(shape.rows as usize, GEMV_TILED_TPTG, None)
} else {
let bytes = (shape.cols as usize).saturating_mul(4);
let limit = rt.max_threadgroup_memory();
if bytes > limit {
return Err(format!(
"{entry}: caching x needs {bytes} bytes of threadgroup memory but this \
device allows {limit}; cols {} is too large for this kernel",
shape.cols
));
}
let t = reduction_tptg(
p.maxTotalThreadsPerThreadgroup(),
GEMV_ROW_TPTG,
GEMV_ROW_TPTG,
)
.min(shape.rows as usize)
.max(1);
((shape.rows as usize).div_ceil(t), t, Some((0, bytes)))
};
dispatch_tg_1d(rt, &p, groups, tptg, tg_mem, |bnd| {
set_gpu_buf(bnd, bank.packed, 0);
set_gpu_buf(bnd, bank.scales, 1);
set_gpu_buf(bnd, bank.zeros, 2);
set_gpu_buf(bnd, x, 3);
set_gpu_buf(bnd, y, 4);
scalars(bnd);
})
}
const GEMV_ROW_TPTG: usize = 128;
const GEMV_TILED_TPTG: usize = 128;
#[allow(clippy::too_many_arguments)]
pub fn embed_lookup_q4(
rt: &Arc<GpuRuntime>,
bank: Q4Bank<'_>,
token_ids: &GpuBuffer,
out: &GpuBuffer,
vocab: u32,
hidden: u32,
group_size: u32,
n_tokens: u32,
) -> Result<(), String> {
embed_lookup_q4_with_scalars(
rt,
bank,
token_ids,
out,
vocab,
hidden,
group_size,
n_tokens,
|bnd| {
set_u32(bnd, hidden, 5);
set_u32(bnd, group_size, 6);
set_u32(bnd, vocab, 7);
set_u32(bnd, n_tokens, 8);
},
)
}
#[allow(clippy::too_many_arguments)]
pub fn embed_lookup_q4_with_scalars(
rt: &Arc<GpuRuntime>,
bank: Q4Bank<'_>,
token_ids: &GpuBuffer,
out: &GpuBuffer,
vocab: u32,
hidden: u32,
group_size: u32,
n_tokens: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let shape = QuantShape {
rows: vocab,
cols: hidden,
group_size,
};
bank.validate(&shape, "embed_lookup_q4")?;
let total = elems(n_tokens, hidden, "embed_lookup_q4")?;
require::<u32>(token_ids, n_tokens as usize, "embed_lookup_q4 token_ids")?;
require::<f32>(out, total, "embed_lookup_q4 out")?;
let p = rt.pipeline("embed_lookup_q4")?;
dispatch_1d(rt, &p, total, |bnd| {
set_gpu_buf(bnd, bank.packed, 0);
set_gpu_buf(bnd, bank.scales, 1);
set_gpu_buf(bnd, bank.zeros, 2);
set_gpu_buf(bnd, token_ids, 3);
set_gpu_buf(bnd, out, 4);
scalars(bnd);
})
}
#[allow(clippy::too_many_arguments)]
pub fn embed_lookup_q4_mlx(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
token_ids: &GpuBuffer,
out: &GpuBuffer,
vocab: u32,
hidden: u32,
group_size: u32,
n_tokens: u32,
) -> Result<(), String> {
embed_lookup_q4_mlx_with_scalars(
rt,
bank,
token_ids,
out,
vocab,
hidden,
group_size,
n_tokens,
|bnd| {
set_u32(bnd, hidden, 5);
set_u32(bnd, group_size, 6);
set_u32(bnd, vocab, 7);
set_u32(bnd, n_tokens, 8);
},
)
}
#[allow(clippy::too_many_arguments)]
pub fn embed_lookup_q4_mlx_with_scalars(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
token_ids: &GpuBuffer,
out: &GpuBuffer,
vocab: u32,
hidden: u32,
group_size: u32,
n_tokens: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let shape = QuantShape {
rows: vocab,
cols: hidden,
group_size,
};
bank.validate(&shape, "embed_lookup_q4_mlx")?;
let total = elems(n_tokens, hidden, "embed_lookup_q4_mlx")?;
require::<u32>(
token_ids,
n_tokens as usize,
"embed_lookup_q4_mlx token_ids",
)?;
require::<f32>(out, total, "embed_lookup_q4_mlx out")?;
let p = rt.pipeline("embed_lookup_q4_mlx")?;
dispatch_1d(rt, &p, total, |bnd| {
set_gpu_buf(bnd, bank.packed, 0);
set_gpu_buf(bnd, bank.scales_biases, 1);
set_gpu_buf(bnd, bank.scales_biases, 2);
set_gpu_buf(bnd, token_ids, 3);
set_gpu_buf(bnd, out, 4);
scalars(bnd);
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Q4MlxLayout {
RowMajor,
Interleaved4,
}
const SIMD_ROWS_PER_TG: usize = 8;
const SIMD_TPTG: usize = 64;
pub fn simd_gemv_threadgroups(rows: u32) -> usize {
(rows as usize).div_ceil(SIMD_ROWS_PER_TG)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Q4MlxRowVariant {
Standard,
Wide,
Tiled,
}
impl Q4MlxRowVariant {
fn entry(self) -> &'static str {
match self {
Self::Standard => "gemv_q4_mlx",
Self::Wide => "gemv_q4_mlx_wide",
Self::Tiled => "gemv_q4_mlx_tiled",
}
}
}
pub fn gemv_q4_mlx(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
x: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
variant: Q4MlxRowVariant,
) -> Result<(), String> {
gemv_q4_mlx_with_scalars(rt, bank, x, y, shape, variant, |bnd| {
set_u32(bnd, shape.rows, 5);
set_u32(bnd, shape.cols, 6);
set_u32(bnd, shape.group_size, 7);
})
}
pub fn gemv_q4_mlx_with_scalars(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
x: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
variant: Q4MlxRowVariant,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let entry = variant.entry();
bank.validate(&shape, entry)?;
require::<f32>(x, shape.cols as usize, &format!("{entry} x"))?;
require::<f32>(y, shape.rows as usize, &format!("{entry} y"))?;
if shape.rows == 0 {
return Ok(());
}
let p = rt.pipeline(entry)?;
let (groups, tptg, tg_mem) = if variant == Q4MlxRowVariant::Tiled {
(shape.rows as usize, GEMV_TILED_TPTG, None)
} else {
let bytes = (shape.cols as usize).saturating_mul(4);
let limit = rt.max_threadgroup_memory();
if bytes > limit {
return Err(format!(
"{entry}: caching x needs {bytes} bytes of threadgroup memory but this \
device allows {limit}; cols {} is too large for this kernel",
shape.cols
));
}
let t = reduction_tptg(
p.maxTotalThreadsPerThreadgroup(),
GEMV_ROW_TPTG,
GEMV_ROW_TPTG,
)
.min(shape.rows as usize)
.max(1);
((shape.rows as usize).div_ceil(t), t, Some((0, bytes)))
};
dispatch_tg_1d(rt, &p, groups, tptg, tg_mem, |bnd| {
bind_mlx_bank(bnd, &bank, 0);
set_gpu_buf(bnd, x, 3);
set_gpu_buf(bnd, y, 4);
scalars(bnd);
})
}
fn bind_mlx_bank(bnd: &mut Binder<'_>, bank: &Q4MlxBank<'_>, base: usize) {
set_gpu_buf(bnd, bank.packed, base);
set_gpu_buf(bnd, bank.scales_biases, base + 1);
set_gpu_buf(bnd, bank.scales_biases, base + 2);
}
const GEMV_BN: usize = 16;
const GEMV_LANES: usize = 16;
const GEMV_X_TILE: usize = 4096;
pub fn gemv_q4_mlx_blocked(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
x: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
) -> Result<(), String> {
gemv_q4_mlx_blocked_with_scalars(rt, bank, x, y, shape, |bnd| {
set_u32(bnd, shape.rows, 5);
set_u32(bnd, shape.cols, 6);
set_u32(bnd, shape.group_size, 7);
})
}
pub fn gemv_q4_mlx_blocked_with_scalars(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
x: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
bank.validate(&shape, "gemv_q4_mlx_blocked")?;
require::<f32>(x, shape.cols as usize, "gemv_q4_mlx_blocked x")?;
require::<f32>(y, shape.rows as usize, "gemv_q4_mlx_blocked y")?;
if shape.rows == 0 {
return Ok(());
}
let p = rt.pipeline("gemv_q4_mlx_blocked")?;
let groups = (shape.rows as usize).div_ceil(GEMV_BN);
let tg_mem = (shape.cols as usize).min(GEMV_X_TILE) * 4;
dispatch_tg_1d(
rt,
&p,
groups,
GEMV_BN * GEMV_LANES,
Some((0, tg_mem)),
|bnd| {
bind_mlx_bank(bnd, &bank, 0);
set_gpu_buf(bnd, x, 3);
set_gpu_buf(bnd, y, 4);
scalars(bnd);
},
)
}
pub fn gemv_q4_mlx_simd(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
x_bf16: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
layout: Q4MlxLayout,
resid: Option<&GpuBuffer>,
) -> Result<(), String> {
gemv_q4_mlx_simd_with_scalars(rt, bank, x_bf16, y, shape, layout, resid, |bnd| {
set_u32(bnd, shape.rows, 5);
set_u32(bnd, shape.cols, 6);
set_u32(bnd, shape.group_size, 7);
})
}
#[allow(clippy::too_many_arguments)]
pub fn gemv_q4_mlx_simd_with_scalars(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
x_bf16: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
layout: Q4MlxLayout,
resid: Option<&GpuBuffer>,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let entry = match (resid.is_some(), layout) {
(false, Q4MlxLayout::RowMajor) => "gemv_q4_mlx_simd",
(false, Q4MlxLayout::Interleaved4) => "gemv_q4_mlx_simd_i4",
(true, Q4MlxLayout::RowMajor) => "gemv_q4_mlx_simd_add",
(true, Q4MlxLayout::Interleaved4) => "gemv_q4_mlx_simd_add_i4",
};
bank.validate(&shape, entry)?;
require::<u16>(x_bf16, shape.cols as usize, &format!("{entry} x_bf16"))?;
require::<f32>(y, shape.rows as usize, &format!("{entry} y"))?;
if let Some(r) = resid {
require::<f32>(r, shape.rows as usize, &format!("{entry} resid"))?;
}
if shape.rows == 0 {
return Ok(());
}
let p = rt.pipeline(entry)?;
let groups = simd_gemv_threadgroups(shape.rows);
dispatch_tg_1d(rt, &p, groups, SIMD_TPTG, None, |bnd| {
bind_mlx_bank(bnd, &bank, 0);
set_gpu_buf(bnd, x_bf16, 3);
set_gpu_buf(bnd, y, 4);
scalars(bnd);
if let Some(r) = resid {
set_gpu_buf(bnd, r, 8);
}
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GateUpDispatch {
Simd(Q4MlxLayout),
Blocked,
}
#[allow(clippy::too_many_arguments)]
pub fn gemv_q4_mlx_gate_up_gelu(
rt: &Arc<GpuRuntime>,
gate: Q4MlxBank<'_>,
up: Q4MlxBank<'_>,
x: &GpuBuffer,
mid: &GpuBuffer,
shape: QuantShape,
dispatch: GateUpDispatch,
mid_as_bf16: bool,
) -> Result<(), String> {
gemv_q4_mlx_gate_up_gelu_with_scalars(
rt,
gate,
up,
x,
mid,
shape,
dispatch,
mid_as_bf16,
|bnd| {
set_u32(bnd, shape.rows, 8);
set_u32(bnd, shape.cols, 9);
set_u32(bnd, shape.group_size, 10);
set_u32(bnd, u32::from(mid_as_bf16), 11);
},
)
}
#[allow(clippy::too_many_arguments)]
pub fn gemv_q4_mlx_gate_up_gelu_with_scalars(
rt: &Arc<GpuRuntime>,
gate: Q4MlxBank<'_>,
up: Q4MlxBank<'_>,
x: &GpuBuffer,
mid: &GpuBuffer,
shape: QuantShape,
dispatch: GateUpDispatch,
mid_as_bf16: bool,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let entry = match dispatch {
GateUpDispatch::Simd(l) => match l {
Q4MlxLayout::RowMajor => "gemv_q4_mlx_simd_gate_up_gelu",
Q4MlxLayout::Interleaved4 => "gemv_q4_mlx_simd_gate_up_gelu_i4",
},
GateUpDispatch::Blocked => "gemv_q4_mlx_blocked_gate_up_gelu",
};
gate.validate(&shape, &format!("{entry} gate"))?;
up.validate(&shape, &format!("{entry} up"))?;
match dispatch {
GateUpDispatch::Simd(_) => require::<u16>(x, shape.cols as usize, &format!("{entry} x"))?,
GateUpDispatch::Blocked => require::<f32>(x, shape.cols as usize, &format!("{entry} x"))?,
}
if mid_as_bf16 {
require::<u16>(mid, shape.rows as usize, &format!("{entry} mid (bf16)"))?;
} else {
require::<f32>(mid, shape.rows as usize, &format!("{entry} mid"))?;
}
if shape.rows == 0 {
return Ok(());
}
let p = rt.pipeline(entry)?;
let (groups, tptg, tg_mem) = match dispatch {
GateUpDispatch::Simd(_) => (simd_gemv_threadgroups(shape.rows), SIMD_TPTG, None),
GateUpDispatch::Blocked => (
(shape.rows as usize).div_ceil(GEMV_BN),
GEMV_BN * GEMV_LANES,
Some((0, (shape.cols as usize).min(GEMV_X_TILE) * 4)),
),
};
dispatch_tg_1d(rt, &p, groups, tptg, tg_mem, |bnd| {
bind_mlx_bank(bnd, &gate, 0);
bind_mlx_bank(bnd, &up, 3);
set_gpu_buf(bnd, x, 6);
set_gpu_buf(bnd, mid, 7);
scalars(bnd);
})
}
#[allow(clippy::too_many_arguments)]
pub fn gemv_q4_mlx_kv(
rt: &Arc<GpuRuntime>,
k: Q4MlxBank<'_>,
v: Q4MlxBank<'_>,
x_bf16: &GpuBuffer,
k_out: &GpuBuffer,
v_out: &GpuBuffer,
shape: QuantShape,
layout: Q4MlxLayout,
) -> Result<(), String> {
let tg_k = simd_gemv_threadgroups(shape.rows) as u32;
gemv_q4_mlx_kv_with_scalars(rt, k, v, x_bf16, k_out, v_out, shape, layout, |bnd| {
set_u32(bnd, shape.rows, 9);
set_u32(bnd, shape.cols, 10);
set_u32(bnd, shape.group_size, 11);
set_u32(bnd, tg_k, 12);
})
}
#[allow(clippy::too_many_arguments)]
pub fn gemv_q4_mlx_kv_with_scalars(
rt: &Arc<GpuRuntime>,
k: Q4MlxBank<'_>,
v: Q4MlxBank<'_>,
x_bf16: &GpuBuffer,
k_out: &GpuBuffer,
v_out: &GpuBuffer,
shape: QuantShape,
layout: Q4MlxLayout,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let entry = match layout {
Q4MlxLayout::RowMajor => "gemv_q4_mlx_simd_kv",
Q4MlxLayout::Interleaved4 => "gemv_q4_mlx_simd_kv_i4",
};
k.validate(&shape, &format!("{entry} k"))?;
v.validate(&shape, &format!("{entry} v"))?;
require::<u16>(x_bf16, shape.cols as usize, &format!("{entry} x_bf16"))?;
require::<f32>(k_out, shape.rows as usize, &format!("{entry} k_out"))?;
require::<f32>(v_out, shape.rows as usize, &format!("{entry} v_out"))?;
if shape.rows == 0 {
return Ok(());
}
let p = rt.pipeline(entry)?;
let per_matrix = simd_gemv_threadgroups(shape.rows);
dispatch_tg_1d(rt, &p, per_matrix * 2, SIMD_TPTG, None, |bnd| {
bind_mlx_bank(bnd, &k, 0);
bind_mlx_bank(bnd, &v, 3);
set_gpu_buf(bnd, x_bf16, 6);
set_gpu_buf(bnd, k_out, 7);
set_gpu_buf(bnd, v_out, 8);
scalars(bnd);
})
}
#[allow(clippy::too_many_arguments)]
pub fn gemv_q4_mlx_qkv(
rt: &Arc<GpuRuntime>,
q: Q4MlxBank<'_>,
k: Q4MlxBank<'_>,
v: Q4MlxBank<'_>,
x_bf16: &GpuBuffer,
out: QkvOutputs<'_>,
rows_q: u32,
rows_kv: u32,
cols: u32,
group_size: u32,
layout: Q4MlxLayout,
) -> Result<(), String> {
let tg_q = simd_gemv_threadgroups(rows_q) as u32;
let tg_k = simd_gemv_threadgroups(rows_kv) as u32;
gemv_q4_mlx_qkv_with_scalars(
rt,
q,
k,
v,
x_bf16,
out,
rows_q,
rows_kv,
cols,
group_size,
layout,
|bnd| {
set_u32(bnd, rows_q, 10);
set_u32(bnd, rows_kv, 11);
set_u32(bnd, cols, 12);
set_u32(bnd, group_size, 13);
set_u32(bnd, tg_q, 14);
set_u32(bnd, tg_k, 15);
},
)
}
#[derive(Clone, Copy)]
pub struct QkvOutputs<'a> {
pub q_out: &'a GpuBuffer,
pub k_out: &'a GpuBuffer,
pub v_out: &'a GpuBuffer,
}
#[allow(clippy::too_many_arguments)]
pub fn gemv_q4_mlx_qkv_with_scalars(
rt: &Arc<GpuRuntime>,
q: Q4MlxBank<'_>,
k: Q4MlxBank<'_>,
v: Q4MlxBank<'_>,
x_bf16: &GpuBuffer,
out: QkvOutputs<'_>,
rows_q: u32,
rows_kv: u32,
cols: u32,
group_size: u32,
layout: Q4MlxLayout,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let entry = match layout {
Q4MlxLayout::RowMajor => "gemv_q4_mlx_simd_qkv",
Q4MlxLayout::Interleaved4 => "gemv_q4_mlx_simd_qkv_i4",
};
let q_shape = QuantShape {
rows: rows_q,
cols,
group_size,
};
let kv_shape = QuantShape {
rows: rows_kv,
cols,
group_size,
};
q.validate(&q_shape, &format!("{entry} q"))?;
k.validate(&kv_shape, &format!("{entry} k"))?;
v.validate(&kv_shape, &format!("{entry} v"))?;
require::<u16>(x_bf16, cols as usize, &format!("{entry} x_bf16"))?;
require::<f32>(out.q_out, rows_q as usize, &format!("{entry} q_out"))?;
require::<f32>(out.k_out, rows_kv as usize, &format!("{entry} k_out"))?;
require::<f32>(out.v_out, rows_kv as usize, &format!("{entry} v_out"))?;
if rows_q == 0 && rows_kv == 0 {
return Ok(());
}
let p = rt.pipeline(entry)?;
let groups = simd_gemv_threadgroups(rows_q) + 2 * simd_gemv_threadgroups(rows_kv);
dispatch_tg_1d(rt, &p, groups, SIMD_TPTG, None, |bnd| {
set_gpu_buf(bnd, q.packed, 0);
set_gpu_buf(bnd, q.scales_biases, 1);
set_gpu_buf(bnd, k.packed, 2);
set_gpu_buf(bnd, k.scales_biases, 3);
set_gpu_buf(bnd, v.packed, 4);
set_gpu_buf(bnd, v.scales_biases, 5);
set_gpu_buf(bnd, x_bf16, 6);
set_gpu_buf(bnd, out.q_out, 7);
set_gpu_buf(bnd, out.k_out, 8);
set_gpu_buf(bnd, out.v_out, 9);
scalars(bnd);
})
}
pub const GEMM_Q4_MLX_MAX_M: u32 = 8;
#[allow(clippy::too_many_arguments)]
pub fn gemm_q4_mlx(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
x_bf16: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
m: u32,
layout: Q4MlxLayout,
resid: Option<&GpuBuffer>,
) -> Result<(), String> {
gemm_q4_mlx_with_scalars(rt, bank, x_bf16, y, shape, m, layout, resid, |bnd| {
set_u32(bnd, shape.rows, 5);
set_u32(bnd, shape.cols, 6);
set_u32(bnd, shape.group_size, 7);
set_u32(bnd, m, 8);
})
}
#[allow(clippy::too_many_arguments)]
pub fn gemm_q4_mlx_with_scalars(
rt: &Arc<GpuRuntime>,
bank: Q4MlxBank<'_>,
x_bf16: &GpuBuffer,
y: &GpuBuffer,
shape: QuantShape,
m: u32,
layout: Q4MlxLayout,
resid: Option<&GpuBuffer>,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
let entry = match (resid.is_some(), layout) {
(false, Q4MlxLayout::RowMajor) => "gemm_q4_mlx_simd",
(false, Q4MlxLayout::Interleaved4) => "gemm_q4_mlx_simd_i4",
(true, Q4MlxLayout::RowMajor) => "gemm_q4_mlx_simd_add",
(true, Q4MlxLayout::Interleaved4) => "gemm_q4_mlx_simd_add_i4",
};
if m == 0 {
return Ok(());
}
if m > GEMM_Q4_MLX_MAX_M {
return Err(format!(
"{entry}: M = {m} exceeds GEMM_MAX_M = {GEMM_Q4_MLX_MAX_M}; the kernel \
clamps to that internally, so rows {GEMM_Q4_MLX_MAX_M}..{m} would be \
left unwritten rather than computed"
));
}
bank.validate(&shape, entry)?;
let out_elems = elems(m, shape.rows, entry)?;
require::<u16>(
x_bf16,
elems(m, shape.cols, entry)?,
&format!("{entry} x_bf16"),
)?;
require::<f32>(y, out_elems, &format!("{entry} y"))?;
if let Some(r) = resid {
require::<f32>(r, out_elems, &format!("{entry} resid"))?;
}
if shape.rows == 0 {
return Ok(());
}
let p = rt.pipeline(entry)?;
let groups = simd_gemv_threadgroups(shape.rows);
dispatch_tg_1d(rt, &p, groups, SIMD_TPTG, None, |bnd| {
bind_mlx_bank(bnd, &bank, 0);
set_gpu_buf(bnd, x_bf16, 3);
set_gpu_buf(bnd, y, 4);
scalars(bnd);
if let Some(r) = resid {
set_gpu_buf(bnd, r, 9);
}
})
}
const REDUCE_MAX_TG: usize = 1024;
fn reduce_tptg(max_threads: usize, cols: usize) -> usize {
reduction_tptg(max_threads, cols.next_power_of_two().max(1), REDUCE_MAX_TG)
}
pub fn softmax_rows_f32(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
out: &GpuBuffer,
rows: u32,
cols: u32,
) -> Result<(), String> {
row_reduce(rt, "softmax_rows_f32", x, out, rows, cols, cols)
}
pub fn row_sum_f32(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
out: &GpuBuffer,
rows: u32,
cols: u32,
) -> Result<(), String> {
row_reduce(rt, "row_sum_f32", x, out, rows, cols, 1)
}
pub fn row_max_f32(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
out: &GpuBuffer,
rows: u32,
cols: u32,
) -> Result<(), String> {
row_reduce(rt, "row_max_f32", x, out, rows, cols, 1)
}
fn row_reduce(
rt: &Arc<GpuRuntime>,
entry: &str,
x: &GpuBuffer,
out: &GpuBuffer,
rows: u32,
cols: u32,
out_per_row: u32,
) -> Result<(), String> {
if cols == 0 {
return Err(format!("{entry}: cols must be non-zero"));
}
require::<f32>(x, elems(rows, cols, entry)?, &format!("{entry} x"))?;
require::<f32>(
out,
elems(rows, out_per_row, entry)?,
&format!("{entry} out"),
)?;
if rows == 0 {
return Ok(());
}
let p = rt.pipeline(entry)?;
let tptg = reduce_tptg(p.maxTotalThreadsPerThreadgroup(), cols as usize);
dispatch_tg_1d(rt, &p, rows as usize, tptg, None, |bnd| {
set_gpu_buf(bnd, x, 0);
set_gpu_buf(bnd, out, 1);
set_u32(bnd, cols, 2);
})
}
#[allow(clippy::too_many_arguments)]
pub fn gemm_i8_dequant(
rt: &Arc<GpuRuntime>,
a: &GpuBuffer,
b: &GpuBuffer,
c: &GpuBuffer,
m: u32,
n: u32,
k: u32,
a_scale: f32,
b_scale: Option<&GpuBuffer>,
) -> Result<(), String> {
if m == 0 || n == 0 || k == 0 {
return Err("gemm_i8_dequant: m, n and k must be non-zero".into());
}
if !a_scale.is_finite() {
return Err(format!(
"gemm_i8_dequant: a_scale must be finite, got {a_scale}"
));
}
const MAX_K_EXACT: u32 = 131_072;
if k > MAX_K_EXACT {
return Err(format!(
"gemm_i8_dequant: k = {k} exceeds {MAX_K_EXACT}, past which an int32 \
accumulator can overflow at full int8 range and the result wraps \
silently rather than saturating"
));
}
require::<i8>(a, elems(m, k, "gemm_i8_dequant")?, "gemm_i8_dequant a")?;
require::<i8>(b, elems(k, n, "gemm_i8_dequant")?, "gemm_i8_dequant b")?;
require::<f32>(c, elems(m, n, "gemm_i8_dequant")?, "gemm_i8_dequant c")?;
if let Some(sc) = b_scale {
require::<f32>(sc, n as usize, "gemm_i8_dequant b_scale")?;
}
let p = rt.pipeline("matmul2d_tensorops_i8_f32")?;
const SM: usize = 128;
const SN: usize = 64;
let tiles_n = (n as usize).div_ceil(SN);
let tiles_m = (m as usize).div_ceil(SM);
let groups = tiles_n * tiles_m;
let tptg = 32 * 4; let has_scale = u32::from(b_scale.is_some());
let scale_buf = b_scale.unwrap_or(c);
dispatch_tg_1d(rt, &p, groups, tptg, None, |bnd| {
set_gpu_buf(bnd, a, 0);
set_gpu_buf(bnd, b, 1);
set_gpu_buf(bnd, c, 2);
set_u32(bnd, m, 3);
set_u32(bnd, n, 4);
set_u32(bnd, k, 5);
set_u32(bnd, tiles_n as u32, 6);
set_u32(bnd, tiles_m as u32, 7);
set_gpu_buf(bnd, scale_buf, 8);
set_f32(bnd, a_scale, 9);
set_u32(bnd, has_scale, 10);
})
}