use crate::core::mlx_safetensors_loader::MlxAffineLinear;
use crate::quantize::imatrix::{intercept_qmatmul_with_hint, ImatrixHint};
use crate::serve::gpu::QuantWeightInfo;
use anyhow::Result;
use mlx_native::ops::dense_gemm::DenseGemmF16Params;
use mlx_native::{GgmlQuantizedMatmulParams, GraphSession, MlxBuffer, MlxDevice};
pub struct MlxAffineExtra {
pub scales: MlxBuffer,
pub biases: MlxBuffer,
pub bits: u32,
pub group_size: u32,
}
pub struct MlxQWeight {
pub buffer: MlxBuffer,
pub info: QuantWeightInfo,
pub affine: Option<MlxAffineExtra>,
pub f16_shadow: Option<MlxBuffer>,
pub decode_record_q6k_m1: std::sync::OnceLock<Option<mlx_native::DispatchRecord>>,
}
impl MlxQWeight {
pub fn matmul_params(&self, m: u32) -> Result<GgmlQuantizedMatmulParams> {
Ok(GgmlQuantizedMatmulParams {
m,
n: self.info.rows as u32,
k: self.info.cols as u32,
ggml_type: self.info.ggml_dtype,
})
}
pub fn from_mlx_affine_linear(device: &MlxDevice, linear: &MlxAffineLinear) -> Result<Self> {
if linear.bits != 4 {
anyhow::bail!(
"MlxQWeight::from_mlx_affine_linear: only bits=4 supported in AC#5 Iter B; got {}",
linear.bits
);
}
if linear.group_size != 32 {
anyhow::bail!(
"MlxQWeight::from_mlx_affine_linear: only group_size=32 supported in AC#5 Iter B; got {}",
linear.group_size
);
}
let n = linear.n;
let k = linear.k;
let pack_factor = (32 / linear.bits) as usize;
if k % pack_factor != 0 {
anyhow::bail!(
"MlxQWeight::from_mlx_affine_linear: K ({k}) must be divisible by pack_factor ({pack_factor})"
);
}
let k_packed = k / pack_factor;
let groups_per_row = k / linear.group_size;
let mut packed = vec![0u32; n * k_packed];
for row in 0..n {
for kp in 0..k_packed {
let mut word: u32 = 0;
for j in 0..pack_factor {
let code = linear.q_int[row * k + kp * pack_factor + j] as u32;
debug_assert!(code <= 0xF);
word |= (code & 0xF) << (j * 4);
}
packed[row * k_packed + kp] = word;
}
}
let mut weight_buf = device
.alloc_buffer(
n * k_packed * std::mem::size_of::<u32>(),
mlx_native::DType::U32,
vec![n, k_packed],
)
.map_err(|e| anyhow::anyhow!("affine weight alloc: {e}"))?;
weight_buf
.as_mut_slice::<u32>()
.map_err(|e| anyhow::anyhow!("affine weight slice: {e}"))?
.copy_from_slice(&packed);
let mut scales_buf = device
.alloc_buffer(
n * groups_per_row * std::mem::size_of::<f32>(),
mlx_native::DType::F32,
vec![n, groups_per_row],
)
.map_err(|e| anyhow::anyhow!("affine scales alloc: {e}"))?;
scales_buf
.as_mut_slice::<f32>()
.map_err(|e| anyhow::anyhow!("affine scales slice: {e}"))?
.copy_from_slice(&linear.scales);
let mut biases_buf = device
.alloc_buffer(
n * groups_per_row * std::mem::size_of::<f32>(),
mlx_native::DType::F32,
vec![n, groups_per_row],
)
.map_err(|e| anyhow::anyhow!("affine biases alloc: {e}"))?;
biases_buf
.as_mut_slice::<f32>()
.map_err(|e| anyhow::anyhow!("affine biases slice: {e}"))?
.copy_from_slice(&linear.biases);
Ok(Self {
buffer: weight_buf,
info: QuantWeightInfo {
ggml_dtype: mlx_native::GgmlType::F32, rows: n,
cols: k,
},
affine: Some(MlxAffineExtra {
scales: scales_buf,
biases: biases_buf,
bits: linear.bits,
group_size: linear.group_size as u32,
}),
f16_shadow: None,
decode_record_q6k_m1: std::sync::OnceLock::new(),
})
}
}
#[derive(Clone)]
pub struct MlxAffineMoeStack {
pub weight: MlxBuffer,
pub scales: MlxBuffer,
pub biases: MlxBuffer,
pub n: usize,
pub k: usize,
pub bits: u32,
pub group_size: u32,
pub num_experts: usize,
}
pub(crate) fn load_gguf_qweight(
gguf: &mlx_native::gguf::GgufFile,
name: &str,
device: &MlxDevice,
) -> Result<MlxQWeight> {
let full_name = if name.ends_with(".weight") {
name.to_string()
} else {
format!("{name}.weight")
};
let info = gguf
.tensor_info(&full_name)
.ok_or_else(|| anyhow::anyhow!("tensor '{}' not found in GGUF", full_name))?;
let buffer = gguf
.load_tensor(&full_name, device)
.map_err(|e| anyhow::anyhow!("load {}: {e}", full_name))?;
let rows = info.shape.first().copied().unwrap_or(1);
let cols = if info.shape.len() > 1 {
info.shape[1]
} else {
1
};
Ok(MlxQWeight {
buffer,
info: QuantWeightInfo {
ggml_dtype: info.ggml_type,
rows,
cols,
},
affine: None,
f16_shadow: None,
decode_record_q6k_m1: std::sync::OnceLock::new(),
})
}
pub(crate) fn populate_f16_shadow_if_enabled(
qweight: &mut MlxQWeight,
device: &MlxDevice,
registry: &mut mlx_native::KernelRegistry,
tensor_name: &str,
) -> Result<()> {
let enabled = std::env::var("HF2Q_F16_SHADOW")
.ok()
.map(|v| !matches!(v.as_str(), "0" | "false" | "off"))
.unwrap_or(true);
if !enabled {
return Ok(());
}
if qweight.affine.is_some() {
return Ok(());
}
use mlx_native::GgmlType;
match qweight.info.ggml_dtype {
GgmlType::Q4_0
| GgmlType::Q8_0
| GgmlType::Q5_1
| GgmlType::IQ4_NL
| GgmlType::Q4_K
| GgmlType::Q5_K
| GgmlType::Q6_K => {}
_ => return Ok(()),
}
let n_rows = qweight.info.rows as u32;
let n_cols = qweight.info.cols as u32;
let f16 = mlx_native::ops::dequant_to_f16::materialize_f16_shadow(
device,
registry,
&qweight.buffer,
n_rows,
n_cols,
qweight.info.ggml_dtype,
)
.map_err(|e| anyhow::anyhow!("F16 shadow materialize for '{}': {e}", tensor_name))?;
qweight.f16_shadow = Some(f16);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn dispatch_qmatmul(
session: &mut GraphSession<'_>,
registry: &mut mlx_native::KernelRegistry,
device: &MlxDevice,
input: &MlxBuffer,
weight: &MlxQWeight,
output: &MlxBuffer,
m: u32,
imatrix_hint: ImatrixHint<'_>,
) -> Result<()> {
intercept_qmatmul_with_hint(imatrix_hint, m as usize, weight.info.cols, || {
if let Err(e) = session.encoder_mut().commit_wait_and_rotate() {
eprintln!("[hf2q imatrix intercept] commit_wait_and_rotate failed: {e}");
return None;
}
match input.as_slice::<f32>() {
Ok(slice) => Some(slice.to_vec()),
Err(e) => {
eprintln!("[hf2q imatrix intercept] input.as_slice::<f32>() failed: {e}");
None
}
}
})
.map_err(|e| anyhow::anyhow!("imatrix intercept: {e}"))?;
{
let encoder = session.encoder_mut();
if encoder.is_capturing() {
let input_range = {
let start = input.contents_ptr() as usize;
(start, start + input.byte_len())
};
let output_range = {
let start = output.contents_ptr() as usize;
(start, start + output.byte_len())
};
encoder.set_pending_buffer_ranges(vec![input_range], vec![output_range]);
}
}
if let Some(extra) = weight.affine.as_ref() {
if extra.bits != 4 || extra.group_size != 32 {
return Err(anyhow::anyhow!(
"dispatch_qmatmul affine: only bits=4, group_size=32 supported in AC#5 Iter C; got bits={} gs={}",
extra.bits,
extra.group_size,
));
}
let n = weight.info.rows as u32;
let k = weight.info.cols as u32;
let mut meta = device
.alloc_buffer(16, mlx_native::DType::U32, vec![4])
.map_err(|e| anyhow::anyhow!("affine meta alloc: {e}"))?;
meta.as_mut_slice::<u32>()
.map_err(|e| anyhow::anyhow!("affine meta slice: {e}"))?
.copy_from_slice(&[m, n, k, extra.group_size]);
return mlx_native::ops::qmm_affine::dispatch_qmm_affine_t_packed_simd4_b4(
session.encoder_mut(),
registry,
device.metal_device(),
input,
&weight.buffer,
&extra.scales,
&extra.biases,
output,
&meta,
m,
n,
k,
extra.group_size,
extra.bits,
)
.map_err(|e| anyhow::anyhow!("qmm_affine_t_packed_simd4_b4 failed: {e}"));
}
if m > mlx_native::ops::quantized_matmul_ggml::MM_ROUTING_THRESHOLD {
if let Some(ref f16w) = weight.f16_shadow {
let n = weight.info.rows as u32;
let k = weight.info.cols as u32;
return mlx_native::ops::quantized_matmul_ggml::dispatch_mm_v2_f16(
session.encoder_mut(),
registry,
device,
f16w,
input,
output,
m,
n,
k,
)
.map_err(|e| anyhow::anyhow!("dispatch_qmatmul F16-shadow V2 path failed: {e}"));
}
}
if weight.info.ggml_dtype == mlx_native::GgmlType::F32 {
let n = weight.info.rows as u32;
let k = weight.info.cols as u32;
let f32_matvec_default = std::env::var("HF2Q_F32_MATVEC")
.ok()
.map(|v| !matches!(v.as_str(), "0" | "false" | "off"))
.unwrap_or(true);
if m == 1 && f32_matvec_default {
let params = DenseGemmF16Params { m, n, k };
return mlx_native::ops::dense_gemm::dispatch_dense_matvec_f32(
session.encoder_mut(),
registry,
device.metal_device(),
input,
&weight.buffer,
output,
¶ms,
)
.map_err(|e| anyhow::anyhow!("dispatch_dense_matvec_f32 failed: {e}"));
}
let params = mlx_native::DenseMmF32F32Params {
m,
n,
k,
src0_batch: 1,
src1_batch: 1,
};
return mlx_native::dense_matmul_f32_f32_tensor(
session.encoder_mut(),
registry,
device,
&weight.buffer,
input,
output,
¶ms,
)
.map_err(|e| anyhow::anyhow!("dense_matmul_f32_f32_tensor failed: {e}"));
}
if weight.info.ggml_dtype == mlx_native::GgmlType::F16 {
let n = weight.info.rows as u32;
let k = weight.info.cols as u32;
if m == 1 {
let params = DenseGemmF16Params { m, n, k };
return mlx_native::ops::dense_gemm::dispatch_dense_matvec_f16w_f32io(
session.encoder_mut(),
registry,
device.metal_device(),
input,
&weight.buffer,
output,
¶ms,
)
.map_err(|e| {
anyhow::anyhow!("dispatch_dense_matvec_f16w_f32io (native F16) failed: {e}")
});
}
return mlx_native::ops::quantized_matmul_ggml::dispatch_mm_v2_f16(
session.encoder_mut(),
registry,
device,
&weight.buffer,
input,
output,
m,
n,
k,
)
.map_err(|e| anyhow::anyhow!("dispatch_mm_v2_f16 (native F16) failed: {e}"));
}
if m == 1 && weight.info.ggml_dtype == mlx_native::GgmlType::Q6_K {
let n = weight.info.rows as u32;
let k = weight.info.cols as u32;
let record_opt = weight.decode_record_q6k_m1.get_or_init(|| {
mlx_native::ops::quantized_matmul_ggml::build_q6k_nr2_m1_record(
registry,
device.metal_device(),
n,
k,
)
.ok()
.flatten()
});
if let Some(record) = record_opt {
session
.encoder_mut()
.dispatch_record(record, &[&weight.buffer, input, output]);
return Ok(());
}
}
let params = weight.matmul_params(m)?;
session
.quantized_matmul_ggml(registry, device, input, &weight.buffer, output, ¶ms)
.map_err(|e| anyhow::anyhow!("quantized_matmul_ggml failed: {e}"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DwqOverlayRole {
AttnQ,
AttnK,
AttnV,
AttnOutput,
FfnGate,
FfnUp,
FfnDown,
MoeExpert,
Unknown,
}
pub fn parse_dwq_overlay_metadata(
metadata: Option<&std::collections::HashMap<String, String>>,
) -> Result<(u32, usize)> {
match metadata {
Some(meta) => {
if let Some(format_str) = meta.get("format") {
if format_str != "mlx-affine-dwq-v1" {
anyhow::bail!(
"DWQ overlay: unsupported format '{}' (expected 'mlx-affine-dwq-v1')",
format_str
);
}
}
let bits = meta
.get("bits")
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(4u32);
let group_size = meta
.get("group_size")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(32usize);
Ok((bits, group_size))
}
None => Ok((4u32, 32usize)),
}
}
pub fn parse_dwq_overlay_role(role: &str) -> DwqOverlayRole {
match role {
"attn_q" => DwqOverlayRole::AttnQ,
"attn_k" => DwqOverlayRole::AttnK,
"attn_v" => DwqOverlayRole::AttnV,
"attn_output" => DwqOverlayRole::AttnOutput,
"ffn_gate" => DwqOverlayRole::FfnGate,
"ffn_up" => DwqOverlayRole::FfnUp,
"ffn_down" => DwqOverlayRole::FfnDown,
r if r.starts_with("ffn_gate_up.")
|| r.starts_with("ffn_gate.")
|| r.starts_with("ffn_up.")
|| r.starts_with("ffn_down.") =>
{
DwqOverlayRole::MoeExpert
}
_ => DwqOverlayRole::Unknown,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MoeBaseRole {
GateUp,
Gate,
Up,
Down,
}
pub fn parse_dwq_moe_expert_role(role: &str) -> Option<(MoeBaseRole, usize)> {
let (base, rest) = if let Some(rest) = role.strip_prefix("ffn_gate_up.") {
(MoeBaseRole::GateUp, rest)
} else if let Some(rest) = role.strip_prefix("ffn_gate.") {
(MoeBaseRole::Gate, rest)
} else if let Some(rest) = role.strip_prefix("ffn_up.") {
(MoeBaseRole::Up, rest)
} else if let Some(rest) = role.strip_prefix("ffn_down.") {
(MoeBaseRole::Down, rest)
} else {
return None;
};
rest.parse::<usize>().ok().map(|e| (base, e))
}
pub struct RmsNormPerHeadArgs<'a> {
pub input: &'a MlxBuffer,
pub output: &'a MlxBuffer,
pub params_buf: &'a MlxBuffer,
pub rows: u32,
pub dim: u32,
}
pub fn dispatch_rms_norm_unit_perhead(
encoder: &mut mlx_native::CommandEncoder,
registry: &mut mlx_native::KernelRegistry,
device: &mlx_native::metal::DeviceRef,
args: &RmsNormPerHeadArgs<'_>,
) -> Result<()> {
let v2_env_off = matches!(
std::env::var("HF2Q_RMS_NORM_V2").ok().as_deref(),
Some(v) if v.eq_ignore_ascii_case("0")
|| v.eq_ignore_ascii_case("false")
|| v.eq_ignore_ascii_case("off")
);
let use_v2 = (args.dim % 4 == 0) && !v2_env_off;
let kernel_name = if use_v2 {
"rms_norm_no_scale_f32_v2"
} else {
"rms_norm_no_scale_f32"
};
let pipeline = registry
.get_pipeline(kernel_name, device)
.map_err(|e| anyhow::anyhow!("{kernel_name} pipeline: {e}"))?;
let mut tg_size = std::cmp::min(256, args.dim.next_power_of_two()) as u64;
if use_v2 && tg_size < 32 {
tg_size = 32;
}
let shared_mem_bytes = if use_v2 {
(tg_size / 32).max(1) * 4
} else {
tg_size * 4
};
encoder.encode_threadgroups_with_shared(
pipeline,
&[(0, args.input), (1, args.output), (2, args.params_buf)],
&[(0, shared_mem_bytes)],
mlx_native::MTLSize::new(args.rows as u64, 1, 1),
mlx_native::MTLSize::new(tg_size, 1, 1),
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn dispatch_rms_norm_unit_perhead_dual_perm(
encoder: &mut mlx_native::CommandEncoder,
registry: &mut mlx_native::KernelRegistry,
device: &mlx_native::metal::DeviceRef,
input: &MlxBuffer,
output: &MlxBuffer,
output_bf16_perm: &MlxBuffer,
params_buf: &MlxBuffer,
n_heads: u32,
seq_len: u32,
dim: u32,
) -> Result<()> {
use mlx_native::ops::encode_helpers::{encode_threadgroups_with_args_and_shared, KernelArg};
let pipeline = registry
.get_pipeline("rms_norm_no_scale_f32_dual_perm", device)
.map_err(|e| anyhow::anyhow!("rms_norm_no_scale_f32_dual_perm pipeline: {e}"))?;
let rows = (n_heads as u64) * (seq_len as u64);
let tg_size = std::cmp::min(256, dim.next_power_of_two()) as u64;
let shared_mem_bytes = tg_size * 4;
let aux_bytes: [u32; 2] = [n_heads, seq_len];
let aux_bytes_b: &[u8] = unsafe {
std::slice::from_raw_parts(
aux_bytes.as_ptr() as *const u8,
std::mem::size_of_val(&aux_bytes),
)
};
encode_threadgroups_with_args_and_shared(
encoder,
pipeline,
&[
(0, KernelArg::Buffer(input)),
(1, KernelArg::Buffer(output)),
(2, KernelArg::Buffer(params_buf)),
(3, KernelArg::Buffer(output_bf16_perm)),
(4, KernelArg::Bytes(aux_bytes_b)),
],
&[(0, shared_mem_bytes)],
mlx_native::MTLSize::new(rows, 1, 1),
mlx_native::MTLSize::new(tg_size, 1, 1),
);
Ok(())
}
#[inline(always)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn rms_norm_f32_hs_cached(
cache: &std::sync::OnceLock<Option<mlx_native::DispatchRecord>>,
session: &mut mlx_native::graph::GraphSession<'_>,
reg: &mut mlx_native::KernelRegistry,
metal_dev: &mlx_native::metal::DeviceRef,
input: &mlx_native::MlxBuffer,
weight: &mlx_native::MlxBuffer,
output: &mlx_native::MlxBuffer,
params: &mlx_native::MlxBuffer,
hs: u32,
) -> Result<()> {
let rec = cache.get_or_init(|| {
mlx_native::ops::rms_norm::build_rms_norm_decode_record(
reg,
metal_dev,
mlx_native::DType::F32,
1,
hs,
)
.ok()
.flatten()
});
if let Some(r) = rec {
session
.encoder_mut()
.dispatch_record(r, &[input, weight, output, params]);
return Ok(());
}
session
.rms_norm(reg, metal_dev, input, weight, output, params, 1, hs)
.map_err(|e| anyhow::anyhow!("rms_norm cached fallback: {e}"))
}
#[allow(dead_code)]
pub fn cosine_pairwise_f32(a: &[f32], b: &[f32]) -> f32 {
debug_assert_eq!(a.len(), b.len(), "cosine vectors must match length");
let n = a.len().min(b.len());
let mut dot: f64 = 0.0;
let mut na2: f64 = 0.0;
let mut nb2: f64 = 0.0;
for i in 0..n {
let x = a[i] as f64;
let y = b[i] as f64;
dot += x * y;
na2 += x * x;
nb2 += y * y;
}
let na = na2.sqrt();
let nb = nb2.sqrt();
if na == 0.0 || nb == 0.0 {
f32::NAN
} else {
(dot / (na * nb)) as f32
}
}
#[cfg(test)]
mod cosine_tests {
use super::cosine_pairwise_f32;
#[test]
fn identity_is_one() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let a = vec![1.0_f32, 2.0, 3.0, -4.5, 0.25];
let s = cosine_pairwise_f32(&a, &a);
assert!((s - 1.0).abs() < 1e-6, "identity cosine = {s}");
}
#[test]
fn antiparallel_is_negative_one() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let a: Vec<f32> = (0..128).map(|i| (i as f32) - 64.0).collect();
let neg: Vec<f32> = a.iter().map(|x| -x).collect();
let s = cosine_pairwise_f32(&a, &neg);
assert!((s + 1.0).abs() < 1e-6, "antiparallel cosine = {s}");
}
#[test]
fn zero_norm_is_nan() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let a = vec![0.0_f32; 32];
let b: Vec<f32> = (0..32).map(|i| i as f32).collect();
let s = cosine_pairwise_f32(&a, &b);
assert!(s.is_nan(), "zero-norm cosine should be NaN, got {s}");
let s2 = cosine_pairwise_f32(&b, &a);
assert!(
s2.is_nan(),
"zero-norm cosine (rhs) should be NaN, got {s2}"
);
let z = vec![0.0_f32; 32];
let s3 = cosine_pairwise_f32(&z, &z);
assert!(s3.is_nan(), "both-zero cosine should be NaN, got {s3}");
}
#[test]
fn orthogonal_is_zero() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let mut a = vec![0.0_f32; 16];
let mut b = vec![0.0_f32; 16];
a[0] = 1.0;
b[1] = 1.0;
let s = cosine_pairwise_f32(&a, &b);
assert!(s.abs() < 1e-6, "orthogonal cosine = {s}");
}
#[test]
fn matches_python_reference_within_tolerance() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let a: Vec<f32> = (0..512).map(|i| ((i as f32) * 0.013).sin()).collect();
let b: Vec<f32> = (0..512)
.map(|i| ((i as f32) * 0.013).sin() + 1e-3)
.collect();
let s = cosine_pairwise_f32(&a, &b);
let py_dot: f64 = a
.iter()
.zip(&b)
.map(|(x, y)| (*x as f64) * (*y as f64))
.sum();
let py_na: f64 = a.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
let py_nb: f64 = b.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
let py = (py_dot / (py_na * py_nb)) as f32;
assert!(
(s - py).abs() < 1e-6,
"rust cosine={s} vs python-equiv={py}"
);
}
}
#[cfg(test)]
mod dispatch_qmatmul_f32_router_test {
use super::*;
#[test]
fn f32_router_weight_routes_to_dense_matmul() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let device = match mlx_native::MlxDevice::new() {
Ok(d) => d,
Err(_) => {
eprintln!("skipping f32_router_weight_routes_to_dense_matmul: no MlxDevice");
return;
}
};
let mut registry = mlx_native::KernelRegistry::new();
let n: usize = 4;
let k: usize = 64;
let m: usize = 2;
let mut state: u64 = 0xDEAD_BEEF_F00D_F00D;
let mut next = || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((state >> 33) as f32) / (u32::MAX as f32) * 2.0 - 1.0
};
let weight: Vec<f32> = (0..(n * k)).map(|_| next()).collect();
let input: Vec<f32> = (0..(m * k)).map(|_| next()).collect();
let mut expected = vec![0.0f32; m * n];
for mi in 0..m {
for ni in 0..n {
let mut acc = 0.0f64;
for ki in 0..k {
acc += (input[mi * k + ki] as f64) * (weight[ni * k + ki] as f64);
}
expected[mi * n + ni] = acc as f32;
}
}
let f32_sz = std::mem::size_of::<f32>();
let mut weight_buf = device
.alloc_buffer(n * k * f32_sz, mlx_native::DType::F32, vec![n, k])
.expect("alloc weight");
weight_buf
.as_mut_slice::<f32>()
.expect("weight write")
.copy_from_slice(&weight);
let mut input_buf = device
.alloc_buffer(m * k * f32_sz, mlx_native::DType::F32, vec![m, k])
.expect("alloc input");
input_buf
.as_mut_slice::<f32>()
.expect("input write")
.copy_from_slice(&input);
let mut output_buf = device
.alloc_buffer(m * n * f32_sz, mlx_native::DType::F32, vec![m, n])
.expect("alloc output");
let qweight = MlxQWeight {
buffer: weight_buf,
info: crate::serve::gpu::QuantWeightInfo {
ggml_dtype: mlx_native::GgmlType::F32,
rows: n,
cols: k,
},
affine: None,
f16_shadow: None,
decode_record_q6k_m1: std::sync::OnceLock::new(),
};
let executor = mlx_native::GraphExecutor::new(device.clone());
let mut session = executor.begin().expect("begin session");
dispatch_qmatmul(
&mut session,
&mut registry,
&device,
&input_buf,
&qweight,
&mut output_buf,
m as u32,
crate::quantize::imatrix::ImatrixHint::None,
)
.expect("dispatch_qmatmul F32 path");
session.finish().expect("session finish");
let got: &[f32] = output_buf.as_slice().expect("read output");
let mut max_abs_diff = 0.0f32;
for i in 0..(m * n) {
let d = (got[i] - expected[i]).abs();
if d > max_abs_diff {
max_abs_diff = d;
}
}
assert!(
max_abs_diff < 1e-4,
"F32 dispatch_qmatmul mismatch: max|diff|={max_abs_diff}, got={:?}, expected={:?}",
got,
expected
);
}
}
#[cfg(test)]
mod ac5_iter_b_affine_qweight_roundtrip {
use super::*;
use crate::core::mlx_safetensors_loader::MlxAffineLinear;
use mlx_native::ops::qmm_affine::dispatch_qmm_affine_t_packed_simd4_b4;
#[test]
fn from_mlx_affine_linear_roundtrips_through_packed_kernel() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let device = match mlx_native::MlxDevice::new() {
Ok(d) => d,
Err(_) => {
eprintln!("skipping ac5_iter_b: no MlxDevice");
return;
}
};
let mut registry = mlx_native::KernelRegistry::new();
let m = 16usize;
let n = 64usize;
let k = 96usize;
let group_size = 32usize;
let bits = 4u32;
let pack_factor = (32 / bits) as usize;
let groups_per_row = k / group_size;
let q_int: Vec<u8> = (0..(n * k)).map(|i| ((i * 11 + 5) % 16) as u8).collect();
let scales: Vec<f32> = (0..(n * groups_per_row))
.map(|i| 0.05 + (i as f32) * 0.0017)
.collect();
let biases: Vec<f32> = (0..(n * groups_per_row))
.map(|i| -0.13 + (i as f32) * 0.0023)
.collect();
let linear = MlxAffineLinear {
n,
k,
group_size,
bits,
q_int: q_int.clone(),
scales: scales.clone(),
biases: biases.clone(),
};
let qweight =
MlxQWeight::from_mlx_affine_linear(&device, &linear).expect("from_mlx_affine_linear");
assert_eq!(qweight.info.rows, n);
assert_eq!(qweight.info.cols, k);
let extra = qweight.affine.as_ref().expect("affine extra");
assert_eq!(extra.bits, bits);
assert_eq!(extra.group_size, group_size as u32);
assert_eq!(qweight.buffer.element_count(), n * (k / pack_factor));
assert_eq!(extra.scales.element_count(), n * groups_per_row);
assert_eq!(extra.biases.element_count(), n * groups_per_row);
let x: Vec<f32> = (0..(m * k))
.map(|i| ((i as f32) * 0.013 - 0.4).sin() * 0.6)
.collect();
let mut x_buf = device
.alloc_buffer(m * k * 4, mlx_native::DType::F32, vec![m, k])
.expect("x");
x_buf.as_mut_slice::<f32>().unwrap().copy_from_slice(&x);
let y_buf = device
.alloc_buffer(m * n * 4, mlx_native::DType::F32, vec![m, n])
.expect("y");
let mut meta = device
.alloc_buffer(16, mlx_native::DType::U32, vec![4])
.unwrap();
meta.as_mut_slice::<u32>().unwrap().copy_from_slice(&[
m as u32,
n as u32,
k as u32,
group_size as u32,
]);
let mut encoder = device.command_encoder().unwrap();
dispatch_qmm_affine_t_packed_simd4_b4(
&mut encoder,
&mut registry,
device.metal_device(),
&x_buf,
&qweight.buffer,
&extra.scales,
&extra.biases,
&y_buf,
&meta,
m as u32,
n as u32,
k as u32,
group_size as u32,
bits,
)
.expect("dispatch packed simd4");
encoder.commit_and_wait().unwrap();
let mut expected = vec![0.0f32; m * n];
for r in 0..m {
for col in 0..n {
let mut acc = 0.0f64;
for g in 0..groups_per_row {
let s = scales[col * groups_per_row + g] as f64;
let b = biases[col * groups_per_row + g] as f64;
for i in 0..group_size {
let kk = g * group_size + i;
let q = q_int[col * k + kk] as f64;
acc += (x[r * k + kk] as f64) * (q * s + b);
}
}
expected[r * n + col] = acc as f32;
}
}
let got = y_buf.as_slice::<f32>().unwrap();
let mut max_abs = 0.0f32;
for i in 0..(m * n) {
let d = (got[i] - expected[i]).abs();
if d > max_abs {
max_abs = d;
}
}
assert!(
max_abs < 1e-3,
"max|y - oracle| = {max_abs} (m={m}, n={n}, k={k})"
);
}
#[test]
fn dispatch_qmatmul_routes_affine_weight_to_packed_kernel() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let device = match mlx_native::MlxDevice::new() {
Ok(d) => d,
Err(_) => {
eprintln!("skipping: no MlxDevice");
return;
}
};
let mut registry = mlx_native::KernelRegistry::new();
let m = 8usize;
let n = 32usize;
let k = 64usize;
let gs = 32usize;
let bits = 4u32;
let groups_per_row = k / gs;
let q_int: Vec<u8> = (0..(n * k)).map(|i| ((i * 7 + 3) % 16) as u8).collect();
let scales: Vec<f32> = (0..(n * groups_per_row))
.map(|i| 0.07 + (i as f32) * 0.0011)
.collect();
let biases: Vec<f32> = (0..(n * groups_per_row))
.map(|i| -0.09 + (i as f32) * 0.0027)
.collect();
let linear = MlxAffineLinear {
n,
k,
group_size: gs,
bits,
q_int: q_int.clone(),
scales: scales.clone(),
biases: biases.clone(),
};
let qweight =
MlxQWeight::from_mlx_affine_linear(&device, &linear).expect("from_mlx_affine_linear");
let x: Vec<f32> = (0..(m * k))
.map(|i| ((i as f32) * 0.011 - 0.3).cos() * 0.5)
.collect();
let mut x_buf = device
.alloc_buffer(m * k * 4, mlx_native::DType::F32, vec![m, k])
.expect("x");
x_buf.as_mut_slice::<f32>().unwrap().copy_from_slice(&x);
let mut y_buf = device
.alloc_buffer(m * n * 4, mlx_native::DType::F32, vec![m, n])
.expect("y");
let executor = mlx_native::GraphExecutor::new(device.clone());
let mut session = executor.begin().expect("begin session");
dispatch_qmatmul(
&mut session,
&mut registry,
&device,
&x_buf,
&qweight,
&mut y_buf,
m as u32,
crate::quantize::imatrix::ImatrixHint::None,
)
.expect("dispatch_qmatmul affine route");
session.finish().expect("finish");
let mut expected = vec![0.0f32; m * n];
for r in 0..m {
for col in 0..n {
let mut acc = 0.0f64;
for g in 0..groups_per_row {
let s = scales[col * groups_per_row + g] as f64;
let b = biases[col * groups_per_row + g] as f64;
for i in 0..gs {
let kk = g * gs + i;
let q = q_int[col * k + kk] as f64;
acc += (x[r * k + kk] as f64) * (q * s + b);
}
}
expected[r * n + col] = acc as f32;
}
}
let got = y_buf.as_slice::<f32>().unwrap();
let mut max_abs = 0.0f32;
for i in 0..(m * n) {
let d = (got[i] - expected[i]).abs();
if d > max_abs {
max_abs = d;
}
}
assert!(
max_abs < 1e-3,
"dispatch_qmatmul affine route: max|y - oracle| = {max_abs}"
);
}
#[test]
fn dispatch_qmatmul_affine_equals_direct_kernel() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let device = match mlx_native::MlxDevice::new() {
Ok(d) => d,
Err(_) => {
eprintln!("skipping: no MlxDevice");
return;
}
};
let mut registry = mlx_native::KernelRegistry::new();
let m = 4usize;
let n = 32usize;
let k = 32usize;
let gs = 32usize;
let bits = 4u32;
let groups_per_row = k / gs;
let q_int: Vec<u8> = (0..(n * k)).map(|i| ((i * 5 + 1) % 16) as u8).collect();
let scales: Vec<f32> = (0..(n * groups_per_row))
.map(|i| 0.05 + (i as f32) * 0.001)
.collect();
let biases: Vec<f32> = (0..(n * groups_per_row))
.map(|i| -0.1 + (i as f32) * 0.002)
.collect();
let linear = MlxAffineLinear {
n,
k,
group_size: gs,
bits,
q_int,
scales,
biases,
};
let qweight =
MlxQWeight::from_mlx_affine_linear(&device, &linear).expect("from_mlx_affine_linear");
let extra = qweight.affine.as_ref().unwrap();
let x: Vec<f32> = (0..(m * k))
.map(|i| ((i as f32) * 0.017 + 0.2).sin() * 0.4)
.collect();
let mut x_buf = device
.alloc_buffer(m * k * 4, mlx_native::DType::F32, vec![m, k])
.expect("x");
x_buf.as_mut_slice::<f32>().unwrap().copy_from_slice(&x);
let mut y_via_dispatch = device
.alloc_buffer(m * n * 4, mlx_native::DType::F32, vec![m, n])
.expect("y_d");
let y_direct = device
.alloc_buffer(m * n * 4, mlx_native::DType::F32, vec![m, n])
.expect("y_k");
let mut meta = device
.alloc_buffer(16, mlx_native::DType::U32, vec![4])
.unwrap();
meta.as_mut_slice::<u32>()
.unwrap()
.copy_from_slice(&[m as u32, n as u32, k as u32, gs as u32]);
let mut encoder = device.command_encoder().unwrap();
mlx_native::ops::qmm_affine::dispatch_qmm_affine_t_packed_simd4_b4(
&mut encoder,
&mut registry,
device.metal_device(),
&x_buf,
&qweight.buffer,
&extra.scales,
&extra.biases,
&y_direct,
&meta,
m as u32,
n as u32,
k as u32,
gs as u32,
bits,
)
.unwrap();
encoder.commit_and_wait().unwrap();
let executor = mlx_native::GraphExecutor::new(device.clone());
let mut session = executor.begin().expect("begin session");
dispatch_qmatmul(
&mut session,
&mut registry,
&device,
&x_buf,
&qweight,
&mut y_via_dispatch,
m as u32,
crate::quantize::imatrix::ImatrixHint::None,
)
.expect("dispatch_qmatmul");
session.finish().expect("finish");
let direct = y_direct.as_slice::<f32>().unwrap();
let dispatch = y_via_dispatch.as_slice::<f32>().unwrap();
for i in 0..(m * n) {
assert_eq!(
dispatch[i].to_bits(),
direct[i].to_bits(),
"y[{i}] (m={m} n={n}): dispatch={} direct={}",
dispatch[i],
direct[i],
);
}
}
#[test]
fn parse_dwq_moe_expert_role_covers_all_bases() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
use super::{parse_dwq_moe_expert_role, MoeBaseRole};
assert_eq!(
parse_dwq_moe_expert_role("ffn_gate_up.0"),
Some((MoeBaseRole::GateUp, 0))
);
assert_eq!(
parse_dwq_moe_expert_role("ffn_gate_up.127"),
Some((MoeBaseRole::GateUp, 127))
);
assert_eq!(
parse_dwq_moe_expert_role("ffn_gate.5"),
Some((MoeBaseRole::Gate, 5))
);
assert_eq!(
parse_dwq_moe_expert_role("ffn_up.7"),
Some((MoeBaseRole::Up, 7))
);
assert_eq!(
parse_dwq_moe_expert_role("ffn_down.42"),
Some((MoeBaseRole::Down, 42))
);
assert_ne!(
parse_dwq_moe_expert_role("ffn_gate_up.3"),
Some((MoeBaseRole::Gate, 3))
);
assert_eq!(parse_dwq_moe_expert_role("ffn_gate_up.abc"), None);
assert_eq!(parse_dwq_moe_expert_role("ffn_gate"), None);
assert_eq!(parse_dwq_moe_expert_role("attn_q.0"), None);
assert_eq!(parse_dwq_moe_expert_role(""), None);
}
#[test]
fn parse_dwq_overlay_role_covers_all_dense_stems() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
use super::{parse_dwq_overlay_role, DwqOverlayRole};
assert_eq!(parse_dwq_overlay_role("attn_q"), DwqOverlayRole::AttnQ);
assert_eq!(parse_dwq_overlay_role("attn_k"), DwqOverlayRole::AttnK);
assert_eq!(parse_dwq_overlay_role("attn_v"), DwqOverlayRole::AttnV);
assert_eq!(
parse_dwq_overlay_role("attn_output"),
DwqOverlayRole::AttnOutput
);
assert_eq!(parse_dwq_overlay_role("ffn_gate"), DwqOverlayRole::FfnGate);
assert_eq!(parse_dwq_overlay_role("ffn_up"), DwqOverlayRole::FfnUp);
assert_eq!(parse_dwq_overlay_role("ffn_down"), DwqOverlayRole::FfnDown);
assert_eq!(
parse_dwq_overlay_role("ffn_gate.0"),
DwqOverlayRole::MoeExpert
);
assert_eq!(
parse_dwq_overlay_role("ffn_up.255"),
DwqOverlayRole::MoeExpert
);
assert_eq!(
parse_dwq_overlay_role("ffn_down.42"),
DwqOverlayRole::MoeExpert
);
assert_eq!(
parse_dwq_overlay_role("token_embd"),
DwqOverlayRole::Unknown
);
assert_eq!(parse_dwq_overlay_role(""), DwqOverlayRole::Unknown);
assert_eq!(parse_dwq_overlay_role("output"), DwqOverlayRole::Unknown);
}
#[test]
fn parse_dwq_overlay_metadata_handles_all_cases() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
use super::parse_dwq_overlay_metadata;
use std::collections::HashMap;
let (bits, gs) = parse_dwq_overlay_metadata(None).unwrap();
assert_eq!(bits, 4);
assert_eq!(gs, 32);
let mut bad_format = HashMap::new();
bad_format.insert("format".to_string(), "wrong-format".to_string());
assert!(parse_dwq_overlay_metadata(Some(&bad_format)).is_err());
let mut meta = HashMap::new();
meta.insert("format".to_string(), "mlx-affine-dwq-v1".to_string());
meta.insert("bits".to_string(), "8".to_string());
meta.insert("group_size".to_string(), "64".to_string());
let (bits, gs) = parse_dwq_overlay_metadata(Some(&meta)).unwrap();
assert_eq!(bits, 8);
assert_eq!(gs, 64);
let mut nofmt = HashMap::new();
nofmt.insert("bits".to_string(), "4".to_string());
nofmt.insert("group_size".to_string(), "32".to_string());
let (bits, gs) = parse_dwq_overlay_metadata(Some(&nofmt)).unwrap();
assert_eq!(bits, 4);
assert_eq!(gs, 32);
let mut garbage = HashMap::new();
garbage.insert("format".to_string(), "mlx-affine-dwq-v1".to_string());
garbage.insert("bits".to_string(), "not-a-number".to_string());
let (bits, gs) = parse_dwq_overlay_metadata(Some(&garbage)).unwrap();
assert_eq!(bits, 4);
assert_eq!(gs, 32);
}
#[test]
fn dwq_safetensors_metadata_roundtrip() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
use crate::core::mlx_safetensors_loader::{MlxAffineLinear, MlxAffineLinearBytes};
use safetensors::tensor::{serialize, Dtype};
use std::collections::HashMap;
let n = 32usize;
let k = 64usize;
let group_size = 32usize;
let bits = 4u32;
let groups_per_row = k / group_size;
let q_int: Vec<u8> = (0..(n * k)).map(|i| ((i * 3 + 7) % 16) as u8).collect();
let scales: Vec<f32> = (0..(n * groups_per_row))
.map(|i| 0.05 + (i as f32) * 0.001)
.collect();
let biases: Vec<f32> = (0..(n * groups_per_row))
.map(|i| -0.1 + (i as f32) * 0.002)
.collect();
let linear = MlxAffineLinear {
n,
k,
group_size,
bits,
q_int: q_int.clone(),
scales: scales.clone(),
biases: biases.clone(),
};
let stem = "blk.0.attn_q";
let bytes_owned: MlxAffineLinearBytes = linear.to_safetensors_bytes(Dtype::F32).unwrap();
let (w, s, b) = bytes_owned.to_safetensors_views().unwrap();
let pairs: Vec<(String, _)> = vec![
(format!("{stem}.weight"), w),
(format!("{stem}.scales"), s),
(format!("{stem}.biases"), b),
];
let mut metadata = HashMap::new();
metadata.insert("format".to_string(), "mlx-affine-dwq-v1".to_string());
metadata.insert("bits".to_string(), bits.to_string());
metadata.insert("group_size".to_string(), group_size.to_string());
let serialized =
serialize(pairs.iter().map(|(k, v)| (k.as_str(), v)), Some(metadata)).unwrap();
let (_n, md) = safetensors::SafeTensors::read_metadata(&serialized).unwrap();
let meta_map = md.metadata().as_ref().expect("metadata present");
assert_eq!(meta_map.get("format").unwrap(), "mlx-affine-dwq-v1");
assert_eq!(meta_map.get("bits").unwrap(), "4");
assert_eq!(meta_map.get("group_size").unwrap(), "32");
let (parsed_bits, parsed_gs) = super::parse_dwq_overlay_metadata(Some(meta_map)).unwrap();
assert_eq!(parsed_bits, bits);
assert_eq!(parsed_gs, group_size);
let st = safetensors::SafeTensors::deserialize(&serialized).unwrap();
let stems: Vec<&str> = st
.names()
.iter()
.filter_map(|n| n.strip_suffix(".weight"))
.collect();
assert_eq!(stems.len(), 1);
assert_eq!(stems[0], stem);
let rebuilt = MlxAffineLinear::from_safetensors(&st, stem, parsed_bits, parsed_gs).unwrap();
assert_eq!(rebuilt.n, n);
assert_eq!(rebuilt.k, k);
assert_eq!(rebuilt.bits, bits);
assert_eq!(rebuilt.group_size, group_size);
assert_eq!(rebuilt.q_int, q_int);
assert_eq!(rebuilt.scales, scales);
assert_eq!(rebuilt.biases, biases);
}
#[test]
fn from_mlx_affine_linear_rejects_unsupported_bits() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let device = match mlx_native::MlxDevice::new() {
Ok(d) => d,
Err(_) => {
eprintln!("skipping: no MlxDevice");
return;
}
};
let linear = MlxAffineLinear {
n: 32,
k: 32,
group_size: 32,
bits: 8,
q_int: vec![0u8; 32 * 32],
scales: vec![0.1f32; 32],
biases: vec![0.0f32; 32],
};
let res = MlxQWeight::from_mlx_affine_linear(&device, &linear);
assert!(res.is_err(), "should reject bits=8");
}
}