use super::{Ctx, WeightArena};
use metal::{Buffer, ComputeCommandEncoderRef, ComputePipelineState, MTLSize};
use std::ffi::c_void;
use std::sync::OnceLock;
const GMSL: &str = include_str!("gemma4_msl.metal");
pub(super) struct Pipes {
pub(super) gelut: ComputePipelineState,
pub(super) mgu_gelut: ComputePipelineState,
pub(super) vnorm: ComputePipelineState,
pub(super) nas: ComputePipelineState,
pub(super) rqkn16: ComputePipelineState,
pub(super) cropekv16: ComputePipelineState,
pub(super) gqat16: ComputePipelineState,
pub(super) gqablk16: ComputePipelineState,
}
pub(crate) fn enabled() -> bool {
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| std::env::var("CMF_METAL_GEMMA4").as_deref() != Ok("0"))
}
pub(crate) fn w16_on() -> bool {
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| std::env::var("CMF_METAL_GEMMA4_W16").as_deref() != Ok("0"))
}
static PIPES: OnceLock<Result<Pipes, String>> = OnceLock::new();
pub(super) fn pipes(c: &Ctx) -> Option<&'static Pipes> {
let r = PIPES.get_or_init(|| {
let src = match std::env::var("CMF_GEMMA4_MSL") {
Ok(path) => std::fs::read_to_string(&path).map_err(|e| format!("{path}: {e}"))?,
Err(_) => GMSL.to_string(),
};
let opts = metal::CompileOptions::new();
opts.set_language_version(metal::MTLLanguageVersion::V3_0);
let lib = c
._device
.new_library_with_source(&src, &opts)
.map_err(|e| format!("gemma4 MSL compile: {e}"))?;
let pso = |name: &str| -> Result<ComputePipelineState, String> {
let f = lib
.get_function(name, None)
.map_err(|e| format!("kernel {name}: {e}"))?;
c._device
.new_compute_pipeline_state_with_function(&f)
.map_err(|e| format!("pipeline {name}: {e}"))
};
Ok(Pipes {
gelut: pso("gelu_tanh_mul_pre")?,
mgu_gelut: pso("q4tp_matvec_m_gu_gelut")?,
vnorm: pso("vnorm_rows")?,
nas: pso("norm_add_scale_rows")?,
rqkn16: pso("attn_rope_qkn_w16")?,
cropekv16: pso("chunk_rope_kv_w16")?,
gqat16: pso("gqa_attend_w16")?,
gqablk16: pso("gqa_attend_blk_w16")?,
})
});
match r {
Ok(p) => Some(p),
Err(e) => {
static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !SAID.swap(true, std::sync::atomic::Ordering::Relaxed) {
tracing::warn!(
"Gemma-4 Metal kernels unavailable ({e}) — the model stays off the graphs"
);
}
None
}
}
}
fn set_u32(enc: &ComputeCommandEncoderRef, slot: u64, v: u32) {
enc.set_bytes(slot, 4, &v as *const u32 as *const c_void);
}
fn set_f32(enc: &ComputeCommandEncoderRef, slot: u64, v: f32) {
enc.set_bytes(slot, 4, &v as *const f32 as *const c_void);
}
pub(super) fn enc_vnorm(
p: &Pipes,
enc: &ComputeCommandEncoderRef,
v: &Buffer,
nkv: usize,
hd: usize,
nb: usize,
eps: f32,
) {
enc.set_compute_pipeline_state(&p.vnorm);
enc.set_buffer(0, Some(v), 0);
set_u32(enc, 1, nkv as u32);
set_u32(enc, 2, hd as u32);
set_f32(enc, 3, eps);
enc.dispatch_thread_groups(
MTLSize::new(nkv as u64, nb as u64, 1),
MTLSize::new(32, 1, 1),
);
}
#[allow(clippy::too_many_arguments)]
pub(super) fn enc_norm_add_scale(
p: &Pipes,
enc: &ComputeCommandEncoderRef,
h: &Buffer,
d: &Buffer,
w: Option<&Buffer>,
n: usize,
gemma: bool,
eps: f32,
sc: f32,
nb: usize,
) {
enc.set_compute_pipeline_state(&p.nas);
enc.set_buffer(0, Some(h), 0);
enc.set_buffer(1, Some(d), 0);
enc.set_buffer(2, Some(w.unwrap_or(d)), 0);
set_u32(enc, 3, n as u32);
set_u32(enc, 4, gemma as u32);
set_f32(enc, 5, eps);
set_u32(enc, 6, w.is_some() as u32);
set_f32(enc, 7, sc);
enc.dispatch_thread_groups(MTLSize::new(nb as u64, 1, 1), MTLSize::new(256, 1, 1));
}
pub(super) fn enc_gelut(
p: &Pipes,
enc: &ComputeCommandEncoderRef,
g: &Buffer,
u: &Buffer,
act: &Buffer,
n: usize,
) {
enc.set_compute_pipeline_state(&p.gelut);
enc.set_buffer(0, Some(g), 0);
enc.set_buffer(1, Some(u), 0);
enc.set_buffer(2, Some(g), 0); enc.set_buffer(3, Some(act), 0);
set_u32(enc, 4, n as u32);
set_u32(enc, 5, 0);
enc.dispatch_threads(MTLSize::new(n as u64, 1, 1), MTLSize::new(256, 1, 1));
}
#[allow(clippy::too_many_arguments)]
pub(super) fn enc_mgu_gelut(
p: &Pipes,
enc: &ComputeCommandEncoderRef,
fbuf: &WeightArena,
abs_g: usize,
abs_u: usize,
xs: &Buffer,
act: &Buffer,
rows: usize,
gpr: usize,
) {
enc.set_compute_pipeline_state(&p.mgu_gelut);
fbuf.bind(enc, 0, abs_g);
enc.set_buffer(1, Some(xs), 0);
enc.set_buffer(2, Some(act), 0);
set_u32(enc, 3, gpr as u32);
set_u32(enc, 4, rows as u32);
fbuf.bind(enc, 5, abs_u);
let sgs = 4u64;
enc.dispatch_thread_groups(
MTLSize::new((rows as u64).div_ceil(sgs * 4), 1, 1),
MTLSize::new(sgs * 32, 1, 1),
);
}