use crate::cost::{SgemmVariant, hw_model};
use crate::device::metal_device;
use crate::kernels::kernels;
use crate::mtl::{Buffer, ComputeCommandEncoderRef, MTLSize};
fn dispatch_sgemm_variant(enc: &ComputeCommandEncoderRef, m: usize, k: usize, n: usize) {
let kk = kernels();
match hw_model().pick_sgemm(m, k, n) {
SgemmVariant::Mps => {
enc.set_compute_pipeline_state(&kk.sgemm_simd_4x4);
let tg_count = MTLSize {
width: n.div_ceil(32) as u64,
height: m.div_ceil(32) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 512,
height: 1,
depth: 1,
},
);
}
SgemmVariant::Simd4x4 => {
enc.set_compute_pipeline_state(&kk.sgemm_simd_4x4);
let tg_count = MTLSize {
width: n.div_ceil(32) as u64,
height: m.div_ceil(32) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 512,
height: 1,
depth: 1,
},
);
}
SgemmVariant::Simd64 | SgemmVariant::Simd64SplitK => {
enc.set_compute_pipeline_state(&kk.sgemm_simd64);
enc.dispatch_thread_groups(
MTLSize {
width: (n / 64) as u64,
height: (m / 64) as u64,
depth: 1,
},
MTLSize {
width: 32,
height: 8,
depth: 1,
},
);
}
SgemmVariant::Simd => {
enc.set_compute_pipeline_state(&kk.sgemm_simd);
let tg_count = MTLSize {
width: n.div_ceil(8) as u64,
height: m.div_ceil(8) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 32,
height: 1,
depth: 1,
},
);
}
SgemmVariant::SimdPadded => {
enc.set_compute_pipeline_state(&kk.sgemm_simd_padded);
let tg_count = MTLSize {
width: n.div_ceil(8) as u64,
height: m.div_ceil(8) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 32,
height: 1,
depth: 1,
},
);
}
SgemmVariant::Tiled => {
enc.set_compute_pipeline_state(&kk.sgemm_tiled);
let grid_w = n.div_ceil(16) * 16;
let grid_h = m.div_ceil(16) * 16;
let grid = MTLSize {
width: grid_w as u64,
height: grid_h as u64,
depth: 1,
};
enc.dispatch_threads(
grid,
MTLSize {
width: 16,
height: 16,
depth: 1,
},
);
}
SgemmVariant::Naive => {
enc.set_compute_pipeline_state(&kk.sgemm);
let grid = MTLSize {
width: n as u64,
height: m as u64,
depth: 1,
};
let tg_w = 16u64.min(n as u64);
let tg_h = 16u64.min(m as u64);
enc.dispatch_threads(
grid,
MTLSize {
width: tg_w,
height: tg_h,
depth: 1,
},
);
}
}
}
pub fn encode_sgemm_buffers(
enc: &ComputeCommandEncoderRef,
a: &Buffer,
b: &Buffer,
c: &Buffer,
m: usize,
k: usize,
n: usize,
) {
let m_u = m as u32;
let k_u = k as u32;
let n_u = n as u32;
enc.set_buffer(0, Some(a), 0);
enc.set_buffer(1, Some(b), 0);
enc.set_buffer(2, Some(c), 0);
enc.set_bytes(
3,
std::mem::size_of::<u32>() as u64,
&m_u as *const _ as *const _,
);
enc.set_bytes(
4,
std::mem::size_of::<u32>() as u64,
&k_u as *const _ as *const _,
);
enc.set_bytes(
5,
std::mem::size_of::<u32>() as u64,
&n_u as *const _ as *const _,
);
dispatch_sgemm_variant(enc, m, k, n);
}
pub fn buffers_sgemm_sync(a: &Buffer, b: &Buffer, c: &Buffer, m: usize, k: usize, n: usize) {
let Some(dev) = metal_device() else {
return;
};
let cmd = dev.queue.new_command_buffer();
let enc = cmd.new_compute_command_encoder();
encode_sgemm_buffers(enc, a, b, c, m, k, n);
enc.end_encoding();
cmd.commit();
cmd.wait_until_completed();
}
pub fn metal_sgemm_bufs(
enc: &ComputeCommandEncoderRef,
a: &Buffer,
a_off: usize,
b: &Buffer,
b_off: usize,
c: &Buffer,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
let m_u = m as u32;
let k_u = k as u32;
let n_u = n as u32;
enc.set_buffer(0, Some(a), a_off as u64);
enc.set_buffer(1, Some(b), b_off as u64);
enc.set_buffer(2, Some(c), c_off as u64);
enc.set_bytes(
3,
std::mem::size_of::<u32>() as u64,
&m_u as *const _ as *const _,
);
enc.set_bytes(
4,
std::mem::size_of::<u32>() as u64,
&k_u as *const _ as *const _,
);
enc.set_bytes(
5,
std::mem::size_of::<u32>() as u64,
&n_u as *const _ as *const _,
);
if m == 1 && n >= 64 && rlx_ir::env::var("RLX_METAL_GEMV_SPLITK").as_deref() != Some("0") {
const KSPLIT: u64 = 32;
let kk = kernels();
enc.set_compute_pipeline_state(&kk.gemv_f32_splitk);
enc.set_bytes(3, 4, &k_u as *const _ as *const _);
enc.set_bytes(4, 4, &n_u as *const _ as *const _);
enc.dispatch_thread_groups(
MTLSize {
width: (n as u64).div_ceil(32),
height: 1,
depth: 1,
},
MTLSize {
width: 32,
height: KSPLIT,
depth: 1,
},
);
return;
}
if matches!(hw_model().pick_sgemm(m, k, n), SgemmVariant::Simd64SplitK) {
let kk = kernels();
let s = hw_model().ksplits(m, k, n).max(1);
let cn = (m * n) as u32;
enc.set_compute_pipeline_state(&kk.zero_f32);
enc.set_buffer(0, Some(c), c_off as u64);
enc.set_bytes(
1,
std::mem::size_of::<u32>() as u64,
&cn as *const _ as *const _,
);
enc.dispatch_threads(
MTLSize {
width: (m * n) as u64,
height: 1,
depth: 1,
},
MTLSize {
width: 256,
height: 1,
depth: 1,
},
);
enc.set_buffer(0, Some(a), a_off as u64);
enc.set_buffer(1, Some(b), b_off as u64);
enc.set_buffer(2, Some(c), c_off as u64);
enc.set_bytes(
6,
std::mem::size_of::<u32>() as u64,
&s as *const _ as *const _,
);
enc.set_compute_pipeline_state(&kk.sgemm_simd64_splitk);
enc.dispatch_thread_groups(
MTLSize {
width: (n / 64) as u64,
height: (m / 64) as u64,
depth: s as u64,
},
MTLSize {
width: 32,
height: 8,
depth: 1,
},
);
return;
}
dispatch_sgemm_variant(enc, m, k, n);
}
pub fn metal_sgemm(
enc: &ComputeCommandEncoderRef,
arena: &Buffer,
a_off: usize,
b_off: usize,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
metal_sgemm_bufs(enc, arena, a_off, arena, b_off, arena, c_off, m, k, n);
}
#[must_use]
pub fn metal_sgemm_residual_bufs(
enc: &ComputeCommandEncoderRef,
a: &Buffer,
a_off: usize,
b: &Buffer,
b_off: usize,
c: &Buffer,
c_off: usize,
r: &Buffer,
r_off: usize,
m: usize,
k: usize,
n: usize,
) -> bool {
if !matches!(hw_model().pick_sgemm(m, k, n), SgemmVariant::SimdPadded) {
metal_sgemm_bufs(enc, a, a_off, b, b_off, c, c_off, m, k, n);
return false;
}
let kk = kernels();
let (m_u, k_u, n_u) = (m as u32, k as u32, n as u32);
enc.set_compute_pipeline_state(&kk.sgemm_simd_padded_residual);
enc.set_buffer(0, Some(a), a_off as u64);
enc.set_buffer(1, Some(b), b_off as u64);
enc.set_buffer(2, Some(c), c_off as u64);
enc.set_bytes(
3,
std::mem::size_of::<u32>() as u64,
&m_u as *const _ as *const _,
);
enc.set_bytes(
4,
std::mem::size_of::<u32>() as u64,
&k_u as *const _ as *const _,
);
enc.set_bytes(
5,
std::mem::size_of::<u32>() as u64,
&n_u as *const _ as *const _,
);
enc.set_buffer(6, Some(r), r_off as u64);
let tg_count = MTLSize {
width: n.div_ceil(8) as u64,
height: m.div_ceil(8) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 32,
height: 1,
depth: 1,
},
);
true
}
pub fn metal_sgemm_f16w_bufs(
enc: &ComputeCommandEncoderRef,
a: &Buffer,
a_off: usize,
b: &Buffer,
b_off: usize,
c: &Buffer,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
let kk = kernels();
let m_u = m as u32;
let k_u = k as u32;
let n_u = n as u32;
enc.set_buffer(0, Some(a), a_off as u64);
enc.set_buffer(1, Some(b), b_off as u64);
enc.set_buffer(2, Some(c), c_off as u64);
enc.set_bytes(3, 4, &m_u as *const _ as *const _);
enc.set_bytes(4, 4, &k_u as *const _ as *const _);
enc.set_bytes(5, 4, &n_u as *const _ as *const _);
let kpart_env = rlx_ir::env::var("RLX_METAL_GEMV_KPART");
let base_tg = (n as u64).div_ceil(64);
if m == 1 && n >= 64 && n.is_multiple_of(2) && kpart_env.is_some() && base_tg < 48
{
let kparts: u64 = kpart_env
.as_deref()
.and_then(|s| s.parse::<u64>().ok())
.filter(|&p| p > 1)
.unwrap_or(8)
.min((k as u64 / 64).max(1));
enc.set_compute_pipeline_state(&kk.gemv_zero_f32);
enc.set_buffer(0, Some(c), c_off as u64);
enc.set_bytes(1, 4, &n_u as *const _ as *const _);
enc.dispatch_threads(
MTLSize {
width: n as u64,
height: 1,
depth: 1,
},
MTLSize {
width: 64u64.min(n as u64),
height: 1,
depth: 1,
},
);
enc.set_compute_pipeline_state(&kk.gemv_f16w_kpart);
enc.set_buffer(0, Some(a), a_off as u64);
enc.set_buffer(1, Some(b), b_off as u64);
enc.set_buffer(2, Some(c), c_off as u64);
enc.set_bytes(3, 4, &k_u as *const _ as *const _);
enc.set_bytes(4, 4, &n_u as *const _ as *const _);
let kparts_u = kparts as u32;
enc.set_bytes(5, 4, &kparts_u as *const _ as *const _);
enc.dispatch_thread_groups(
MTLSize {
width: (n as u64).div_ceil(64),
height: 1,
depth: kparts,
},
MTLSize {
width: 32,
height: 32,
depth: 1,
},
);
return;
}
if m == 1
&& n >= 64
&& n.is_multiple_of(2)
&& rlx_ir::env::var("RLX_METAL_GEMV_SPLITK").as_deref() != Some("0")
{
const KSPLIT: u64 = 32;
enc.set_compute_pipeline_state(&kk.gemv_f16w_splitk);
enc.set_bytes(3, 4, &k_u as *const _ as *const _);
enc.set_bytes(4, 4, &n_u as *const _ as *const _);
let tg_count = MTLSize {
width: (n as u64).div_ceil(64),
height: 1,
depth: 1,
};
let tg = MTLSize {
width: 32,
height: KSPLIT,
depth: 1,
};
enc.dispatch_thread_groups(tg_count, tg);
return;
}
if m <= 4 && n >= 64 {
enc.set_compute_pipeline_state(&kk.sgemm_f16w_small_m);
let grid = MTLSize {
width: n as u64,
height: 1,
depth: 1,
};
let tg = MTLSize {
width: 64u64.min(n as u64).max(1),
height: 1,
depth: 1,
};
enc.dispatch_threads(grid, tg);
return;
}
if m > 1 && rlx_ir::env::flag("RLX_METAL_PREFILL_TRACE") {
use std::sync::atomic::{AtomicBool, Ordering};
static SEEN: AtomicBool = AtomicBool::new(false);
if !SEEN.swap(true, Ordering::Relaxed) {
eprintln!("[prefill-trace] metal_sgemm_f16w_bufs m={m} k={k} n={n} → padded kernel");
}
}
let use_padded = matches!(
hw_model().pick_sgemm(m, k, n),
SgemmVariant::SimdPadded
| SgemmVariant::Simd
| SgemmVariant::Simd4x4
| SgemmVariant::Mps
| SgemmVariant::Tiled
) && k >= 256
&& n >= 256;
if use_padded {
let force_wide = rlx_ir::env::flag("RLX_METAL_WIDE_GEMM");
let auto_wide = !rlx_ir::env::flag("RLX_METAL_NO_AUTO_WIDE_GEMM")
&& m > 1
&& m >= 64
&& k >= 512
&& n >= 1024
&& k.is_multiple_of(8)
&& n.is_multiple_of(64);
if (force_wide && m >= 48) || auto_wide {
if m > 1 {
crate::prefill_stats::record_f16_wide();
}
enc.set_compute_pipeline_state(&kk.sgemm_wide8x64_f16w);
let tg_count = MTLSize {
width: (n as u64).div_ceil(64),
height: (m as u64).div_ceil(64),
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 256,
height: 1,
depth: 1,
},
);
return;
}
enc.set_compute_pipeline_state(&kk.sgemm_simd_padded_f16w);
if m > 1 {
crate::prefill_stats::record_f16_padded();
}
let tg_count = MTLSize {
width: n.div_ceil(8) as u64,
height: m.div_ceil(8) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 32,
height: 1,
depth: 1,
},
);
} else {
enc.set_compute_pipeline_state(&kk.sgemm_f16w);
let grid = MTLSize {
width: n as u64,
height: m as u64,
depth: 1,
};
let tg_w = 16u64.min(n as u64);
let tg_h = 16u64.min(m as u64);
enc.dispatch_threads(
grid,
MTLSize {
width: tg_w,
height: tg_h,
depth: 1,
},
);
}
}
pub fn metal_sgemm_f16w(
enc: &ComputeCommandEncoderRef,
arena: &Buffer,
a_off: usize,
b_off: usize,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
metal_sgemm_f16w_bufs(enc, arena, a_off, arena, b_off, arena, c_off, m, k, n);
}
pub fn metal_sgemm_f16a_bufs(
enc: &ComputeCommandEncoderRef,
a: &Buffer,
a_off: usize,
b: &Buffer,
b_off: usize,
c: &Buffer,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
let kk = kernels();
let (m_u, k_u, n_u) = (m as u32, k as u32, n as u32);
enc.set_buffer(0, Some(a), a_off as u64);
enc.set_buffer(1, Some(b), b_off as u64);
enc.set_buffer(2, Some(c), c_off as u64);
enc.set_bytes(3, 4, &m_u as *const _ as *const _);
enc.set_bytes(4, 4, &k_u as *const _ as *const _);
enc.set_bytes(5, 4, &n_u as *const _ as *const _);
enc.set_compute_pipeline_state(&kk.sgemm_f16a);
let grid = MTLSize {
width: n as u64,
height: m as u64,
depth: 1,
};
let tg_w = 16u64.min(n as u64);
let tg_h = 16u64.min(m as u64);
enc.dispatch_threads(
grid,
MTLSize {
width: tg_w,
height: tg_h,
depth: 1,
},
);
}
#[repr(u32)]
#[derive(Copy, Clone)]
pub enum FusedAct {
None = 0,
Gelu = 1,
Silu = 2,
}
pub fn metal_sgemm_bias(
enc: &ComputeCommandEncoderRef,
arena: &Buffer,
a_off: usize,
b_off: usize,
bias_off: usize,
c_off: usize,
m: usize,
k: usize,
n: usize,
act: FusedAct,
) {
let kk = kernels();
let m_u = m as u32;
let k_u = k as u32;
let n_u = n as u32;
let act_u = act as u32;
match hw_model().pick_sgemm(m, k, n) {
SgemmVariant::Simd4x4 => {
enc.set_buffer(0, Some(arena), a_off as u64);
enc.set_buffer(1, Some(arena), b_off as u64);
enc.set_buffer(2, Some(arena), bias_off as u64);
enc.set_buffer(3, Some(arena), c_off as u64);
enc.set_bytes(4, 4, &m_u as *const _ as *const _);
enc.set_bytes(5, 4, &k_u as *const _ as *const _);
enc.set_bytes(6, 4, &n_u as *const _ as *const _);
enc.set_bytes(7, 4, &act_u as *const _ as *const _);
enc.set_compute_pipeline_state(&kk.sgemm_simd_4x4_bias);
let tg_count = MTLSize {
width: n.div_ceil(32) as u64,
height: m.div_ceil(32) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 512,
height: 1,
depth: 1,
},
);
}
SgemmVariant::Simd => {
enc.set_buffer(0, Some(arena), a_off as u64);
enc.set_buffer(1, Some(arena), b_off as u64);
enc.set_buffer(2, Some(arena), bias_off as u64);
enc.set_buffer(3, Some(arena), c_off as u64);
enc.set_bytes(4, 4, &m_u as *const _ as *const _);
enc.set_bytes(5, 4, &k_u as *const _ as *const _);
enc.set_bytes(6, 4, &n_u as *const _ as *const _);
enc.set_bytes(7, 4, &act_u as *const _ as *const _);
enc.set_compute_pipeline_state(&kk.sgemm_simd_bias);
let tg_count = MTLSize {
width: n.div_ceil(8) as u64,
height: m.div_ceil(8) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 32,
height: 1,
depth: 1,
},
);
}
SgemmVariant::SimdPadded => {
enc.set_buffer(0, Some(arena), a_off as u64);
enc.set_buffer(1, Some(arena), b_off as u64);
enc.set_buffer(2, Some(arena), bias_off as u64);
enc.set_buffer(3, Some(arena), c_off as u64);
enc.set_bytes(4, 4, &m_u as *const _ as *const _);
enc.set_bytes(5, 4, &k_u as *const _ as *const _);
enc.set_bytes(6, 4, &n_u as *const _ as *const _);
enc.set_bytes(7, 4, &act_u as *const _ as *const _);
enc.set_compute_pipeline_state(&kk.sgemm_simd_padded_bias);
let tg_count = MTLSize {
width: n.div_ceil(8) as u64,
height: m.div_ceil(8) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 32,
height: 1,
depth: 1,
},
);
}
_ => {
metal_sgemm(enc, arena, a_off, b_off, c_off, m, k, n);
enc.set_compute_pipeline_state(&kk.bias_add);
enc.set_buffer(0, Some(arena), c_off as u64);
enc.set_buffer(1, Some(arena), bias_off as u64);
enc.set_bytes(2, 4, &m_u as *const _ as *const _);
enc.set_bytes(3, 4, &n_u as *const _ as *const _);
let grid = MTLSize {
width: n as u64,
height: m as u64,
depth: 1,
};
let tg = MTLSize {
width: 16u64.min(n as u64),
height: 16u64.min(m as u64),
depth: 1,
};
enc.dispatch_threads(grid, tg);
if !matches!(act, FusedAct::None) {
let pipeline = match act {
FusedAct::Gelu => &kk.gelu_inplace,
FusedAct::Silu => &kk.silu_inplace,
FusedAct::None => unreachable!(),
};
enc.set_compute_pipeline_state(pipeline);
enc.set_buffer(0, Some(arena), c_off as u64);
let len = (m * n) as u32;
enc.set_bytes(1, 4, &len as *const _ as *const _);
let tg_w = pipeline.thread_execution_width().min(len as u64);
enc.dispatch_threads(
MTLSize {
width: len as u64,
height: 1,
depth: 1,
},
MTLSize {
width: tg_w,
height: 1,
depth: 1,
},
);
}
}
}
}
pub fn metal_hgemm(
enc: &ComputeCommandEncoderRef,
arena: &Buffer,
a_off: usize,
b_off: usize,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
metal_hgemm_bufs(enc, arena, a_off, arena, b_off, arena, c_off, m, k, n);
}
pub fn metal_hgemm_bufs(
enc: &ComputeCommandEncoderRef,
a: &Buffer,
a_off: usize,
b: &Buffer,
b_off: usize,
c: &Buffer,
c_off: usize,
m: usize,
k: usize,
n: usize,
) {
let kk = kernels();
let m_u = m as u32;
let k_u = k as u32;
let n_u = n as u32;
enc.set_buffer(0, Some(a), a_off as u64);
enc.set_buffer(1, Some(b), b_off as u64);
enc.set_buffer(2, Some(c), c_off as u64);
enc.set_bytes(3, 4, &m_u as *const _ as *const _);
enc.set_bytes(4, 4, &k_u as *const _ as *const _);
enc.set_bytes(5, 4, &n_u as *const _ as *const _);
enc.set_compute_pipeline_state(&kk.hgemm_simd_4x4);
let tg_count = MTLSize {
width: n.div_ceil(32) as u64,
height: m.div_ceil(32) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 512,
height: 1,
depth: 1,
},
);
}
pub fn metal_hgemm_bias(
enc: &ComputeCommandEncoderRef,
arena: &Buffer,
a_off: usize,
b_off: usize,
bias_off: usize,
c_off: usize,
m: usize,
k: usize,
n: usize,
act: FusedAct,
) {
let kk = kernels();
let m_u = m as u32;
let k_u = k as u32;
let n_u = n as u32;
let act_u = act as u32;
enc.set_buffer(0, Some(arena), a_off as u64);
enc.set_buffer(1, Some(arena), b_off as u64);
enc.set_buffer(2, Some(arena), bias_off as u64);
enc.set_buffer(3, Some(arena), c_off as u64);
enc.set_bytes(4, 4, &m_u as *const _ as *const _);
enc.set_bytes(5, 4, &k_u as *const _ as *const _);
enc.set_bytes(6, 4, &n_u as *const _ as *const _);
enc.set_bytes(7, 4, &act_u as *const _ as *const _);
enc.set_compute_pipeline_state(&kk.hgemm_simd_4x4_bias);
let tg_count = MTLSize {
width: n.div_ceil(32) as u64,
height: m.div_ceil(32) as u64,
depth: 1,
};
enc.dispatch_thread_groups(
tg_count,
MTLSize {
width: 512,
height: 1,
depth: 1,
},
);
}
pub fn new_command_buffer() -> crate::mtl::CommandBuffer {
let dev = metal_device().expect("Metal device required");
dev.queue.new_command_buffer().to_owned()
}