mod scalar;
#[cfg(target_arch = "x86_64")]
mod avx2;
#[cfg(target_arch = "x86_64")]
mod avx512;
use super::{SUPER_BLOCK_BYTES, SUPER_BLOCK_SIZE};
pub use scalar::{matmul_q4k_f32, matmul_q4k_f32_scalar};
#[allow(unused_imports)]
pub(crate) use scalar::compute_chunk_q4k_scalar;
#[inline]
pub fn matmul_q4k_f32_dispatch(
q4k_data: &[u8],
input: &[f32],
out_dim: usize,
in_dim: usize,
) -> Vec<f32> {
debug_assert_eq!(input.len(), in_dim, "Q4K dispatch: input length mismatch");
debug_assert!(
q4k_data.len() >= crate::contracts::Q4_K.expected_bytes(out_dim, in_dim),
"Q4K dispatch: buffer too small: {} bytes for [{}, {}] (need {})",
q4k_data.len(),
out_dim,
in_dim,
crate::contracts::Q4_K.expected_bytes(out_dim, in_dim),
);
#[cfg(target_arch = "x86_64")]
{
let total_work = out_dim * in_dim;
if total_work >= 8_000_000 {
return matmul_q4k_f32_parallel(q4k_data, input, out_dim, in_dim);
}
if is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512bw")
&& is_x86_feature_detected!("fma")
{
return unsafe { avx512::matmul_q4k_f32_avx512(q4k_data, input, out_dim, in_dim) };
}
if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
return unsafe { avx2::matmul_q4k_f32_avx2(q4k_data, input, out_dim, in_dim) };
}
}
scalar::matmul_q4k_f32(q4k_data, input, out_dim, in_dim)
}
#[cfg(target_arch = "x86_64")]
fn matmul_q4k_f32_parallel(
q4k_data: &[u8],
input: &[f32],
out_dim: usize,
in_dim: usize,
) -> Vec<f32> {
use std::thread;
let num_threads = thread::available_parallelism().map(|p| p.get()).unwrap_or(4).min(12);
let chunk_size = (out_dim + num_threads - 1) / num_threads;
let num_blocks_per_row = (in_dim + SUPER_BLOCK_SIZE - 1) / SUPER_BLOCK_SIZE;
let row_bytes = num_blocks_per_row * SUPER_BLOCK_BYTES;
let mut output: Vec<f32> = Vec::with_capacity(out_dim);
unsafe {
output.set_len(out_dim);
}
let has_avx512 = is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512bw")
&& is_x86_feature_detected!("fma");
let has_avx2 = is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma");
thread::scope(|s| {
let input_ref = input;
let q4k_ref = q4k_data;
for (chunk_idx, chunk) in output.chunks_mut(chunk_size).enumerate() {
let start_row = chunk_idx * chunk_size;
s.spawn(move || {
if has_avx512 {
unsafe {
avx512::compute_chunk_q4k_avx512(
q4k_ref,
input_ref,
chunk,
start_row,
out_dim,
in_dim,
num_blocks_per_row,
row_bytes,
);
}
} else if has_avx2 {
unsafe {
avx2::compute_chunk_q4k_avx2(
q4k_ref,
input_ref,
chunk,
start_row,
out_dim,
in_dim,
num_blocks_per_row,
row_bytes,
);
}
} else {
scalar::compute_chunk_q4k_scalar(
q4k_ref,
input_ref,
chunk,
start_row,
out_dim,
in_dim,
num_blocks_per_row,
row_bytes,
);
}
});
}
});
output
}
#[cfg(not(target_arch = "x86_64"))]
fn matmul_q4k_f32_parallel(
q4k_data: &[u8],
input: &[f32],
out_dim: usize,
in_dim: usize,
) -> Vec<f32> {
use std::thread;
let num_threads = thread::available_parallelism().map(|p| p.get()).unwrap_or(4).min(12);
if num_threads <= 1 || out_dim == 0 {
return scalar::matmul_q4k_f32(q4k_data, input, out_dim, in_dim);
}
let chunk_size = out_dim.div_ceil(num_threads);
let num_blocks_per_row = in_dim.div_ceil(SUPER_BLOCK_SIZE);
let row_bytes = num_blocks_per_row * SUPER_BLOCK_BYTES;
let mut output: Vec<f32> = Vec::with_capacity(out_dim);
unsafe {
output.set_len(out_dim);
}
thread::scope(|s| {
let input_ref = input;
let q4k_ref = q4k_data;
for (chunk_idx, chunk) in output.chunks_mut(chunk_size).enumerate() {
let start_row = chunk_idx * chunk_size;
s.spawn(move || {
scalar::compute_chunk_q4k_scalar(
q4k_ref,
input_ref,
chunk,
start_row,
out_dim,
in_dim,
num_blocks_per_row,
row_bytes,
);
});
}
});
output
}
#[cfg(test)]
mod issue_2567_aarch64_parallel_tests {
use super::*;
fn q4k_buffer(out_dim: usize, in_dim: usize) -> Vec<u8> {
let blocks = in_dim.div_ceil(SUPER_BLOCK_SIZE);
let n = out_dim * blocks * SUPER_BLOCK_BYTES;
(0..n).map(|i| ((i * 31 + 7) % 0x40) as u8).collect()
}
fn input_vec(in_dim: usize) -> Vec<f32> {
(0..in_dim).map(|i| ((i % 17) as f32 - 8.0) * 0.125).collect()
}
#[test]
fn the_chunk_boundary_is_invisible() {
for (out_dim, in_dim) in [(1, 256), (7, 256), (16, 512), (33, 256), (64, 256)] {
let data = q4k_buffer(out_dim, in_dim);
let input = input_vec(in_dim);
let num_blocks_per_row = in_dim.div_ceil(SUPER_BLOCK_SIZE);
let row_bytes = num_blocks_per_row * SUPER_BLOCK_BYTES;
let run = |threads: usize| -> Vec<f32> {
let chunk_size = out_dim.div_ceil(threads);
let mut out = vec![0.0f32; out_dim];
for (idx, chunk) in out.chunks_mut(chunk_size).enumerate() {
scalar::compute_chunk_q4k_scalar(
&data,
&input,
chunk,
idx * chunk_size,
out_dim,
in_dim,
num_blocks_per_row,
row_bytes,
);
}
out
};
let one = run(1);
for threads in [2usize, 3, 5, 12, 64] {
let many = run(threads);
for (i, (a, b)) in one.iter().zip(many.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"[{out_dim}, {in_dim}] threads={threads} row {i}: \
{a} != {b}. A chunk boundary changed the arithmetic."
);
}
}
}
}
#[test]
fn the_kernel_switch_is_only_summation_order() {
for (out_dim, in_dim) in [(7, 256), (33, 256), (64, 512)] {
let data = q4k_buffer(out_dim, in_dim);
let input = input_vec(in_dim);
let old = scalar::matmul_q4k_f32(&data, &input, out_dim, in_dim);
let num_blocks_per_row = in_dim.div_ceil(SUPER_BLOCK_SIZE);
let row_bytes = num_blocks_per_row * SUPER_BLOCK_BYTES;
let mut new = vec![0.0f32; out_dim];
scalar::compute_chunk_q4k_scalar(
&data,
&input,
&mut new,
0,
out_dim,
in_dim,
num_blocks_per_row,
row_bytes,
);
for (i, (a, b)) in old.iter().zip(new.iter()).enumerate() {
assert_eq!(
a.is_finite(),
b.is_finite(),
"[{out_dim}, {in_dim}] row {i}: finiteness disagrees ({a} vs {b})"
);
if !a.is_finite() {
continue;
}
let denom = a.abs().max(b.abs()).max(1.0);
let rel = (a - b).abs() / denom;
assert!(
rel < 1e-4,
"[{out_dim}, {in_dim}] row {i}: {a} vs {b} (rel {rel:.3e}) — \
larger than summation-order reassociation explains"
);
}
}
}
#[test]
fn degenerate_shapes_are_safe() {
for (out_dim, in_dim) in [(1, 256), (2, 256), (3, 256)] {
let data = q4k_buffer(out_dim, in_dim);
let input = input_vec(in_dim);
let out = matmul_q4k_f32_parallel(&data, &input, out_dim, in_dim);
assert_eq!(out.len(), out_dim);
}
}
}
#[cfg(test)]
mod issue_2567_measure {
use super::*;
use std::time::Instant;
#[test]
#[ignore = "timing observation; run explicitly on the host under test"]
fn parallel_vs_serial_on_an_ffn_shaped_matmul() {
let (out_dim, in_dim) = (8960usize, 1536usize);
let blocks = in_dim.div_ceil(SUPER_BLOCK_SIZE);
let data: Vec<u8> = (0..out_dim * blocks * SUPER_BLOCK_BYTES)
.map(|i| ((i * 31 + 7) % 0x40) as u8)
.collect();
let input: Vec<f32> = (0..in_dim).map(|i| ((i % 17) as f32 - 8.0) * 0.125).collect();
let time = |f: &dyn Fn() -> Vec<f32>| -> Vec<f64> {
let mut out = Vec::new();
for i in 0..17 {
let t = Instant::now();
let r = f();
std::hint::black_box(&r);
let ms = t.elapsed().as_secs_f64() * 1000.0;
if i >= 10 {
out.push(ms);
}
}
out
};
let serial = time(&|| scalar::matmul_q4k_f32(&data, &input, out_dim, in_dim));
let parallel = time(&|| matmul_q4k_f32_parallel(&data, &input, out_dim, in_dim));
let median = |v: &[f64]| {
let mut s = v.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).expect("finite timings"));
s[s.len() / 2]
};
let (ms_s, ms_p) = (median(&serial), median(¶llel));
println!("arch {}", std::env::consts::ARCH);
println!("threads {:?}", std::thread::available_parallelism());
println!("shape {out_dim}x{in_dim}");
println!("serial median {ms_s:.2} ms samples {serial:?}");
println!("parallel median {ms_p:.2} ms samples {parallel:?}");
println!("speedup {:.2}x", ms_s / ms_p);
if cfg!(target_arch = "x86_64") {
println!("note x86: ratio is SIMD+threads vs scalar, not parallelism alone");
}
assert!(
ms_p <= ms_s * 1.5,
"parallel ({ms_p:.2} ms) is materially slower than serial ({ms_s:.2} ms)"
);
}
}