use anyhow::{Context, Result};
use mlx_native::metal;
use mlx_native::ops::encode_helpers::{as_bytes, encode_with_args, KernelArg};
use mlx_native::{DType, KernelRegistry, MlxBuffer, MlxDevice};
use super::reader::GlpMode;
const PROJECT_SHADER: &str = include_str!("shaders/glp_project.metal");
const PROJECT_MHC_SHADER: &str = include_str!("shaders/glp_project_mhc.metal");
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct GlpProjectMhcParams {
rows: u32,
hc: u32,
hidden: u32,
per_stream_directions: u32,
alpha: f32,
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct GlpProjectParams {
m: u32,
n: u32,
alpha: f32,
d_norm_sq: f32,
}
pub fn apply_layer_gpu(
hidden: &mut MlxBuffer,
direction: &MlxBuffer,
mode: GlpMode,
alpha: f32,
device: &MlxDevice,
registry: &mut KernelRegistry,
seq_len: u32,
h: u32,
) -> Result<()> {
match mode {
GlpMode::Add => apply_additive(hidden, direction, alpha, device, registry, seq_len, h),
GlpMode::Project => {
apply_projection(hidden, direction, alpha, device, registry, seq_len, h)
}
}
}
fn apply_additive(
hidden: &mut MlxBuffer,
direction: &MlxBuffer,
alpha: f32,
device: &MlxDevice,
registry: &mut KernelRegistry,
seq_len: u32,
h: u32,
) -> Result<()> {
let scaled: Vec<f32> = direction
.as_slice::<f32>()
.context("GLP direction must be CPU-readable for additive scaling")?
.iter()
.map(|x| x * alpha)
.collect();
let scaled_buf = {
let byte_len = std::mem::size_of_val(scaled.as_slice());
let raw = device.metal_device().new_buffer_with_data(
scaled.as_ptr().cast(),
byte_len as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![scaled.len()])
};
let mut enc = device
.command_encoder()
.context("GLP additive encoder")?;
mlx_native::ops::add_bias_row_2d::register(registry);
mlx_native::ops::add_bias_row_2d::dispatch_add_bias_row_2d_f32(
&mut enc,
registry,
device.metal_device(),
hidden,
&scaled_buf,
hidden,
seq_len,
h,
)
.context("GLP additive dispatch")?;
enc.commit_and_wait().context("GLP additive commit")?;
Ok(())
}
fn apply_projection(
hidden: &mut MlxBuffer,
direction: &MlxBuffer,
alpha: f32,
device: &MlxDevice,
registry: &mut KernelRegistry,
seq_len: u32,
h: u32,
) -> Result<()> {
let d_norm_sq: f32 = direction
.as_slice::<f32>()
.context("GLP direction must be CPU-readable for norm computation")?
.iter()
.map(|x| x * x)
.sum();
if d_norm_sq <= f32::EPSILON {
anyhow::bail!("GLP project direction has near-zero norm");
}
registry.register_source("glp_project_f32", PROJECT_SHADER);
let params = GlpProjectParams {
m: seq_len,
n: h,
alpha,
d_norm_sq,
};
let pipeline = registry
.get_pipeline("glp_project_f32", device.metal_device())
.context("GLP projection pipeline")?;
let grid = metal::MTLSize::new((seq_len as u64) * 256, 1, 1);
let tg = metal::MTLSize::new(256, 1, 1);
let mut enc = device
.command_encoder()
.context("GLP projection encoder")?;
encode_with_args(
&mut enc,
pipeline,
&[
(0, KernelArg::Bytes(as_bytes(¶ms))),
(1, KernelArg::Buffer(direction)),
(2, KernelArg::Buffer(hidden)),
],
grid,
tg,
);
enc.commit_and_wait().context("GLP projection commit")?;
Ok(())
}
pub fn apply_layer_gpu_mhc(
state: &mut MlxBuffer,
direction: &MlxBuffer,
mode: GlpMode,
alpha: f32,
device: &MlxDevice,
registry: &mut KernelRegistry,
rows: u32,
hc: u32,
hidden: u32,
) -> Result<()> {
if mode != GlpMode::Project {
anyhow::bail!("GLP mHC apply supports project mode only (add is untested on HC state)");
}
let dir_width = direction.shape().first().copied().unwrap_or(0) as u32;
let per_stream = (dir_width == hc * hidden) as u32;
anyhow::ensure!(
dir_width == hidden || dir_width == hc * hidden,
"GLP mHC direction width {dir_width} must equal hidden {hidden} or hc*hidden {}",
hc * hidden
);
let dir_values: Vec<f32> = direction
.as_slice::<f32>()
.context("GLP direction must be CPU-readable for norm computation")?
.to_vec();
let mut norms = Vec::with_capacity(hc as usize);
for stream in 0..hc as usize {
let slice = if per_stream == 1 {
&dir_values[stream * hidden as usize..(stream + 1) * hidden as usize]
} else {
&dir_values[..hidden as usize]
};
let norm_sq: f64 = slice.iter().map(|x| (*x as f64) * (*x as f64)).sum();
if !norm_sq.is_finite() || norm_sq <= 0.0 {
anyhow::bail!(
"GLP mHC direction stream {stream} has an invalid norm ({norm_sq})"
);
}
norms.push(norm_sq as f32);
}
let norms_buf = {
let byte_len = std::mem::size_of_val(norms.as_slice());
let raw = device.metal_device().new_buffer_with_data(
norms.as_ptr().cast(),
byte_len as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![norms.len()])
};
registry.register_source("glp_project_mhc_f32", PROJECT_MHC_SHADER);
let params = GlpProjectMhcParams {
rows,
hc,
hidden,
per_stream_directions: per_stream,
alpha,
};
let pipeline = registry
.get_pipeline("glp_project_mhc_f32", device.metal_device())
.context("GLP mHC projection pipeline")?;
let grid = metal::MTLSize::new((rows as u64) * (hc as u64) * 256, 1, 1);
let tg = metal::MTLSize::new(256, 1, 1);
let mut enc = device
.command_encoder()
.context("GLP mHC projection encoder")?;
encode_with_args(
&mut enc,
pipeline,
&[
(0, KernelArg::Bytes(as_bytes(¶ms))),
(1, KernelArg::Buffer(direction)),
(2, KernelArg::Buffer(state)),
(3, KernelArg::Buffer(&norms_buf)),
],
grid,
tg,
);
enc.commit_and_wait().context("GLP mHC projection commit")?;
Ok(())
}
pub fn apply_layer_gpu_mhc_in_session(
session: &mut mlx_native::graph::GraphSession<'_>,
registry: &mut KernelRegistry,
state: &MlxBuffer,
direction: &MlxBuffer,
alpha: f32,
rows: u32,
hc: u32,
hidden: u32,
) -> Result<()> {
let dir_width = direction.shape().first().copied().unwrap_or(0) as u32;
let per_stream = (dir_width == hc * hidden) as u32;
anyhow::ensure!(
dir_width == hidden || dir_width == hc * hidden,
"GLP mHC direction width {dir_width} must equal hidden {hidden} or hc*hidden {}",
hc * hidden
);
let dir_values: Vec<f32> = direction
.as_slice::<f32>()
.context("GLP direction must be CPU-readable for norm computation")?
.to_vec();
let mut norms = Vec::with_capacity(hc as usize);
for stream in 0..hc as usize {
let slice = if per_stream == 1 {
&dir_values[stream * hidden as usize..(stream + 1) * hidden as usize]
} else {
&dir_values[..hidden as usize]
};
let norm_sq: f64 = slice.iter().map(|x| (*x as f64) * (*x as f64)).sum();
if !norm_sq.is_finite() || norm_sq <= 0.0 {
anyhow::bail!(
"GLP mHC direction stream {stream} has an invalid norm ({norm_sq})"
);
}
norms.push(norm_sq as f32);
}
let device = session.device().clone();
let norms_buf = {
let byte_len = std::mem::size_of_val(norms.as_slice());
let raw = device.metal_device().new_buffer_with_data(
norms.as_ptr().cast(),
byte_len as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![norms.len()])
};
registry.register_source("glp_project_mhc_f32", PROJECT_MHC_SHADER);
let params = GlpProjectMhcParams {
rows,
hc,
hidden,
per_stream_directions: per_stream,
alpha,
};
let pipeline = registry
.get_pipeline("glp_project_mhc_f32", device.metal_device())
.context("GLP mHC projection pipeline")?;
let grid = metal::MTLSize::new((rows as u64) * (hc as u64) * 256, 1, 1);
let tg = metal::MTLSize::new(256, 1, 1);
encode_with_args(
session.encoder_mut(),
pipeline,
&[
(0, KernelArg::Bytes(as_bytes(¶ms))),
(1, KernelArg::Buffer(direction)),
(2, KernelArg::Buffer(state)),
(3, KernelArg::Buffer(&norms_buf)),
],
grid,
tg,
);
Ok(())
}
pub fn apply_layer_gpu_in_session(
session: &mut mlx_native::graph::GraphSession<'_>,
registry: &mut KernelRegistry,
state: &MlxBuffer,
direction: &MlxBuffer,
mode: GlpMode,
alpha: f32,
rows: u32,
hidden: u32,
) -> Result<()> {
let dir_width = direction.shape().first().copied().unwrap_or(0) as u32;
anyhow::ensure!(
dir_width == hidden,
"GLP dense direction width {dir_width} must equal hidden {hidden}"
);
match mode {
GlpMode::Project => {
let d_norm_sq: f32 = direction
.as_slice::<f32>()
.context("GLP direction must be CPU-readable for norm computation")?
.iter()
.map(|x| x * x)
.sum();
if d_norm_sq <= f32::EPSILON {
anyhow::bail!("GLP project direction has near-zero norm");
}
registry.register_source("glp_project_f32", PROJECT_SHADER);
let params = GlpProjectParams {
m: rows,
n: hidden,
alpha,
d_norm_sq,
};
let pipeline = registry
.get_pipeline("glp_project_f32", session.device().metal_device())
.context("GLP projection pipeline")?;
let grid = metal::MTLSize::new((rows as u64) * 256, 1, 1);
let tg = metal::MTLSize::new(256, 1, 1);
encode_with_args(
session.encoder_mut(),
pipeline,
&[
(0, KernelArg::Bytes(as_bytes(¶ms))),
(1, KernelArg::Buffer(direction)),
(2, KernelArg::Buffer(state)),
],
grid,
tg,
);
Ok(())
}
GlpMode::Add => {
let device = session.device().clone();
let scaled: Vec<f32> = direction
.as_slice::<f32>()
.context("GLP direction must be CPU-readable for additive scaling")?
.iter()
.map(|x| x * alpha)
.collect();
let scaled_buf = {
let byte_len = std::mem::size_of_val(scaled.as_slice());
let raw = device.metal_device().new_buffer_with_data(
scaled.as_ptr().cast(),
byte_len as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![scaled.len()])
};
mlx_native::ops::add_bias_row_2d::register(registry);
mlx_native::ops::add_bias_row_2d::dispatch_add_bias_row_2d_f32(
session.encoder_mut(),
registry,
device.metal_device(),
state,
&scaled_buf,
state,
rows,
hidden,
)
.context("GLP in-session additive dispatch")?;
Ok(())
}
}
}
#[cfg(test)]
mod tests {
}
#[cfg(test)]
mod kernel_hardware_tests {
use super::*;
use mlx_native::MlxDevice;
#[test]
fn mhc_kernel_subtracts_scale_dot_direction() {
let device = MlxDevice::new().expect("MlxDevice");
let mut registry = mlx_native::KernelRegistry::new();
let rows: u32 = 1;
let hc: u32 = 4;
let hidden: u32 = 4096;
let alpha: f32 = 1.0;
let dir_host: Vec<f32> = (0..hidden)
.map(|_| 1.0f32 / (hidden as f32).sqrt())
.collect();
let total = (rows * hc * hidden) as usize;
let mut state_host = vec![0.0f32; total];
state_host[0] = 1.0;
let mut st_buf = device
.alloc_buffer(state_host.len() * 4, DType::F32, vec![state_host.len()])
.expect("alloc test state");
st_buf
.as_logical_mut_slice::<f32>()
.expect("write test state")
.copy_from_slice(&state_host);
let dir_buf = {
let raw = device.metal_device().new_buffer_with_data(
dir_host.as_ptr().cast(),
(dir_host.len() * 4) as u64,
mlx_native::metal::MTLResourceOptions::StorageModeShared,
);
let n = dir_host.len();
MlxBuffer::from_raw(raw, DType::F32, vec![n])
};
apply_layer_gpu_mhc(
&mut st_buf,
&dir_buf,
GlpMode::Project,
alpha,
&device,
&mut registry,
rows,
hc,
hidden,
)
.unwrap();
let out = st_buf.as_slice::<f32>().unwrap();
let expected: f32 = 1.0 - (1.0 / (hidden as f32).sqrt().powi(2));
assert!(
(out[0] - expected).abs() < 1e-4,
"kernel wrong: out[0]={} expected≈{}",
out[0],
expected
);
}
#[test]
fn dense_kernel_subtracts_scale_dot_direction() {
let device = MlxDevice::new().expect("MlxDevice");
let mut registry = mlx_native::KernelRegistry::new();
let rows: u32 = 2;
let hidden: u32 = 4096;
let alpha: f32 = 1.0;
let dir_host: Vec<f32> = (0..hidden)
.map(|_| 1.0f32 / (hidden as f32).sqrt())
.collect();
let total = (rows * hidden) as usize;
let mut state_host = vec![0.0f32; total];
state_host[0] = 1.0;
let mut st_buf = device
.alloc_buffer(total * 4, DType::F32, vec![total])
.expect("alloc test state");
st_buf
.as_logical_mut_slice::<f32>()
.expect("write test state")
.copy_from_slice(&state_host);
let dir_buf = {
let raw = device.metal_device().new_buffer_with_data(
dir_host.as_ptr().cast(),
(dir_host.len() * 4) as u64,
mlx_native::metal::MTLResourceOptions::StorageModeShared,
);
let n = dir_host.len();
MlxBuffer::from_raw(raw, DType::F32, vec![n])
};
let executor = mlx_native::graph::GraphExecutor::new(device.clone());
let mut session = executor.begin().expect("begin session");
apply_layer_gpu_in_session(
&mut session,
&mut registry,
&st_buf,
&dir_buf,
GlpMode::Project,
alpha,
rows,
hidden,
)
.unwrap();
session.finish().expect("commit session");
let out = st_buf.as_slice::<f32>().unwrap();
let d0 = 1.0f32 / (hidden as f32).sqrt();
let expected: f32 = 1.0 - d0 * d0; assert!(
(out[0] - expected).abs() < 1e-4,
"dense kernel wrong: out[0]={} expected≈{} (untouched would be 1.0)",
out[0],
expected
);
assert!(
out[hidden as usize].abs() < 1e-6,
"row 1 changed despite zero dot: {}",
out[hidden as usize]
);
}
}
#[cfg(test)]
mod projection_reference_tests {
use super::*;
#[test]
fn qwen_path_projection_matches_cpu_reference_at_model_widths() {
let device = MlxDevice::new().expect("MlxDevice");
let mut registry = KernelRegistry::new();
for rows in [1u32, 3, 17] {
for width in [2048u32, 5120] {
let direction: Vec<f32> = (0..width)
.map(|i| ((i % 13) as f32 - 6.0) * 0.05)
.collect();
let state: Vec<f32> = (0..(rows as usize * width as usize))
.map(|i| (((i * 7 + 3) % 31) as f32 - 15.0) * 0.1)
.collect();
let dir_buf = {
let raw = device.metal_device().new_buffer_with_data(
direction.as_ptr().cast(),
std::mem::size_of_val(direction.as_slice()) as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![direction.len()])
};
let mut hidden = {
let raw = device.metal_device().new_buffer_with_data(
state.as_ptr().cast(),
std::mem::size_of_val(state.as_slice()) as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![state.len()])
};
apply_layer_gpu(
&mut hidden,
&dir_buf,
GlpMode::Project,
0.0,
&device,
&mut registry,
rows,
width,
)
.expect("zero-dose dispatch");
let out = hidden.as_slice::<f32>().unwrap();
for (a, b) in out.iter().zip(state.iter()) {
assert_eq!(a, b, "zero dose must be the exact identity");
}
let alpha = 1.7f32;
let mut hidden = {
let raw = device.metal_device().new_buffer_with_data(
state.as_ptr().cast(),
std::mem::size_of_val(state.as_slice()) as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![state.len()])
};
apply_layer_gpu(
&mut hidden,
&dir_buf,
GlpMode::Project,
alpha,
&device,
&mut registry,
rows,
width,
)
.expect("live-dose dispatch");
let gpu = hidden.as_slice::<f32>().unwrap().to_vec();
let mut cpu = state.clone();
for row in 0..rows as usize {
let slice =
&mut cpu[row * width as usize..(row + 1) * width as usize];
super::super::apply::apply_layer_project(slice, &direction, alpha)
.expect("cpu reference");
}
let mut max_rel: f32 = 0.0;
for (g, c) in gpu.iter().zip(cpu.iter()) {
let denom = c.abs().max(1e-3);
max_rel = max_rel.max((g - c).abs() / denom);
}
assert!(
max_rel < 1e-4,
"GPU projection must match the CPU reference \
(rows={rows}, width={width}, max_rel={max_rel})"
);
}
}
}
}
#[cfg(test)]
mod mhc_reference_tests {
use super::*;
#[test]
fn mhc_projection_matches_cpu_reference_across_rows_and_streams() {
let device = MlxDevice::new().expect("MlxDevice");
let mut registry = KernelRegistry::new();
let rows = 3u32;
let hc = 4u32;
let hidden = 1024u32;
let alpha = 1.3f32;
let mut direction: Vec<f32> = Vec::with_capacity((hc * hidden) as usize);
for stream in 0..hc {
let scale = (stream + 1) as f32 * 0.25; for col in 0..hidden {
let v = scale * (((col % 11) as f32) - 5.0) * 0.1;
direction.push(v);
}
}
let state: Vec<f32> = (0..(rows * hc * hidden))
.map(|i| (((i * 13 + 5) % 29) as f32 - 14.0) * 0.1)
.collect();
let dir_buf = {
let raw = device.metal_device().new_buffer_with_data(
direction.as_ptr().cast(),
std::mem::size_of_val(direction.as_slice()) as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![direction.len()])
};
let mut state_buf = {
let raw = device.metal_device().new_buffer_with_data(
state.as_ptr().cast(),
std::mem::size_of_val(state.as_slice()) as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![state.len()])
};
apply_layer_gpu_mhc(
&mut state_buf,
&dir_buf,
GlpMode::Project,
alpha,
&device,
&mut registry,
rows,
hc,
hidden,
)
.expect("mhc dispatch");
let gpu = state_buf.as_slice::<f32>().unwrap().to_vec();
let mut cpu = state.clone();
for row in 0..rows as usize {
for stream in 0..hc as usize {
let base = (row * hc as usize + stream) * hidden as usize;
let slice = &mut cpu[base..base + hidden as usize];
let d = &direction[stream * hidden as usize..(stream + 1) * hidden as usize];
super::super::apply::apply_layer_project(slice, d, alpha)
.expect("cpu reference");
}
}
let mut max_rel: f32 = 0.0;
for (g, c) in gpu.iter().zip(cpu.iter()) {
let denom = c.abs().max(1e-3);
max_rel = max_rel.max((g - c).abs() / denom);
}
assert!(
max_rel < 1e-4,
"mHC per-stream projection must match the CPU reference (max_rel={max_rel})"
);
let shared: Vec<f32> = direction[..hidden as usize].to_vec();
let shared_buf = {
let raw = device.metal_device().new_buffer_with_data(
shared.as_ptr().cast(),
std::mem::size_of_val(shared.as_slice()) as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![shared.len()])
};
let mut state_buf = {
let raw = device.metal_device().new_buffer_with_data(
state.as_ptr().cast(),
std::mem::size_of_val(state.as_slice()) as u64,
metal::MTLResourceOptions::StorageModeShared,
);
MlxBuffer::from_raw(raw, DType::F32, vec![state.len()])
};
apply_layer_gpu_mhc(
&mut state_buf,
&shared_buf,
GlpMode::Project,
alpha,
&device,
&mut registry,
rows,
hc,
hidden,
)
.expect("mhc shared dispatch");
let gpu = state_buf.as_slice::<f32>().unwrap().to_vec();
let mut cpu = state.clone();
for row in 0..rows as usize {
for stream in 0..hc as usize {
let base = (row * hc as usize + stream) * hidden as usize;
let slice = &mut cpu[base..base + hidden as usize];
super::super::apply::apply_layer_project(slice, &shared, alpha)
.expect("cpu reference");
}
}
let mut max_rel: f32 = 0.0;
for (g, c) in gpu.iter().zip(cpu.iter()) {
let denom = c.abs().max(1e-3);
max_rel = max_rel.max((g - c).abs() / denom);
}
assert!(
max_rel < 1e-4,
"mHC shared-direction projection must match the CPU reference (max_rel={max_rel})"
);
}
}