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, validate_dispatch_geometry, 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> {
elems_product(&[rows, dim], what)
}
fn elems_product(dims: &[u32], what: &str) -> Result<usize, String> {
dims.iter()
.try_fold(1usize, |product, &dim| product.checked_mul(dim as usize))
.ok_or_else(|| format!("{what}: dimension product overflows usize"))
}
fn require_1d_indexable(n: usize, what: &str) -> Result<(), String> {
if n > u32::MAX as usize {
return Err(format!("{what}: 1D extent {n} exceeds Metal uint indexing"));
}
Ok(())
}
fn require_runtime(rt: &GpuRuntime, buf: &GpuBuffer, what: &str) -> Result<(), String> {
if !buf.belongs_to(rt) {
return Err(format!("{what}: buffer belongs to another runtime"));
}
Ok(())
}
fn require_capacity<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(())
}
fn require<T>(rt: &GpuRuntime, buf: &GpuBuffer, need: usize, what: &str) -> Result<(), String> {
require_runtime(rt, buf, what)?;
require_capacity::<T>(buf, need, what)
}
fn require_disjoint_writes(
entry: &str,
writes: &[(&str, &GpuBuffer)],
reads: &[(&str, &GpuBuffer)],
) -> Result<(), String> {
for (i, &(lhs_name, lhs)) in writes.iter().enumerate() {
for &(rhs_name, rhs) in &writes[i + 1..] {
if lhs.aliases(rhs) {
return Err(format!(
"{entry}: writable buffers {lhs_name} and {rhs_name} overlap"
));
}
}
for &(read_name, read) in reads {
if lhs.aliases(read) {
return Err(format!(
"{entry}: writable buffer {lhs_name} overlaps read-only buffer {read_name}"
));
}
}
}
Ok(())
}
fn validate_rms_scalars(dim: u32, eps: f32, what: &str) -> Result<(), String> {
if dim == 0 {
return Err(format!("{what}: dim must be non-zero"));
}
if !eps.is_finite() || eps <= 0.0 {
return Err(format!("{what}: eps must be finite and positive"));
}
Ok(())
}
pub fn attn_kv_capacity(
k: &GpuBuffer,
v: &GpuBuffer,
batch: u32,
heads_kv: u32,
head_dim: u32,
) -> Result<u32, String> {
attn_kv_capacity_for(k, v, batch, heads_kv, head_dim, "attention")
}
fn attn_kv_capacity_for(
k: &GpuBuffer,
v: &GpuBuffer,
batch: u32,
heads_kv: u32,
head_dim: u32,
what: &str,
) -> Result<u32, String> {
if heads_kv == 0 {
return Err(format!("{what}: heads_kv must be non-zero"));
}
if head_dim == 0 {
return Err(format!("{what}: head_dim must be non-zero"));
}
let per_position = elems_product(&[batch, heads_kv, head_dim], what)?;
if per_position == 0 {
return Ok(0);
}
let k_positions = capacity_of::<f32>(k) / per_position;
let v_positions = capacity_of::<f32>(v) / per_position;
if k_positions != v_positions {
return Err(format!(
"{what}: K and V imply different fixed capacities ({k_positions} vs \
{v_positions} positions)"
));
}
u32::try_from(k_positions)
.map_err(|_| format!("{what}: KV capacity {k_positions} exceeds the device u32 range"))
}
pub fn validate_attn_storage(
dims: &AttnDims,
d: u32,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
out_bf16: bool,
) -> Result<u32, String> {
validate_attn_storage_for(dims, d, q, k, v, o, "attention", out_bf16)
}
#[allow(clippy::too_many_arguments)]
fn validate_attn_storage_for(
dims: &AttnDims,
d: u32,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
what: &str,
out_bf16: bool,
) -> Result<u32, String> {
validate_attn_dims(dims, d, q, o, what, out_bf16)?;
let capacity = attn_kv_capacity_for(k, v, dims.batch, dims.heads_kv, d, what)?;
let has_work = dims.batch != 0 && dims.tq != 0 && dims.heads != 0;
if !has_work {
return Ok(capacity);
}
if out_bf16 && o.aliases(q) {
return Err(format!(
"{what}: bf16 output must not alias the f32 q input"
));
}
for (input, name) in [(k, "k"), (v, "v")] {
if o.aliases(input) {
return Err(format!(
"{what}: output must not alias read-only {name} input"
));
}
}
if capacity == 0 {
return Err(format!(
"{what}: K/V buffers do not jointly back one complete KV position"
));
}
Ok(capacity)
}
fn require_attn_runtime(
rt: &GpuRuntime,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
what: &str,
) -> Result<(), String> {
for (name, buffer) in [("q", q), ("k", k), ("v", v), ("o", o)] {
require_runtime(rt, buffer, &format!("{what} {name}"))?;
}
Ok(())
}
fn validate_attn_live_scalar_aliases(
dims: &AttnDims,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
what: &str,
) -> Result<(), String> {
validate_attn_output_scalar_aliases_for(
dims,
o,
&[
("tkv", tkv),
("q_pos_offset", q_pos_offset),
("kv_pos_offset", kv_pos_offset),
],
what,
)
}
pub fn validate_attn_output_scalar_aliases(
dims: &AttnDims,
o: &GpuBuffer,
scalars: &[(&str, &GpuBuffer)],
) -> Result<(), String> {
validate_attn_output_scalar_aliases_for(dims, o, scalars, "attention")
}
fn validate_attn_output_scalar_aliases_for(
dims: &AttnDims,
o: &GpuBuffer,
scalars: &[(&str, &GpuBuffer)],
what: &str,
) -> Result<(), String> {
if dims.batch == 0 || dims.tq == 0 || dims.heads == 0 {
return Ok(());
}
for &(name, scalar) in scalars {
if o.aliases(scalar) {
return Err(format!("{what}: output must not alias live {name} scalar"));
}
}
Ok(())
}
pub fn rms_norm_f32(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
weight: &GpuBuffer,
out: &GpuBuffer,
rows: u32,
dim: u32,
eps: f32,
) -> Result<(), String> {
validate_rms_scalars(dim, eps, "rms_norm_f32")?;
unsafe {
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 unsafe 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>(rt, x, n, "rms_norm_f32 x")?;
require::<f32>(rt, weight, dim as usize, "rms_norm_f32 weight")?;
require::<f32>(rt, out, n, "rms_norm_f32 out")?;
if rows == 0 {
return Ok(());
}
require_disjoint_writes("rms_norm_f32", &[("out", out)], &[("weight", weight)])?;
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> {
validate_rms_scalars(dim, eps, "rms_norm_bf16")?;
unsafe {
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 unsafe 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>(rt, x, n, "rms_norm_bf16 x")?;
require::<f32>(rt, weight, dim as usize, "rms_norm_bf16 weight")?;
require::<u16>(rt, out, n, "rms_norm_bf16 out")?;
if rows == 0 {
return Ok(());
}
require_disjoint_writes(
"rms_norm_bf16",
&[("out", out)],
&[("x", x), ("weight", weight)],
)?;
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> {
validate_rms_scalars(dim, eps, "rms_norm_residual_add_f32")?;
unsafe {
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 unsafe 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>(rt, x, n, "rms_norm_residual_add_f32 x")?;
require::<f32>(rt, weight, dim as usize, "rms_norm_residual_add_f32 weight")?;
require::<f32>(rt, resid, n, "rms_norm_residual_add_f32 resid")?;
if rows == 0 {
return Ok(());
}
require_disjoint_writes(
"rms_norm_residual_add_f32",
&[("resid", resid)],
&[("weight", weight)],
)?;
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> {
unsafe { mlp_silu_with_scalars(rt, gate, up, out, n, |bnd| set_u32(bnd, n, 3)) }
}
pub unsafe 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>(rt, gate, n_us, "mlp_silu gate")?;
require::<f32>(rt, up, n_us, "mlp_silu up")?;
require::<f32>(rt, 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> {
unsafe { mlp_gelu_tanh_with_scalars(rt, gate, up, out, n, |bnd| set_u32(bnd, n, 3)) }
}
pub unsafe 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>(rt, gate, n_us, "mlp_gelu_tanh gate")?;
require::<f32>(rt, up, n_us, "mlp_gelu_tanh up")?;
require::<f32>(rt, 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> {
unsafe { mlp_gelu_tanh_bf16_with_scalars(rt, gate, up, out, n, |bnd| set_u32(bnd, n, 3)) }
}
pub unsafe 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>(rt, gate, n_us, "mlp_gelu_tanh_bf16 gate")?;
require::<f32>(rt, up, n_us, "mlp_gelu_tanh_bf16 up")?;
require::<u16>(rt, out, n_us, "mlp_gelu_tanh_bf16 out")?;
if n == 0 {
return Ok(());
}
require_disjoint_writes(
"mlp_gelu_tanh_bf16",
&[("out", out)],
&[("gate", gate), ("up", up)],
)?;
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> {
unsafe {
scale_f32_inplace_with_scalars(rt, x, n, |bnd| {
set_f32(bnd, scale, 1);
set_u32(bnd, n, 2);
})
}
}
pub unsafe fn scale_f32_inplace_with_scalars(
rt: &Arc<GpuRuntime>,
x: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
require::<f32>(rt, 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> {
unsafe {
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 unsafe 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>(rt, packed, weights, "gemv_q8 packed")?;
require::<f32>(rt, scales, groups, "gemv_q8 scales")?;
require::<f32>(rt, zeros, groups, "gemv_q8 zeros")?;
require::<f32>(rt, x, cols as usize, "gemv_q8 x")?;
require::<f32>(rt, y, rows as usize, "gemv_q8 y")?;
if rows == 0 {
return Ok(());
}
require_disjoint_writes(
"gemv_q8",
&[("y", y)],
&[
("packed", packed),
("scales", scales),
("zeros", zeros),
("x", x),
],
)?;
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,
dst_capacity: u32,
) -> Result<(), String> {
unsafe {
kv_store_timestep_with_scalars(
rt,
src,
dst,
dst_offset,
n,
dst_capacity,
|bnd, capacity| {
set_u32(bnd, n, 2);
set_u32(bnd, capacity, 4);
},
)
}
}
pub unsafe fn kv_store_timestep_with_scalars(
rt: &Arc<GpuRuntime>,
src: &GpuBuffer,
dst: &GpuBuffer,
dst_offset: &GpuBuffer,
n: u32,
dst_capacity: u32,
scalars: impl FnOnce(&mut Binder<'_>, u32),
) -> Result<(), String> {
require::<f32>(rt, src, n as usize, "kv_store_timestep src")?;
require::<u32>(rt, dst_offset, 1, "kv_store_timestep dst_offset")?;
if dst_capacity < n {
return Err(format!(
"kv_store_timestep: dst_capacity {dst_capacity} is smaller than n {n}"
));
}
require::<f32>(
rt,
dst,
dst_capacity as usize,
"kv_store_timestep dst capacity",
)?;
if n == 0 {
return Ok(());
}
require_disjoint_writes(
"kv_store_timestep",
&[("dst", dst)],
&[("src", src), ("dst_offset", dst_offset)],
)?;
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, dst_capacity);
set_gpu_buf(bnd, dst_offset, 3);
})
}
#[allow(clippy::too_many_arguments)]
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,
dst_capacity: u32,
) -> Result<(), String> {
unsafe {
kv_store_timestep_pair_with_scalars(
rt,
src_k,
src_v,
dst_k,
dst_v,
dst_offset,
n,
dst_capacity,
|bnd, capacity| {
set_u32(bnd, n, 4);
set_u32(bnd, capacity, 6);
},
)
}
}
#[allow(clippy::too_many_arguments)]
pub unsafe 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,
dst_capacity: u32,
scalars: impl FnOnce(&mut Binder<'_>, u32),
) -> Result<(), String> {
let n_us = n as usize;
require::<f32>(rt, src_k, n_us, "kv_store_timestep_pair src_k")?;
require::<f32>(rt, src_v, n_us, "kv_store_timestep_pair src_v")?;
require::<u32>(rt, dst_offset, 1, "kv_store_timestep_pair dst_offset")?;
if dst_capacity < n {
return Err(format!(
"kv_store_timestep_pair: dst_capacity {dst_capacity} is smaller than n {n}"
));
}
require::<f32>(
rt,
dst_k,
dst_capacity as usize,
"kv_store_timestep_pair dst_k capacity",
)?;
require::<f32>(
rt,
dst_v,
dst_capacity as usize,
"kv_store_timestep_pair dst_v capacity",
)?;
if n == 0 {
return Ok(());
}
require_disjoint_writes(
"kv_store_timestep_pair",
&[("dst_k", dst_k), ("dst_v", dst_v)],
&[
("src_k", src_k),
("src_v", src_v),
("dst_offset", 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, dst_capacity);
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> {
unsafe {
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 unsafe 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_1d_indexable(ring, "kv_ring_densify fixed grid")?;
require::<f32>(rt, src, ring, "kv_ring_densify src")?;
require::<f32>(rt, dst, ring, "kv_ring_densify dst")?;
require::<u32>(rt, filled, 1, "kv_ring_densify filled")?;
require::<u32>(rt, start, 1, "kv_ring_densify start")?;
if ring == 0 {
return Ok(());
}
require_disjoint_writes(
"kv_ring_densify",
&[("dst", dst)],
&[("src", src), ("filled", filled), ("start", 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",
}
}
pub fn kernel(self) -> &'static str {
self.entry()
}
fn br(self) -> usize {
8
}
}
pub const DECODE_KV_CHUNK: usize = 256;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DecodeChunk {
C64,
C128,
C256,
}
impl DecodeChunk {
pub fn keys(self) -> usize {
match self {
DecodeChunk::C64 => 64,
DecodeChunk::C128 => 128,
DecodeChunk::C256 => 256,
}
}
pub fn parse(v: &str) -> Result<Self, String> {
match v {
"64" => Ok(DecodeChunk::C64),
"128" => Ok(DecodeChunk::C128),
"256" => Ok(DecodeChunk::C256),
other => Err(format!(
"decode chunk must be 64, 128 or 256; got {other:?}"
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecodeHeadBlock {
One,
Group,
AllHeads,
}
impl DecodeHeadBlock {
pub fn simdgroups(self, heads: usize, group: usize) -> Option<usize> {
let n = match self {
DecodeHeadBlock::One => 1,
DecodeHeadBlock::Group => group,
DecodeHeadBlock::AllHeads => heads,
};
((1..=32).contains(&n) && heads % n == 0).then_some(n)
}
pub fn parse(v: &str) -> Result<Self, String> {
match v {
"one" => Ok(DecodeHeadBlock::One),
"group" => Ok(DecodeHeadBlock::Group),
"all" => Ok(DecodeHeadBlock::AllHeads),
other => Err(format!(
"decode head block must be one, group or all; got {other:?}"
)),
}
}
}
pub const DECODE_HEAD_BLOCK_D128: DecodeHeadBlock = DecodeHeadBlock::Group;
pub const DECODE_HEAD_BLOCK_D256: DecodeHeadBlock = DecodeHeadBlock::Group;
pub const DECODE_HEAD_BLOCK_D512: DecodeHeadBlock = DecodeHeadBlock::AllHeads;
pub fn decode_head_block_for(d: u32) -> DecodeHeadBlock {
match d {
512 => DECODE_HEAD_BLOCK_D512,
256 => DECODE_HEAD_BLOCK_D256,
_ => DECODE_HEAD_BLOCK_D128,
}
}
pub const DECODE_REDUCE_THREADS: usize = 256;
pub const DECODE_CHUNK_D128: DecodeChunk = DecodeChunk::C256;
pub const DECODE_CHUNK_D256: DecodeChunk = DecodeChunk::C128;
pub const DECODE_CHUNK_D512: DecodeChunk = DecodeChunk::C128;
pub fn decode_chunk_for(d: u32) -> DecodeChunk {
match d {
512 => DECODE_CHUNK_D512,
256 => DECODE_CHUNK_D256,
_ => DECODE_CHUNK_D128,
}
}
fn decode_entries(d: u32, c: DecodeChunk, r: RowsLanes) -> Option<(String, String)> {
if !matches!(d, 128 | 256 | 512) {
return None;
}
Some((
format!(
"flash_attn_decode_partial_h{d}_c{}_r{}",
c.keys(),
r.width()
),
format!("flash_attn_decode_reduce_h{d}_c{}", c.keys()),
))
}
pub const DECODE_LANES_D128: RowsLanes = RowsLanes::R8;
pub const DECODE_LANES_D256: RowsLanes = RowsLanes::R16;
pub const DECODE_LANES_D512: RowsLanes = RowsLanes::R32;
pub fn decode_lanes_for(d: u32) -> RowsLanes {
match d {
512 => DECODE_LANES_D512,
256 => DECODE_LANES_D256,
_ => DECODE_LANES_D128,
}
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_decode(
rt: &Arc<GpuRuntime>,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
dims: AttnDims,
head_dim: u32,
kv_capacity: usize,
out_bf16: bool,
) -> Result<(), String> {
flash_attn_decode_with_chunk(
rt,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
head_dim,
kv_capacity,
decode_chunk_for(head_dim),
decode_lanes_for(head_dim),
None,
None,
out_bf16,
)
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_decode_with_chunk(
rt: &Arc<GpuRuntime>,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
dims: AttnDims,
head_dim: u32,
kv_capacity: usize,
chunk: DecodeChunk,
lanes: RowsLanes,
reduce_threads: Option<usize>,
head_block: Option<DecodeHeadBlock>,
out_bf16: bool,
) -> Result<(), String> {
let (partial_entry, reduce_entry) =
decode_entries(head_dim, chunk, lanes).ok_or_else(|| {
format!("flash_attn_decode: head dim {head_dim} has no decode kernel (128, 256 or 512)")
})?;
if dims.tq != 1 {
return Err(format!(
"flash_attn_decode is the Tq == 1 path; got Tq = {}. Use flash_attn_swa \
or flash_attn_global_h512 for prefill.",
dims.tq
));
}
require_attn_runtime(rt, q, k, v, o, "flash_attn_decode")?;
let actual_kv_capacity =
validate_attn_storage_for(&dims, head_dim, q, k, v, o, "flash_attn_decode", out_bf16)?;
validate_attn_live_scalar_aliases(
&dims,
o,
tkv,
q_pos_offset,
kv_pos_offset,
"flash_attn_decode",
)?;
require::<u32>(rt, tkv, 1, "flash_attn_decode tkv")?;
require::<u32>(rt, q_pos_offset, 1, "flash_attn_decode q_pos_offset")?;
require::<u32>(rt, kv_pos_offset, 1, "flash_attn_decode kv_pos_offset")?;
let kv_capacity = u32::try_from(kv_capacity)
.map_err(|_| "flash_attn_decode: kv_capacity exceeds the device u32 range")?;
if kv_capacity == 0 {
return Err("flash_attn_decode: kv_capacity must be at least 1".into());
}
let bh = elems_product(&[dims.batch, dims.heads], "flash_attn_decode B*H")?;
if bh == 0 {
return Ok(());
}
if kv_capacity != actual_kv_capacity {
return Err(format!(
"flash_attn_decode: declared kv_capacity {kv_capacity} does not match the fixed K/V \
layout capacity {actual_kv_capacity}"
));
}
let chunks = (kv_capacity as usize).div_ceil(chunk.keys()).max(1);
let stride = head_dim as usize + 2;
let scratch_elems = bh
.checked_mul(chunks)
.and_then(|x| x.checked_mul(stride))
.ok_or("flash_attn_decode: partial scratch size overflows")?;
let scratch_bytes = scratch_elems
.checked_mul(std::mem::size_of::<f32>())
.ok_or("flash_attn_decode: partial scratch byte size overflows")?;
let scratch = rt.alloc_buffer(scratch_bytes)?;
let heads = dims.heads.max(1) as usize;
let group = (dims.heads / dims.heads_kv.max(1)).max(1) as usize;
let want = head_block.unwrap_or_else(|| decode_head_block_for(head_dim));
let partial_sgs = [want, DecodeHeadBlock::Group, DecodeHeadBlock::One]
.into_iter()
.find_map(|p| p.simdgroups(heads, group))
.unwrap_or(1);
let partial_y = (dims.batch as usize)
.checked_mul(heads / partial_sgs)
.ok_or("flash_attn_decode: partial grid height overflows")?;
let p = rt.pipeline(&partial_entry)?;
let r = rt.pipeline(&reduce_entry)?;
let reduce_width = reduce_threads
.unwrap_or(DECODE_REDUCE_THREADS)
.min(head_dim as usize)
.max(32);
let partial_groups = mtl_size(chunks, partial_y, 1);
let partial_threads = mtl_size(partial_sgs * 32, 1, 1);
let reduce_groups = mtl_size(1, bh, 1);
let reduce_threads = mtl_size(reduce_width, 1, 1);
validate_dispatch_geometry(
partial_groups,
partial_threads,
Some(p.maxTotalThreadsPerThreadgroup()),
)?;
validate_dispatch_geometry(
reduce_groups,
reduce_threads,
Some(r.maxTotalThreadsPerThreadgroup()),
)?;
rt.with_binder(|bnd| {
bnd.set_pipeline(&p);
set_gpu_buf(bnd, q, 0);
set_gpu_buf(bnd, k, 1);
set_gpu_buf(bnd, v, 2);
set_gpu_buf(bnd, &scratch, 3);
set_u32(bnd, dims.batch, 4);
set_gpu_buf(bnd, tkv, 6);
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);
set_gpu_buf(bnd, q_pos_offset, 11);
set_gpu_buf(bnd, kv_pos_offset, 12);
set_u32(bnd, kv_capacity, 13);
bnd.dispatch(partial_groups, partial_threads);
if bnd.needs_explicit_barriers() {
bnd.barrier();
}
bnd.set_pipeline(&r);
set_gpu_buf(bnd, &scratch, 0);
set_gpu_buf(bnd, o, 1);
set_u32(bnd, dims.batch, 2);
set_gpu_buf(bnd, tkv, 3);
set_u32(bnd, dims.heads, 4);
set_u32(bnd, u32::from(out_bf16), 5);
set_u32(bnd, kv_capacity, 6);
bnd.dispatch(reduce_groups, reduce_threads);
Ok(())
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowsGroups {
G8,
G16,
G32,
}
impl RowsGroups {
pub fn count(self) -> usize {
match self {
RowsGroups::G8 => 8,
RowsGroups::G16 => 16,
RowsGroups::G32 => 32,
}
}
pub fn parse(v: &str) -> Result<Self, String> {
match v {
"8" => Ok(RowsGroups::G8),
"16" => Ok(RowsGroups::G16),
"32" => Ok(RowsGroups::G32),
other => Err(format!(
"rows simdgroups-per-threadgroup must be 8, 16 or 32; got {other:?}"
)),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RowsLanes {
R8,
R16,
R32,
}
impl RowsLanes {
pub fn width(self) -> usize {
match self {
RowsLanes::R8 => 8,
RowsLanes::R16 => 16,
RowsLanes::R32 => 32,
}
}
pub fn parse(v: &str) -> Result<Self, String> {
match v {
"8" => Ok(RowsLanes::R8),
"16" => Ok(RowsLanes::R16),
"32" => Ok(RowsLanes::R32),
other => Err(format!(
"rows lanes-per-row must be 8, 16 or 32; got {other:?}"
)),
}
}
}
pub fn rows_lanes_for(d: u32) -> RowsLanes {
match d {
512 => ROWS_LANES_D512,
256 => ROWS_LANES_D256,
_ => ROWS_LANES_D128,
}
}
pub const ROWS_LANES_D128: RowsLanes = RowsLanes::R8;
pub const ROWS_LANES_D256: RowsLanes = RowsLanes::R16;
pub const ROWS_LANES_D512: RowsLanes = RowsLanes::R32;
pub const ROWS_GROUPS_D128: RowsGroups = RowsGroups::G8;
pub const ROWS_GROUPS_D256: RowsGroups = RowsGroups::G32;
pub const ROWS_GROUPS_D512: RowsGroups = RowsGroups::G32;
pub fn rows_groups_for(d: u32) -> RowsGroups {
match d {
512 => ROWS_GROUPS_D512,
256 => ROWS_GROUPS_D256,
_ => ROWS_GROUPS_D128,
}
}
fn rows_entry(d: u32, r: RowsLanes, g: RowsGroups) -> Option<String> {
if !matches!(d, 128 | 256 | 512) {
return None;
}
Some(format!(
"flash_attn_rows_h{d}_r{}_g{}",
r.width(),
g.count()
))
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_rows(
rt: &Arc<GpuRuntime>,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
dims: AttnDims,
head_dim: u32,
out_bf16: bool,
) -> Result<(), String> {
flash_attn_rows_with_lanes(
rt,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
head_dim,
rows_lanes_for(head_dim),
rows_groups_for(head_dim),
out_bf16,
)
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_rows_with_lanes(
rt: &Arc<GpuRuntime>,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
dims: AttnDims,
head_dim: u32,
lanes: RowsLanes,
groups: RowsGroups,
out_bf16: bool,
) -> Result<(), String> {
let entry = rows_entry(head_dim, lanes, groups).ok_or_else(|| {
format!("flash_attn_rows: head dim {head_dim} has no kernel (128, 256 or 512)")
})?;
require_attn_runtime(rt, q, k, v, o, "flash_attn_rows")?;
let kv_capacity =
validate_attn_storage_for(&dims, head_dim, q, k, v, o, "flash_attn_rows", out_bf16)?;
validate_attn_live_scalar_aliases(
&dims,
o,
tkv,
q_pos_offset,
kv_pos_offset,
"flash_attn_rows",
)?;
require::<u32>(rt, tkv, 1, "flash_attn_rows tkv")?;
require::<u32>(rt, q_pos_offset, 1, "flash_attn_rows q_pos_offset")?;
require::<u32>(rt, kv_pos_offset, 1, "flash_attn_rows kv_pos_offset")?;
let rows_per_tg = groups.count() * (32 / lanes.width());
let groups_x = (dims.tq as usize).div_ceil(rows_per_tg);
let groups_y = elems_product(&[dims.batch, dims.heads], "flash_attn_rows grid")?;
let p = rt.pipeline(&entry)?;
dispatch_2d_tg(rt, &p, groups_x, groups_y, groups.count() * 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_u32(bnd, dims.batch, 4);
set_u32(bnd, dims.tq, 5);
set_gpu_buf(bnd, tkv, 6);
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);
set_gpu_buf(bnd, q_pos_offset, 11);
set_gpu_buf(bnd, kv_pos_offset, 12);
set_u32(bnd, u32::from(out_bf16), 13);
set_u32(bnd, kv_capacity, 14);
})
}
fn tiled_attn_forced() -> bool {
static FORCED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*FORCED.get_or_init(|| std::env::var_os("TESSL_ATTN_TILED").is_some())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttnKernel {
SplitKv,
Rows,
}
pub fn attn_kernel_for(tq: u32, kv_capacity: usize) -> AttnKernel {
if tq == 1 && kv_capacity > 0 {
AttnKernel::SplitKv
} else {
AttnKernel::Rows
}
}
#[allow(clippy::too_many_arguments)]
fn route_attn(
rt: &Arc<GpuRuntime>,
q: &GpuBuffer,
k: &GpuBuffer,
v: &GpuBuffer,
o: &GpuBuffer,
tkv: &GpuBuffer,
q_pos_offset: &GpuBuffer,
kv_pos_offset: &GpuBuffer,
dims: AttnDims,
head_dim: u32,
out_bf16: bool,
) -> Result<(), String> {
let capacity = attn_kv_capacity_for(k, v, dims.batch, dims.heads_kv, head_dim, "flash_attn")?;
match attn_kernel_for(dims.tq, capacity as usize) {
AttnKernel::SplitKv => flash_attn_decode(
rt,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
head_dim,
capacity as usize,
out_bf16,
),
AttnKernel::Rows => flash_attn_rows(
rt,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
head_dim,
out_bf16,
),
}
}
#[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> {
if !tiled_attn_forced() {
return route_attn(
rt,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
head_dim.dim(),
false,
);
}
flash_attn_swa_tiled(
rt,
head_dim,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
)
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_swa_tiled(
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> {
unsafe {
flash_attn_swa_with_scalars(
rt,
head_dim,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
|bnd, kv_capacity| {
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);
match head_dim {
AttnHeadDim::D128 => set_u32(bnd, kv_capacity, 13),
AttnHeadDim::D256 => {
set_u32(bnd, 0, 13);
set_u32(bnd, kv_capacity, 14);
}
}
},
)
}
}
#[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 unsafe 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<'_>, u32),
) -> Result<(), String> {
let d = head_dim.dim();
require_attn_runtime(rt, q, k, v, o, "flash_attn_swa")?;
let kv_capacity = validate_attn_storage_for(&dims, d, q, k, v, o, "flash_attn_swa", false)?;
validate_attn_live_scalar_aliases(
&dims,
o,
tkv,
q_pos_offset,
kv_pos_offset,
"flash_attn_swa",
)?;
require::<u32>(rt, tkv, 1, "flash_attn_swa tkv")?;
require::<u32>(rt, q_pos_offset, 1, "flash_attn_swa q_pos_offset")?;
require::<u32>(rt, kv_pos_offset, 1, "flash_attn_swa kv_pos_offset")?;
let p = rt.pipeline(head_dim.entry())?;
let groups_x = (dims.tq as usize).div_ceil(head_dim.br());
let groups_y = elems_product(&[dims.batch, dims.heads], "flash_attn_swa grid")?;
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, 11);
set_gpu_buf(bnd, kv_pos_offset, 12);
scalars(bnd, kv_capacity);
if head_dim == AttnHeadDim::D256 {
set_u32(bnd, 0, 13);
}
})
}
#[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> {
if !tiled_attn_forced() {
let global = AttnDims { window: 0, ..dims };
return route_attn(
rt,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
global,
512,
out_bf16,
);
}
flash_attn_global_h512_tiled(
rt,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
out_bf16,
)
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_global_h512_tiled(
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> {
unsafe {
flash_attn_global_h512_with_scalars(
rt,
q,
k,
v,
o,
tkv,
q_pos_offset,
kv_pos_offset,
dims,
out_bf16,
|bnd, kv_capacity| {
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);
set_u32(bnd, kv_capacity, 13);
},
)
}
}
#[allow(clippy::too_many_arguments)]
pub unsafe 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<'_>, u32),
) -> Result<(), String> {
const D: u32 = 512;
const BR: usize = 4;
require_attn_runtime(rt, q, k, v, o, "flash_attn_global_h512")?;
let kv_capacity =
validate_attn_storage_for(&dims, D, q, k, v, o, "flash_attn_global_h512", out_bf16)?;
validate_attn_live_scalar_aliases(
&dims,
o,
tkv,
q_pos_offset,
kv_pos_offset,
"flash_attn_global_h512",
)?;
require::<u32>(rt, tkv, 1, "flash_attn_global_h512 tkv")?;
require::<u32>(rt, q_pos_offset, 1, "flash_attn_global_h512 q_pos_offset")?;
require::<u32>(rt, kv_pos_offset, 1, "flash_attn_global_h512 kv_pos_offset")?;
let p = rt.pipeline("flash_attn_global_h512")?;
let groups_x = (dims.tq as usize).div_ceil(BR);
let groups_y = elems_product(&[dims.batch, dims.heads], "flash_attn_global grid")?;
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, kv_capacity);
})
}
fn validate_attn_dims(
dims: &AttnDims,
d: u32,
q: &GpuBuffer,
o: &GpuBuffer,
what: &str,
out_bf16: bool,
) -> Result<(), String> {
if d == 0 {
return Err(format!("{what}: head_dim must be non-zero"));
}
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 grid_y = elems_product(&[dims.batch, dims.heads], what)?;
if grid_y > u32::MAX as usize {
return Err(format!(
"{what}: B*H grid extent {grid_y} exceeds Metal uint indexing"
));
}
let n = elems_product(&[dims.batch, dims.tq, dims.heads, d], what)?;
require_capacity::<f32>(q, n, &format!("{what} q"))?;
if out_bf16 {
require_capacity::<u16>(o, n, &format!("{what} o (bf16)"))?;
} else {
require_capacity::<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)) => kv
.checked_mul(2)
.and_then(|kv_twice| q.checked_add(kv_twice))
.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,
pub capacity: u32,
}
#[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> {
unsafe {
rms_qkv_rope_with_scalars(
rt,
variant,
qkv,
dims,
pos_offset_buf,
kv_store,
q_only,
|bnd, kv_capacity| {
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);
if let Some(capacity) = kv_capacity {
set_u32(bnd, capacity, 17);
}
},
)
}
}
#[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 validate_rms_qkv_rope(
rt: &Arc<GpuRuntime>,
variant: QkvRopeVariant,
qkv: QkvBuffers<'_>,
dims: QkvRopeDims,
pos_offset_buf: Option<&GpuBuffer>,
kv_store: Option<KvStoreTarget<'_>>,
q_only: bool,
) -> Result<(), String> {
dims.validate("rms_qkv_rope")?;
if q_only && variant == QkvRopeVariant::PosBufferKvStore {
return Err(
"rms_qkv_rope: q_only cannot use PosBufferKvStore because its cache guard can suppress Q"
.into(),
);
}
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>(rt, 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 = elems_product(&[dims.t, dims.heads_q], "rms_qkv_rope q heads")?;
let kv_heads = elems_product(&[dims.t, dims.heads_kv], "rms_qkv_rope kv heads")?;
let q_elems = q_heads
.checked_mul(d)
.ok_or("rms_qkv_rope q extent overflows usize")?;
let kv_elems = kv_heads
.checked_mul(d)
.ok_or("rms_qkv_rope kv extent overflows usize")?;
require::<f32>(rt, qkv.q, q_elems, "rms_qkv_rope q")?;
require::<f32>(rt, qkv.q_weight, d, "rms_qkv_rope q_weight")?;
if !q_only {
require::<f32>(rt, qkv.k, kv_elems, "rms_qkv_rope k")?;
require::<f32>(rt, qkv.v, kv_elems, "rms_qkv_rope v")?;
require::<f32>(rt, qkv.k_weight, d, "rms_qkv_rope k_weight")?;
require::<f32>(rt, qkv.v_weight, d, "rms_qkv_rope v_weight")?;
}
if let Some(t) = &kv_store {
if (t.capacity as usize) < kv_elems {
return Err(format!(
"rms_qkv_rope: cache capacity {} is smaller than the K/V span {kv_elems}",
t.capacity
));
}
require::<f32>(
rt,
t.dst_k,
t.capacity as usize,
"rms_qkv_rope dst_k capacity",
)?;
require::<f32>(
rt,
t.dst_v,
t.capacity as usize,
"rms_qkv_rope dst_v capacity",
)?;
require::<u32>(rt, t.dst_offset, 1, "rms_qkv_rope kv_dst_offset")?;
}
let n = dims.head_count(q_only)?;
require_1d_indexable(n, "rms_qkv_rope head grid")?;
if n == 0 {
return Ok(());
}
if q_only {
match pos_offset_buf {
Some(pos) => require_disjoint_writes(
"rms_qkv_rope",
&[("q", qkv.q)],
&[("q_weight", qkv.q_weight), ("pos_offset_buf", pos)],
)?,
None => require_disjoint_writes(
"rms_qkv_rope",
&[("q", qkv.q)],
&[("q_weight", qkv.q_weight)],
)?,
}
} else {
match (pos_offset_buf, kv_store) {
(None, None) => require_disjoint_writes(
"rms_qkv_rope",
&[("q", qkv.q), ("k", qkv.k), ("v", qkv.v)],
&[
("q_weight", qkv.q_weight),
("k_weight", qkv.k_weight),
("v_weight", qkv.v_weight),
],
)?,
(Some(pos), None) => require_disjoint_writes(
"rms_qkv_rope",
&[("q", qkv.q), ("k", qkv.k), ("v", qkv.v)],
&[
("q_weight", qkv.q_weight),
("k_weight", qkv.k_weight),
("v_weight", qkv.v_weight),
("pos_offset_buf", pos),
],
)?,
(Some(pos), Some(target)) => require_disjoint_writes(
"rms_qkv_rope",
&[
("q", qkv.q),
("k", qkv.k),
("v", qkv.v),
("dst_k", target.dst_k),
("dst_v", target.dst_v),
],
&[
("q_weight", qkv.q_weight),
("k_weight", qkv.k_weight),
("v_weight", qkv.v_weight),
("pos_offset_buf", pos),
("dst_offset", target.dst_offset),
],
)?,
(None, Some(target)) => require_disjoint_writes(
"rms_qkv_rope",
&[
("q", qkv.q),
("k", qkv.k),
("v", qkv.v),
("dst_k", target.dst_k),
("dst_v", target.dst_v),
],
&[
("q_weight", qkv.q_weight),
("k_weight", qkv.k_weight),
("v_weight", qkv.v_weight),
("dst_offset", target.dst_offset),
],
)?,
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub unsafe 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<'_>, Option<u32>),
) -> Result<(), String> {
validate_rms_qkv_rope(rt, variant, qkv, dims, pos_offset_buf, kv_store, q_only)?;
let n = dims.head_count(q_only)?;
if n == 0 {
return Ok(());
}
let kv_capacity = kv_store.map(|target| target.capacity);
let p = rt.pipeline(variant.entry())?;
let (rows_per_tg, threads_per_tg) = rope_row_geometry(&p)?;
dispatch_tg_1d(
rt,
&p,
n.div_ceil(rows_per_tg),
threads_per_tg,
None,
|bnd| {
set_gpu_buf(bnd, qkv.q, 0);
set_gpu_buf(bnd, qkv.q_weight, 3);
if q_only {
set_gpu_buf(bnd, qkv.q, 1);
set_gpu_buf(bnd, qkv.q, 2);
set_gpu_buf(bnd, qkv.q_weight, 4);
set_gpu_buf(bnd, qkv.q_weight, 5);
} else {
set_gpu_buf(bnd, qkv.k, 1);
set_gpu_buf(bnd, qkv.v, 2);
set_gpu_buf(bnd, qkv.k_weight, 4);
set_gpu_buf(bnd, qkv.v_weight, 5);
}
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);
}
scalars(bnd, kv_capacity);
if q_only {
set_u32(bnd, 0, 8);
}
},
)
}
const ROPE_SIMD_WIDTH: usize = 32;
const ROPE_ROWS_PER_TG: usize = 8;
fn rope_row_geometry(
pipeline: &ProtocolObject<dyn MTLComputePipelineState>,
) -> Result<(usize, usize), String> {
let width = pipeline.threadExecutionWidth();
if width != ROPE_SIMD_WIDTH {
return Err(format!(
"rms_qkv_rope: kernel assumes a {ROPE_SIMD_WIDTH}-lane simdgroup, \
pipeline reports an execution width of {width}"
));
}
let max_threads = pipeline.maxTotalThreadsPerThreadgroup();
if max_threads < ROPE_SIMD_WIDTH {
return Err(format!(
"rms_qkv_rope: pipeline allows {max_threads} threads per \
threadgroup, fewer than one simdgroup"
));
}
let rows = (max_threads / ROPE_SIMD_WIDTH).min(ROPE_ROWS_PER_TG);
Ok((rows, rows * ROPE_SIMD_WIDTH))
}
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> {
unsafe { softcap_logits_with_scalars(rt, logits, softcap, n, |bnd| set_u32(bnd, n, 2)) }
}
pub unsafe fn softcap_logits_with_scalars(
rt: &Arc<GpuRuntime>,
logits: &GpuBuffer,
softcap: &GpuBuffer,
n: u32,
scalars: impl FnOnce(&mut Binder<'_>),
) -> Result<(), String> {
require::<f32>(rt, logits, n as usize, "softcap_logits logits")?;
require::<f32>(rt, softcap, 1, "softcap_logits softcap")?;
require_disjoint_writes(
"softcap_logits",
&[("logits", logits)],
&[("softcap", 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());
unsafe {
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 unsafe 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>(rt, logits, n as usize, "argmax_f32_pass logits")?;
require::<u32>(rt, out_idx, groups, "argmax_f32_pass out_idx")?;
require::<f32>(rt, out_val, groups, "argmax_f32_pass out_val")?;
require::<f32>(rt, softcap, 1, "argmax_f32_pass softcap")?;
if let Some(b) = idx_in {
require::<u32>(rt, b, n as usize, "argmax_f32_pass idx_in")?;
}
match idx_in {
Some(indices) => require_disjoint_writes(
"argmax_f32_pass",
&[("out_idx", out_idx), ("out_val", out_val)],
&[
("logits", logits),
("idx_in", indices),
("softcap", softcap),
],
)?,
None => require_disjoint_writes(
"argmax_f32_pass",
&[("out_idx", out_idx), ("out_val", out_val)],
&[("logits", logits), ("softcap", softcap)],
)?,
}
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> {
unsafe {
softcap_sample_with_scalars(rt, logits, out_token, softcap, n, |bnd| set_u32(bnd, n, 3))
}
}
pub unsafe 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>(rt, logits, n as usize, "softcap_sample logits")?;
require::<u32>(rt, out_token, 1, "softcap_sample out_token")?;
require::<f32>(rt, softcap, 1, "softcap_sample softcap")?;
require_disjoint_writes(
"softcap_sample",
&[("logits", logits), ("out_token", out_token)],
&[("softcap", 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> {
unsafe {
softcap_argmax_one_pass_with_scalars(rt, logits, out_token, softcap, n, |bnd| {
set_u32(bnd, n, 3)
})
}
}
pub unsafe 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>(rt, logits, n as usize, "softcap_argmax_one_pass logits")?;
require::<u32>(rt, out_token, 1, "softcap_argmax_one_pass out_token")?;
require::<f32>(rt, softcap, 1, "softcap_argmax_one_pass softcap")?;
require_disjoint_writes(
"softcap_argmax_one_pass",
&[("out_token", out_token)],
&[("logits", logits), ("softcap", 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, rt: &GpuRuntime, shape: &QuantShape, what: &str) -> Result<(), String> {
shape.validate(what)?;
if shape.group_size % 8 != 0 {
return Err(format!(
"{what}: group_size {} must be a multiple of 8; the kernels peel each \
group through 4-byte `uint` loads at `packed + row * cols / 2 + \
g * group_size / 2`, which is aligned for every row and group only \
when group_size is a multiple of 8",
shape.group_size
));
}
let groups = shape.groups()?;
let weights = elems(shape.rows, shape.cols, what)?;
require::<u8>(
rt,
self.packed,
weights.div_ceil(2),
&format!("{what} packed"),
)?;
require::<f32>(rt, self.scales, groups, &format!("{what} scales"))?;
require::<f32>(rt, 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, rt: &GpuRuntime, shape: &QuantShape, what: &str) -> Result<(), String> {
shape.validate(what)?;
const SIMD_BLOCK: u32 = 512;
if shape.group_size % 32 != 0 || SIMD_BLOCK % shape.group_size != 0 {
return Err(format!(
"{what}: group_size {} is outside the MLX kernel domain \
{{32, 64, 128, 256, 512}}; the row kernels peel each group through \
16-byte loads and the simdgroup kernels stride their scale pointer \
by 512 / group_size",
shape.group_size
));
}
let groups = shape.groups()?;
let weights = elems(shape.rows, shape.cols, what)?;
require::<u8>(
rt,
self.packed,
weights.div_ceil(2),
&format!("{what} packed"),
)?;
require::<u32>(
rt,
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> {
unsafe {
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 unsafe 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(rt, &shape, entry)?;
require::<f32>(rt, x, shape.cols as usize, &format!("{entry} x"))?;
require::<f32>(rt, y, shape.rows as usize, &format!("{entry} y"))?;
if shape.rows == 0 {
return Ok(());
}
require_disjoint_writes(
"gemv_q4",
&[("y", y)],
&[
("packed", bank.packed),
("scales", bank.scales),
("zeros", bank.zeros),
("x", x),
],
)?;
let p = rt.pipeline(entry)?;
let (groups, tptg) = if tiled {
(shape.rows as usize, GEMV_TILED_TPTG)
} else {
(simd_gemv_threadgroups(shape.rows), SIMD_TPTG)
};
dispatch_tg_1d(rt, &p, groups, tptg, None, |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> {
unsafe {
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 unsafe 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(rt, &shape, "embed_lookup_q4")?;
let total = elems(n_tokens, hidden, "embed_lookup_q4")?;
require::<u32>(
rt,
token_ids,
n_tokens as usize,
"embed_lookup_q4 token_ids",
)?;
require::<f32>(rt, out, total, "embed_lookup_q4 out")?;
require_disjoint_writes(
"embed_lookup_q4",
&[("out", out)],
&[
("packed", bank.packed),
("scales", bank.scales),
("zeros", bank.zeros),
("token_ids", token_ids),
],
)?;
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> {
unsafe {
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 unsafe 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(rt, &shape, "embed_lookup_q4_mlx")?;
let total = elems(n_tokens, hidden, "embed_lookup_q4_mlx")?;
require::<u32>(
rt,
token_ids,
n_tokens as usize,
"embed_lookup_q4_mlx token_ids",
)?;
require::<f32>(rt, out, total, "embed_lookup_q4_mlx out")?;
require_disjoint_writes(
"embed_lookup_q4_mlx",
&[("out", out)],
&[
("packed", bank.packed),
("scales_biases", bank.scales_biases),
("token_ids", token_ids),
],
)?;
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> {
unsafe {
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 unsafe 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(rt, &shape, entry)?;
require::<f32>(rt, x, shape.cols as usize, &format!("{entry} x"))?;
require::<f32>(rt, y, shape.rows as usize, &format!("{entry} y"))?;
if shape.rows == 0 {
return Ok(());
}
require_disjoint_writes(
"gemv_q4_mlx",
&[("y", y)],
&[
("packed", bank.packed),
("scales_biases", bank.scales_biases),
("x", x),
],
)?;
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> {
unsafe {
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 unsafe 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(rt, &shape, "gemv_q4_mlx_blocked")?;
require::<f32>(rt, x, shape.cols as usize, "gemv_q4_mlx_blocked x")?;
require::<f32>(rt, y, shape.rows as usize, "gemv_q4_mlx_blocked y")?;
if shape.rows == 0 {
return Ok(());
}
require_disjoint_writes(
"gemv_q4_mlx_blocked",
&[("y", y)],
&[
("packed", bank.packed),
("scales_biases", bank.scales_biases),
("x", x),
],
)?;
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> {
unsafe {
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 unsafe 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(rt, &shape, entry)?;
require::<u16>(rt, x_bf16, shape.cols as usize, &format!("{entry} x_bf16"))?;
require::<f32>(rt, y, shape.rows as usize, &format!("{entry} y"))?;
if let Some(r) = resid {
require::<f32>(rt, r, shape.rows as usize, &format!("{entry} resid"))?;
}
if shape.rows == 0 {
return Ok(());
}
require_disjoint_writes(
"gemv_q4_mlx_simd",
&[("y", y)],
&[
("packed", bank.packed),
("scales_biases", bank.scales_biases),
("x_bf16", x_bf16),
],
)?;
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> {
unsafe {
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 unsafe 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",
};
if dispatch == GateUpDispatch::Blocked && shape.cols as usize > GEMV_X_TILE {
return Err(format!(
"{entry}: cols {} exceeds the blocked kernel x-cache capacity {GEMV_X_TILE}",
shape.cols
));
}
gate.validate(rt, &shape, &format!("{entry} gate"))?;
up.validate(rt, &shape, &format!("{entry} up"))?;
match dispatch {
GateUpDispatch::Simd(_) => {
require::<u16>(rt, x, shape.cols as usize, &format!("{entry} x"))?
}
GateUpDispatch::Blocked => {
require::<f32>(rt, x, shape.cols as usize, &format!("{entry} x"))?
}
}
if mid_as_bf16 {
require::<u16>(rt, mid, shape.rows as usize, &format!("{entry} mid (bf16)"))?;
} else {
require::<f32>(rt, mid, shape.rows as usize, &format!("{entry} mid"))?;
}
if shape.rows == 0 {
return Ok(());
}
require_disjoint_writes(
"gemv_q4_mlx_gate_up_gelu",
&[("mid", mid)],
&[
("gate_packed", gate.packed),
("gate_scales_biases", gate.scales_biases),
("up_packed", up.packed),
("up_scales_biases", up.scales_biases),
("x", x),
],
)?;
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;
unsafe {
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 unsafe 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(rt, &shape, &format!("{entry} k"))?;
v.validate(rt, &shape, &format!("{entry} v"))?;
require::<u16>(rt, x_bf16, shape.cols as usize, &format!("{entry} x_bf16"))?;
require::<f32>(rt, k_out, shape.rows as usize, &format!("{entry} k_out"))?;
require::<f32>(rt, v_out, shape.rows as usize, &format!("{entry} v_out"))?;
if shape.rows == 0 {
return Ok(());
}
require_disjoint_writes(
"gemv_q4_mlx_kv",
&[("k_out", k_out), ("v_out", v_out)],
&[
("k_packed", k.packed),
("k_scales_biases", k.scales_biases),
("v_packed", v.packed),
("v_scales_biases", v.scales_biases),
("x_bf16", x_bf16),
],
)?;
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;
unsafe {
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 unsafe 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(rt, &q_shape, &format!("{entry} q"))?;
k.validate(rt, &kv_shape, &format!("{entry} k"))?;
v.validate(rt, &kv_shape, &format!("{entry} v"))?;
require::<u16>(rt, x_bf16, cols as usize, &format!("{entry} x_bf16"))?;
require::<f32>(rt, out.q_out, rows_q as usize, &format!("{entry} q_out"))?;
require::<f32>(rt, out.k_out, rows_kv as usize, &format!("{entry} k_out"))?;
require::<f32>(rt, out.v_out, rows_kv as usize, &format!("{entry} v_out"))?;
if rows_q == 0 && rows_kv == 0 {
return Ok(());
}
match (rows_q == 0, rows_kv == 0) {
(true, false) => require_disjoint_writes(
entry,
&[("k_out", out.k_out), ("v_out", out.v_out)],
&[
("k_packed", k.packed),
("k_scales_biases", k.scales_biases),
("v_packed", v.packed),
("v_scales_biases", v.scales_biases),
("x_bf16", x_bf16),
],
)?,
(false, true) => require_disjoint_writes(
entry,
&[("q_out", out.q_out)],
&[
("q_packed", q.packed),
("q_scales_biases", q.scales_biases),
("x_bf16", x_bf16),
],
)?,
(false, false) => require_disjoint_writes(
entry,
&[
("q_out", out.q_out),
("k_out", out.k_out),
("v_out", out.v_out),
],
&[
("q_packed", q.packed),
("q_scales_biases", q.scales_biases),
("k_packed", k.packed),
("k_scales_biases", k.scales_biases),
("v_packed", v.packed),
("v_scales_biases", v.scales_biases),
("x_bf16", x_bf16),
],
)?,
(true, true) => unreachable!("zero-output case returned above"),
}
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> {
unsafe {
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 unsafe 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(rt, &shape, entry)?;
let out_elems = elems(m, shape.rows, entry)?;
require::<u16>(
rt,
x_bf16,
elems(m, shape.cols, entry)?,
&format!("{entry} x_bf16"),
)?;
require::<f32>(rt, y, out_elems, &format!("{entry} y"))?;
if let Some(r) = resid {
require::<f32>(rt, r, out_elems, &format!("{entry} resid"))?;
}
if shape.rows == 0 {
return Ok(());
}
require_disjoint_writes(
"gemm_q4_mlx",
&[("y", y)],
&[
("packed", bank.packed),
("scales_biases", bank.scales_biases),
("x_bf16", x_bf16),
],
)?;
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>(rt, x, elems(rows, cols, entry)?, &format!("{entry} x"))?;
require::<f32>(
rt,
out,
elems(rows, out_per_row, entry)?,
&format!("{entry} out"),
)?;
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>(rt, a, elems(m, k, "gemm_i8_dequant")?, "gemm_i8_dequant a")?;
require::<i8>(rt, b, elems(k, n, "gemm_i8_dequant")?, "gemm_i8_dequant b")?;
require::<f32>(rt, c, elems(m, n, "gemm_i8_dequant")?, "gemm_i8_dequant c")?;
if let Some(sc) = b_scale {
require::<f32>(rt, sc, n as usize, "gemm_i8_dequant b_scale")?;
}
match b_scale {
Some(sc) => require_disjoint_writes(
"gemm_i8_dequant",
&[("c", c)],
&[("a", a), ("b", b), ("b_scale", sc)],
)?,
None => require_disjoint_writes("gemm_i8_dequant", &[("c", c)], &[("a", a), ("b", b)])?,
}
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);
})
}