use mlx_native::ops::quantized_matmul_ggml::{
quantized_matmul_ggml, GgmlQuantizedMatmulParams, GgmlType,
};
use mlx_native::{DType, KernelRegistry, MlxBuffer, MlxDevice};
const N_DISPATCHES: usize = 200;
const CANDLE_FLUSH: usize = 50; const WARMUP: usize = 5;
const MEASURE: usize = 20;
struct ProbeShape {
label: &'static str,
n: u32,
k: u32,
qtype: GgmlType,
}
const SHAPES: &[ProbeShape] = &[
ProbeShape { label: "Router_Q5K", n: 128, k: 2816, qtype: GgmlType::Q5_K },
ProbeShape { label: "Q_sliding_Q5K", n: 4096, k: 2816, qtype: GgmlType::Q5_K },
ProbeShape { label: "lmhead_Q6K", n: 262144, k: 2816, qtype: GgmlType::Q6_K },
];
fn alloc_weight(device: &MlxDevice, shape: &ProbeShape) -> MlxBuffer {
let blocks_per_row = (shape.k as u64) / (shape.qtype.block_values() as u64);
let total_bytes = (shape.n as u64) * blocks_per_row * (shape.qtype.block_bytes() as u64);
device
.alloc_buffer(total_bytes as usize, DType::U8, vec![total_bytes as usize])
.expect("alloc weight")
}
fn alloc_f32(device: &MlxDevice, n: usize, label: &str) -> MlxBuffer {
device
.alloc_buffer(n * 4, DType::F32, vec![n])
.unwrap_or_else(|e| panic!("alloc {label}: {e}"))
}
fn percentile(sorted: &[f64], p: f64) -> f64 {
let idx = ((sorted.len() as f64) * p).floor() as usize;
sorted[idx.min(sorted.len() - 1)]
}
fn params(shape: &ProbeShape) -> GgmlQuantizedMatmulParams {
GgmlQuantizedMatmulParams {
m: 1,
n: shape.n,
k: shape.k,
ggml_type: shape.qtype,
}
}
fn run_single_encoder(
shape: &ProbeShape,
device: &MlxDevice,
registry: &mut KernelRegistry,
input: &MlxBuffer,
weight: &MlxBuffer,
output: &MlxBuffer,
) -> f64 {
let p = params(shape);
let t0 = std::time::Instant::now();
let mut enc = device.command_encoder().expect("encoder");
for _ in 0..N_DISPATCHES {
quantized_matmul_ggml(&mut enc, registry, device, input, weight, output, &p)
.expect("dispatch A");
}
enc.commit_and_wait().expect("commit A");
t0.elapsed().as_secs_f64() * 1_000_000.0
}
fn run_per_n_flush(
shape: &ProbeShape,
device: &MlxDevice,
registry: &mut KernelRegistry,
input: &MlxBuffer,
weight: &MlxBuffer,
output: &MlxBuffer,
) -> f64 {
let p = params(shape);
let n_chunks = N_DISPATCHES.div_ceil(CANDLE_FLUSH);
let t0 = std::time::Instant::now();
let mut last: Option<mlx_native::CommandEncoder> = None;
for chunk in 0..n_chunks {
let start = chunk * CANDLE_FLUSH;
let end = ((chunk + 1) * CANDLE_FLUSH).min(N_DISPATCHES);
let mut enc = device.command_encoder().expect("encoder");
for _ in start..end {
quantized_matmul_ggml(&mut enc, registry, device, input, weight, output, &p)
.expect("dispatch B");
}
if chunk + 1 < n_chunks {
enc.commit();
} else {
last = Some(enc);
}
}
last.expect("last").commit_and_wait().expect("commit_and_wait B");
t0.elapsed().as_secs_f64() * 1_000_000.0
}
fn run_per_op_encoder(
shape: &ProbeShape,
device: &MlxDevice,
registry: &mut KernelRegistry,
input: &MlxBuffer,
weight: &MlxBuffer,
output: &MlxBuffer,
) -> f64 {
let p = params(shape);
let t0 = std::time::Instant::now();
for _ in 0..N_DISPATCHES {
let mut enc = device.command_encoder().expect("encoder");
quantized_matmul_ggml(&mut enc, registry, device, input, weight, output, &p)
.expect("dispatch C");
enc.commit_and_wait().expect("commit C");
}
t0.elapsed().as_secs_f64() * 1_000_000.0
}
fn measure<F: FnMut() -> f64>(label: &str, mut f: F) -> (f64, f64) {
for _ in 0..WARMUP {
let _ = f();
}
let mut samples = Vec::with_capacity(MEASURE);
for _ in 0..MEASURE {
samples.push(f());
}
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let p50 = percentile(&samples, 0.5);
let p10 = percentile(&samples, 0.1);
let p90 = percentile(&samples, 0.9);
let per_disp = p50 / N_DISPATCHES as f64;
println!(
" {:<34} | total p50={:>8.2}us [p10={:>8.2}, p90={:>8.2}] | per-dispatch p50={:>6.3}us",
label, p50, p10, p90, per_disp,
);
(p50, per_disp)
}
fn bench_shape(shape: &ProbeShape, device: &MlxDevice, registry: &mut KernelRegistry) {
let input = alloc_f32(device, shape.k as usize, "input");
let output = alloc_f32(device, shape.n as usize, "output");
let weight = alloc_weight(device, shape);
let bytes_read_mb = (shape.n as f64) * (shape.k as f64) * (shape.qtype.block_bytes() as f64)
/ (shape.qtype.block_values() as f64) / 1_048_576.0;
println!("[{}] n={} k={} qtype={:?} | read={:.3} MB", shape.label, shape.n, shape.k, shape.qtype, bytes_read_mb);
let (a_total, a_per) = measure("A. mlx-native single-encoder", || {
run_single_encoder(shape, device, registry, &input, &weight, &output)
});
let (b_total, b_per) = measure(&format!("B. candle default ({}/CB flush)", CANDLE_FLUSH), || {
run_per_n_flush(shape, device, registry, &input, &weight, &output)
});
let (c_total, c_per) = measure("C. per-op encoder (worst case)", || {
run_per_op_encoder(shape, device, registry, &input, &weight, &output)
});
println!(" Ratios vs A: B/A={:.2}× wall, {:.2}×/disp | C/A={:.2}× wall, {:.2}×/disp",
b_total / a_total, b_per / a_per, c_total / a_total, c_per / a_per);
println!();
}
fn main() {
let device = MlxDevice::new().expect("MlxDevice::new");
let mut registry = KernelRegistry::new();
println!("README-quote bench: encoder pattern comparison");
println!(" N per token: {} dispatches, Warmup: {}, Measure: {}", N_DISPATCHES, WARMUP, MEASURE);
println!();
for shape in SHAPES {
bench_shape(shape, &device, &mut registry);
}
}