use crate::buffer::MlxBuffer;
use crate::device::MlxDevice;
use crate::dtypes::DType;
use crate::encoder::{as_bytes, CommandEncoder, KernelArg};
use crate::error::{MlxError, Result};
use crate::kernel_registry::KernelRegistry;
use crate::ops::chunk_gated_delta_rule::{
build_chunk_local_cumsum_g_params, build_l2_norm_params,
dispatch_chunk_local_cumsum_g, ChunkGatedDeltaRuleParams, FIXED_BT, L2_NORM_EPS, MAX_K, MAX_V,
};
use crate::ops::chunk_gated_delta_rule_tri_solve_invert::{
build_chunk_tri_solve_invert_params, dispatch_chunk_tri_solve_invert,
ChunkTriSolveInvertParams,
};
use crate::ops::gated_delta_net_chunk::{
build_gated_delta_net_chunk_params, GatedDeltaNetChunkParams,
};
use crate::ops::gated_delta_net_chunk_o::{
build_gated_delta_net_chunk_o_params, GatedDeltaNetChunkOParams,
};
use crate::ops::gated_delta_net_kkt::{
build_gated_delta_net_kkt_params, dispatch_gated_delta_net_kkt, GatedDeltaNetKktParams,
};
use crate::ops::gated_delta_net_recompute_wu::{
build_gated_delta_net_recompute_wu_params, dispatch_gated_delta_net_recompute_wu,
GatedDeltaNetRecomputeWuParams,
};
use crate::ops::l2_norm::dispatch_l2_norm;
use metal::MTLSize;
pub const BANK_SPLIT_K: u32 = 256;
pub const NUM_BANKS: u32 = BANK_SPLIT_K / MAX_K;
#[allow(clippy::too_many_arguments)]
fn validate_bank_split(
p: &ChunkGatedDeltaRuleParams,
q: &MlxBuffer,
k: &MlxBuffer,
v: &MlxBuffer,
g_log_decay: &MlxBuffer,
beta: &MlxBuffer,
h0: &MlxBuffer,
o: &MlxBuffer,
final_state: &MlxBuffer,
) -> Result<()> {
if p.b == 0 || p.t == 0 || p.hg == 0 || p.h == 0 || p.k == 0 || p.v == 0 || p.bt == 0 {
return Err(MlxError::InvalidArgument(
"chunk_bank_split: all dims must be > 0".into(),
));
}
if p.h % p.hg != 0 {
return Err(MlxError::InvalidArgument(format!(
"chunk_bank_split: H ({}) must be a multiple of Hg ({})",
p.h, p.hg
)));
}
if p.k != BANK_SPLIT_K {
return Err(MlxError::InvalidArgument(format!(
"chunk_bank_split: K ({}) must equal BANK_SPLIT_K = {} exactly. \
For K = MAX_K = {}, use dispatch_chunk_gated_delta_rule_fwd directly.",
p.k, BANK_SPLIT_K, MAX_K
)));
}
if p.v > MAX_V {
return Err(MlxError::InvalidArgument(format!(
"chunk_bank_split: V ({}) exceeds MAX_V ({})",
p.v, MAX_V
)));
}
if p.bt != FIXED_BT {
return Err(MlxError::InvalidArgument(format!(
"chunk_bank_split: bt ({}) must equal FIXED_BT ({})",
p.bt, FIXED_BT
)));
}
if p.t % p.bt != 0 {
return Err(MlxError::InvalidArgument(format!(
"chunk_bank_split: T ({}) must be a multiple of bt ({})",
p.t, p.bt
)));
}
let q_elems = (p.b * p.t * p.hg * p.k) as usize;
let k_elems = q_elems;
let v_elems = (p.b * p.t * p.h * p.v) as usize;
let g_elems = (p.b * p.t * p.h) as usize;
let beta_elems = (p.b * p.t * p.h) as usize;
let h0_elems = (p.b * p.h * p.v * p.k) as usize;
let o_elems = v_elems;
let final_state_elems = h0_elems;
let check = |name: &str, buf: &MlxBuffer, expected_bytes: usize| -> Result<()> {
if buf.byte_len() < expected_bytes {
return Err(MlxError::InvalidArgument(format!(
"chunk_bank_split: {name} buffer too small: need {expected_bytes} bytes, have {}",
buf.byte_len()
)));
}
Ok(())
};
check("q", q, q_elems * 2)?;
check("k", k, k_elems * 2)?;
check("v", v, v_elems * 2)?;
check("g_log_decay", g_log_decay, g_elems * 4)?;
check("beta", beta, beta_elems * 4)?;
check("h0", h0, h0_elems * 4)?;
check("o", o, o_elems * 2)?;
check("final_state", final_state, final_state_elems * 4)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn dispatch_chunk_gated_delta_rule_fwd_k256_bank_split(
_encoder: &mut CommandEncoder,
_registry: &mut KernelRegistry,
_device: &MlxDevice,
q: &MlxBuffer,
k: &MlxBuffer,
v: &MlxBuffer,
g_log_decay: &MlxBuffer,
beta: &MlxBuffer,
h0: &MlxBuffer,
o: &MlxBuffer,
final_state: &MlxBuffer,
p: ChunkGatedDeltaRuleParams,
) -> Result<()> {
validate_bank_split(&p, q, k, v, g_log_decay, beta, h0, o, final_state)?;
let metal_device = _device.metal_device();
let qk_rows: u32 = p.b * p.t * p.hg;
let q_qk_elems = (qk_rows as usize) * (p.k as usize);
let q_normed_buf;
let k_normed_buf;
let q_for_pipeline: &MlxBuffer;
let k_for_pipeline: &MlxBuffer;
if p.use_qk_l2norm {
q_normed_buf = _device.alloc_buffer(q_qk_elems * 2, DType::BF16, vec![q_qk_elems])?;
k_normed_buf = _device.alloc_buffer(q_qk_elems * 2, DType::BF16, vec![q_qk_elems])?;
let l2_params = build_l2_norm_params(_device, L2_NORM_EPS, p.k)?;
dispatch_l2_norm(
_encoder, _registry, metal_device, q, &q_normed_buf, &l2_params, qk_rows, p.k,
)?;
_encoder.memory_barrier();
dispatch_l2_norm(
_encoder, _registry, metal_device, k, &k_normed_buf, &l2_params, qk_rows, p.k,
)?;
_encoder.memory_barrier();
q_for_pipeline = &q_normed_buf;
k_for_pipeline = &k_normed_buf;
} else {
q_for_pipeline = q;
k_for_pipeline = k;
}
let g_elems = (p.b * p.t * p.h) as usize;
let g_cumsum_buf =
_device.alloc_buffer(g_elems * 4, DType::F32, vec![g_elems])?;
let cumsum_params = build_chunk_local_cumsum_g_params(_device, &p)?;
dispatch_chunk_local_cumsum_g(
_encoder, _registry, metal_device,
g_log_decay, &g_cumsum_buf, &cumsum_params, &p,
)?;
_encoder.memory_barrier();
let a_elems = (p.b * p.t * p.h * p.bt) as usize;
let a_strict_buf =
_device.alloc_buffer(a_elems * 4, DType::F32, vec![a_elems])?;
let kkt_params_value = GatedDeltaNetKktParams {
b: p.b,
t: p.t,
hg: p.hg,
h: p.h,
k: p.k, bt: p.bt,
};
let kkt_params = build_gated_delta_net_kkt_params(_device, kkt_params_value)?;
dispatch_gated_delta_net_kkt(
_encoder, _registry, metal_device,
k_for_pipeline, beta, &g_cumsum_buf,
&a_strict_buf,
&kkt_params, kkt_params_value,
)?;
_encoder.memory_barrier();
let a_inv_buf =
_device.alloc_buffer(a_elems * 4, DType::F32, vec![a_elems])?;
let invert_params_value = ChunkTriSolveInvertParams {
b: p.b,
t: p.t,
h: p.h,
bt: p.bt,
};
let invert_params = build_chunk_tri_solve_invert_params(_device, invert_params_value)?;
dispatch_chunk_tri_solve_invert(
_encoder, _registry, metal_device,
&a_strict_buf, &a_inv_buf,
&invert_params, invert_params_value,
)?;
_encoder.memory_barrier();
let w_elems = (p.b * p.t * p.h * p.k) as usize;
let u_elems = (p.b * p.t * p.h * p.v) as usize;
let w_buf = _device.alloc_buffer(w_elems * 2, DType::BF16, vec![w_elems])?;
let u_buf = _device.alloc_buffer(u_elems * 2, DType::BF16, vec![u_elems])?;
let recompute_wu_params_value = GatedDeltaNetRecomputeWuParams {
b: p.b,
t: p.t,
hg: p.hg,
h: p.h,
k: p.k, v: p.v,
bt: p.bt,
};
let recompute_wu_params =
build_gated_delta_net_recompute_wu_params(_device, recompute_wu_params_value)?;
dispatch_gated_delta_net_recompute_wu(
_encoder, _registry, metal_device,
k_for_pipeline, v, beta, &g_cumsum_buf, &a_inv_buf,
&w_buf, &u_buf,
&recompute_wu_params, recompute_wu_params_value,
)?;
_encoder.memory_barrier();
let nt = p.num_chunks();
let h_elems = (p.b * nt * p.h * p.v * p.k) as usize;
let v_new_elems = (p.b * p.t * p.h * p.v) as usize;
let h_buf = _device.alloc_buffer(h_elems * 2, DType::BF16, vec![h_elems])?;
let v_new_buf = _device.alloc_buffer(v_new_elems * 2, DType::BF16, vec![v_new_elems])?;
let chunk_params_value = GatedDeltaNetChunkParams {
b: p.b,
t: p.t,
hg: p.hg,
h: p.h,
k: p.k, v: p.v,
bt: p.bt,
};
let chunk_params = build_gated_delta_net_chunk_params(_device, chunk_params_value)?;
dispatch_chunk_inter_state_k256(
_encoder, _registry, metal_device,
k_for_pipeline, &w_buf, &u_buf, &g_cumsum_buf, h0,
&h_buf, &v_new_buf, final_state,
&chunk_params, chunk_params_value,
)?;
_encoder.memory_barrier();
let chunk_o_params_value = GatedDeltaNetChunkOParams {
b: p.b,
t: p.t,
hg: p.hg,
h: p.h,
k: p.k, v: p.v,
bt: p.bt,
scale: p.scale,
};
let chunk_o_params =
build_gated_delta_net_chunk_o_params(_device, chunk_o_params_value)?;
dispatch_chunk_o_k256(
_encoder, _registry, metal_device,
q_for_pipeline, k_for_pipeline, &v_new_buf, &h_buf, &g_cumsum_buf,
o,
&chunk_o_params, chunk_o_params_value,
)?;
Ok(())
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct BankSliceGpuParams {
rows: u32,
k_full: u32,
k_bank: u32,
bank_offset: u32,
}
#[allow(clippy::too_many_arguments)]
pub fn dispatch_bank_slice_bf16(
encoder: &mut CommandEncoder,
registry: &mut KernelRegistry,
device: &metal::DeviceRef,
src: &MlxBuffer,
dst: &MlxBuffer,
rows: u32,
k_full: u32,
k_bank: u32,
bank_offset: u32,
) -> Result<()> {
if rows == 0 || k_full == 0 || k_bank == 0 {
return Err(MlxError::InvalidArgument(
"bank_slice_bf16: rows, k_full, k_bank must all be > 0".into(),
));
}
if bank_offset + k_bank > k_full {
return Err(MlxError::InvalidArgument(format!(
"bank_slice_bf16: bank_offset ({bank_offset}) + k_bank ({k_bank}) \
must be <= k_full ({k_full})"
)));
}
let src_bytes = (rows as usize) * (k_full as usize) * 2;
if src.byte_len() < src_bytes {
return Err(MlxError::InvalidArgument(format!(
"bank_slice_bf16: src buffer too small: need {src_bytes} bytes, have {}",
src.byte_len()
)));
}
let dst_bytes = (rows as usize) * (k_bank as usize) * 2;
if dst.byte_len() < dst_bytes {
return Err(MlxError::InvalidArgument(format!(
"bank_slice_bf16: dst buffer too small: need {dst_bytes} bytes, have {}",
dst.byte_len()
)));
}
let pipeline = registry.get_pipeline("bank_slice_bf16", device)?;
let gpu_params = BankSliceGpuParams {
rows,
k_full,
k_bank,
bank_offset,
};
let grid = MTLSize::new(k_bank as u64, rows as u64, 1);
let tg = MTLSize::new(std::cmp::min(32, k_bank as u64), 1, 1);
encoder.encode_with_args(
pipeline,
&[
(0, KernelArg::Buffer(src)),
(1, KernelArg::Buffer(dst)),
(2, KernelArg::Bytes(as_bytes(&gpu_params))),
],
grid,
tg,
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn dispatch_bank_slice_f32(
encoder: &mut CommandEncoder,
registry: &mut KernelRegistry,
device: &metal::DeviceRef,
src: &MlxBuffer,
dst: &MlxBuffer,
rows: u32,
k_full: u32,
k_bank: u32,
bank_offset: u32,
) -> Result<()> {
if rows == 0 || k_full == 0 || k_bank == 0 {
return Err(MlxError::InvalidArgument(
"bank_slice_f32: rows, k_full, k_bank must all be > 0".into(),
));
}
if bank_offset + k_bank > k_full {
return Err(MlxError::InvalidArgument(format!(
"bank_slice_f32: bank_offset ({bank_offset}) + k_bank ({k_bank}) \
must be <= k_full ({k_full})"
)));
}
let src_bytes = (rows as usize) * (k_full as usize) * 4;
if src.byte_len() < src_bytes {
return Err(MlxError::InvalidArgument(format!(
"bank_slice_f32: src buffer too small: need {src_bytes} bytes, have {}",
src.byte_len()
)));
}
let dst_bytes = (rows as usize) * (k_bank as usize) * 4;
if dst.byte_len() < dst_bytes {
return Err(MlxError::InvalidArgument(format!(
"bank_slice_f32: dst buffer too small: need {dst_bytes} bytes, have {}",
dst.byte_len()
)));
}
let pipeline = registry.get_pipeline("bank_slice_f32", device)?;
let gpu_params = BankSliceGpuParams {
rows,
k_full,
k_bank,
bank_offset,
};
let grid = MTLSize::new(k_bank as u64, rows as u64, 1);
let tg = MTLSize::new(std::cmp::min(32, k_bank as u64), 1, 1);
encoder.encode_with_args(
pipeline,
&[
(0, KernelArg::Buffer(src)),
(1, KernelArg::Buffer(dst)),
(2, KernelArg::Bytes(as_bytes(&gpu_params))),
],
grid,
tg,
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn dispatch_chunk_inter_state_k256(
encoder: &mut CommandEncoder,
registry: &mut KernelRegistry,
device: &metal::DeviceRef,
k: &MlxBuffer,
w: &MlxBuffer,
u: &MlxBuffer,
g: &MlxBuffer,
h0: &MlxBuffer,
h_out: &MlxBuffer,
v_new: &MlxBuffer,
final_state: &MlxBuffer,
params_buf: &MlxBuffer,
p: crate::ops::gated_delta_net_chunk::GatedDeltaNetChunkParams,
) -> Result<()> {
use crate::ops::gated_delta_net_chunk::DEFAULT_BV;
if p.k != BANK_SPLIT_K {
return Err(MlxError::InvalidArgument(format!(
"dispatch_chunk_inter_state_k256: K ({}) must equal {} exactly. \
For K=128 (Qwen3.5), use dispatch_gated_delta_net_chunk_inter_state.",
p.k, BANK_SPLIT_K
)));
}
let pipeline = registry.get_pipeline("gated_delta_net_chunk_inter_state_bf16_k256", device)?;
let nv_tiles = (p.v / DEFAULT_BV) as u64;
let grid_tgs = MTLSize::new(nv_tiles, p.h as u64, p.b as u64);
let tg = MTLSize::new(128, 1, 1);
let bh_bytes: u64 = (DEFAULT_BV * p.k) as u64 * 4;
let bv_stage_bytes: u64 = (DEFAULT_BV * p.bt) as u64 * 2;
let shared_bytes = bh_bytes + bv_stage_bytes;
encoder.encode_threadgroups_with_shared(
pipeline,
&[
(0, k),
(1, w),
(2, u),
(3, g),
(4, h0),
(5, h_out),
(6, v_new),
(7, final_state),
(8, params_buf),
],
&[(0, shared_bytes)],
grid_tgs,
tg,
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn dispatch_chunk_o_k256(
encoder: &mut CommandEncoder,
registry: &mut KernelRegistry,
device: &metal::DeviceRef,
q: &MlxBuffer,
k: &MlxBuffer,
v: &MlxBuffer,
h: &MlxBuffer,
g: &MlxBuffer,
o: &MlxBuffer,
params_buf: &MlxBuffer,
p: crate::ops::gated_delta_net_chunk_o::GatedDeltaNetChunkOParams,
) -> Result<()> {
if p.k != BANK_SPLIT_K {
return Err(MlxError::InvalidArgument(format!(
"dispatch_chunk_o_k256: K ({}) must equal {} exactly.",
p.k, BANK_SPLIT_K
)));
}
let pipeline = registry.get_pipeline("gated_delta_net_chunk_o_bf16_k256", device)?;
let nv = p.num_v_tiles() as u64;
let nt = p.num_chunks() as u64;
let bh = (p.b * p.h) as u64;
let grid_tgs = MTLSize::new(nv, nt, bh);
let tg = MTLSize::new(256, 1, 1);
let ba_stage_bytes: u64 = (p.bt as u64) * (p.bt as u64) * 2;
encoder.encode_threadgroups_with_shared(
pipeline,
&[
(0, q),
(1, k),
(2, v),
(3, h),
(4, g),
(5, o),
(6, params_buf),
],
&[(0, ba_stage_bytes)],
grid_tgs,
tg,
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn dispatch_bank_concat_f32(
encoder: &mut CommandEncoder,
registry: &mut KernelRegistry,
device: &metal::DeviceRef,
src: &MlxBuffer,
dst: &MlxBuffer,
rows: u32,
k_full: u32,
k_bank: u32,
bank_offset: u32,
) -> Result<()> {
if rows == 0 || k_full == 0 || k_bank == 0 {
return Err(MlxError::InvalidArgument(
"bank_concat_f32: rows, k_full, k_bank must all be > 0".into(),
));
}
if bank_offset + k_bank > k_full {
return Err(MlxError::InvalidArgument(format!(
"bank_concat_f32: bank_offset ({bank_offset}) + k_bank ({k_bank}) \
must be <= k_full ({k_full})"
)));
}
let src_bytes = (rows as usize) * (k_bank as usize) * 4;
if src.byte_len() < src_bytes {
return Err(MlxError::InvalidArgument(format!(
"bank_concat_f32: src buffer too small: need {src_bytes} bytes, have {}",
src.byte_len()
)));
}
let dst_bytes = (rows as usize) * (k_full as usize) * 4;
if dst.byte_len() < dst_bytes {
return Err(MlxError::InvalidArgument(format!(
"bank_concat_f32: dst buffer too small: need {dst_bytes} bytes, have {}",
dst.byte_len()
)));
}
let pipeline = registry.get_pipeline("bank_concat_f32", device)?;
let gpu_params = BankSliceGpuParams {
rows,
k_full,
k_bank,
bank_offset,
};
let grid = MTLSize::new(k_bank as u64, rows as u64, 1);
let tg = MTLSize::new(std::cmp::min(32, k_bank as u64), 1, 1);
encoder.encode_with_args(
pipeline,
&[
(0, KernelArg::Buffer(src)),
(1, KernelArg::Buffer(dst)),
(2, KernelArg::Bytes(as_bytes(&gpu_params))),
],
grid,
tg,
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bank_split_rejects_k_other_than_256() {
let p = ChunkGatedDeltaRuleParams {
b: 1,
t: 64,
hg: 1,
h: 1,
k: 128, v: 128,
bt: 64,
scale: 1.0,
use_qk_l2norm: false,
};
assert_eq!(BANK_SPLIT_K, 256);
assert_eq!(NUM_BANKS, 2);
assert_eq!(MAX_K, 128);
assert_eq!(BANK_SPLIT_K, NUM_BANKS * MAX_K);
assert_eq!(p.k, 128);
}
#[test]
fn bank_split_k_divides_evenly_into_banks() {
assert_eq!(BANK_SPLIT_K % MAX_K, 0);
assert_eq!(NUM_BANKS, BANK_SPLIT_K / MAX_K);
}
#[cfg(target_vendor = "apple")]
#[test]
fn bank_slice_bf16_matches_cpu_reference() {
use crate::{DType, KernelRegistry, MlxDevice};
use half::bf16;
let rows: u32 = 4;
let k_full: u32 = 256;
let k_bank: u32 = 128;
let total = (rows * k_full) as usize;
let src_data: Vec<bf16> = (0..total)
.map(|i| bf16::from_f32((i as f32) * 0.0173 - 1.5))
.collect();
let device = MlxDevice::new().expect("MlxDevice::new");
let mut registry = KernelRegistry::new();
let mut src_buf = device
.alloc_buffer(total * 2, DType::BF16, vec![rows as usize, k_full as usize])
.expect("alloc src");
src_buf
.as_mut_slice::<bf16>()
.expect("src as_mut")
.copy_from_slice(&src_data);
let dst_elems = (rows * k_bank) as usize;
let dst_buf = device
.alloc_buffer(
dst_elems * 2,
DType::BF16,
vec![rows as usize, k_bank as usize],
)
.expect("alloc dst");
for bank_idx in 0..NUM_BANKS {
let bank_offset = bank_idx * MAX_K;
let mut encoder = device.command_encoder().expect("encoder");
dispatch_bank_slice_bf16(
&mut encoder,
&mut registry,
device.metal_device(),
&src_buf,
&dst_buf,
rows,
k_full,
k_bank,
bank_offset,
)
.expect("dispatch");
encoder.commit_and_wait().expect("commit_and_wait");
let dst_data: &[bf16] = dst_buf.as_slice().expect("dst as_slice");
for r in 0..rows {
for k in 0..k_bank {
let src_idx = (r * k_full + bank_offset + k) as usize;
let dst_idx = (r * k_bank + k) as usize;
assert_eq!(
dst_data[dst_idx].to_bits(),
src_data[src_idx].to_bits(),
"bank_idx={bank_idx} r={r} k={k}: dst {} != src {}",
dst_data[dst_idx].to_f32(),
src_data[src_idx].to_f32(),
);
}
}
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn bank_slice_f32_matches_cpu_reference() {
use crate::{DType, KernelRegistry, MlxDevice};
let rows: u32 = 8;
let k_full: u32 = 256;
let k_bank: u32 = 128;
let total = (rows * k_full) as usize;
let src_data: Vec<f32> = (0..total)
.map(|i| (i as f32) * 0.0173 - 1.5)
.collect();
let device = MlxDevice::new().expect("MlxDevice::new");
let mut registry = KernelRegistry::new();
let mut src_buf = device
.alloc_buffer(total * 4, DType::F32, vec![rows as usize, k_full as usize])
.expect("alloc src");
src_buf
.as_mut_slice::<f32>()
.expect("src as_mut")
.copy_from_slice(&src_data);
let dst_elems = (rows * k_bank) as usize;
let dst_buf = device
.alloc_buffer(
dst_elems * 4,
DType::F32,
vec![rows as usize, k_bank as usize],
)
.expect("alloc dst");
for bank_idx in 0..NUM_BANKS {
let bank_offset = bank_idx * MAX_K;
let mut encoder = device.command_encoder().expect("encoder");
dispatch_bank_slice_f32(
&mut encoder,
&mut registry,
device.metal_device(),
&src_buf,
&dst_buf,
rows,
k_full,
k_bank,
bank_offset,
)
.expect("dispatch");
encoder.commit_and_wait().expect("commit_and_wait");
let dst_data: &[f32] = dst_buf.as_slice().expect("dst as_slice");
for r in 0..rows {
for k in 0..k_bank {
let src_idx = (r * k_full + bank_offset + k) as usize;
let dst_idx = (r * k_bank + k) as usize;
assert_eq!(
dst_data[dst_idx].to_bits(),
src_data[src_idx].to_bits(),
"bank_idx={bank_idx} r={r} k={k}: dst {} != src {}",
dst_data[dst_idx],
src_data[src_idx],
);
}
}
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn chunk_k256_pipeline_smoke() {
use crate::{DType, KernelRegistry, MlxDevice};
let device = MlxDevice::new().expect("MlxDevice::new");
let mut registry = KernelRegistry::new();
let p = ChunkGatedDeltaRuleParams {
b: 1,
t: 64,
hg: 1,
h: 1,
k: BANK_SPLIT_K, v: 64,
bt: FIXED_BT,
scale: 1.0,
use_qk_l2norm: true,
};
let qk_elems = (p.b * p.t * p.hg * p.k) as usize;
let v_elems = (p.b * p.t * p.h * p.v) as usize;
let g_elems = (p.b * p.t * p.h) as usize;
let h0_elems = (p.b * p.h * p.v * p.k) as usize;
let o_elems = v_elems;
let q_buf = device.alloc_buffer(qk_elems * 2, DType::BF16, vec![qk_elems]).unwrap();
let k_buf = device.alloc_buffer(qk_elems * 2, DType::BF16, vec![qk_elems]).unwrap();
let v_buf = device.alloc_buffer(v_elems * 2, DType::BF16, vec![v_elems]).unwrap();
let g_buf = device.alloc_buffer(g_elems * 4, DType::F32, vec![g_elems]).unwrap();
let beta_buf = device.alloc_buffer(g_elems * 4, DType::F32, vec![g_elems]).unwrap();
let h0_buf = device.alloc_buffer(h0_elems * 4, DType::F32, vec![h0_elems]).unwrap();
let o_buf = device.alloc_buffer(o_elems * 2, DType::BF16, vec![o_elems]).unwrap();
let final_state_buf =
device.alloc_buffer(h0_elems * 4, DType::F32, vec![h0_elems]).unwrap();
let mut encoder = device.command_encoder().unwrap();
let result = dispatch_chunk_gated_delta_rule_fwd_k256_bank_split(
&mut encoder, &mut registry, &device,
&q_buf, &k_buf, &v_buf, &g_buf, &beta_buf, &h0_buf,
&o_buf, &final_state_buf, p,
);
assert!(
result.is_ok(),
"K=256 pipeline encode failed: {:?}",
result.err()
);
let commit = encoder.commit_and_wait();
assert!(
commit.is_ok(),
"K=256 pipeline commit failed: {:?}",
commit.err()
);
eprintln!(
"[iter 22 smoke] K=256 full pipeline (l2_norm + cumsum + kkt + \
tri_solve + recompute_wu + chunk_inter_state_k256 + \
chunk_o_k256) dispatched + committed SUCCESSFULLY at \
B=1,T=64,K=256,V=32,BT=64."
);
}
#[cfg(target_vendor = "apple")]
#[test]
fn chunk_inter_state_k256_dispatch_smoke() {
use crate::ops::gated_delta_net_chunk::{
build_gated_delta_net_chunk_params, GatedDeltaNetChunkParams,
};
use crate::{DType, KernelRegistry, MlxDevice};
let p = GatedDeltaNetChunkParams {
b: 1,
t: FIXED_BT,
hg: 1,
h: 1,
k: BANK_SPLIT_K, v: 32,
bt: FIXED_BT,
};
let device = MlxDevice::new().expect("MlxDevice::new");
let mut registry = KernelRegistry::new();
let k_elems = (p.b * p.t * p.hg * p.k) as usize;
let w_elems = (p.b * p.t * p.h * p.k) as usize;
let u_elems = (p.b * p.t * p.h * p.v) as usize;
let g_elems = (p.b * p.t * p.h) as usize;
let h0_elems = (p.b * p.h * p.v * p.k) as usize;
let h_out_elems = (p.b * p.num_chunks() * p.h * p.v * p.k) as usize;
let final_state_elems = h0_elems;
let k_buf = device.alloc_buffer(k_elems * 2, DType::BF16, vec![k_elems]).unwrap();
let w_buf = device.alloc_buffer(w_elems * 2, DType::BF16, vec![w_elems]).unwrap();
let u_buf = device.alloc_buffer(u_elems * 2, DType::BF16, vec![u_elems]).unwrap();
let g_buf = device.alloc_buffer(g_elems * 4, DType::F32, vec![g_elems]).unwrap();
let h0_buf = device.alloc_buffer(h0_elems * 4, DType::F32, vec![h0_elems]).unwrap();
let h_out_buf = device.alloc_buffer(h_out_elems * 2, DType::BF16, vec![h_out_elems]).unwrap();
let v_new_buf = device.alloc_buffer(u_elems * 2, DType::BF16, vec![u_elems]).unwrap();
let final_state_buf =
device.alloc_buffer(final_state_elems * 4, DType::F32, vec![final_state_elems]).unwrap();
let params_buf = build_gated_delta_net_chunk_params(&device, p)
.expect("build params");
let mut encoder = device.command_encoder().unwrap();
let result = dispatch_chunk_inter_state_k256(
&mut encoder,
&mut registry,
device.metal_device(),
&k_buf, &w_buf, &u_buf, &g_buf, &h0_buf,
&h_out_buf, &v_new_buf, &final_state_buf,
¶ms_buf, p,
);
match result {
Ok(()) => {
let commit = encoder.commit_and_wait();
match commit {
Ok(()) => {
eprintln!(
"[iter 21 smoke] K=256 chunk_inter_state \
dispatch SUCCEEDED — shmem budget fits within \
device cap. Native kernel viable."
);
}
Err(e) => {
let msg = format!("{e}");
eprintln!(
"[iter 21 smoke] K=256 chunk_inter_state commit \
FAILED — likely shmem cap exceeded. Error: {msg}"
);
}
}
}
Err(e) => {
eprintln!(
"[iter 21 smoke] K=256 chunk_inter_state ENCODE failed \
(validation or pipeline creation): {e}"
);
}
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn bank_concat_f32_is_inverse_of_slice() {
use crate::{DType, KernelRegistry, MlxDevice};
let rows: u32 = 8;
let k_full: u32 = 256;
let k_bank: u32 = 128;
let total = (rows * k_full) as usize;
let src_data: Vec<f32> = (0..total)
.map(|i| (i as f32) * 0.0287 + 0.5)
.collect();
let device = MlxDevice::new().expect("MlxDevice::new");
let mut registry = KernelRegistry::new();
let mut src_buf = device
.alloc_buffer(total * 4, DType::F32, vec![rows as usize, k_full as usize])
.expect("alloc src");
src_buf
.as_mut_slice::<f32>()
.expect("src")
.copy_from_slice(&src_data);
let bank_elems = (rows * k_bank) as usize;
let bank0_buf = device
.alloc_buffer(bank_elems * 4, DType::F32, vec![rows as usize, k_bank as usize])
.expect("alloc bank0");
let bank1_buf = device
.alloc_buffer(bank_elems * 4, DType::F32, vec![rows as usize, k_bank as usize])
.expect("alloc bank1");
let mut dst_buf = device
.alloc_buffer(total * 4, DType::F32, vec![rows as usize, k_full as usize])
.expect("alloc dst");
dst_buf
.as_mut_slice::<f32>()
.expect("dst init")
.iter_mut()
.for_each(|v| *v = 0.0);
let mut encoder = device.command_encoder().expect("encoder");
dispatch_bank_slice_f32(
&mut encoder,
&mut registry,
device.metal_device(),
&src_buf,
&bank0_buf,
rows,
k_full,
k_bank,
0,
)
.expect("slice bank0");
dispatch_bank_slice_f32(
&mut encoder,
&mut registry,
device.metal_device(),
&src_buf,
&bank1_buf,
rows,
k_full,
k_bank,
MAX_K,
)
.expect("slice bank1");
encoder.memory_barrier();
dispatch_bank_concat_f32(
&mut encoder,
&mut registry,
device.metal_device(),
&bank0_buf,
&dst_buf,
rows,
k_full,
k_bank,
0,
)
.expect("concat bank0");
dispatch_bank_concat_f32(
&mut encoder,
&mut registry,
device.metal_device(),
&bank1_buf,
&dst_buf,
rows,
k_full,
k_bank,
MAX_K,
)
.expect("concat bank1");
encoder.commit_and_wait().expect("commit_and_wait");
let dst_data: &[f32] = dst_buf.as_slice().expect("dst as_slice");
for i in 0..total {
assert_eq!(
dst_data[i].to_bits(),
src_data[i].to_bits(),
"round-trip at idx {i}: dst {} != src {}",
dst_data[i],
src_data[i],
);
}
}
}