use std::borrow::Cow;
use std::cell::Cell;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicUsize, Ordering};
use onnx_runtime_ep_api::{EpError, Kernel, KernelFactory, Result, TensorMut, TensorView};
use onnx_runtime_ir::{DataType, Node};
use rayon::prelude::*;
use super::matmul::gemm;
use super::{check_arity, to_dense_bytes, to_dense_f32, to_dense_i64, write_dense_f32};
use crate::dtype::{to_dense_f32_widen, write_dense_f32_narrow};
use crate::strided::numel;
mod mm_profile {
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
static WIDEN_NS: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "mlas")]
static GEMV_NS: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "mlas")]
static NARROW_NS: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "mlas")]
static CALLS: AtomicU64 = AtomicU64::new(0);
pub fn enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
std::env::var("ONNX_GENAI_PROFILE_MM").is_ok_and(|v| {
let v = v.trim();
!v.is_empty() && v != "0"
})
})
}
#[inline]
fn timed<T>(bucket: &AtomicU64, f: impl FnOnce() -> T) -> T {
if !enabled() {
return f();
}
let start = Instant::now();
let out = f();
bucket.fetch_add(start.elapsed().as_nanos() as u64, Ordering::Relaxed);
out
}
pub fn time_widen<T>(f: impl FnOnce() -> T) -> T {
timed(&WIDEN_NS, f)
}
#[cfg(feature = "mlas")]
pub fn time_gemv<T>(f: impl FnOnce() -> T) -> T {
timed(&GEMV_NS, f)
}
#[cfg(feature = "mlas")]
pub fn time_narrow<T>(f: impl FnOnce() -> T) -> T {
timed(&NARROW_NS, f)
}
#[cfg(feature = "mlas")]
pub fn tick() {
if !enabled() {
return;
}
let calls = CALLS.fetch_add(1, Ordering::Relaxed) + 1;
if calls.is_multiple_of(512) {
let widen = WIDEN_NS.load(Ordering::Relaxed) as f64 / 1e6;
let gemv = GEMV_NS.load(Ordering::Relaxed) as f64 / 1e6;
let narrow = NARROW_NS.load(Ordering::Relaxed) as f64 / 1e6;
let total = widen + gemv + narrow;
eprintln!(
"[mm_profile] calls={calls} total={total:.1}ms widen={widen:.1}ms \
({wp:.1}%) gemv={gemv:.1}ms ({gp:.1}%) narrow={narrow:.1}ms ({np:.1}%)",
wp = 100.0 * widen / total,
gp = 100.0 * gemv / total,
np = 100.0 * narrow / total,
);
}
}
}
const DECODE_THREADS_ENV: &str = "ONNX_GENAI_CPU_DECODE_THREADS";
static DECODE_THREADS_OVERRIDE: AtomicUsize = AtomicUsize::new(0);
const MAX_TOPOLOGY_DECODE_THREADS: usize = 8;
static DECODE_POOL: OnceLock<std::result::Result<Option<rayon::ThreadPool>, String>> =
OnceLock::new();
const MAX_DENSE_DECODE_THREADS: usize = 32;
static DENSE_DECODE_POOL: OnceLock<std::result::Result<Option<rayon::ThreadPool>, String>> =
OnceLock::new();
#[cfg(feature = "mlas")]
const SQNBIT_DECODE_MIN_ENV: &str = "NXRT_SQNBIT_DECODE_MIN";
#[cfg(feature = "mlas")]
static SQNBIT_DECODE_MIN: OnceLock<usize> = OnceLock::new();
#[cfg(feature = "mlas")]
fn mlas_no_shard() -> bool {
static NO_SHARD: OnceLock<bool> = OnceLock::new();
*NO_SHARD.get_or_init(|| {
std::env::var("ONNX_GENAI_CPU_MM_MLAS_NO_SHARD").is_ok_and(|value| {
let value = value.trim();
!value.is_empty() && value != "0"
})
})
}
#[cfg(feature = "mlas")]
fn mlas_prefill_serial() -> bool {
static PREFILL_SERIAL: OnceLock<bool> = OnceLock::new();
*PREFILL_SERIAL.get_or_init(|| {
std::env::var("ONNX_GENAI_CPU_MM_MLAS_PREFILL_SERIAL").is_ok_and(|value| {
let value = value.trim();
!value.is_empty() && value != "0"
})
})
}
#[cfg(feature = "mlas")]
fn sqnbit_decode_min() -> usize {
*SQNBIT_DECODE_MIN.get_or_init(|| {
let available = available_parallelism();
resolve_decode_min(
std::env::var(SQNBIT_DECODE_MIN_ENV).ok().as_deref(),
available,
)
})
}
#[cfg(feature = "mlas")]
fn resolve_decode_min(raw: Option<&str>, available: usize) -> usize {
raw.and_then(|value| value.trim().parse::<usize>().ok())
.unwrap_or_else(|| default_sqnbit_decode_min(available))
}
pub struct MatMulNBitsKernel {
k: usize,
n: usize,
bits: usize,
block_size: usize,
accuracy_level: i64,
constant_inputs: [bool; 5],
weight_nk: OnceLock<Vec<f32>>,
int8_weight: OnceLock<Int8Weight>,
packed_int4_weight: OnceLock<PackedInt4Weight>,
packed_u8_weight: OnceLock<PackedU8Weight>,
#[cfg(feature = "mlas")]
mlas_shards: OnceLock<Option<Vec<Option<MlasShard>>>>,
}
#[cfg(feature = "mlas")]
struct MlasShard {
start: usize,
len: usize,
packed: mlas_sys::SQNBitPackedB,
}
#[cfg(feature = "mlas")]
const MLAS_SQNBIT_DECODE_SHARD_ALIGN: usize = 16;
struct Int8Weight {
values: Vec<i8>,
scales: Vec<f32>,
block_sums: Vec<i32>,
}
struct PackedInt4Weight {
values: Vec<u8>,
scales: Vec<f32>,
}
struct PackedU8Weight {
values: Vec<u8>,
scales: Vec<f32>,
scaled_zero_points: Vec<f32>,
}
pub struct MatMulNBitsFactory;
impl KernelFactory for MatMulNBitsFactory {
fn create(&self, node: &Node, _input_shapes: &[Vec<usize>]) -> Result<Box<dyn Kernel>> {
let k = required_positive_attr(node, "K")?;
let n = required_positive_attr(node, "N")?;
let bits = optional_int_attr(node, "bits")?.unwrap_or(4);
if !matches!(bits, 2 | 4 | 8) {
return Err(error(format!(
"MatMulNBits CPU supports bits in {{2, 4, 8}}, got bits={bits}. Why: other packed \
widths do not have a validated dequantization path. How to fix: export bits=2, \
bits=4, or bits=8, or select another execution provider"
)));
}
let weight_prepacked = optional_int_attr(node, "weight_prepacked")?.unwrap_or(0);
if weight_prepacked != 0 {
return Err(error(format!(
"weight_prepacked={weight_prepacked} is unsupported: CPU only supports the standard (non-prepacked) layout"
)));
}
let block_size = required_positive_attr(node, "block_size")?;
if block_size < 16 || !block_size.is_power_of_two() {
return Err(error(format!(
"block_size must be a power of two and at least 16, got {block_size}"
)));
}
let accuracy_level = node
.attr("accuracy_level")
.and_then(|value| value.as_int())
.unwrap_or(0);
Ok(Box::new(MatMulNBitsKernel {
k,
n,
bits: bits as usize,
block_size,
accuracy_level,
constant_inputs: [false; 5],
weight_nk: OnceLock::new(),
int8_weight: OnceLock::new(),
packed_int4_weight: OnceLock::new(),
packed_u8_weight: OnceLock::new(),
#[cfg(feature = "mlas")]
mlas_shards: OnceLock::new(),
}))
}
}
impl Kernel for MatMulNBitsKernel {
fn set_constant_inputs(&mut self, constant_inputs: &[bool]) {
for (index, is_constant) in self.constant_inputs.iter_mut().enumerate() {
*is_constant = constant_inputs.get(index).copied().unwrap_or(false);
}
}
fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
check_arity("MatMulNBits", inputs, outputs, 3, 6, 1)?;
require_float_compute_dtype("A", inputs[0].dtype)?;
require_dtype("B", inputs[1].dtype, DataType::Uint8)?;
require_float_compute_dtype("scales", inputs[2].dtype)?;
require_float_compute_dtype("Y", outputs[0].dtype)?;
let a_shape = inputs[0].shape;
if a_shape.is_empty() || a_shape[a_shape.len() - 1] != self.k {
return Err(error(format!(
"A must have rank >= 1 and last dimension K={}, got {:?}",
self.k, a_shape
)));
}
let expected_output_shape = [&a_shape[..a_shape.len() - 1], &[self.n]].concat();
if outputs[0].shape != expected_output_shape {
return Err(error(format!(
"Y must have shape {expected_output_shape:?}, got {:?}",
outputs[0].shape
)));
}
let k_blocks = self.k.div_ceil(self.block_size);
let blob_size = self.block_size * self.bits / 8;
require_shape("B", inputs[1].shape, &[self.n, k_blocks, blob_size])?;
require_flat_or_matrix_shape("scales", inputs[2].shape, self.n, k_blocks)?;
let zero_points = optional_input(inputs, 3);
if let Some(zp) = zero_points {
require_dtype("zero_points", zp.dtype, DataType::Uint8)?;
let zp_blob_size = (k_blocks * self.bits).div_ceil(8);
require_flat_or_matrix_shape("zero_points", zp.shape, self.n, zp_blob_size)?;
}
let group_indices = optional_input(inputs, 4);
if let Some(g_idx) = group_indices {
require_dtype("g_idx", g_idx.dtype, DataType::Int32)?;
let padded_k = k_blocks * self.block_size;
if g_idx.shape != [self.k] && g_idx.shape != [padded_k] {
return Err(error(format!(
"g_idx must have shape [{}] or [{padded_k}], got {:?}",
self.k, g_idx.shape
)));
}
}
let bias = if let Some(bias) = optional_input(inputs, 5) {
require_float_compute_dtype("bias", bias.dtype)?;
require_shape("bias", bias.shape, &[self.n])?;
Some(to_dense_compute_f32(bias)?)
} else {
None
};
let can_prepack = self.constant_inputs[1]
&& self.constant_inputs[2]
&& zero_points.is_none_or(|_| self.constant_inputs[3])
&& group_indices.is_none_or(|_| self.constant_inputs[4]);
let activations = mm_profile::time_widen(|| compute_activations_cow(&inputs[0]))?;
let m = numel(&a_shape[..a_shape.len() - 1]);
crate::trace::record_kernel_metrics(inputs, outputs, || {
let mut flops = (m as u64)
.saturating_mul(self.n as u64)
.saturating_mul(self.k as u64)
.saturating_mul(2);
if bias.is_some() {
flops = flops.saturating_add((m as u64).saturating_mul(self.n as u64));
}
flops
});
let result_len = m * self.n;
let direct_result = outputs[0].dtype == DataType::Float32
&& outputs[0].is_contiguous()
&& outputs[0].device.is_host_accessible();
let mut owned_result;
let result: &mut [f32] = if direct_result {
unsafe { std::slice::from_raw_parts_mut(outputs[0].data_ptr_mut::<f32>(), result_len) }
} else {
owned_result = vec![0.0f32; result_len];
&mut owned_result
};
let dot_kernel = selected_dot_kernel();
#[cfg(feature = "mlas")]
{
if let Some(()) = self.try_mlas_sqnbit(
&inputs[1],
&inputs[2],
zero_points,
group_indices,
can_prepack,
&activations,
m,
bias.as_deref(),
result,
)? {
let out = if direct_result {
Ok(())
} else {
mm_profile::time_narrow(|| write_compute_f32(&mut outputs[0], result))
};
mm_profile::tick();
return out;
}
}
if self.bits == 4
&& self.accuracy_level == 4
&& m == 1
&& self.block_size == 32
&& zero_points.is_none()
&& group_indices.is_none()
&& dot_kernel.uses_vnni_int4_direct()
{
let owned_weight;
let packed_weight = if can_prepack {
if let Some(weight) = self.packed_int4_weight.get() {
weight
} else {
let weight = PackedInt4Weight {
values: to_dense_bytes(&inputs[1])?,
scales: to_dense_compute_f32(&inputs[2])?,
};
let weight = numa_place_int4(weight, self.n);
let _ = self.packed_int4_weight.set(weight);
self.packed_int4_weight
.get()
.expect("constant MatMulNBits packed int4 weight was just initialized")
}
} else {
let built = PackedInt4Weight {
values: to_dense_bytes(&inputs[1])?,
scales: to_dense_compute_f32(&inputs[2])?,
};
owned_weight = numa_place_int4(built, self.n);
&owned_weight
};
with_decode_pool(|| {
int4_matmul_m1(
&activations,
packed_weight,
result,
self.k,
self.n,
dot_kernel,
);
})?;
} else if self.bits == 4 && self.accuracy_level == 4 && group_indices.is_none() {
let owned_weight;
let int8_weight = if can_prepack {
if let Some(weight) = self.int8_weight.get() {
weight
} else {
let weight = self.prepack_int8_weight(&inputs[1], &inputs[2], zero_points)?;
let weight = numa_place_int8(weight, self.n);
let _ = self.int8_weight.set(weight);
self.int8_weight
.get()
.expect("constant MatMulNBits int8 prepack was just initialized")
}
} else {
let built = self.prepack_int8_weight(&inputs[1], &inputs[2], zero_points)?;
owned_weight = numa_place_int8(built, self.n);
&owned_weight
};
if m == 1 {
with_decode_pool(|| {
int8_matmul(
&activations,
int8_weight,
result,
m,
self.k,
self.n,
self.block_size,
dot_kernel,
);
})?;
} else {
#[cfg(target_arch = "x86_64")]
{
if m >= amx::AMX_PREFILL_MIN_M
&& amx::amx_block_size_supported(self.block_size)
&& amx::amx_int8_available()
{
amx::int8_matmul_amx(
&activations,
int8_weight,
result,
m,
self.k,
self.n,
self.block_size,
);
} else {
int8_matmul(
&activations,
int8_weight,
result,
m,
self.k,
self.n,
self.block_size,
dot_kernel,
);
}
}
#[cfg(not(target_arch = "x86_64"))]
{
int8_matmul(
&activations,
int8_weight,
result,
m,
self.k,
self.n,
self.block_size,
dot_kernel,
);
}
}
} else if self.bits == 8 && m == 1 && group_indices.is_none() {
let owned_weight;
let weight_u8 = if can_prepack {
if let Some(weight) = self.packed_u8_weight.get() {
weight
} else {
let weight = self.prepack_u8_weight(&inputs[1], &inputs[2], zero_points)?;
let weight = numa_place_u8(weight, self.n);
let _ = self.packed_u8_weight.set(weight);
self.packed_u8_weight
.get()
.expect("constant MatMulNBits u8 prepack was just initialized")
}
} else {
let built = self.prepack_u8_weight(&inputs[1], &inputs[2], zero_points)?;
owned_weight = numa_place_u8(built, self.n);
&owned_weight
};
with_decode_pool(|| {
if eight_bit_int16_activation() {
gemv_nk_u8_i16(
&activations,
&weight_u8.values,
&weight_u8.scales,
&weight_u8.scaled_zero_points,
result,
self.k,
self.n,
self.block_size,
);
} else {
gemv_nk_u8(
&activations,
&weight_u8.values,
&weight_u8.scales,
&weight_u8.scaled_zero_points,
result,
self.k,
self.n,
self.block_size,
);
}
})?;
} else if m == 1 {
let owned_weight;
let weight_nk = if can_prepack {
if let Some(weight) = self.weight_nk.get() {
weight
} else {
let weight = self.dequantize_weight(
&inputs[1],
&inputs[2],
zero_points,
group_indices,
WeightLayout::Nk,
)?;
let weight = numa_place_nk(weight, self.n);
let _ = self.weight_nk.set(weight);
self.weight_nk
.get()
.expect("constant MatMulNBits prepack was just initialized")
}
} else {
let built = self.dequantize_weight(
&inputs[1],
&inputs[2],
zero_points,
group_indices,
WeightLayout::Nk,
)?;
owned_weight = numa_place_nk(built, self.n);
&owned_weight
};
with_decode_pool(|| {
gemv_nk(&activations, weight_nk, result, self.k, self.n);
})?;
} else {
let used_fast_nt = self.try_prefill_mlas_nt(
&inputs[1],
&inputs[2],
zero_points,
group_indices,
can_prepack,
&activations,
m,
result,
)?;
if !used_fast_nt {
let weight_kn = self.dequantize_weight(
&inputs[1],
&inputs[2],
zero_points,
group_indices,
WeightLayout::Kn,
)?;
gemm(&activations, &weight_kn, result, m, self.k, self.n)?;
}
}
if let Some(bias) = bias {
for row in result.chunks_exact_mut(self.n) {
for (value, bias) in row.iter_mut().zip(&bias) {
*value += bias;
}
}
}
if direct_result {
Ok(())
} else {
write_compute_f32(&mut outputs[0], result)
}
}
fn supports_strided_input(&self, _input_idx: usize) -> bool {
true
}
}
impl MatMulNBitsKernel {
#[cfg(feature = "mlas")]
#[allow(clippy::too_many_arguments)]
fn try_mlas_sqnbit(
&self,
packed: &TensorView,
scales: &TensorView,
zero_points: Option<&TensorView>,
group_indices: Option<&TensorView>,
can_prepack: bool,
activations: &[f32],
m: usize,
bias: Option<&[f32]>,
result: &mut [f32],
) -> Result<Option<()>> {
use crate::backend::CpuBackend;
let hand_decode_is_fast = self.bits == 4 && self.accuracy_level == 4;
if m < sqnbit_decode_min() && hand_decode_is_fast {
return Ok(None);
}
let backend_is_mlas = CpuBackend::auto_detect() == CpuBackend::Mlas;
let use_mlas = backend_is_mlas || self.accuracy_level != 4;
if self.bits != 4 || group_indices.is_some() || !use_mlas {
return Ok(None);
}
let comp = if self.accuracy_level == 4 {
mlas_sys::SQNBitComputeType::Int8
} else {
mlas_sys::SQNBitComputeType::Fp32
};
if matches!(comp, mlas_sys::SQNBitComputeType::Int8)
&& m == 1
&& zero_points.is_some()
&& !host_has_mlas_sqnbit_avx512()
{
return Ok(None);
}
if can_prepack && !mlas_no_shard() {
let shards = if let Some(shards) = self.mlas_shards.get() {
shards
} else {
let built = self.build_mlas_shards(packed, scales, zero_points, comp)?;
let _ = self.mlas_shards.set(built);
self.mlas_shards
.get()
.expect("constant MatMulNBits MLAS shards were just initialized")
};
let Some(shards) = shards.as_ref() else {
return Ok(None);
};
mm_profile::time_gemv(|| self.run_mlas_shards(shards, activations, m, bias, result));
return Ok(Some(()));
}
let owned = self.build_mlas_packed(packed, scales, zero_points, comp)?;
let Some(packed_weight) = owned.as_ref() else {
return Ok(None);
};
mm_profile::time_gemv(|| {
mlas_sys::sqnbit_gemm(packed_weight, m, activations, bias, result, true)
});
Ok(Some(()))
}
#[cfg(feature = "mlas")]
#[allow(clippy::too_many_arguments)]
fn try_prefill_mlas_nt(
&self,
packed: &TensorView,
scales: &TensorView,
zero_points: Option<&TensorView>,
group_indices: Option<&TensorView>,
can_prepack: bool,
activations: &[f32],
m: usize,
result: &mut [f32],
) -> Result<bool> {
use crate::backend::CpuBackend;
if CpuBackend::auto_detect() != CpuBackend::Mlas {
return Ok(false);
}
let owned_weight;
let weight_nk: &[f32] = if can_prepack {
if let Some(weight) = self.weight_nk.get() {
weight
} else {
let weight = self.dequantize_weight(
packed,
scales,
zero_points,
group_indices,
WeightLayout::Nk,
)?;
let weight = numa_place_nk(weight, self.n);
let _ = self.weight_nk.set(weight);
self.weight_nk
.get()
.expect("constant MatMulNBits Nk prepack was just initialized")
}
} else {
let built = self.dequantize_weight(
packed,
scales,
zero_points,
group_indices,
WeightLayout::Nk,
)?;
owned_weight = numa_place_nk(built, self.n);
&owned_weight
};
mm_profile::time_gemv(|| {
mlas_sys::sgemm(
false,
true,
m,
self.n,
self.k,
1.0,
activations,
self.k,
weight_nk,
self.k,
0.0,
result,
self.n,
);
});
Ok(true)
}
#[cfg(not(feature = "mlas"))]
#[allow(clippy::too_many_arguments)]
fn try_prefill_mlas_nt(
&self,
_packed: &TensorView,
_scales: &TensorView,
_zero_points: Option<&TensorView>,
_group_indices: Option<&TensorView>,
_can_prepack: bool,
_activations: &[f32],
_m: usize,
_result: &mut [f32],
) -> Result<bool> {
Ok(false)
}
#[cfg(feature = "mlas")]
fn run_mlas_shards(
&self,
shards: &[Option<MlasShard>],
activations: &[f32],
m: usize,
bias: Option<&[f32]>,
result: &mut [f32],
) {
if let Some(spmd) = (m == 1).then(spmd_decode_active).flatten() {
spmd.dispatch_output_rows_indexed(
result,
MLAS_SQNBIT_DECODE_SHARD_ALIGN,
&|global_index, start, outputs| {
let Some(shard) = shards.get(global_index).and_then(Option::as_ref) else {
return;
};
debug_assert_eq!(shard.start, start);
debug_assert_eq!(shard.len, outputs.len());
let bias = bias.map(|bias| &bias[start..start + outputs.len()]);
mlas_sys::sqnbit_gemm(&shard.packed, 1, activations, bias, outputs, false);
},
);
return;
}
let base = result.as_mut_ptr();
let active = shards.iter().filter(|shard| shard.is_some()).count();
if m > 1 && active > 1 && !mlas_prefill_serial() {
let n = self.n;
let k = self.k;
let threads = rayon::current_num_threads().max(1);
let row_blocks = (threads / active).clamp(1, m);
let rows_per_block = m.div_ceil(row_blocks);
let live: Vec<&MlasShard> = shards.iter().flatten().collect();
let mut tiles: Vec<(usize, usize, usize)> = Vec::with_capacity(live.len() * row_blocks);
for (shard_index, _) in live.iter().enumerate() {
let mut row = 0;
while row < m {
let rows = rows_per_block.min(m - row);
tiles.push((shard_index, row, rows));
row += rows;
}
}
struct OutputBase(*mut f32);
unsafe impl Sync for OutputBase {}
let out = OutputBase(base);
let out = &out;
let live = &live;
tiles
.par_iter()
.for_each(|&(shard_index, row_start, rows)| {
let shard = live[shard_index];
let bias = bias.map(|bias| &bias[shard.start..shard.start + shard.len]);
let activations = &activations[row_start * k..(row_start + rows) * k];
let dst = unsafe { out.0.add(row_start * n + shard.start) };
unsafe {
mlas_sys::sqnbit_gemm_into(
&shard.packed,
rows,
activations,
bias,
dst,
n,
false,
);
}
});
return;
}
for shard in shards.iter().flatten() {
let bias = bias.map(|bias| &bias[shard.start..shard.start + shard.len]);
unsafe {
mlas_sys::sqnbit_gemm_into(
&shard.packed,
m,
activations,
bias,
base.add(shard.start),
self.n,
true,
);
}
}
}
#[cfg(feature = "mlas")]
fn mlas_shard_segments(&self) -> Vec<(usize, usize)> {
match crate::decode_spmd::pools() {
Some(spmd) => spmd.output_column_segments(self.n, MLAS_SQNBIT_DECODE_SHARD_ALIGN),
None => vec![(0, self.n)],
}
}
#[cfg(feature = "mlas")]
fn build_mlas_shards(
&self,
packed: &TensorView,
scales: &TensorView,
zero_points: Option<&TensorView>,
comp: mlas_sys::SQNBitComputeType,
) -> Result<Option<Vec<Option<MlasShard>>>> {
let packed = to_dense_bytes(packed)?;
let scales = to_dense_compute_f32(scales)?;
let zero_points = zero_points.map(to_dense_bytes).transpose()?;
let k_blocks = self.k.div_ceil(self.block_size);
let blob_size = self.block_size * self.bits / 8;
let zp_blob_size = (k_blocks * self.bits).div_ceil(8);
let mut shards = Vec::new();
for (start, len) in self.mlas_shard_segments() {
if len == 0 {
shards.push(None);
continue;
}
let packed_shard =
&packed[start * k_blocks * blob_size..(start + len) * k_blocks * blob_size];
let scales_shard = &scales[start * k_blocks..(start + len) * k_blocks];
let zero_points_shard = zero_points
.as_ref()
.map(|zp| &zp[start * zp_blob_size..(start + len) * zp_blob_size]);
match mlas_sys::SQNBitPackedB::new(
len,
self.k,
self.bits,
self.block_size,
comp,
packed_shard,
scales_shard,
zero_points_shard,
) {
Some(packed) => shards.push(Some(MlasShard { start, len, packed })),
None => return Ok(None),
}
}
Ok(Some(shards))
}
#[cfg(feature = "mlas")]
fn build_mlas_packed(
&self,
packed: &TensorView,
scales: &TensorView,
zero_points: Option<&TensorView>,
comp: mlas_sys::SQNBitComputeType,
) -> Result<Option<mlas_sys::SQNBitPackedB>> {
let packed = to_dense_bytes(packed)?;
let scales = to_dense_compute_f32(scales)?;
let zero_points = zero_points.map(to_dense_bytes).transpose()?;
Ok(mlas_sys::SQNBitPackedB::new(
self.n,
self.k,
self.bits,
self.block_size,
comp,
&packed,
&scales,
zero_points.as_deref(),
))
}
fn prepack_int8_weight(
&self,
packed: &TensorView,
scales: &TensorView,
zero_points: Option<&TensorView>,
) -> Result<Int8Weight> {
let packed = to_dense_bytes(packed)?;
let scales = to_dense_compute_f32(scales)?;
let packed_zero_points = zero_points.map(to_dense_bytes).transpose()?;
let k_blocks = self.k.div_ceil(self.block_size);
debug_assert_eq!(self.bits, 4);
let blob_size = self.block_size / 2;
let zp_row_bytes = k_blocks.div_ceil(2);
let padded_k = k_blocks * self.block_size;
let mut values = vec![0i8; self.n * padded_k];
let mut block_sums = vec![0i32; self.n * k_blocks];
for output in 0..self.n {
for block in 0..k_blocks {
let zero_point = packed_zero_points.as_ref().map_or(8, |points| {
let byte = points[output * zp_row_bytes + block / 2];
if block.is_multiple_of(2) {
byte & 0x0f
} else {
byte >> 4
}
});
let block_start = block * self.block_size;
let valid = self.k.saturating_sub(block_start).min(self.block_size);
let packed_start = (output * k_blocks + block) * blob_size;
let values_start = output * padded_k + block_start;
let mut sum = 0i32;
for offset in 0..valid {
let byte = packed[packed_start + offset / 2];
let quantized = if offset.is_multiple_of(2) {
byte & 0x0f
} else {
byte >> 4
};
let value = quantized as i8 - zero_point as i8;
values[values_start + offset] = value;
sum += value as i32;
}
block_sums[output * k_blocks + block] = sum;
}
}
Ok(Int8Weight {
values,
scales,
block_sums,
})
}
fn prepack_u8_weight(
&self,
packed: &TensorView,
scales: &TensorView,
zero_points: Option<&TensorView>,
) -> Result<PackedU8Weight> {
debug_assert_eq!(self.bits, 8);
let packed = to_dense_bytes(packed)?;
let scales = to_dense_compute_f32(scales)?;
let packed_zero_points = zero_points.map(to_dense_bytes).transpose()?;
let k_blocks = self.k.div_ceil(self.block_size);
let blob_size = self.block_size;
let zp_row_bytes = k_blocks;
let mut values = vec![0u8; self.n * self.k];
let mut scaled_zero_points = vec![0.0f32; self.n * k_blocks];
for output in 0..self.n {
for block in 0..k_blocks {
let zero_point = packed_zero_points
.as_ref()
.map_or(128u8, |points| points[output * zp_row_bytes + block]);
let scale = scales[output * k_blocks + block];
scaled_zero_points[output * k_blocks + block] = scale * zero_point as f32;
let block_start = block * self.block_size;
let valid = self.k.saturating_sub(block_start).min(self.block_size);
let packed_start = (output * k_blocks + block) * blob_size;
let values_start = output * self.k + block_start;
values[values_start..values_start + valid]
.copy_from_slice(&packed[packed_start..packed_start + valid]);
}
}
Ok(PackedU8Weight {
values,
scales,
scaled_zero_points,
})
}
fn dequantize_weight(
&self,
packed: &TensorView,
scales: &TensorView,
zero_points: Option<&TensorView>,
group_indices: Option<&TensorView>,
layout: WeightLayout,
) -> Result<Vec<f32>> {
let packed = to_dense_bytes(packed)?;
let scales = to_dense_compute_f32(scales)?;
let packed_zero_points = zero_points.map(to_dense_bytes).transpose()?;
let group_indices = group_indices.map(to_dense_i64).transpose()?;
let k_blocks = self.k.div_ceil(self.block_size);
if let Some(indices) = &group_indices {
for (index, &group) in indices.iter().enumerate() {
if group < 0 || group as usize >= k_blocks {
return Err(error(format!(
"g_idx[{index}]={group} is outside 0..{k_blocks}"
)));
}
}
}
let blob_size = self.block_size * self.bits / 8;
let zp_row_bytes = (k_blocks * self.bits).div_ceil(8);
let quantized_mask = if self.bits == 8 {
u8::MAX
} else {
(1u8 << self.bits) - 1
};
let default_zero_point = 1u8 << (self.bits - 1);
let mut weight_kn = vec![0.0f32; self.k * self.n];
if matches!(layout, WeightLayout::Nk) && group_indices.is_none() {
let bits = self.bits;
let block_size = self.block_size;
weight_kn
.par_chunks_mut(self.k)
.enumerate()
.for_each(|(output, row)| {
let packed_start = output * k_blocks * blob_size;
let scale_start = output * k_blocks;
let zero_point_start = output * zp_row_bytes;
let packed_row = &packed[packed_start..packed_start + k_blocks * blob_size];
let scale_row = &scales[scale_start..scale_start + k_blocks];
let zero_point_row = packed_zero_points
.as_ref()
.map(|points| &points[zero_point_start..zero_point_start + zp_row_bytes]);
dequantize_nbits_row(
packed_row,
scale_row,
zero_point_row,
row,
bits,
block_size,
);
});
return Ok(weight_kn);
}
for output in 0..self.n {
if group_indices.is_none() {
let packed_start = output * k_blocks * blob_size;
let scale_start = output * k_blocks;
let zero_point_start = output * zp_row_bytes;
let packed_row = &packed[packed_start..packed_start + k_blocks * blob_size];
let scale_row = &scales[scale_start..scale_start + k_blocks];
let zero_point_row = packed_zero_points
.as_ref()
.map(|points| &points[zero_point_start..zero_point_start + zp_row_bytes]);
for depth in 0..self.k {
let index = match layout {
WeightLayout::Kn => depth * self.n + output,
WeightLayout::Nk => output * self.k + depth,
};
weight_kn[index] = dequantize_nbits_value(
packed_row,
scale_row,
zero_point_row,
depth,
self.bits,
self.block_size,
);
}
continue;
}
for depth in 0..self.k {
let block = depth / self.block_size;
let within_block = depth % self.block_size;
let bit_offset = within_block * self.bits;
let byte = packed[(output * k_blocks + block) * blob_size + bit_offset / 8];
let quantized = (byte >> (bit_offset % 8)) & quantized_mask;
let group = group_indices
.as_ref()
.map_or(block, |indices| indices[depth] as usize);
let zero_point = packed_zero_points
.as_ref()
.map_or(default_zero_point, |points| {
let bit_offset = group * self.bits;
let byte = points[output * zp_row_bytes + bit_offset / 8];
(byte >> (bit_offset % 8)) & quantized_mask
});
let index = match layout {
WeightLayout::Kn => depth * self.n + output,
WeightLayout::Nk => output * self.k + depth,
};
weight_kn[index] =
(quantized as f32 - zero_point as f32) * scales[output * k_blocks + group];
}
}
Ok(weight_kn)
}
}
pub(super) fn dequantize_nbits_row(
packed: &[u8],
scales: &[f32],
zero_points: Option<&[u8]>,
output: &mut [f32],
bits: usize,
block_size: usize,
) {
for (depth, value) in output.iter_mut().enumerate() {
*value = dequantize_nbits_value(packed, scales, zero_points, depth, bits, block_size);
}
}
#[inline]
fn dequantize_nbits_value(
packed: &[u8],
scales: &[f32],
zero_points: Option<&[u8]>,
depth: usize,
bits: usize,
block_size: usize,
) -> f32 {
let mask = if bits == 8 {
u8::MAX
} else {
(1u8 << bits) - 1
};
let default_zero_point = 1u8 << (bits - 1);
let block = depth / block_size;
let within_block = depth % block_size;
let bit_offset = within_block * bits;
let quantized =
(packed[block * block_size * bits / 8 + bit_offset / 8] >> (bit_offset % 8)) & mask;
let zero_point = zero_points.map_or(default_zero_point, |points| {
let bit_offset = block * bits;
(points[bit_offset / 8] >> (bit_offset % 8)) & mask
});
(quantized as f32 - zero_point as f32) * scales[block]
}
fn configured_decode_threads() -> Option<usize> {
let value = std::env::var(DECODE_THREADS_ENV).ok();
let available = available_parallelism();
resolve_decode_threads_with_override(decode_threads_override(), value.as_deref(), available)
}
pub fn configured_persistent_decode_threads() -> Option<usize> {
let value = std::env::var(DECODE_THREADS_ENV).ok();
let available = available_parallelism();
resolve_persistent_decode_threads_with_override(
decode_threads_override(),
value.as_deref(),
available,
)
}
pub fn set_decode_thread_budget(threads: Option<usize>) -> std::result::Result<(), &'static str> {
if threads == Some(0) {
return Err("CPU decode thread budget must be greater than zero");
}
DECODE_THREADS_OVERRIDE.store(threads.unwrap_or(0), Ordering::Release);
Ok(())
}
fn decode_threads_override() -> Option<usize> {
std::num::NonZeroUsize::new(DECODE_THREADS_OVERRIDE.load(Ordering::Acquire))
.map(std::num::NonZeroUsize::get)
}
fn resolve_rayon_global_threads(
override_threads: Option<usize>,
raw: Option<&str>,
available: usize,
) -> Option<usize> {
let available = std::num::NonZeroUsize::new(available)?.get();
let requested = match override_threads {
Some(threads) if threads > 0 => threads,
Some(_) => return None,
None => match raw?.trim().parse::<usize>() {
Ok(0) => return None,
Ok(threads) => threads,
Err(_) => return None,
},
};
Some(requested.min(available))
}
static PROCESS_BUDGET_BOUND: OnceLock<()> = OnceLock::new();
pub fn bound_process_to_decode_budget() {
if PROCESS_BUDGET_BOUND.set(()).is_err() {
return;
}
let raw = std::env::var(DECODE_THREADS_ENV).ok();
let available = available_parallelism();
let Some(threads) =
resolve_rayon_global_threads(decode_threads_override(), raw.as_deref(), available)
else {
return;
};
#[cfg(target_os = "linux")]
if crate::decode_affinity::explicit_decode_affinity_requested() {
eprintln!(
"onnx-genai: CPU decode budget {threads} bounds the prefill/MLAS Rayon pool; \
process CPU affinity is left to the explicit ONNX_GENAI_CPU_DECODE_AFFINITY setting"
);
} else if let Some(cpus) = crate::decode_affinity::select_budget_cpus(threads) {
match crate::decode_affinity::set_current_thread_affinity(&cpus) {
Ok(()) => eprintln!(
"onnx-genai: CPU decode budget {threads} confined the process to {count} CPUs \
{cpus:?} (prefill/MLAS + decode stay off the rest of the machine)",
count = cpus.len()
),
Err(message) => eprintln!(
"onnx-genai: CPU decode budget {threads}: could not apply process CPU affinity \
({message}); the Rayon thread-count bound still applies"
),
}
}
match rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build_global()
{
Ok(()) => eprintln!(
"onnx-genai: CPU decode budget {threads} bounded the global Rayon pool \
(prefill/MLAS parallelism capped at {threads} workers)"
),
Err(err) => eprintln!(
"onnx-genai: CPU decode budget {threads}: the global Rayon pool was already built \
and cannot be resized ({err}); set the budget before the first inference to bound \
prefill/MLAS parallelism"
),
}
}
fn default_persistent_threads(available: usize) -> Option<usize> {
let available = std::num::NonZeroUsize::new(available)?.get();
Some((available / 2).max(1))
}
#[cfg(test)]
fn resolve_persistent_decode_threads(raw: Option<&str>, available: usize) -> Option<usize> {
resolve_persistent_decode_threads_with_override(None, raw, available)
}
fn resolve_persistent_decode_threads_with_override(
override_threads: Option<usize>,
raw: Option<&str>,
available: usize,
) -> Option<usize> {
let available = std::num::NonZeroUsize::new(available)?.get();
let default = default_persistent_threads(available)?;
let threads = match override_threads {
Some(threads) => threads,
None => match raw {
Some("0") => return None,
Some(raw) => raw
.parse::<usize>()
.ok()
.filter(|threads| *threads > 0)
.unwrap_or(default),
None => default,
},
};
Some(threads.min(available))
}
fn available_parallelism() -> usize {
std::thread::available_parallelism()
.map(std::num::NonZeroUsize::get)
.unwrap_or(1)
}
fn default_decode_threads(available: usize) -> Option<usize> {
let available = std::num::NonZeroUsize::new(available)?.get();
let ceil_log2 = usize::BITS as usize - (available - 1).leading_zeros() as usize;
Some(
(ceil_log2 + 1)
.min(MAX_TOPOLOGY_DECODE_THREADS)
.min(available),
)
}
#[cfg(test)]
fn resolve_decode_threads(raw: Option<&str>, available: usize) -> Option<usize> {
resolve_decode_threads_with_override(None, raw, available)
}
fn resolve_decode_threads_with_override(
override_threads: Option<usize>,
raw: Option<&str>,
available: usize,
) -> Option<usize> {
let available = std::num::NonZeroUsize::new(available)?.get();
let default = default_decode_threads(available)?;
let threads = match override_threads {
Some(threads) => threads,
None => match raw {
Some("0") => return None,
Some(raw) => raw.parse::<usize>().unwrap_or(default),
None => default,
},
};
(threads > 0).then(|| threads.min(available))
}
fn default_dense_decode_threads(available: usize) -> Option<usize> {
let available = std::num::NonZeroUsize::new(available)?.get();
let scaled = (available / 4).max(1);
Some(
scaled
.clamp(8.min(available), MAX_DENSE_DECODE_THREADS)
.min(available),
)
}
#[cfg(test)]
fn resolve_dense_decode_threads(raw: Option<&str>, available: usize) -> Option<usize> {
resolve_dense_decode_threads_with_override(None, raw, available)
}
fn resolve_dense_decode_threads_with_override(
override_threads: Option<usize>,
raw: Option<&str>,
available: usize,
) -> Option<usize> {
let available = std::num::NonZeroUsize::new(available)?.get();
let default = default_dense_decode_threads(available)?;
let threads = match override_threads {
Some(threads) => threads,
None => match raw {
Some("0") => return None,
Some(raw) => raw
.parse::<usize>()
.ok()
.filter(|threads| *threads > 0)
.unwrap_or(default),
None => default,
},
};
Some(threads.min(available))
}
fn configured_dense_decode_threads() -> Option<usize> {
let value = std::env::var(DECODE_THREADS_ENV).ok();
let available = available_parallelism();
resolve_dense_decode_threads_with_override(
decode_threads_override(),
value.as_deref(),
available,
)
}
#[cfg(feature = "mlas")]
fn default_sqnbit_decode_min(available: usize) -> usize {
default_decode_threads(available)
.unwrap_or(1)
.saturating_mul(2)
}
fn build_decode_pool(
threads: Option<usize>,
) -> std::result::Result<Option<rayon::ThreadPool>, String> {
threads
.map(|threads| {
let affinity_cpus = decode_affinity_cpus(threads)?;
let mut builder = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.thread_name(|index| format!("onnx-genai-decode-{index}"));
if let Some(cpus) = affinity_cpus {
builder = builder.start_handler(move |worker_index| {
let cpu = cpus[worker_index % cpus.len()];
if let Err(message) = crate::decode_affinity::pin_current_thread_to_cpu(cpu) {
report_decode_affinity_failure(&message);
}
});
}
builder
.build()
.map_err(|err| format!("failed to build {DECODE_THREADS_ENV} pool: {err}"))
})
.transpose()
}
fn decode_affinity_cpus(threads: usize) -> std::result::Result<Option<Vec<usize>>, String> {
let plan = crate::decode_affinity::plan_decode_affinity(threads)?;
if let Some(message) = plan.log {
report_decode_affinity_policy(&message);
}
Ok(plan.cpus)
}
fn report_decode_affinity_policy(message: &str) {
static REPORTED: OnceLock<()> = OnceLock::new();
if REPORTED.set(()).is_ok() {
eprintln!("onnx-genai: decode affinity policy: {message}");
}
}
fn report_decode_affinity_failure(message: &str) {
static REPORTED: OnceLock<()> = OnceLock::new();
if REPORTED.set(()).is_ok() {
eprintln!(
"onnx-genai: decode-pool CPU affinity unavailable; \
continuing without pinning ({message})"
);
}
}
fn with_decode_pool<T: Send>(operation: impl FnOnce() -> T + Send) -> Result<T> {
if IN_DECODE_POOL.with(Cell::get) {
return Ok(operation());
}
match DECODE_POOL.get_or_init(|| build_decode_pool(configured_decode_threads())) {
Ok(Some(pool)) => Ok(pool.install(operation)),
Ok(None) => Ok(operation()),
Err(message) => Err(error(message.clone())),
}
}
thread_local! {
static IN_DECODE_POOL: Cell<bool> = const { Cell::new(false) };
static IN_NUMA_SCOPE: Cell<bool> = const { Cell::new(false) };
static IN_SPMD_SCOPE: Cell<bool> = const { Cell::new(false) };
}
fn numa_pools() -> Option<&'static crate::decode_numa::NumaDecodePools> {
static NUMA_POOLS: OnceLock<Option<crate::decode_numa::NumaDecodePools>> = OnceLock::new();
NUMA_POOLS
.get_or_init(|| crate::decode_numa::build_from_env(configured_persistent_decode_threads()))
.as_ref()
}
fn numa_decode_active() -> Option<&'static crate::decode_numa::NumaDecodePools> {
if IN_NUMA_SCOPE.with(Cell::get) {
numa_pools()
} else {
None
}
}
fn spmd_decode_active() -> Option<&'static crate::decode_spmd::SpmdDecodePools> {
if IN_SPMD_SCOPE.with(Cell::get) {
crate::decode_spmd::pools()
} else {
None
}
}
#[cfg(test)]
static SPMD_TEST_DISPATCHES: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
fn parallel_output_rows<F>(result: &mut [f32], k: usize, compute: F)
where
F: Fn(usize, &mut [f32]) + Sync,
{
if let Some(numa) = numa_decode_active() {
numa.dispatch_output_rows(result, k, &compute);
return;
}
if let Some(spmd) = spmd_decode_active() {
#[cfg(test)]
SPMD_TEST_DISPATCHES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
spmd.dispatch_output_rows(result, k, &compute);
return;
}
let chunk = output_chunk_len(result.len(), k);
if chunk < result.len() {
result
.par_chunks_mut(chunk)
.enumerate()
.for_each(|(chunk_index, outputs)| compute(chunk_index * chunk, outputs));
} else {
compute(0, result);
}
}
pub fn decode_parallel_output_row_blocks<F>(
result: &mut [f32],
row_len: usize,
num_rows: usize,
compute: F,
) where
F: Fn(usize, &mut [f32]) + Sync,
{
if let Some(spmd) = spmd_decode_active() {
spmd.dispatch_output_row_blocks(result, row_len, num_rows, &compute);
return;
}
result.par_chunks_mut(row_len).enumerate().for_each_init(
|| crate::trace::worker_span("MatMulNBits.decode_rows"),
|_span, (row_index, row)| compute(row_index, row),
);
}
pub fn decode_parallel_index_tasks<F>(num_tasks: usize, compute: F)
where
F: Fn(usize) + Sync + Send,
{
if let Some(spmd) = spmd_decode_active() {
spmd.dispatch_index_tasks(num_tasks, &compute);
return;
}
(0..num_tasks).into_par_iter().for_each(compute);
}
pub fn active_decode_worker_count() -> usize {
if let Some(numa) = numa_decode_active() {
return numa.total_workers();
}
if let Some(spmd) = spmd_decode_active() {
return spmd.total_workers();
}
rayon::current_num_threads()
}
fn numa_place_int4(weight: PackedInt4Weight, n: usize) -> PackedInt4Weight {
if let Some(numa) = numa_decode_active() {
return PackedInt4Weight {
values: numa.place_rows(&weight.values, n),
scales: numa.place_rows(&weight.scales, n),
};
}
if let Some(spmd) = spmd_decode_active() {
return PackedInt4Weight {
values: spmd.place_rows(&weight.values, n),
scales: spmd.place_rows(&weight.scales, n),
};
}
weight
}
fn numa_place_int8(weight: Int8Weight, n: usize) -> Int8Weight {
if let Some(numa) = numa_decode_active() {
return Int8Weight {
values: numa.place_rows(&weight.values, n),
scales: numa.place_rows(&weight.scales, n),
block_sums: numa.place_rows(&weight.block_sums, n),
};
}
if let Some(spmd) = spmd_decode_active() {
return Int8Weight {
values: spmd.place_rows(&weight.values, n),
scales: spmd.place_rows(&weight.scales, n),
block_sums: spmd.place_rows(&weight.block_sums, n),
};
}
weight
}
fn numa_place_u8(weight: PackedU8Weight, n: usize) -> PackedU8Weight {
if let Some(numa) = numa_decode_active() {
return PackedU8Weight {
values: numa.place_rows(&weight.values, n),
scales: numa.place_rows(&weight.scales, n),
scaled_zero_points: numa.place_rows(&weight.scaled_zero_points, n),
};
}
if let Some(spmd) = spmd_decode_active() {
return PackedU8Weight {
values: spmd.place_rows(&weight.values, n),
scales: spmd.place_rows(&weight.scales, n),
scaled_zero_points: spmd.place_rows(&weight.scaled_zero_points, n),
};
}
weight
}
fn numa_place_nk(weight: Vec<f32>, n: usize) -> Vec<f32> {
if let Some(numa) = numa_decode_active() {
return numa.place_rows(&weight, n);
}
if let Some(spmd) = spmd_decode_active() {
return spmd.place_rows(&weight, n);
}
weight
}
struct NumaScopeGuard {
previous: bool,
}
impl NumaScopeGuard {
fn enter() -> Self {
let previous = IN_NUMA_SCOPE.with(|flag| flag.replace(true));
Self { previous }
}
}
impl Drop for NumaScopeGuard {
fn drop(&mut self) {
let previous = self.previous;
IN_NUMA_SCOPE.with(|flag| flag.set(previous));
}
}
struct SpmdScopeGuard {
previous: bool,
}
impl SpmdScopeGuard {
fn enter() -> Self {
let previous = IN_SPMD_SCOPE.with(|flag| flag.replace(true));
Self { previous }
}
}
impl Drop for SpmdScopeGuard {
fn drop(&mut self) {
let previous = self.previous;
IN_SPMD_SCOPE.with(|flag| flag.set(previous));
}
}
struct DecodeResidencyGuard {
previous: bool,
}
impl DecodeResidencyGuard {
fn enter() -> Self {
let previous = IN_DECODE_POOL.with(|flag| flag.replace(true));
Self { previous }
}
}
impl Drop for DecodeResidencyGuard {
fn drop(&mut self) {
let previous = self.previous;
IN_DECODE_POOL.with(|flag| flag.set(previous));
}
}
pub fn with_decode_pool_scope<R: Send>(
model_uses_spmd_pool: bool,
f: impl FnOnce() -> R + Send,
) -> R {
let spmd_pool_eligible = model_uses_spmd_pool || crate::decode_spmd::is_forced();
if !spmd_pool_eligible {
return with_dense_decode_pool_scope(f);
}
let both_requested = crate::decode_spmd::is_forced()
&& std::env::var(crate::decode_affinity::DECODE_AFFINITY_ENV)
.is_ok_and(|value| value.trim() == "numa-split");
if let Some(numa) = numa_pools() {
if both_requested {
report_decode_strategy_precedence(
"ONNX_GENAI_CPU_DECODE_AFFINITY=numa-split and the forced persistent \
SPMD decode pool (ONNX_GENAI_CPU_DECODE_PERSISTENT_POOL=1) are mutually \
exclusive; numa-split is active because it has precedence and its two-level \
NUMA layout was built successfully. Unset \
ONNX_GENAI_CPU_DECODE_PERSISTENT_POOL to silence this if intentional",
);
}
return numa.install_scope(move || {
let _numa_guard = NumaScopeGuard::enter();
let _decode_guard = DecodeResidencyGuard::enter();
f()
});
}
if crate::decode_spmd::pools().is_some() {
if both_requested {
report_decode_strategy_precedence(
"ONNX_GENAI_CPU_DECODE_AFFINITY=numa-split and the forced persistent \
SPMD decode pool (ONNX_GENAI_CPU_DECODE_PERSISTENT_POOL=1) are mutually \
exclusive; persistent SPMD is active because the higher-precedence \
numa-split layout was unavailable",
);
}
if crate::decode_spmd::is_forced() {
return with_spmd_decode_scope(f);
}
return with_auto_calibrated_decode_scope(f);
}
if both_requested {
report_decode_strategy_precedence(
"ONNX_GENAI_CPU_DECODE_AFFINITY=numa-split and the forced persistent SPMD \
decode pool (ONNX_GENAI_CPU_DECODE_PERSISTENT_POOL=1) are mutually exclusive; \
neither strategy is active because no bounded decode worker count or usable \
numa-split layout is available",
);
}
with_flat_decode_pool_scope(f)
}
fn with_spmd_decode_scope<R: Send>(f: impl FnOnce() -> R + Send) -> R {
let _spmd_guard = SpmdScopeGuard::enter();
let _decode_guard = DecodeResidencyGuard::enter();
f()
}
fn with_flat_decode_pool_scope<R: Send>(f: impl FnOnce() -> R + Send) -> R {
match DECODE_POOL.get_or_init(|| build_decode_pool(configured_decode_threads())) {
Ok(Some(pool)) => pool.install(move || {
let _guard = DecodeResidencyGuard::enter();
f()
}),
_ => f(),
}
}
fn with_auto_calibrated_decode_scope<R: Send>(f: impl FnOnce() -> R + Send) -> R {
use crate::decode_spmd::AutoPath;
let path = crate::decode_spmd::auto_choose_path();
let start = std::time::Instant::now();
let result = match path {
AutoPath::Pool => with_spmd_decode_scope(f),
AutoPath::Flat => with_flat_decode_pool_scope(f),
};
crate::decode_spmd::auto_record_sample(path, start.elapsed());
result
}
fn with_dense_decode_pool_scope<R: Send>(f: impl FnOnce() -> R + Send) -> R {
match DENSE_DECODE_POOL.get_or_init(|| build_decode_pool(configured_dense_decode_threads())) {
Ok(Some(pool)) => pool.install(move || {
let _guard = DecodeResidencyGuard::enter();
f()
}),
_ => f(),
}
}
fn report_decode_strategy_precedence(message: &str) {
static REPORTED: OnceLock<()> = OnceLock::new();
if REPORTED.set(()).is_ok() {
eprintln!("onnx-genai: decode strategy selection: {message}");
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DotKernel {
#[cfg_attr(target_arch = "aarch64", allow(dead_code))]
Scalar,
#[cfg(target_arch = "x86_64")]
Avx2,
#[cfg(target_arch = "x86_64")]
AvxVnni,
#[cfg(target_arch = "x86_64")]
Avx512Vnni,
#[cfg(target_arch = "aarch64")]
Neon,
}
impl DotKernel {
fn uses_vnni_int4_direct(self) -> bool {
#[cfg(target_arch = "x86_64")]
{
matches!(self, DotKernel::AvxVnni | DotKernel::Avx512Vnni)
}
#[cfg(not(target_arch = "x86_64"))]
{
false
}
}
}
fn selected_dot_kernel() -> DotKernel {
#[cfg(target_arch = "x86_64")]
{
if std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("avx512f")
&& std::arch::is_x86_feature_detected!("avx512bw")
&& std::arch::is_x86_feature_detected!("avx512vnni")
&& std::arch::is_x86_feature_detected!("avx512vl")
{
return DotKernel::Avx512Vnni;
}
if std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("avxvnni")
{
return DotKernel::AvxVnni;
}
if std::arch::is_x86_feature_detected!("avx2") {
return DotKernel::Avx2;
}
}
#[cfg(target_arch = "aarch64")]
{
DotKernel::Neon
}
#[cfg(not(target_arch = "aarch64"))]
{
DotKernel::Scalar
}
}
#[cfg(feature = "mlas")]
fn host_has_mlas_sqnbit_avx512() -> bool {
#[cfg(target_arch = "x86_64")]
{
std::arch::is_x86_feature_detected!("avx512f")
&& std::arch::is_x86_feature_detected!("avx512bw")
&& std::arch::is_x86_feature_detected!("avx512dq")
&& std::arch::is_x86_feature_detected!("avx512vl")
}
#[cfg(not(target_arch = "x86_64"))]
{
false
}
}
fn quantize_activation_signed(
activation: &[f32],
padded_k: usize,
block_size: usize,
) -> (Vec<i8>, Vec<f32>) {
let k_blocks = padded_k / block_size;
let mut quantized = vec![0i8; padded_k];
let mut scales = vec![0.0f32; k_blocks];
for (block, (out_block, scale)) in quantized
.chunks_mut(block_size)
.zip(scales.iter_mut())
.enumerate()
{
let start = block * block_size;
let real_end = (start + block_size).min(activation.len());
if real_end <= start {
continue;
}
let src = &activation[start..real_end];
*scale = crate::kernels::simd_quant::quantize_block_i8(src, &mut out_block[..src.len()]);
}
(quantized, scales)
}
fn int4_matmul_m1(
activation: &[f32],
weight: &PackedInt4Weight,
result: &mut [f32],
k: usize,
n: usize,
dot_kernel: DotKernel,
) {
const BLOCK_SIZE: usize = 32;
const PACKED_BLOCK_SIZE: usize = BLOCK_SIZE / 2;
let k_blocks = k.div_ceil(BLOCK_SIZE);
let padded_k = k_blocks * BLOCK_SIZE;
debug_assert_eq!(activation.len(), k);
debug_assert_eq!(weight.values.len(), n * k_blocks * PACKED_BLOCK_SIZE);
debug_assert_eq!(weight.scales.len(), n * k_blocks);
debug_assert_eq!(result.len(), n);
let (activation, activation_scales) =
quantize_activation_signed(activation, padded_k, BLOCK_SIZE);
#[cfg(target_arch = "x86_64")]
let use_simd = dot_kernel.uses_vnni_int4_direct();
#[cfg(not(target_arch = "x86_64"))]
let use_simd = false;
let deinterleaved;
let activation: &[i8] = if use_simd {
deinterleaved = deinterleave_activation_int4(&activation);
&deinterleaved
} else {
&activation
};
#[cfg(target_arch = "x86_64")]
let precompute_act_sums = matches!(dot_kernel, DotKernel::Avx512Vnni);
#[cfg(not(target_arch = "x86_64"))]
let precompute_act_sums = false;
let act_sum8: Vec<i32> = if precompute_act_sums {
activation_block_sums8(activation, k_blocks)
} else {
Vec::new()
};
let compute = |output_start: usize, outputs: &mut [f32]| {
for (offset, output) in outputs.iter_mut().enumerate() {
let output_index = output_start + offset;
let packed_start = output_index * k_blocks * PACKED_BLOCK_SIZE;
let packed_end = packed_start + k_blocks * PACKED_BLOCK_SIZE;
let scale_start = output_index * k_blocks;
let scale_end = scale_start + k_blocks;
*output = int4_dot_row(
activation,
&weight.values[packed_start..packed_end],
&weight.scales[scale_start..scale_end],
&activation_scales,
&act_sum8,
dot_kernel,
);
}
};
parallel_output_rows(result, padded_k, compute);
}
fn deinterleave_activation_int4(activation: &[i8]) -> Vec<i8> {
debug_assert_eq!(activation.len() % 32, 0);
let mut out = vec![0i8; activation.len()];
for (block_in, block_out) in activation.chunks_exact(32).zip(out.chunks_exact_mut(32)) {
for i in 0..16 {
block_out[i] = block_in[2 * i];
block_out[16 + i] = block_in[2 * i + 1];
}
}
out
}
fn activation_block_sums8(activation: &[i8], k_blocks: usize) -> Vec<i32> {
debug_assert!(activation.len() >= k_blocks * 32);
let mut sums = vec![0i32; k_blocks * 8];
for block in 0..k_blocks {
let base = block * 32;
for lane in 0..8 {
let o = base + lane * 4;
let sum = activation[o] as i32
+ activation[o + 1] as i32
+ activation[o + 2] as i32
+ activation[o + 3] as i32;
sums[block * 8 + lane] = sum << 3;
}
}
sums
}
fn int4_dot_row(
activation: &[i8],
packed_weight: &[u8],
scales: &[f32],
activation_scales: &[f32],
act_sum8: &[i32],
_kernel: DotKernel,
) -> f32 {
#[cfg(target_arch = "x86_64")]
{
match _kernel {
DotKernel::Avx2 => {}
DotKernel::AvxVnni => {
return unsafe {
int4_dot_row_avxvnni(activation, packed_weight, scales, activation_scales)
};
}
DotKernel::Avx512Vnni => {
return unsafe {
int4_dot_row_avx512vnni(
activation,
packed_weight,
scales,
activation_scales,
act_sum8,
)
};
}
DotKernel::Scalar => {}
}
}
#[cfg(not(target_arch = "x86_64"))]
let _ = act_sum8;
int4_dot_row_scalar(activation, packed_weight, scales, activation_scales)
}
fn int4_dot_row_scalar(
activation: &[i8],
packed_weight: &[u8],
scales: &[f32],
activation_scales: &[f32],
) -> f32 {
debug_assert_eq!(activation.len(), scales.len() * 32);
debug_assert_eq!(packed_weight.len(), scales.len() * 16);
debug_assert_eq!(activation_scales.len(), scales.len());
let mut value = 0.0f32;
for (block, &scale) in scales.iter().enumerate() {
let activation = &activation[block * 32..(block + 1) * 32];
let packed = &packed_weight[block * 16..(block + 1) * 16];
let mut dot = 0i32;
for (pair, &byte) in packed.iter().enumerate() {
dot += activation[pair * 2] as i32 * (i32::from(byte & 0x0f) - 8);
dot += activation[pair * 2 + 1] as i32 * (i32::from(byte >> 4) - 8);
}
value += dot as f32 * (scale * activation_scales[block]);
}
value
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,avxvnni")]
unsafe fn int4_dot_row_avxvnni(
activation: &[i8],
packed_weight: &[u8],
scales: &[f32],
activation_scales: &[f32],
) -> f32 {
use std::arch::x86_64::*;
let mut acc0 = _mm256_setzero_ps();
let mut acc1 = _mm256_setzero_ps();
let low_mask = _mm_set1_epi8(0x0f);
let zero_point = _mm256_set1_epi8(8);
let block_scaled = |block: usize| -> __m256 {
let packed = unsafe { _mm_loadu_si128(packed_weight.as_ptr().add(block * 16).cast()) };
let low = _mm_and_si128(packed, low_mask);
let high = _mm_and_si128(_mm_srli_epi16(packed, 4), low_mask);
let weight = _mm256_sub_epi8(_mm256_set_m128i(high, low), zero_point);
let activation = unsafe { _mm256_loadu_si256(activation.as_ptr().add(block * 32).cast()) };
let absolute_weight = _mm256_sign_epi8(weight, weight);
let signed_activation = _mm256_sign_epi8(activation, weight);
let dot =
_mm256_dpbusd_avx_epi32(_mm256_setzero_si256(), absolute_weight, signed_activation);
let block_scale = scales[block] * activation_scales[block];
_mm256_mul_ps(_mm256_cvtepi32_ps(dot), _mm256_set1_ps(block_scale))
};
let block_count = scales.len();
let mut block = 0usize;
while block + 2 <= block_count {
acc0 = _mm256_add_ps(acc0, block_scaled(block));
acc1 = _mm256_add_ps(acc1, block_scaled(block + 1));
block += 2;
}
if block < block_count {
acc0 = _mm256_add_ps(acc0, block_scaled(block));
}
horizontal_sum_f32_256(_mm256_add_ps(acc0, acc1))
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,avx512f,avx512bw,avx512vnni,avx512vl")]
#[inline]
unsafe fn int4_pair_scaled_avx512(
activation: &[i8],
packed_weight: &[u8],
scales: &[f32],
activation_scales: &[f32],
act_sum8: &[i32],
b0: usize,
) -> std::arch::x86_64::__m512 {
use std::arch::x86_64::*;
let low_mask256 = _mm256_set1_epi8(0x0f);
let perm_idx = _mm512_set_epi64(11, 10, 3, 2, 9, 8, 1, 0);
let b1 = b0 + 1;
let packed = unsafe { _mm256_loadu_si256(packed_weight.as_ptr().add(b0 * 16).cast()) };
let low = _mm256_and_si256(packed, low_mask256);
let high = _mm256_and_si256(_mm256_srli_epi16(packed, 4), low_mask256);
let weight = _mm512_permutex2var_epi64(
_mm512_castsi256_si512(low),
perm_idx,
_mm512_castsi256_si512(high),
);
let act = unsafe { _mm512_loadu_si512(activation.as_ptr().add(b0 * 32).cast()) };
let wdot = _mm512_dpbusd_epi32(_mm512_setzero_si512(), weight, act);
let asum8 = unsafe { _mm512_loadu_si512(act_sum8.as_ptr().add(b0 * 8).cast()) };
let dot = _mm512_sub_epi32(wdot, asum8);
let s0 = scales[b0] * activation_scales[b0];
let s1 = scales[b1] * activation_scales[b1];
let scale_vec = _mm512_set_ps(
s1, s1, s1, s1, s1, s1, s1, s1, s0, s0, s0, s0, s0, s0, s0, s0,
);
_mm512_mul_ps(_mm512_cvtepi32_ps(dot), scale_vec)
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,avx512f,avx512bw,avx512vnni,avx512vl")]
unsafe fn int4_dot_row_avx512vnni(
activation: &[i8],
packed_weight: &[u8],
scales: &[f32],
activation_scales: &[f32],
act_sum8: &[i32],
) -> f32 {
use std::arch::x86_64::*;
let low_mask = _mm_set1_epi8(0x0f);
let block_weight = |block: usize| -> __m256i {
let packed = unsafe { _mm_loadu_si128(packed_weight.as_ptr().add(block * 16).cast()) };
let low = _mm_and_si128(packed, low_mask);
let high = _mm_and_si128(_mm_srli_epi16(packed, 4), low_mask);
_mm256_set_m128i(high, low)
};
let block_count = scales.len();
let pairs = block_count / 2;
let mut acc0 = _mm512_setzero_ps();
let mut acc1 = _mm512_setzero_ps();
let mut acc2 = _mm512_setzero_ps();
let mut acc3 = _mm512_setzero_ps();
let mut pair = 0usize;
while pair + 4 <= pairs {
unsafe {
_mm_prefetch(
packed_weight.as_ptr().add((pair + 4) * 2 * 16).cast(),
_MM_HINT_T0,
);
}
acc0 = _mm512_add_ps(acc0, unsafe {
int4_pair_scaled_avx512(
activation,
packed_weight,
scales,
activation_scales,
act_sum8,
pair * 2,
)
});
acc1 = _mm512_add_ps(acc1, unsafe {
int4_pair_scaled_avx512(
activation,
packed_weight,
scales,
activation_scales,
act_sum8,
(pair + 1) * 2,
)
});
acc2 = _mm512_add_ps(acc2, unsafe {
int4_pair_scaled_avx512(
activation,
packed_weight,
scales,
activation_scales,
act_sum8,
(pair + 2) * 2,
)
});
acc3 = _mm512_add_ps(acc3, unsafe {
int4_pair_scaled_avx512(
activation,
packed_weight,
scales,
activation_scales,
act_sum8,
(pair + 3) * 2,
)
});
pair += 4;
}
while pair < pairs {
acc0 = _mm512_add_ps(acc0, unsafe {
int4_pair_scaled_avx512(
activation,
packed_weight,
scales,
activation_scales,
act_sum8,
pair * 2,
)
});
pair += 1;
}
let accumulator = _mm512_add_ps(_mm512_add_ps(acc0, acc1), _mm512_add_ps(acc2, acc3));
let mut value = _mm512_reduce_add_ps(accumulator);
if block_count % 2 == 1 {
let block = block_count - 1;
let weight = block_weight(block);
let act = unsafe { _mm256_loadu_si256(activation.as_ptr().add(block * 32).cast()) };
let wdot = _mm256_dpbusd_epi32(_mm256_setzero_si256(), weight, act);
let asum8 = unsafe { _mm256_loadu_si256(act_sum8.as_ptr().add(block * 8).cast()) };
let dot = _mm256_sub_epi32(wdot, asum8);
let block_scale = scales[block] * activation_scales[block];
let scaled = _mm256_mul_ps(_mm256_cvtepi32_ps(dot), _mm256_set1_ps(block_scale));
value += horizontal_sum_f32_256(scaled);
}
value
}
#[cfg(target_arch = "x86_64")]
fn horizontal_sum_f32_256(value: std::arch::x86_64::__m256) -> f32 {
let lanes: [f32; 8] = unsafe { std::mem::transmute(value) };
lanes.into_iter().sum()
}
fn quantize_activation(
activation: &[f32],
padded_k: usize,
block_size: usize,
) -> (Vec<u8>, Vec<f32>) {
let k_blocks = padded_k / block_size;
let mut quantized = vec![128u8; padded_k];
let mut scales = vec![0.0f32; k_blocks];
for (block, (out_block, scale)) in quantized
.chunks_mut(block_size)
.zip(scales.iter_mut())
.enumerate()
{
let start = block * block_size;
let real_end = (start + block_size).min(activation.len());
if real_end <= start {
continue;
}
let src = &activation[start..real_end];
*scale =
crate::kernels::simd_quant::quantize_block_u8_offset(src, &mut out_block[..src.len()]);
}
(quantized, scales)
}
fn int8_matmul(
activations: &[f32],
weight: &Int8Weight,
result: &mut [f32],
m: usize,
k: usize,
n: usize,
block_size: usize,
dot_kernel: DotKernel,
) {
let k_blocks = k.div_ceil(block_size);
let padded_k = k_blocks * block_size;
debug_assert_eq!(weight.values.len(), n * padded_k);
debug_assert_eq!(weight.scales.len(), n * k_blocks);
debug_assert_eq!(weight.block_sums.len(), n * k_blocks);
if m == 1 {
let (activation, activation_scales) =
quantize_activation(activations, padded_k, block_size);
int8_row(
&activation,
&activation_scales,
weight,
result,
k_blocks,
padded_k,
block_size,
dot_kernel,
true,
);
} else {
let parallel_columns =
m < rayon::current_num_threads() && output_chunk_len(n, padded_k) < n;
result
.par_chunks_mut(n)
.zip(activations.par_chunks_exact(k))
.for_each(|(output, activation)| {
let (activation, activation_scales) =
quantize_activation(activation, padded_k, block_size);
int8_row(
&activation,
&activation_scales,
weight,
output,
k_blocks,
padded_k,
block_size,
dot_kernel,
parallel_columns,
);
});
}
}
#[allow(clippy::too_many_arguments)]
fn int8_row(
activation: &[u8],
activation_scales: &[f32],
weight: &Int8Weight,
result: &mut [f32],
k_blocks: usize,
padded_k: usize,
block_size: usize,
dot_kernel: DotKernel,
parallel: bool,
) {
let compute = |output_start: usize, outputs: &mut [f32]| {
for (offset, output) in outputs.iter_mut().enumerate() {
let output_index = output_start + offset;
let mut value = 0.0f32;
let weight_row = &weight.values[output_index * padded_k..(output_index + 1) * padded_k];
let block_sums =
&weight.block_sums[output_index * k_blocks..(output_index + 1) * k_blocks];
let weight_scales =
&weight.scales[output_index * k_blocks..(output_index + 1) * k_blocks];
for (block, &activation_scale) in activation_scales.iter().enumerate() {
let start = block * block_size;
let end = start + block_size;
let unsigned_dot =
dot_u8_i8(&activation[start..end], &weight_row[start..end], dot_kernel);
let signed_dot = unsigned_dot - 128 * block_sums[block];
value += signed_dot as f32 * (activation_scale * weight_scales[block]);
}
*output = value;
}
};
let chunk = output_chunk_len(result.len(), padded_k);
if parallel && chunk < result.len() {
parallel_output_rows(result, padded_k, compute);
} else {
compute(0, result);
}
}
#[cfg(target_arch = "x86_64")]
mod amx {
use std::arch::asm;
use std::sync::OnceLock;
use rayon::prelude::*;
use super::{Int8Weight, quantize_activation};
pub(super) const AMX_PREFILL_MIN_M: usize = 16;
#[repr(C, align(64))]
struct TileConfig {
palette: u8,
start_row: u8,
reserved: [u8; 14],
colsb: [u16; 16],
rows: [u8; 16],
}
impl TileConfig {
fn new(ksub: usize) -> Self {
let mut cfg = TileConfig {
palette: 1,
start_row: 0,
reserved: [0; 14],
colsb: [0; 16],
rows: [0; 16],
};
cfg.rows[0] = 16;
cfg.colsb[0] = 64;
cfg.rows[1] = 16;
cfg.colsb[1] = ksub as u16;
cfg.rows[2] = (ksub / 4) as u8;
cfg.colsb[2] = 64;
cfg
}
}
fn request_amx_tile_permission() -> bool {
const SYS_ARCH_PRCTL: i64 = 158;
const ARCH_REQ_XCOMP_PERM: i64 = 0x1023;
const XFEATURE_XTILEDATA: i64 = 18;
let ret: i64;
unsafe {
asm!(
"syscall",
inlateout("rax") SYS_ARCH_PRCTL => ret,
in("rdi") ARCH_REQ_XCOMP_PERM,
in("rsi") XFEATURE_XTILEDATA,
out("rcx") _,
out("r11") _,
options(nostack),
);
}
ret == 0
}
pub(super) fn amx_int8_available() -> bool {
static AVAILABLE: OnceLock<bool> = OnceLock::new();
*AVAILABLE.get_or_init(|| {
let leaf7 = std::arch::x86_64::__cpuid_count(7, 0);
let has_amx_tile = (leaf7.edx >> 24) & 1 == 1;
let has_amx_int8 = (leaf7.edx >> 25) & 1 == 1;
has_amx_tile && has_amx_int8 && request_amx_tile_permission()
})
}
pub(super) fn amx_block_size_supported(block_size: usize) -> bool {
let ksub = block_size.min(64);
ksub >= 4 && ksub.is_multiple_of(4) && block_size.is_multiple_of(ksub)
}
#[inline]
#[allow(clippy::too_many_arguments)]
unsafe fn amx_block(
a_ptr: *const u8,
a_stride: u64,
a_advance: u64,
b_ptr: *const i8,
b_advance: u64,
steps: u64,
c_buf: *mut i32,
) {
unsafe {
asm!(
"tilezero tmm0",
"2:",
"tileloadd tmm1, [{a} + {a_stride}]",
"tileloadd tmm2, [{b} + {b_stride}]",
"tdpbusd tmm0, tmm1, tmm2",
"add {a}, {a_advance}",
"add {b}, {b_advance}",
"dec {steps}",
"jnz 2b",
"tilestored [{c} + {c_stride}], tmm0",
a = inout(reg) a_ptr => _,
b = inout(reg) b_ptr => _,
steps = inout(reg) steps => _,
a_stride = in(reg) a_stride,
b_stride = in(reg) 64u64,
a_advance = in(reg) a_advance,
b_advance = in(reg) b_advance,
c = in(reg) c_buf,
c_stride = in(reg) 64u64,
out("tmm0") _,
out("tmm1") _,
out("tmm2") _,
options(nostack),
);
}
}
fn pack_b_vnni4(weight: &Int8Weight, n: usize, padded_k: usize) -> Vec<i8> {
let n_tiles = n.div_ceil(16);
let n_tile_stride = (padded_k / 4) * 64;
let mut packed = vec![0i8; n_tiles * n_tile_stride];
for col in 0..n {
let n_tile = col / 16;
let nn = col % 16;
let src = &weight.values[col * padded_k..(col + 1) * padded_k];
let base = n_tile * n_tile_stride + nn * 4;
for (k, &value) in src.iter().enumerate() {
packed[base + (k / 4) * 64 + (k % 4)] = value;
}
}
packed
}
pub(super) fn int8_matmul_amx(
activations: &[f32],
weight: &Int8Weight,
result: &mut [f32],
m: usize,
k: usize,
n: usize,
block_size: usize,
) {
let k_blocks = k.div_ceil(block_size);
let padded_k = k_blocks * block_size;
debug_assert_eq!(result.len(), m * n);
debug_assert_eq!(weight.values.len(), n * padded_k);
debug_assert_eq!(weight.scales.len(), n * k_blocks);
debug_assert_eq!(weight.block_sums.len(), n * k_blocks);
let ksub = block_size.min(64);
let steps = (block_size / ksub) as u64;
let a_advance = ksub as u64;
let b_advance = (ksub / 4 * 64) as u64;
let n_tiles = n.div_ceil(16);
let n_tile_stride = (padded_k / 4) * 64;
let b_packed = pack_b_vnni4(weight, n, padded_k);
result.par_chunks_mut(16 * n).enumerate().for_each_init(
|| crate::trace::worker_span("MatMulNBits.prefill_tiles"),
|_span, (m_tile, out_rows)| {
let m0 = m_tile * 16;
let mr = out_rows.len() / n;
let mut act = vec![128u8; 16 * padded_k];
let mut act_scales = vec![0.0f32; 16 * k_blocks];
for row in 0..mr {
let (q, s) = quantize_activation(
&activations[(m0 + row) * k..(m0 + row + 1) * k],
padded_k,
block_size,
);
act[row * padded_k..(row + 1) * padded_k].copy_from_slice(&q);
act_scales[row * k_blocks..(row + 1) * k_blocks].copy_from_slice(&s);
}
let cfg = TileConfig::new(ksub);
unsafe { asm!("ldtilecfg [{0}]", in(reg) &cfg, options(nostack, readonly)) };
let mut c_buf = [0i32; 16 * 16];
for n_tile in 0..n_tiles {
let n0 = n_tile * 16;
let nr = (n - n0).min(16);
let b_tile_base = n_tile * n_tile_stride;
let mut acc = [0.0f32; 16 * 16];
for block in 0..k_blocks {
let k0 = block * block_size;
unsafe {
amx_block(
act.as_ptr().add(k0),
padded_k as u64,
a_advance,
b_packed.as_ptr().add(b_tile_base + (k0 / 4) * 64),
b_advance,
steps,
c_buf.as_mut_ptr(),
);
}
for i in 0..mr {
let activation_scale = act_scales[i * k_blocks + block];
for j in 0..nr {
let weight_index = (n0 + j) * k_blocks + block;
let signed_dot =
c_buf[i * 16 + j] - 128 * weight.block_sums[weight_index];
acc[i * 16 + j] += signed_dot as f32
* (activation_scale * weight.scales[weight_index]);
}
}
}
for i in 0..mr {
for j in 0..nr {
out_rows[i * n + n0 + j] = acc[i * 16 + j];
}
}
}
unsafe { asm!("tilerelease", options(nostack)) };
},
);
}
}
fn dot_u8_i8(activation: &[u8], weight: &[i8], _kernel: DotKernel) -> i32 {
debug_assert_eq!(activation.len(), weight.len());
#[cfg(target_arch = "x86_64")]
{
match _kernel {
DotKernel::Avx2 => {
return unsafe { dot_u8_i8_avx2(activation, weight) };
}
DotKernel::AvxVnni => {
return unsafe { dot_u8_i8_avxvnni(activation, weight) };
}
DotKernel::Avx512Vnni => {
return unsafe { dot_u8_i8_avx512vnni(activation, weight) };
}
DotKernel::Scalar => {}
}
}
#[cfg(target_arch = "aarch64")]
{
match _kernel {
DotKernel::Neon => {
return unsafe { dot_u8_i8_neon(activation, weight) };
}
DotKernel::Scalar => {}
}
}
dot_u8_i8_scalar(activation, weight)
}
fn dot_u8_i8_scalar(activation: &[u8], weight: &[i8]) -> i32 {
activation
.iter()
.zip(weight)
.map(|(&activation, &weight)| activation as i32 * weight as i32)
.sum()
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn dot_u8_i8_avx2(activation: &[u8], weight: &[i8]) -> i32 {
use std::arch::x86_64::*;
let len = activation.len();
let vector_len = len / 16 * 16;
let mut accumulator = _mm256_setzero_si256();
for index in (0..vector_len).step_by(16) {
let a8 = unsafe { _mm_loadu_si128(activation.as_ptr().add(index).cast()) };
let b8 = unsafe { _mm_loadu_si128(weight.as_ptr().add(index).cast()) };
let a16 = _mm256_cvtepu8_epi16(a8);
let b16 = _mm256_cvtepi8_epi16(b8);
accumulator = _mm256_add_epi32(accumulator, _mm256_madd_epi16(a16, b16));
}
horizontal_sum_256(accumulator)
+ dot_u8_i8_scalar(&activation[vector_len..], &weight[vector_len..])
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avxvnni")]
unsafe fn dot_u8_i8_avxvnni(activation: &[u8], weight: &[i8]) -> i32 {
use std::arch::x86_64::*;
let vector_len = activation.len() / 32 * 32;
let mut accumulator = _mm256_setzero_si256();
for index in (0..vector_len).step_by(32) {
let a = unsafe { _mm256_loadu_si256(activation.as_ptr().add(index).cast()) };
let b = unsafe { _mm256_loadu_si256(weight.as_ptr().add(index).cast()) };
accumulator = _mm256_dpbusd_avx_epi32(accumulator, a, b);
}
horizontal_sum_256(accumulator)
+ dot_u8_i8_scalar(&activation[vector_len..], &weight[vector_len..])
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,avx512f,avx512vnni,avx512vl")]
unsafe fn dot_u8_i8_avx512vnni(activation: &[u8], weight: &[i8]) -> i32 {
use std::arch::x86_64::*;
let len = activation.len();
let wide_len = len / 64 * 64;
let mut wide = _mm512_setzero_si512();
for index in (0..wide_len).step_by(64) {
let a = unsafe { _mm512_loadu_si512(activation.as_ptr().add(index).cast()) };
let b = unsafe { _mm512_loadu_si512(weight.as_ptr().add(index).cast()) };
wide = _mm512_dpbusd_epi32(wide, a, b);
}
let mut sum = _mm512_reduce_add_epi32(wide);
let mut index = wide_len;
if index + 32 <= len {
let a = unsafe { _mm256_loadu_si256(activation.as_ptr().add(index).cast()) };
let b = unsafe { _mm256_loadu_si256(weight.as_ptr().add(index).cast()) };
sum += horizontal_sum_256(_mm256_dpbusd_epi32(_mm256_setzero_si256(), a, b));
index += 32;
}
sum + dot_u8_i8_scalar(&activation[index..], &weight[index..])
}
#[cfg(target_arch = "x86_64")]
fn horizontal_sum_256(value: std::arch::x86_64::__m256i) -> i32 {
let lanes: [i32; 8] = unsafe { std::mem::transmute(value) };
lanes.into_iter().sum()
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn dot_u8_i8_neon(activation: &[u8], weight: &[i8]) -> i32 {
use std::arch::aarch64::*;
let len = activation.len();
let vector_len = len / 16 * 16;
let mut accumulator = vdupq_n_s32(0);
for index in (0..vector_len).step_by(16) {
let a = unsafe { vld1q_u8(activation.as_ptr().add(index)) };
let b = unsafe { vld1q_s8(weight.as_ptr().add(index)) };
let a_lo = vreinterpretq_s16_u16(vmovl_u8(vget_low_u8(a)));
let a_hi = vreinterpretq_s16_u16(vmovl_u8(vget_high_u8(a)));
let b_lo = vmovl_s8(vget_low_s8(b));
let b_hi = vmovl_s8(vget_high_s8(b));
accumulator = vmlal_s16(accumulator, vget_low_s16(a_lo), vget_low_s16(b_lo));
accumulator = vmlal_s16(accumulator, vget_high_s16(a_lo), vget_high_s16(b_lo));
accumulator = vmlal_s16(accumulator, vget_low_s16(a_hi), vget_low_s16(b_hi));
accumulator = vmlal_s16(accumulator, vget_high_s16(a_hi), vget_high_s16(b_hi));
}
vaddvq_s32(accumulator) + dot_u8_i8_scalar(&activation[vector_len..], &weight[vector_len..])
}
#[derive(Clone, Copy)]
enum WeightLayout {
Kn,
Nk,
}
fn gemv_nk(activation: &[f32], weight_nk: &[f32], result: &mut [f32], k: usize, n: usize) {
debug_assert_eq!(activation.len(), k);
debug_assert_eq!(weight_nk.len(), n * k);
debug_assert_eq!(result.len(), n);
let compute = |output_start: usize, outputs: &mut [f32]| {
let weights = &weight_nk[output_start * k..(output_start + outputs.len()) * k];
for (output, weight) in outputs.iter_mut().zip(weights.chunks_exact(k)) {
*output = activation.iter().zip(weight).map(|(&a, &b)| a * b).sum();
}
};
let chunk = output_chunk_len(n, k);
if chunk < n {
parallel_output_rows(result, k, compute);
} else {
compute(0, result);
}
}
#[inline]
fn dot_u8_f32(weight: &[u8], activation: &[f32]) -> f32 {
debug_assert_eq!(weight.len(), activation.len());
const LANES: usize = 16;
let mut acc = [0.0f32; LANES];
let mut weight_chunks = weight.chunks_exact(LANES);
let mut activation_chunks = activation.chunks_exact(LANES);
for (w, a) in weight_chunks.by_ref().zip(activation_chunks.by_ref()) {
for lane in 0..LANES {
acc[lane] += w[lane] as f32 * a[lane];
}
}
let mut tail = 0.0f32;
for (w, a) in weight_chunks
.remainder()
.iter()
.zip(activation_chunks.remainder())
{
tail += *w as f32 * *a;
}
tail + acc.iter().sum::<f32>()
}
#[allow(clippy::too_many_arguments)]
fn gemv_nk_u8(
activation: &[f32],
values: &[u8],
scales: &[f32],
scaled_zero_points: &[f32],
result: &mut [f32],
k: usize,
n: usize,
block_size: usize,
) {
debug_assert_eq!(activation.len(), k);
debug_assert_eq!(values.len(), n * k);
debug_assert_eq!(result.len(), n);
let k_blocks = k.div_ceil(block_size);
debug_assert_eq!(scales.len(), n * k_blocks);
debug_assert_eq!(scaled_zero_points.len(), n * k_blocks);
let mut block_activation_sums = vec![0.0f32; k_blocks];
for (block, sum) in block_activation_sums.iter_mut().enumerate() {
let start = block * block_size;
let end = (start + block_size).min(k);
*sum = activation[start..end].iter().sum();
}
let compute = |output_start: usize, outputs: &mut [f32]| {
for (index, output) in outputs.iter_mut().enumerate() {
let row = output_start + index;
let weights = &values[row * k..row * k + k];
let scale_row = &scales[row * k_blocks..row * k_blocks + k_blocks];
let zp_row = &scaled_zero_points[row * k_blocks..row * k_blocks + k_blocks];
let mut acc = 0.0f32;
for block in 0..k_blocks {
let start = block * block_size;
let end = (start + block_size).min(k);
let dot = dot_u8_f32(&weights[start..end], &activation[start..end]);
acc += scale_row[block] * dot - zp_row[block] * block_activation_sums[block];
}
*output = acc;
}
};
let chunk = output_chunk_len(n, k);
if chunk < n {
parallel_output_rows(result, k, compute);
} else {
compute(0, result);
}
}
fn eight_bit_int16_activation() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| match std::env::var("ONNX_GENAI_CPU_8BIT_ACT") {
Ok(value) => {
let value = value.trim().to_ascii_lowercase();
!matches!(value.as_str(), "fp32" | "f32" | "0" | "off" | "false")
}
Err(_) => true,
})
}
fn quantize_block_i16(activation: &[f32], out: &mut [i16]) -> f32 {
debug_assert_eq!(activation.len(), out.len());
crate::kernels::simd_quant::quantize_block_i16(activation, out)
}
fn activation_quant_group() -> usize {
static GROUP: OnceLock<usize> = OnceLock::new();
*GROUP.get_or_init(|| {
let requested = std::env::var("ONNX_GENAI_CPU_8BIT_ACT_QGROUP")
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.unwrap_or(32);
requested.max(16).div_ceil(16) * 16
})
}
#[allow(clippy::too_many_arguments)]
fn gemv_nk_u8_i16(
activation: &[f32],
values: &[u8],
scales: &[f32],
scaled_zero_points: &[f32],
result: &mut [f32],
k: usize,
n: usize,
block_size: usize,
) {
debug_assert_eq!(activation.len(), k);
debug_assert_eq!(values.len(), n * k);
debug_assert_eq!(result.len(), n);
let k_blocks = k.div_ceil(block_size);
debug_assert_eq!(scales.len(), n * k_blocks);
debug_assert_eq!(scaled_zero_points.len(), n * k_blocks);
let group = activation_quant_group().min(block_size.max(1));
let k_groups = k.div_ceil(group);
let mut quantized = vec![0i16; k];
let mut group_scales = vec![0.0f32; k_groups];
let mut block_activation_sums = vec![0.0f32; k_blocks];
#[allow(clippy::needless_range_loop)]
for grp in 0..k_groups {
let start = grp * group;
let end = (start + group).min(k);
group_scales[grp] = quantize_block_i16(&activation[start..end], &mut quantized[start..end]);
}
#[allow(clippy::needless_range_loop)]
for block in 0..k_blocks {
let start = block * block_size;
let end = (start + block_size).min(k);
block_activation_sums[block] = activation[start..end].iter().sum();
}
let compute = |output_start: usize, outputs: &mut [f32]| {
for (index, output) in outputs.iter_mut().enumerate() {
let row = output_start + index;
let weights = &values[row * k..row * k + k];
let scale_row = &scales[row * k_blocks..row * k_blocks + k_blocks];
let zp_row = &scaled_zero_points[row * k_blocks..row * k_blocks + k_blocks];
let mut acc = 0.0f32;
for block in 0..k_blocks {
let block_start = block * block_size;
let block_end = (block_start + block_size).min(k);
let first_group = block_start / group;
let last_group = (block_end - 1) / group;
let block_partial = block_dot_u8_i16(
&weights[block_start..block_end],
&quantized[block_start..block_end],
&group_scales[first_group..=last_group],
group,
);
acc +=
scale_row[block] * block_partial - zp_row[block] * block_activation_sums[block];
}
*output = acc;
}
};
let chunk = output_chunk_len(n, k);
if chunk < n {
parallel_output_rows(result, k, compute);
} else {
compute(0, result);
}
}
#[inline]
fn block_dot_u8_i16(weights: &[u8], activation: &[i16], group_scales: &[f32], group: usize) -> f32 {
debug_assert_eq!(weights.len(), activation.len());
#[cfg(target_arch = "x86_64")]
{
if have_avx512bw() {
return unsafe { block_dot_u8_i16_avx512bw(weights, activation, group_scales, group) };
}
if have_avx2() {
return unsafe { block_dot_u8_i16_avx2(weights, activation, group_scales, group) };
}
}
block_dot_u8_i16_scalar(weights, activation, group_scales, group)
}
fn block_dot_u8_i16_scalar(
weights: &[u8],
activation: &[i16],
group_scales: &[f32],
group: usize,
) -> f32 {
let mut acc = 0.0f32;
for (group_index, chunk) in weights.chunks(group).enumerate() {
let start = group_index * group;
let dot: i32 = chunk
.iter()
.zip(&activation[start..start + chunk.len()])
.map(|(&w, &a)| w as i32 * a as i32)
.sum();
acc += group_scales[group_index] * dot as f32;
}
acc
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn block_dot_u8_i16_avx2(
weights: &[u8],
activation: &[i16],
group_scales: &[f32],
group: usize,
) -> f32 {
use std::arch::x86_64::*;
let len = weights.len();
let mut acc = _mm256_setzero_ps();
let mut scalar_tail = 0.0f32;
let mut position = 0usize;
let mut group_index = 0usize;
while position < len {
let group_end = (position + group).min(len);
let group_scale = group_scales[group_index];
let mut group_acc = _mm256_setzero_si256();
let mut inner = position;
while inner + 16 <= group_end {
let weight_bytes = unsafe { _mm_loadu_si128(weights.as_ptr().add(inner).cast()) };
let weight_i16 = _mm256_cvtepu8_epi16(weight_bytes);
let activation_i16 =
unsafe { _mm256_loadu_si256(activation.as_ptr().add(inner).cast()) };
group_acc = _mm256_add_epi32(group_acc, _mm256_madd_epi16(weight_i16, activation_i16));
inner += 16;
}
acc = _mm256_add_ps(
acc,
_mm256_mul_ps(_mm256_cvtepi32_ps(group_acc), _mm256_set1_ps(group_scale)),
);
if inner < group_end {
let dot = dot_u8_i16_scalar(&weights[inner..group_end], &activation[inner..group_end]);
scalar_tail += group_scale * dot as f32;
}
position = group_end;
group_index += 1;
}
horizontal_sum_f32_256(acc) + scalar_tail
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,avx512f,avx512bw")]
unsafe fn block_dot_u8_i16_avx512bw(
weights: &[u8],
activation: &[i16],
group_scales: &[f32],
group: usize,
) -> f32 {
use std::arch::x86_64::*;
let len = weights.len();
let mut acc = _mm512_setzero_ps();
let mut scalar_tail = 0.0f32;
let mut position = 0usize;
let mut group_index = 0usize;
while position < len {
let group_end = (position + group).min(len);
let group_scale = group_scales[group_index];
let mut group_acc = _mm512_setzero_si512();
let mut inner = position;
while inner + 32 <= group_end {
let weight_bytes = unsafe { _mm256_loadu_si256(weights.as_ptr().add(inner).cast()) };
let weight_i16 = _mm512_cvtepu8_epi16(weight_bytes);
let activation_i16 =
unsafe { _mm512_loadu_si512(activation.as_ptr().add(inner).cast()) };
group_acc = _mm512_add_epi32(group_acc, _mm512_madd_epi16(weight_i16, activation_i16));
inner += 32;
}
acc = _mm512_add_ps(
acc,
_mm512_mul_ps(_mm512_cvtepi32_ps(group_acc), _mm512_set1_ps(group_scale)),
);
if inner + 16 <= group_end {
let weight_bytes = unsafe { _mm_loadu_si128(weights.as_ptr().add(inner).cast()) };
let weight_i16 = _mm256_cvtepu8_epi16(weight_bytes);
let activation_i16 =
unsafe { _mm256_loadu_si256(activation.as_ptr().add(inner).cast()) };
let partial = _mm256_madd_epi16(weight_i16, activation_i16);
let dot = horizontal_sum_256(partial);
scalar_tail += group_scale * dot as f32;
inner += 16;
}
if inner < group_end {
let dot = dot_u8_i16_scalar(&weights[inner..group_end], &activation[inner..group_end]);
scalar_tail += group_scale * dot as f32;
}
position = group_end;
group_index += 1;
}
horizontal_sum_f32_512(acc) + scalar_tail
}
#[cfg(target_arch = "x86_64")]
fn horizontal_sum_f32_512(value: std::arch::x86_64::__m512) -> f32 {
let lanes: [f32; 16] = unsafe { std::mem::transmute(value) };
lanes.into_iter().sum()
}
#[cfg(target_arch = "x86_64")]
fn dot_u8_i16_scalar(weight: &[u8], activation: &[i16]) -> i32 {
debug_assert_eq!(weight.len(), activation.len());
weight
.iter()
.zip(activation)
.map(|(&w, &a)| w as i32 * a as i32)
.sum()
}
#[cfg(target_arch = "x86_64")]
fn have_avx2() -> bool {
static AVX2: OnceLock<bool> = OnceLock::new();
*AVX2.get_or_init(|| std::arch::is_x86_feature_detected!("avx2"))
}
#[cfg(target_arch = "x86_64")]
fn have_avx512bw() -> bool {
static AVX512BW: OnceLock<bool> = OnceLock::new();
*AVX512BW.get_or_init(|| {
std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("avx512f")
&& std::arch::is_x86_feature_detected!("avx512bw")
})
}
const MIN_PARALLEL_DOT_PRODUCTS_PER_TASK: usize = 32 * 1024;
const MIN_PARALLEL_DOT_PRODUCTS_PER_THREAD: usize = 8 * 1024;
const MANY_THREAD_DOT_PRODUCTS_PER_THREAD: usize = 64 * 1024;
const MIN_OUTPUTS_PER_TASK: usize = 16;
const MANY_THREAD_CUTOFF: usize = 48;
pub(crate) fn output_chunk_len(n: usize, k: usize) -> usize {
let threads = rayon::current_num_threads();
let total_work = n.saturating_mul(k);
let work_per_thread = if threads <= MANY_THREAD_CUTOFF {
MIN_PARALLEL_DOT_PRODUCTS_PER_THREAD
} else {
MANY_THREAD_DOT_PRODUCTS_PER_THREAD
};
if threads <= 1 || total_work < threads.saturating_mul(work_per_thread) {
return n.max(1);
}
let max_tasks = if threads <= MANY_THREAD_CUTOFF {
threads.saturating_mul(2)
} else {
threads
};
let tasks = total_work
.div_ceil(MIN_PARALLEL_DOT_PRODUCTS_PER_TASK)
.min(max_tasks)
.min(n);
if tasks < 2 {
return n.max(1);
}
n.div_ceil(tasks).max(MIN_OUTPUTS_PER_TASK).min(n)
}
fn optional_input<'a>(inputs: &'a [TensorView<'a>], index: usize) -> Option<&'a TensorView<'a>> {
inputs.get(index).filter(|input| !input.is_absent())
}
fn required_positive_attr(node: &Node, name: &str) -> Result<usize> {
let value = optional_int_attr(node, name)?
.ok_or_else(|| error(format!("missing required integer attribute '{name}'")))?;
if value <= 0 {
return Err(error(format!(
"attribute '{name}' must be positive, got {value}"
)));
}
Ok(value as usize)
}
fn optional_int_attr(node: &Node, name: &str) -> Result<Option<i64>> {
match node.attr(name) {
Some(attribute) => attribute
.as_int()
.map(Some)
.ok_or_else(|| error(format!("attribute '{name}' must be an integer"))),
None => Ok(None),
}
}
fn require_dtype(name: &str, got: DataType, expected: DataType) -> Result<()> {
if got != expected {
return Err(error(format!(
"{name} must have dtype {expected:?}, got {got:?}"
)));
}
Ok(())
}
fn require_float_compute_dtype(name: &str, got: DataType) -> Result<()> {
if !matches!(
got,
DataType::Float32 | DataType::Float16 | DataType::BFloat16
) {
return Err(error(format!(
"{name} must have dtype Float32, Float16, or BFloat16, got {got:?}"
)));
}
Ok(())
}
fn to_dense_compute_f32(view: &TensorView) -> Result<Vec<f32>> {
match view.dtype {
DataType::Float32 => to_dense_f32(view),
DataType::Float16 | DataType::BFloat16 => {
Ok(to_dense_f32_widen("MatMulNBits", view)?.into_owned())
}
other => Err(error(format!(
"compute input must have dtype Float32, Float16, or BFloat16, got {other:?}"
))),
}
}
fn compute_activations_cow<'a>(view: &'a TensorView<'_>) -> Result<Cow<'a, [f32]>> {
if view.dtype == DataType::Float32 && view.is_contiguous() && view.device.is_host_accessible() {
view.validate()?;
let n = numel(view.shape);
if n == 0 {
return Ok(Cow::Borrowed(&[]));
}
let slice = unsafe { std::slice::from_raw_parts(view.data_ptr::<f32>(), n) };
return Ok(Cow::Borrowed(slice));
}
Ok(Cow::Owned(to_dense_compute_f32(view)?))
}
fn write_compute_f32(out: &mut TensorMut, data: &[f32]) -> Result<()> {
match out.dtype {
DataType::Float32 => write_dense_f32(out, data),
DataType::Float16 | DataType::BFloat16 => write_dense_f32_narrow("MatMulNBits", out, data),
other => Err(error(format!(
"Y must have dtype Float32, Float16, or BFloat16, got {other:?}"
))),
}
}
fn require_shape(name: &str, got: &[usize], expected: &[usize]) -> Result<()> {
if got != expected {
return Err(error(format!(
"{name} must have shape {expected:?}, got {got:?}"
)));
}
Ok(())
}
fn require_flat_or_matrix_shape(
name: &str,
got: &[usize],
rows: usize,
columns: usize,
) -> Result<()> {
if got != [rows * columns] && got != [rows, columns] {
return Err(error(format!(
"{name} must have shape [{}] or [{rows}, {columns}], got {got:?}",
rows * columns
)));
}
Ok(())
}
fn error(message: impl Into<String>) -> EpError {
EpError::KernelFailed(format!("MatMulNBits: {}", message.into()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CpuExecutionProvider;
use crate::kernels::testutil::Owned;
use onnx_runtime_ep_api::ExecutionProvider;
use onnx_runtime_ir::{Attribute, Graph, NodeId, static_shape};
use onnx_runtime_loader::{Model, encode_model_proto};
fn model_node(
a_shape: &[usize],
b_shape: &[usize],
scales_shape: &[usize],
zero_points_shape: Option<&[usize]>,
output_shape: &[usize],
k: usize,
n: usize,
block_size: usize,
) -> (Graph, NodeId) {
let mut graph = Graph::new();
graph.opset_imports.insert("com.microsoft".into(), 1);
let mut inputs = Vec::new();
for (name, dtype, shape) in [
("A", DataType::Float32, a_shape),
("B", DataType::Uint8, b_shape),
("scales", DataType::Float32, scales_shape),
] {
let value = graph.create_named_value(name, dtype, static_shape(shape.iter().copied()));
graph.add_input(value);
inputs.push(Some(value));
}
if let Some(shape) = zero_points_shape {
let value = graph.create_named_value(
"zero_points",
DataType::Uint8,
static_shape(shape.iter().copied()),
);
graph.add_input(value);
inputs.push(Some(value));
}
let output = graph.create_named_value(
"Y",
DataType::Float32,
static_shape(output_shape.iter().copied()),
);
let mut node = Node::new(NodeId(0), "MatMulNBits", inputs, vec![output]);
node.domain = "com.microsoft".into();
node.attributes.insert("K".into(), Attribute::Int(k as i64));
node.attributes.insert("N".into(), Attribute::Int(n as i64));
node.attributes.insert("bits".into(), Attribute::Int(4));
node.attributes
.insert("block_size".into(), Attribute::Int(block_size as i64));
let node = graph.insert_node(node);
graph.add_output(output);
(graph, node)
}
fn test_kernel(k: usize, n: usize, block_size: usize) -> MatMulNBitsKernel {
MatMulNBitsKernel {
k,
n,
bits: 4,
block_size,
accuracy_level: 0,
constant_inputs: [false; 5],
weight_nk: OnceLock::new(),
int8_weight: OnceLock::new(),
packed_u8_weight: OnceLock::new(),
packed_int4_weight: OnceLock::new(),
#[cfg(feature = "mlas")]
mlas_shards: OnceLock::new(),
}
}
fn accuracy4_kernel(k: usize, n: usize, block_size: usize) -> MatMulNBitsKernel {
MatMulNBitsKernel {
accuracy_level: 4,
..test_kernel(k, n, block_size)
}
}
fn prepack_cache_ptr(kernel: &MatMulNBitsKernel) -> Option<*const ()> {
if let Some(w) = kernel.weight_nk.get() {
return Some(w as *const _ as *const ());
}
if let Some(w) = kernel.int8_weight.get() {
return Some(w as *const _ as *const ());
}
if let Some(w) = kernel.packed_int4_weight.get() {
return Some(w as *const _ as *const ());
}
#[cfg(feature = "mlas")]
if let Some(w) = kernel.mlas_shards.get() {
return Some(w as *const _ as *const ());
}
None
}
fn prepack_cache_populated(kernel: &MatMulNBitsKernel) -> bool {
prepack_cache_ptr(kernel).is_some()
}
fn quantize(
weights_nk: &[f32],
n: usize,
k: usize,
block_size: usize,
asymmetric: bool,
) -> (Vec<u8>, Vec<f32>, Option<Vec<u8>>, Vec<f32>) {
let blocks = k.div_ceil(block_size);
let blob = block_size / 2;
let mut packed = vec![0u8; n * blocks * blob];
let mut scales = vec![0.0f32; n * blocks];
let mut zps = vec![0u8; n * blocks.div_ceil(2)];
let mut dequantized = vec![0.0f32; n * k];
for row in 0..n {
for block in 0..blocks {
let start = block * block_size;
let end = (start + block_size).min(k);
let values = &weights_nk[row * k + start..row * k + end];
let (scale, zp) = if asymmetric {
let min = values.iter().copied().fold(f32::INFINITY, f32::min);
let max = values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let scale = ((max - min) / 15.0).max(1e-6);
(scale, (-min / scale).round().clamp(0.0, 15.0) as u8)
} else {
let max_abs = values.iter().map(|value| value.abs()).fold(0.0, f32::max);
((max_abs / 7.0).max(1e-6), 8)
};
scales[row * blocks + block] = scale;
if asymmetric {
let byte = &mut zps[row * blocks.div_ceil(2) + block / 2];
*byte |= zp << (4 * (block % 2));
}
for (offset, &value) in values.iter().enumerate() {
let q = (value / scale + zp as f32).round().clamp(0.0, 15.0) as u8;
packed[(row * blocks + block) * blob + offset / 2] |= q << (4 * (offset % 2));
dequantized[row * k + start + offset] = (q as f32 - zp as f32) * scale;
}
}
}
(packed, scales, asymmetric.then_some(zps), dequantized)
}
fn reference(a: &[f32], weights_nk: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
let mut output = vec![0.0f32; m * n];
for row in 0..m {
for column in 0..n {
for depth in 0..k {
output[row * n + column] += a[row * k + depth] * weights_nk[column * k + depth];
}
}
}
output
}
fn dequantize_reference(
packed: &[u8],
scales: &[f32],
zero_points: Option<&[u8]>,
n: usize,
k: usize,
block_size: usize,
) -> Vec<f32> {
let blocks = k.div_ceil(block_size);
let blob_size = block_size / 2;
let zp_row_bytes = blocks.div_ceil(2);
let mut weights = vec![0.0; n * k];
for output in 0..n {
for depth in 0..k {
let block = depth / block_size;
let within_block = depth % block_size;
let byte = packed[(output * blocks + block) * blob_size + within_block / 2];
let q = if within_block.is_multiple_of(2) {
byte & 0x0f
} else {
byte >> 4
};
let zero_point = zero_points.map_or(8, |points| {
let byte = points[output * zp_row_bytes + block / 2];
if block.is_multiple_of(2) {
byte & 0x0f
} else {
byte >> 4
}
});
weights[output * k + depth] =
(q as f32 - zero_point as f32) * scales[output * blocks + block];
}
}
weights
}
fn quantize_symmetric_2bit(
weights_nk: &[f32],
n: usize,
k: usize,
block_size: usize,
) -> (Vec<u8>, Vec<f32>) {
let blocks = k.div_ceil(block_size);
let blob_size = block_size / 4;
let mut packed = vec![0u8; n * blocks * blob_size];
let mut scales = vec![0.0f32; n * blocks];
for output in 0..n {
for block in 0..blocks {
let start = block * block_size;
let end = (start + block_size).min(k);
let values = &weights_nk[output * k + start..output * k + end];
let max_abs = values.iter().map(|value| value.abs()).fold(0.0, f32::max);
let scale = max_abs.max(1e-6);
scales[output * blocks + block] = scale;
for (offset, &value) in values.iter().enumerate() {
let q = (value / scale + 2.0).round().clamp(0.0, 3.0) as u8;
packed[(output * blocks + block) * blob_size + offset / 4] |=
q << (2 * (offset % 4));
}
}
}
(packed, scales)
}
fn dequantize_2bit_reference(
packed: &[u8],
scales: &[f32],
n: usize,
k: usize,
block_size: usize,
) -> Vec<f32> {
let blocks = k.div_ceil(block_size);
let blob_size = block_size / 4;
let mut dequantized = vec![0.0f32; n * k];
for output in 0..n {
for depth in 0..k {
let block = depth / block_size;
let within_block = depth % block_size;
let byte = packed[(output * blocks + block) * blob_size + within_block / 4];
let q = (byte >> (2 * (within_block % 4))) & 0x03;
dequantized[output * k + depth] =
(q as f32 - 2.0) * scales[output * blocks + block];
}
}
dequantized
}
fn assert_close(actual: &[f32], expected: &[f32]) {
assert_eq!(actual.len(), expected.len());
for (index, (&actual, &expected)) in actual.iter().zip(expected).enumerate() {
assert!(
(actual - expected).abs() <= 1e-5,
"index {index}: actual={actual}, expected={expected}"
);
}
}
fn assert_int8_close(actual: &[f32], expected: &[f32]) {
assert_eq!(actual.len(), expected.len());
for (index, (&actual, &expected)) in actual.iter().zip(expected).enumerate() {
let tolerance = 0.05 + 0.05 * expected.abs();
assert!(
(actual - expected).abs() <= tolerance,
"index {index}: actual={actual}, expected={expected}, tolerance={tolerance}"
);
}
}
fn run_decode_case(
activations: &[f32],
packed: &[u8],
scales: &[f32],
n: usize,
block_size: usize,
accuracy_level: Option<i64>,
) -> Vec<f32> {
let k = activations.len();
let blocks = k.div_ceil(block_size);
let (mut graph, node) = model_node(
&[1, k],
&[n, blocks, block_size / 2],
&[n, blocks],
None,
&[1, n],
k,
n,
block_size,
);
if let Some(level) = accuracy_level {
graph
.node_mut(node)
.attributes
.insert("accuracy_level".into(), Attribute::Int(level));
}
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let a = Owned::f32(&[1, k], activations);
let b = Owned::u8(&[n, blocks, block_size / 2], packed);
let scales = Owned::f32(&[n, blocks], scales);
let mut y = Owned::zeros_f32(&[1, n]);
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
y.to_f32()
}
fn accuracy4_model(m: usize, k: usize, n: usize, block_size: usize) -> (Graph, NodeId) {
let blocks = k.div_ceil(block_size);
let (mut graph, node) = model_node(
&[m, k],
&[n, blocks, block_size / 2],
&[n, blocks],
None,
&[m, n],
k,
n,
block_size,
);
graph
.node_mut(node)
.attributes
.insert("accuracy_level".into(), Attribute::Int(4));
let proto = encode_model_proto(&Model::new(&graph)).expect("IR model must encode to ONNX");
let attribute = &proto.graph.as_ref().unwrap().node[0].attribute;
assert!(
attribute
.iter()
.any(|attr| attr.name == "accuracy_level" && attr.i == 4)
);
(graph, node)
}
fn run_accuracy4_case(m: usize, k: usize, n: usize, block_size: usize) {
let a_values: Vec<f32> = (0..m * k)
.map(|i| ((i * 17 % 43) as f32 - 21.0) / 13.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 19 % 47) as f32 - 23.0) / 12.0)
.collect();
let (packed, scales, _, _) = quantize(&weights, n, k, block_size, false);
let dequantized = dequantize_reference(&packed, &scales, None, n, k, block_size);
let expected = reference(&a_values, &dequantized, m, k, n);
let (graph, node) = accuracy4_model(m, k, n, block_size);
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let a = Owned::f32(&[m, k], &a_values);
let b = Owned::u8(&[n, k.div_ceil(block_size), block_size / 2], &packed);
let scales = Owned::f32(&[n, k.div_ceil(block_size)], &scales);
let mut y = Owned::zeros_f32(&[m, n]);
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
assert_int8_close(&y.to_f32(), &expected);
}
#[test]
fn matmulnbits_accuracy4_block32_partial_k_m1_matches_fp32_reference() {
run_accuracy4_case(1, 45, 9, 32);
}
#[test]
fn matmulnbits_accuracy4_block128_partial_k_batched_matches_fp32_reference() {
run_accuracy4_case(3, 141, 7, 128);
}
#[test]
fn matmulnbits_activation_borrow_matches_strided_copy_bit_exact() {
for &(m, k, n, block_size) in &[(1usize, 128usize, 16usize, 32usize), (4, 96, 8, 32)] {
let a_values: Vec<f32> = (0..m * k)
.map(|i| ((i * 17 % 43) as f32 - 21.0) / 13.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 19 % 47) as f32 - 23.0) / 12.0)
.collect();
let (packed, scales, _, _) = quantize(&weights, n, k, block_size, false);
let (graph, node) = accuracy4_model(m, k, n, block_size);
let model = Model::new(&graph);
let blocks = k.div_ceil(block_size);
let b = Owned::u8(&[n, blocks, block_size / 2], &packed);
let scales_t = Owned::f32(&[n, blocks], &scales);
let a_contig = Owned::f32(&[m, k], &a_values);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let mut y_borrow = Owned::zeros_f32(&[m, n]);
kernel
.execute(
&[a_contig.view(), b.view(), scales_t.view()],
&mut [y_borrow.view_mut()],
)
.unwrap();
let pad = 3usize;
let mut padded = vec![0.0f32; m * (k + pad)];
for r in 0..m {
padded[r * (k + pad)..r * (k + pad) + k]
.copy_from_slice(&a_values[r * k..r * k + k]);
}
let a_strided =
Owned::f32(&[m, k + pad], &padded).with_view(&[m, k], &[(k + pad) as i64, 1]);
assert!(
!a_strided.view().is_contiguous(),
"strided activation must be non-contiguous to exercise the copy path"
);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let mut y_copy = Owned::zeros_f32(&[m, n]);
kernel
.execute(
&[a_strided.view(), b.view(), scales_t.view()],
&mut [y_copy.view_mut()],
)
.unwrap();
assert_eq!(
y_borrow.bytes, y_copy.bytes,
"borrowed vs copied activation diverged (m={m}, k={k}, n={n}, block={block_size})"
);
}
}
#[test]
fn matmulnbits_accuracy4_m1_asymmetric_matches_fp32_reference() {
for &(k, n, block_size) in &[(256usize, 96usize, 32usize), (256, 64, 64), (128, 8, 128)] {
let m = 1;
let a_values: Vec<f32> = (0..m * k)
.map(|i| ((i * 17 % 43) as f32 - 21.0) / 13.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 19 % 47) as f32 - 23.0) / 12.0)
.collect();
let (packed, scales, zps, _) = quantize(&weights, n, k, block_size, true);
let zps = zps.expect("asymmetric quantization must emit zero points");
let dequantized = dequantize_reference(&packed, &scales, Some(&zps), n, k, block_size);
let expected = reference(&a_values, &dequantized, m, k, n);
let blocks = k.div_ceil(block_size);
let (mut graph, node) = model_node(
&[m, k],
&[n, blocks, block_size / 2],
&[n, blocks],
Some(&[n, blocks.div_ceil(2)]),
&[m, n],
k,
n,
block_size,
);
graph
.node_mut(node)
.attributes
.insert("accuracy_level".into(), Attribute::Int(4));
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let a = Owned::f32(&[m, k], &a_values);
let b = Owned::u8(&[n, blocks, block_size / 2], &packed);
let scales_owned = Owned::f32(&[n, blocks], &scales);
let zero_points = Owned::u8(&[n, blocks.div_ceil(2)], &zps);
let mut y = Owned::zeros_f32(&[m, n]);
kernel
.execute(
&[a.view(), b.view(), scales_owned.view(), zero_points.view()],
&mut [y.view_mut()],
)
.unwrap();
assert_int8_close(&y.to_f32(), &expected);
}
}
#[test]
fn matmulnbits_fp32_activation_is_more_accurate_than_accuracy4() {
let (k, n, block_size) = (128, 2, 128);
let mut weights_nk = vec![0i8; n * k];
weights_nk[0] = 1;
weights_nk[1] = 7;
weights_nk[2] = -6;
weights_nk[k] = 1;
let mut packed = vec![0x88u8; n * block_size / 2];
for (output, weights) in weights_nk.chunks_exact(k).enumerate() {
for (depth, &weight) in weights.iter().enumerate() {
let q = (weight + 8) as u8;
let byte = &mut packed[output * block_size / 2 + depth / 2];
if depth.is_multiple_of(2) {
*byte = (*byte & 0xf0) | q;
} else {
*byte = (*byte & 0x0f) | (q << 4);
}
}
}
let scales = vec![1.0; n];
let dequantized = dequantize_reference(&packed, &scales, None, n, k, block_size);
let mut activations = vec![0.0; k];
activations[..3].copy_from_slice(&[127.0, 0.49, 0.51]);
let expected = reference(&activations, &dequantized, 1, k, n);
let absent = run_decode_case(&activations, &packed, &scales, n, block_size, None);
let level1 = run_decode_case(&activations, &packed, &scales, n, block_size, Some(1));
let level4 = run_decode_case(&activations, &packed, &scales, n, block_size, Some(4));
assert_close(&absent, &expected);
assert_close(&level1, &expected);
assert!(absent[0].total_cmp(&absent[1]).is_gt());
assert!(level1[0].total_cmp(&level1[1]).is_gt());
assert!(level4[1].total_cmp(&level4[0]).is_gt());
let fp32_error = absent
.iter()
.zip(&expected)
.map(|(actual, expected)| (actual - expected).abs())
.fold(0.0, f32::max);
let accuracy4_error = level4
.iter()
.zip(&expected)
.map(|(actual, expected)| (actual - expected).abs())
.fold(0.0, f32::max);
assert!(
accuracy4_error > fp32_error + 1.0,
"accuracy-4 error {accuracy4_error} must materially exceed fp32 error {fp32_error}"
);
let mut on_grid = vec![0.0; k];
on_grid[..3].copy_from_slice(&[127.0, 1.0, 1.0]);
let on_grid_expected = reference(&on_grid, &dequantized, 1, k, n);
assert_close(
&run_decode_case(&on_grid, &packed, &scales, n, block_size, Some(4)),
&on_grid_expected,
);
}
#[test]
fn matmulnbits_accuracy4_prepack_reuses_selected_weight_format() {
let (k, n, block_size) = (45, 5, 32);
let activations: Vec<f32> = (0..k)
.map(|i| ((i * 11 % 37) as f32 - 18.0) / 9.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 13 % 41) as f32 - 20.0) / 11.0)
.collect();
let (packed, scales, _, _) = quantize(&weights, n, k, block_size, false);
let mut kernel = accuracy4_kernel(k, n, block_size);
kernel.set_constant_inputs(&[false, true, true]);
let a = Owned::f32(&[1, k], &activations);
let b = Owned::u8(&[n, 2, 16], &packed);
let scales = Owned::f32(&[n, 2], &scales);
let mut y = Owned::zeros_f32(&[1, n]);
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
let direct_int4 = selected_dot_kernel().uses_vnni_int4_direct();
let cached = if direct_int4 {
kernel
.packed_int4_weight
.get()
.expect("packed int4 weight must be cached")
.values
.as_ptr()
} else {
kernel
.int8_weight
.get()
.expect("int8 weight must be cached")
.values
.as_ptr()
.cast()
};
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
let reused = if direct_int4 {
kernel.packed_int4_weight.get().unwrap().values.as_ptr()
} else {
kernel.int8_weight.get().unwrap().values.as_ptr().cast()
};
assert_eq!(reused, cached);
assert!(kernel.weight_nk.get().is_none());
assert_eq!(kernel.packed_int4_weight.get().is_some(), direct_int4);
assert_eq!(kernel.int8_weight.get().is_some(), !direct_int4);
}
#[test]
fn matmulnbits_accuracy4_vnni_matches_scalar_when_available() {
let activation: Vec<u8> = (0..128).map(|i| ((i * 29 + 7) % 255) as u8).collect();
let weight: Vec<i8> = (0..128).map(|i| ((i * 17 % 31) as i8) - 15).collect();
let scalar = dot_u8_i8(&activation, &weight, DotKernel::Scalar);
let selected = selected_dot_kernel();
#[cfg(target_arch = "x86_64")]
if std::arch::is_x86_feature_detected!("avxvnni")
|| (std::arch::is_x86_feature_detected!("avx512vnni")
&& std::arch::is_x86_feature_detected!("avx512vl"))
{
assert_ne!(
selected,
DotKernel::Scalar,
"a VNNI CPU must select the VNNI path"
);
}
assert_eq!(dot_u8_i8(&activation, &weight, selected), scalar);
#[cfg(target_arch = "x86_64")]
if std::arch::is_x86_feature_detected!("avx2") {
assert_ne!(
selected,
DotKernel::Scalar,
"an AVX2 CPU must not select Scalar"
);
assert_eq!(dot_u8_i8(&activation, &weight, DotKernel::Avx2), scalar);
}
let activations: Vec<f32> = (0..256)
.map(|i| ((i * 23 % 53) as f32 - 26.0) / 17.0)
.collect();
let values: Vec<i8> = (0..384).map(|i| ((i * 11 % 16) as i8) - 8).collect();
let block_sums = values
.chunks_exact(128)
.map(|block| block.iter().map(|&value| value as i32).sum())
.collect();
let prepacked = Int8Weight {
values,
scales: vec![0.01, 0.02, 0.03],
block_sums,
};
let mut scalar_output = vec![0.0; 6];
let mut selected_output = vec![0.0; 6];
int8_matmul(
&activations,
&prepacked,
&mut scalar_output,
2,
128,
3,
128,
DotKernel::Scalar,
);
int8_matmul(
&activations,
&prepacked,
&mut selected_output,
2,
128,
3,
128,
selected,
);
assert_close(&selected_output, &scalar_output);
}
#[cfg(target_arch = "x86_64")]
#[test]
fn int8_matmul_amx_matches_scalar_prefill() {
if !amx::amx_int8_available() {
eprintln!("skipping: AMX-INT8 not available on this host");
return;
}
let mut state = 0x2545_f491_4f6c_dd1du64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let cases = [
(16usize, 64usize, 16usize, 32usize),
(17, 128, 20, 32),
(33, 96, 7, 32),
(64, 256, 64, 64),
(128, 512, 100, 128),
(200, 100, 48, 32),
(16, 33, 16, 32),
(48, 130, 40, 64),
];
for &(m, k, n, block_size) in &cases {
assert!(amx::amx_block_size_supported(block_size));
let k_blocks = k.div_ceil(block_size);
let padded_k = k_blocks * block_size;
let activations: Vec<f32> = (0..m * k)
.map(|_| ((next() % 4000) as f32 - 2000.0) / 173.0)
.collect();
let values: Vec<i8> = (0..n * padded_k)
.map(|index| {
let col = index % padded_k;
if col >= k {
0
} else {
((next() % 15) as i8) - 7
}
})
.collect();
let scales: Vec<f32> = (0..n * k_blocks)
.map(|_| (next() % 100) as f32 / 5000.0 + 0.001)
.collect();
let block_sums: Vec<i32> = (0..n * k_blocks)
.map(|index| {
let start = index * block_size;
values[start..start + block_size]
.iter()
.map(|&w| w as i32)
.sum()
})
.collect();
let weight = Int8Weight {
values,
scales,
block_sums,
};
let mut scalar_out = vec![0.0f32; m * n];
int8_matmul(
&activations,
&weight,
&mut scalar_out,
m,
k,
n,
block_size,
DotKernel::Scalar,
);
let mut amx_out = vec![0.0f32; m * n];
amx::int8_matmul_amx(&activations, &weight, &mut amx_out, m, k, n, block_size);
assert_eq!(
amx_out, scalar_out,
"AMX prefill diverged from scalar for m={m} k={k} n={n} block_size={block_size}"
);
}
}
#[test]
fn matmulnbits_direct_int4_gemv_matches_int8_reference() {
let (k, n, block_size) = (77usize, 9usize, 32usize);
let blocks = k.div_ceil(block_size);
let padded_k = blocks * block_size;
let activations: Vec<f32> = (0..k)
.map(|i| ((i * 23 % 53) as f32 - 26.0) / 17.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 19 % 47) as f32 - 23.0) / 12.0)
.collect();
let (packed, scales, _, _) = quantize(&weights, n, k, block_size, false);
let packed_weight = PackedInt4Weight {
values: packed.clone(),
scales: scales.clone(),
};
let kernel = accuracy4_kernel(k, n, block_size);
let b = Owned::u8(&[n, blocks, block_size / 2], &packed);
let scales_tensor = Owned::f32(&[n, blocks], &scales);
let int8_weight = kernel
.prepack_int8_weight(&b.view(), &scales_tensor.view(), None)
.unwrap();
let mut expected = vec![0.0; n];
let mut scalar = vec![0.0; n];
let mut actual = vec![0.0; n];
int8_matmul(
&activations,
&int8_weight,
&mut expected,
1,
k,
n,
block_size,
DotKernel::Scalar,
);
int4_matmul_m1(
&activations,
&packed_weight,
&mut scalar,
k,
n,
DotKernel::Scalar,
);
int4_matmul_m1(
&activations,
&packed_weight,
&mut actual,
k,
n,
selected_dot_kernel(),
);
assert_eq!(
padded_k,
activations.len().div_ceil(block_size) * block_size
);
for (index, ((&actual, &scalar), &expected)) in
actual.iter().zip(&scalar).zip(&expected).enumerate()
{
let tolerance = 1e-4 + 1e-5 * expected.abs();
assert!(
(actual - expected).abs() <= tolerance,
"index {index}: direct int4={actual}, int8 reference={expected}, tolerance={tolerance}"
);
assert!(
(scalar - expected).abs() <= tolerance,
"index {index}: scalar int4={scalar}, int8 reference={expected}, tolerance={tolerance}"
);
}
}
fn rmse(actual: &[f32], expected: &[f32]) -> f32 {
assert_eq!(actual.len(), expected.len());
let sum: f32 = actual
.iter()
.zip(expected)
.map(|(a, e)| (a - e) * (a - e))
.sum();
(sum / actual.len() as f32).sqrt()
}
fn activation_quant_oracle(
activations: &[f32],
dequantized: &[f32],
k: usize,
n: usize,
block_size: usize,
per_block: bool,
) -> Vec<f32> {
let mut hat = activations.to_vec();
if per_block {
for block in 0..k.div_ceil(block_size) {
let start = block * block_size;
let end = (start + block_size).min(k);
let max_abs = activations[start..end]
.iter()
.map(|v| v.abs())
.fold(0.0, f32::max);
if max_abs == 0.0 {
continue;
}
let scale = max_abs / 127.0;
for i in start..end {
hat[i] = (activations[i] / scale).round().clamp(-127.0, 127.0) * scale;
}
}
} else {
let max_abs = activations.iter().map(|v| v.abs()).fold(0.0, f32::max);
let scale = max_abs / 127.0;
for value in hat.iter_mut() {
*value = (*value / scale).round().clamp(-127.0, 127.0) * scale;
}
}
reference(&hat, dequantized, 1, k, n)
}
#[test]
fn matmulnbits_compint8_per_block_activation_tracks_dequant_f32_oracle() {
let (k, n, block_size) = (256usize, 8usize, 32usize);
let blocks = k.div_ceil(block_size);
let amp_a = |block: usize| if block == 0 { 2.5 } else { 0.06 };
let amp_w = |block: usize| if block == 0 { 0.05 } else { 1.5 };
let activations: Vec<f32> = (0..k)
.map(|i| ((i as f32 * 0.37).sin()) * amp_a(i / block_size))
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i as f32 * 0.11).cos()) * amp_w((i % k) / block_size))
.collect();
let (packed, scales, _, dequantized) = quantize(&weights, n, k, block_size, false);
let oracle = reference(&activations, &dequantized, 1, k, n);
let oracle_rms = rmse(&oracle, &vec![0.0; n]);
let packed_weight = PackedInt4Weight {
values: packed.clone(),
scales: scales.clone(),
};
let mut int4_out = vec![0.0; n];
int4_matmul_m1(
&activations,
&packed_weight,
&mut int4_out,
k,
n,
DotKernel::Scalar,
);
let kernel = accuracy4_kernel(k, n, block_size);
let b = Owned::u8(&[n, blocks, block_size / 2], &packed);
let scales_tensor = Owned::f32(&[n, blocks], &scales);
let int8_weight = kernel
.prepack_int8_weight(&b.view(), &scales_tensor.view(), None)
.unwrap();
let mut int8_out = vec![0.0; n];
int8_matmul(
&activations,
&int8_weight,
&mut int8_out,
1,
k,
n,
block_size,
DotKernel::Scalar,
);
let int4_rel = rmse(&int4_out, &oracle) / oracle_rms;
let int8_rel = rmse(&int8_out, &oracle) / oracle_rms;
assert!(
int4_rel <= 5e-3,
"per-block int4 CompInt8 relative RMSE {int4_rel} exceeds ORT-class 5e-3",
);
assert!(
int8_rel <= 5e-3,
"per-block int8 CompInt8 relative RMSE {int8_rel} exceeds ORT-class 5e-3",
);
let per_row_rel = rmse(
&activation_quant_oracle(&activations, &dequantized, k, n, block_size, false),
&oracle,
) / oracle_rms;
let per_block_rel = rmse(
&activation_quant_oracle(&activations, &dequantized, k, n, block_size, true),
&oracle,
) / oracle_rms;
assert!(
per_block_rel < per_row_rel * 0.25,
"per-block activation quant ({per_block_rel}) must be far better than per-row ({per_row_rel})",
);
}
#[test]
fn matmulnbits_compint8_argmax_matches_dequant_f32_oracle_at_near_tie() {
let (k, n, block_size) = (128usize, 2usize, 32usize);
let mut checked = 0usize;
for seed in 1..=400u32 {
let s = seed as f32;
let activations: Vec<f32> = (0..k)
.map(|i| {
let block = (i / block_size) as f32;
((i as f32 * 0.017 + s * 0.013).sin()) * (0.05 + 0.45 * block)
})
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| (i as f32 * 0.011 + s * 0.019).cos())
.collect();
let (packed, scales, _, dequantized) = quantize(&weights, n, k, block_size, false);
let oracle = reference(&activations, &dequantized, 1, k, n);
let oracle_rms = rmse(&oracle, &vec![0.0; n]).max(1e-6);
let margin_rel = (oracle[1] - oracle[0]).abs() / oracle_rms;
if !(0.02..=0.08).contains(&margin_rel) {
continue;
}
checked += 1;
let packed_weight = PackedInt4Weight {
values: packed.clone(),
scales: scales.clone(),
};
let mut native = vec![0.0; n];
int4_matmul_m1(
&activations,
&packed_weight,
&mut native,
k,
n,
selected_dot_kernel(),
);
let case_rel = rmse(&native, &oracle) / oracle_rms;
assert!(
case_rel <= 5e-3,
"seed {seed}: native CompInt8 relative RMSE {case_rel} exceeds ORT-class 5e-3",
);
assert_eq!(
usize::from(native[1] > native[0]),
usize::from(oracle[1] > oracle[0]),
"seed {seed}: native CompInt8 argmax != f32 oracle (margin_rel {margin_rel}, native {native:?}, oracle {oracle:?})",
);
}
assert!(
checked >= 5,
"deterministic search must exercise several near-tie cases (got {checked})",
);
}
#[test]
fn int4_decode_preserves_f32_argmax_where_per_row_int8_activation_flips() {
let (k, n, block_size) = (128usize, 2usize, 32usize);
let mut near_ties = 0usize;
let mut per_row_flips = 0usize;
for seed in 1..=4000u32 {
let s = seed as f32;
let activations: Vec<f32> = (0..k)
.map(|i| {
let amp = if i / block_size == 0 { 3.0 } else { 0.05 };
(i as f32 * 0.017 + s * 0.013).sin() * amp
})
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| {
let amp = if (i % k) / block_size == 0 { 0.03 } else { 1.5 };
(i as f32 * 0.011 + s * 0.019).cos() * amp
})
.collect();
let (packed, scales, _, dequantized) = quantize(&weights, n, k, block_size, false);
let oracle = reference(&activations, &dequantized, 1, k, n);
let oracle_rms = rmse(&oracle, &vec![0.0; n]).max(1e-6);
let margin_rel = (oracle[1] - oracle[0]).abs() / oracle_rms;
if !(0.005..=0.08).contains(&margin_rel) {
continue;
}
near_ties += 1;
let oracle_argmax = usize::from(oracle[1] > oracle[0]);
let per_row =
activation_quant_oracle(&activations, &dequantized, k, n, block_size, false);
if usize::from(per_row[1] > per_row[0]) != oracle_argmax {
per_row_flips += 1;
}
let packed_weight = PackedInt4Weight {
values: packed.clone(),
scales: scales.clone(),
};
#[cfg_attr(not(target_arch = "x86_64"), allow(unused_mut))]
let mut kernels = vec![DotKernel::Scalar, selected_dot_kernel()];
#[cfg(target_arch = "x86_64")]
if std::arch::is_x86_feature_detected!("avx2") {
kernels.push(DotKernel::Avx2);
}
for kernel in kernels {
let mut native = vec![0.0; n];
int4_matmul_m1(&activations, &packed_weight, &mut native, k, n, kernel);
assert_eq!(
usize::from(native[1] > native[0]),
oracle_argmax,
"seed {seed} ({kernel:?}): native per-block int4 decode flipped the fp32 \
argmax at a near-tie (margin_rel {margin_rel}, native {native:?}, \
oracle {oracle:?}) -- this is the Phi-3.5 index-65 divergence class",
);
}
}
assert!(
near_ties >= 20,
"deterministic search must exercise many near-ties (got {near_ties})",
);
assert!(
per_row_flips >= 3,
"the per-row int8 activation failure mode must actually flip some near-ties \
(got {per_row_flips}); otherwise this test does not witness the divergence class",
);
}
fn quantize_8bit(
weights_nk: &[f32],
n: usize,
k: usize,
block_size: usize,
asymmetric: bool,
) -> (Vec<u8>, Vec<f32>, Option<Vec<u8>>, Vec<f32>) {
let blocks = k.div_ceil(block_size);
let blob = block_size; let mut packed = vec![0u8; n * blocks * blob];
let mut scales = vec![0.0f32; n * blocks];
let mut zps = vec![128u8; n * blocks];
let mut dequantized = vec![0.0f32; n * k];
for row in 0..n {
for block in 0..blocks {
let start = block * block_size;
let end = (start + block_size).min(k);
let values = &weights_nk[row * k + start..row * k + end];
let (scale, zp) = if asymmetric {
let min = values.iter().copied().fold(f32::INFINITY, f32::min);
let max = values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let scale = ((max - min) / 255.0).max(1e-6);
(scale, (-min / scale).round().clamp(0.0, 255.0) as u8)
} else {
let max_abs = values.iter().map(|value| value.abs()).fold(0.0, f32::max);
((max_abs / 127.0).max(1e-6), 128u8)
};
scales[row * blocks + block] = scale;
zps[row * blocks + block] = zp;
for (offset, &value) in values.iter().enumerate() {
let q = (value / scale + zp as f32).round().clamp(0.0, 255.0) as u8;
packed[(row * blocks + block) * blob + offset] = q;
dequantized[row * k + start + offset] = (q as f32 - zp as f32) * scale;
}
}
}
(packed, scales, asymmetric.then_some(zps), dequantized)
}
fn run_8bit_execute(
n: usize,
k: usize,
block_size: usize,
m: usize,
asymmetric: bool,
activations: &[f32],
weights_nk: &[f32],
) -> (Vec<f32>, Vec<f32>) {
let blocks = k.div_ceil(block_size);
let (packed, scales, zps, dequantized) =
quantize_8bit(weights_nk, n, k, block_size, asymmetric);
let zp_shape = zps.as_ref().map(|_| vec![n, blocks]);
let (graph, node) = model_node(
&[m, k],
&[n, blocks, block_size],
&[n, blocks],
zp_shape.as_deref(),
&[m, n],
k,
n,
block_size,
);
let mut graph = graph;
graph
.node_mut(node)
.attributes
.insert("bits".into(), Attribute::Int(8));
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.expect("bits=8 block-128 kernel must build");
let a = Owned::f32(&[m, k], activations);
let b = Owned::u8(&[n, blocks, block_size], &packed);
let scales_tensor = Owned::f32(&[n, blocks], &scales);
let zp_owned = zps.as_ref().map(|z| Owned::u8(&[n, blocks], z));
let mut inputs = vec![a.view(), b.view(), scales_tensor.view()];
if let Some(zp) = zp_owned.as_ref() {
inputs.push(zp.view());
}
let mut y = Owned::zeros_f32(&[m, n]);
kernel
.execute(&inputs, &mut [y.view_mut()])
.expect("8-bit block-128 execute must succeed");
let oracle = reference(activations, &dequantized, m, k, n);
(y.to_f32(), oracle)
}
#[test]
fn matmulnbits_8bit_block128_execute_matches_dequant_f32_oracle() {
let (n, k, block_size) = (48usize, 256usize, 128usize);
let weights_nk: Vec<f32> = (0..n * k)
.map(|i| (i as f32 * 0.013).sin() * 1.3 + (i as f32 * 0.0007).cos() * 0.4)
.collect();
for &asymmetric in &[false, true] {
for &m in &[1usize, 5] {
let activations: Vec<f32> = (0..m * k)
.map(|i| (i as f32 * 0.021 + 0.3).cos() * 0.9)
.collect();
let (out, oracle) =
run_8bit_execute(n, k, block_size, m, asymmetric, &activations, &weights_nk);
let oracle_rms = rmse(&oracle, &vec![0.0; oracle.len()]).max(1e-6);
let rel = rmse(&out, &oracle) / oracle_rms;
assert!(
rel <= 1e-5,
"asymmetric={asymmetric} m={m}: 8-bit execute relative RMSE {rel} exceeds float-reconstruction tolerance 1e-5",
);
for row in 0..m {
let winner = |v: &[f32]| {
(0..n)
.max_by(|&a, &b| v[row * n + a].total_cmp(&v[row * n + b]))
.unwrap()
};
assert_eq!(
winner(&out),
winner(&oracle),
"asymmetric={asymmetric} m={m} row={row}: 8-bit execute argmax != f32 oracle",
);
}
}
}
}
#[test]
fn matmulnbits_8bit_prefill_batched_matches_dequant_f32_oracle() {
let (n, k, block_size) = (96usize, 384usize, 128usize);
let weights_nk: Vec<f32> = (0..n * k)
.map(|i| (i as f32 * 0.011).sin() * 1.1 + (i as f32 * 0.0005).cos() * 0.5)
.collect();
for &asymmetric in &[false, true] {
for &m in &[16usize, 32, 100] {
let activations: Vec<f32> = (0..m * k)
.map(|i| (i as f32 * 0.017 + 0.2).cos() * 0.8)
.collect();
let (out, oracle) =
run_8bit_execute(n, k, block_size, m, asymmetric, &activations, &weights_nk);
let oracle_rms = rmse(&oracle, &vec![0.0; oracle.len()]).max(1e-6);
let rel = rmse(&out, &oracle) / oracle_rms;
assert!(
rel <= 1e-5,
"asymmetric={asymmetric} m={m}: 8-bit prefill relative RMSE {rel} exceeds 1e-5",
);
for row in 0..m {
let winner = |v: &[f32]| {
(0..n)
.max_by(|&a, &b| v[row * n + a].total_cmp(&v[row * n + b]))
.unwrap()
};
assert_eq!(
winner(&out),
winner(&oracle),
"asymmetric={asymmetric} m={m} row={row}: 8-bit prefill argmax != f32 oracle",
);
}
}
}
}
#[test]
fn matmulnbits_8bit_block128_argmax_matches_dequant_f32_oracle_at_near_tie() {
let (n, k, block_size) = (2usize, 128usize, 128usize);
let mut checked = 0usize;
for seed in 1..=400u32 {
let s = seed as f32;
let activations: Vec<f32> = (0..k)
.map(|i| (i as f32 * 0.017 + s * 0.013).sin() * 0.8)
.collect();
let weights_nk: Vec<f32> = (0..n * k)
.map(|i| (i as f32 * 0.011 + s * 0.019).cos())
.collect();
let (out, oracle) =
run_8bit_execute(n, k, block_size, 1, false, &activations, &weights_nk);
let oracle_rms = rmse(&oracle, &vec![0.0; n]).max(1e-6);
let margin_rel = (oracle[1] - oracle[0]).abs() / oracle_rms;
if !(0.001..=0.02).contains(&margin_rel) {
continue;
}
checked += 1;
assert_eq!(
usize::from(out[1] > out[0]),
usize::from(oracle[1] > oracle[0]),
"seed {seed}: 8-bit execute argmax != f32 oracle (margin_rel {margin_rel}, out {out:?}, oracle {oracle:?})",
);
}
assert!(
checked >= 5,
"deterministic search must exercise several 8-bit near-tie cases (got {checked})",
);
}
#[test]
fn dot_u8_f32_matches_serial_reference() {
for len in [0usize, 1, 7, 16, 17, 31, 128, 129] {
let weight: Vec<u8> = (0..len).map(|i| ((i * 37 + 5) % 256) as u8).collect();
let activation: Vec<f32> = (0..len)
.map(|i| (i as f32 * 0.031 - 0.4).sin() * 1.7)
.collect();
let expected: f32 = weight
.iter()
.zip(&activation)
.map(|(&w, &a)| w as f32 * a)
.sum();
let got = dot_u8_f32(&weight, &activation);
assert!(
(got - expected).abs() <= 1e-3 * expected.abs().max(1.0),
"len={len}: dot_u8_f32={got} != serial reference {expected}",
);
}
}
#[test]
fn gemv_nk_u8_matches_dequant_f32_reference() {
let (n, k, block_size) = (40usize, 200usize, 128usize);
let k_blocks = k.div_ceil(block_size);
let weights_nk: Vec<f32> = (0..n * k)
.map(|i| (i as f32 * 0.017).sin() * 1.1 + (i as f32 * 0.0009).cos() * 0.5)
.collect();
let activation: Vec<f32> = (0..k)
.map(|i| (i as f32 * 0.023 + 0.2).cos() * 0.8)
.collect();
for &asymmetric in &[false, true] {
let (packed, scales, zps, dequantized) =
quantize_8bit(&weights_nk, n, k, block_size, asymmetric);
let mut values = vec![0u8; n * k];
let mut scaled_zero_points = vec![0.0f32; n * k_blocks];
for output in 0..n {
for block in 0..k_blocks {
let start = block * block_size;
let valid = k.saturating_sub(start).min(block_size);
let zp = zps.as_ref().map_or(128u8, |z| z[output * k_blocks + block]);
scaled_zero_points[output * k_blocks + block] =
scales[output * k_blocks + block] * zp as f32;
for offset in 0..valid {
values[output * k + start + offset] =
packed[(output * k_blocks + block) * block_size + offset];
}
}
}
let mut out = vec![0.0f32; n];
gemv_nk_u8(
&activation,
&values,
&scales,
&scaled_zero_points,
&mut out,
k,
n,
block_size,
);
let oracle = reference(&activation, &dequantized, 1, k, n);
let oracle_rms = rmse(&oracle, &vec![0.0; n]).max(1e-6);
let rel = rmse(&out, &oracle) / oracle_rms;
assert!(
rel <= 1e-5,
"asymmetric={asymmetric}: gemv_nk_u8 relative RMSE {rel} exceeds 1e-5",
);
}
}
#[test]
fn dot_u8_i16_matches_serial_reference() {
for len in [0usize, 1, 7, 15, 16, 17, 31, 128, 130] {
let weight: Vec<u8> = (0..len).map(|i| ((i * 53 + 11) % 256) as u8).collect();
let activation: Vec<i16> = (0..len)
.map(|i| (((i * 1103) % 65535) as i32 - 32767) as i16)
.collect();
let expected: i32 = weight
.iter()
.zip(&activation)
.map(|(&w, &a)| w as i32 * a as i32)
.sum();
#[cfg(target_arch = "x86_64")]
assert_eq!(
dot_u8_i16_scalar(&weight, &activation),
expected,
"len={len}: dot_u8_i16_scalar mismatch",
);
let got = block_dot_u8_i16(&weight, &activation, &[1.0], len.max(1));
assert!(
(got - expected as f32).abs() <= 1.0 + expected.unsigned_abs() as f32 * 1e-6,
"len={len}: block_dot_u8_i16={got} != reference {expected}",
);
}
}
#[test]
fn block_dot_u8_i16_applies_per_group_scales() {
let group = 16usize;
let len = 3 * group + 5; let weight: Vec<u8> = (0..len).map(|i| ((i * 17 + 3) % 256) as u8).collect();
let activation: Vec<i16> = (0..len)
.map(|i| ((i as i32 * 211) % 4001 - 2000) as i16)
.collect();
let n_groups = len.div_ceil(group);
let group_scales: Vec<f32> = (0..n_groups).map(|g| 0.01 * (g as f32 + 1.0)).collect();
let expected: f32 = (0..n_groups)
.map(|g| {
let start = g * group;
let end = (start + group).min(len);
let dot: i32 = (start..end)
.map(|i| weight[i] as i32 * activation[i] as i32)
.sum();
group_scales[g] * dot as f32
})
.sum();
let got = block_dot_u8_i16(&weight, &activation, &group_scales, group);
assert!(
(got - expected).abs() <= 1e-3 * expected.abs().max(1.0),
"block_dot_u8_i16={got} != per-group reference {expected}",
);
}
#[test]
fn gemv_nk_u8_i16_matches_dequant_f32_reference() {
let (n, k, block_size) = (40usize, 200usize, 128usize);
let k_blocks = k.div_ceil(block_size);
let weights_nk: Vec<f32> = (0..n * k)
.map(|i| (i as f32 * 0.017).sin() * 1.1 + (i as f32 * 0.0009).cos() * 0.5)
.collect();
let activation: Vec<f32> = (0..k)
.map(|i| (i as f32 * 0.023 + 0.2).cos() * 0.8)
.collect();
for &asymmetric in &[false, true] {
let (packed, scales, zps, dequantized) =
quantize_8bit(&weights_nk, n, k, block_size, asymmetric);
let mut values = vec![0u8; n * k];
let mut scaled_zero_points = vec![0.0f32; n * k_blocks];
for output in 0..n {
for block in 0..k_blocks {
let start = block * block_size;
let valid = k.saturating_sub(start).min(block_size);
let zp = zps.as_ref().map_or(128u8, |z| z[output * k_blocks + block]);
scaled_zero_points[output * k_blocks + block] =
scales[output * k_blocks + block] * zp as f32;
for offset in 0..valid {
values[output * k + start + offset] =
packed[(output * k_blocks + block) * block_size + offset];
}
}
}
let mut out = vec![0.0f32; n];
gemv_nk_u8_i16(
&activation,
&values,
&scales,
&scaled_zero_points,
&mut out,
k,
n,
block_size,
);
let oracle = reference(&activation, &dequantized, 1, k, n);
let oracle_rms = rmse(&oracle, &vec![0.0; n]).max(1e-6);
let rel = rmse(&out, &oracle) / oracle_rms;
assert!(
rel <= 1e-3,
"asymmetric={asymmetric}: gemv_nk_u8_i16 relative RMSE {rel} exceeds 1e-3",
);
}
}
#[test]
fn gemv_nk_u8_i16_preserves_argmax_on_massive_activation_channel() {
let (n, k, block_size) = (2usize, 128usize, 128usize);
let mut values = vec![128u8; n * k];
for value in values.iter_mut().take(k).skip(1) {
*value = 132; }
values[k] = 129; let scales = vec![1.0f32; n]; let scaled_zero_points = vec![128.0f32; n]; let mut activation = vec![1.0f32; k];
activation[0] = 300.0;
let dequantized: Vec<f32> = values.iter().map(|&v| v as f32 - 128.0).collect();
let oracle = reference(&activation, &dequantized, 1, k, n);
let oracle_argmax = argmax(&oracle);
let amax = activation.iter().fold(0.0f32, |m, &v| m.max(v.abs()));
let i8_scale = amax / 127.0;
let a_int8: Vec<f32> = activation
.iter()
.map(|&v| (v / i8_scale).round().clamp(-127.0, 127.0) * i8_scale)
.collect();
let int8_out = reference(&a_int8, &dequantized, 1, k, n);
assert_ne!(
argmax(&int8_out),
oracle_argmax,
"test is vacuous: int8-activation did not flip the argmax",
);
let mut out = vec![0.0f32; n];
gemv_nk_u8_i16(
&activation,
&values,
&scales,
&scaled_zero_points,
&mut out,
k,
n,
block_size,
);
assert_eq!(
argmax(&out),
oracle_argmax,
"int16-activation flipped the argmax (oracle={oracle:?}, got={out:?})",
);
}
fn argmax(values: &[f32]) -> usize {
values
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(i, _)| i)
.unwrap()
}
#[cfg(target_arch = "x86_64")]
#[test]
#[ignore]
fn avx512_microbench() {
use std::time::Instant;
if !have_avx512bw() {
eprintln!("avx512 not available; skipping");
return;
}
let median3 = |mut f: Box<dyn FnMut() -> u64>| -> u64 {
let mut runs = [f(), f(), f()];
runs.sort_unstable();
runs[1]
};
let blocks = 4096usize;
let activation: Vec<i8> = (0..blocks * 32)
.map(|i| (((i * 37 + 11) % 255) as i32 - 127) as i8)
.collect();
let packed: Vec<u8> = (0..blocks * 16)
.map(|i| ((i * 53 + 7) % 256) as u8)
.collect();
let activation_deint = deinterleave_activation_int4(&activation);
let act_sum8 = activation_block_sums8(&activation_deint, blocks);
let scales: Vec<f32> = (0..blocks).map(|i| ((i % 17) + 1) as f32 / 100.0).collect();
let ascales: Vec<f32> = (0..blocks).map(|i| ((i % 11) + 1) as f32 / 50.0).collect();
let iters = 2000u32;
let int4_256 = median3(Box::new(|| {
let t = Instant::now();
let mut acc = 0.0f32;
for _ in 0..iters {
acc +=
unsafe { int4_dot_row_avxvnni(&activation_deint, &packed, &scales, &ascales) };
}
std::hint::black_box(acc);
t.elapsed().as_nanos() as u64
}));
let int4_512 = median3(Box::new(|| {
let t = Instant::now();
let mut acc = 0.0f32;
for _ in 0..iters {
acc += unsafe {
int4_dot_row_avx512vnni(
&activation_deint,
&packed,
&scales,
&ascales,
&act_sum8,
)
};
}
std::hint::black_box(acc);
t.elapsed().as_nanos() as u64
}));
let k16 = 65536usize;
let group = 32usize;
let w16: Vec<u8> = (0..k16).map(|i| ((i * 53 + 11) % 256) as u8).collect();
let a16: Vec<i16> = (0..k16)
.map(|i| (((i * 1103) % 65535) as i32 - 32767) as i16)
.collect();
let gs: Vec<f32> = (0..k16.div_ceil(group))
.map(|g| 0.001 * (g % 7 + 1) as f32)
.collect();
let iters16 = 2000u32;
let int16_256 = median3(Box::new(|| {
let t = Instant::now();
let mut acc = 0.0f32;
for _ in 0..iters16 {
acc += unsafe { block_dot_u8_i16_avx2(&w16, &a16, &gs, group) };
}
std::hint::black_box(acc);
t.elapsed().as_nanos() as u64
}));
let int16_512 = median3(Box::new(|| {
let t = Instant::now();
let mut acc = 0.0f32;
for _ in 0..iters16 {
acc += unsafe { block_dot_u8_i16_avx512bw(&w16, &a16, &gs, group) };
}
std::hint::black_box(acc);
t.elapsed().as_nanos() as u64
}));
eprintln!(
"MICROBENCH (median-of-3, {iters} iters):\n\
int4 256-bit avxvnni : {:>10.3} ms\n\
int4 512-bit avx512 : {:>10.3} ms ({:.2}x)\n\
int16 256-bit avx2 : {:>10.3} ms\n\
int16 512-bit avx512bw: {:>10.3} ms ({:.2}x)",
int4_256 as f64 / 1e6,
int4_512 as f64 / 1e6,
int4_256 as f64 / int4_512 as f64,
int16_256 as f64 / 1e6,
int16_512 as f64 / 1e6,
int16_256 as f64 / int16_512 as f64,
);
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,avx512f,avx512bw,avx512vnni,avx512vl")]
unsafe fn int4_dot_row_avx512vnni_old(
activation: &[i8],
packed_weight: &[u8],
scales: &[f32],
activation_scales: &[f32],
) -> f32 {
use std::arch::x86_64::*;
let low_mask = _mm_set1_epi8(0x0f);
let block_weight = |block: usize| -> __m256i {
let packed = unsafe { _mm_loadu_si128(packed_weight.as_ptr().add(block * 16).cast()) };
let low = _mm_and_si128(packed, low_mask);
let high = _mm_and_si128(_mm_srli_epi16(packed, 4), low_mask);
_mm256_set_m128i(_mm_unpackhi_epi8(low, high), _mm_unpacklo_epi8(low, high))
};
let block_count = scales.len();
let ones = _mm512_set1_epi8(1);
let ones256 = _mm256_set1_epi8(1);
let mut accumulator = _mm512_setzero_ps();
for pair in 0..block_count / 2 {
let b0 = pair * 2;
let b1 = b0 + 1;
let weight = _mm512_inserti64x4(
_mm512_castsi256_si512(block_weight(b0)),
block_weight(b1),
1,
);
let act = unsafe { _mm512_loadu_si512(activation.as_ptr().add(b0 * 32).cast()) };
let wdot = _mm512_dpbusd_epi32(_mm512_setzero_si512(), weight, act);
let asum = _mm512_dpbusd_epi32(_mm512_setzero_si512(), ones, act);
let dot = _mm512_sub_epi32(wdot, _mm512_slli_epi32(asum, 3));
let s0 = scales[b0] * activation_scales[b0];
let s1 = scales[b1] * activation_scales[b1];
let scale_vec = _mm512_set_ps(
s1, s1, s1, s1, s1, s1, s1, s1, s0, s0, s0, s0, s0, s0, s0, s0,
);
accumulator = _mm512_add_ps(
accumulator,
_mm512_mul_ps(_mm512_cvtepi32_ps(dot), scale_vec),
);
}
let mut value = _mm512_reduce_add_ps(accumulator);
if block_count % 2 == 1 {
let block = block_count - 1;
let weight = block_weight(block);
let act = unsafe { _mm256_loadu_si256(activation.as_ptr().add(block * 32).cast()) };
let wdot = _mm256_dpbusd_epi32(_mm256_setzero_si256(), weight, act);
let asum = _mm256_dpbusd_epi32(_mm256_setzero_si256(), ones256, act);
let dot = _mm256_sub_epi32(wdot, _mm256_slli_epi32(asum, 3));
let block_scale = scales[block] * activation_scales[block];
let scaled = _mm256_mul_ps(_mm256_cvtepi32_ps(dot), _mm256_set1_ps(block_scale));
value += horizontal_sum_f32_256(scaled);
}
value
}
#[cfg(target_arch = "x86_64")]
#[test]
#[ignore]
fn int4_unpack_before_after_bench() {
use std::time::Instant;
assert!(
std::arch::is_x86_feature_detected!("avx512vnni")
&& std::arch::is_x86_feature_detected!("avx512vl")
&& std::arch::is_x86_feature_detected!("avx512bw"),
"bench requires avx512vnni/vl/bw",
);
let k = 2048usize;
let n = 2048usize;
let blocks = k / 32;
let activation: Vec<i8> = (0..k)
.map(|i| (((i * 37 + 11) % 255) as i32 - 127) as i8)
.collect();
let activation_deint = deinterleave_activation_int4(&activation);
let ascales: Vec<f32> = (0..blocks).map(|i| ((i % 11) + 1) as f32 / 50.0).collect();
let packed: Vec<u8> = (0..n * blocks * 16)
.map(|i| ((i * 53 + 7) % 256) as u8)
.collect();
let scales: Vec<f32> = (0..n * blocks)
.map(|i| ((i % 17) + 1) as f32 / 100.0)
.collect();
let reps = 7u32;
let median = |mut runs: Vec<u64>| -> u64 {
runs.sort_unstable();
runs[runs.len() / 2]
};
let mut old_runs = Vec::new();
let mut new_runs = Vec::new();
for _ in 0..reps {
let t = Instant::now();
let mut acc = 0.0f32;
for row in 0..n {
let ps = &packed[row * blocks * 16..(row + 1) * blocks * 16];
let ss = &scales[row * blocks..(row + 1) * blocks];
acc += unsafe { int4_dot_row_avx512vnni_old(&activation, ps, ss, &ascales) };
}
std::hint::black_box(acc);
old_runs.push(t.elapsed().as_nanos() as u64);
let t = Instant::now();
let mut acc = 0.0f32;
let act = deinterleave_activation_int4(&activation);
let act_sum8 = activation_block_sums8(&act, blocks);
for row in 0..n {
let ps = &packed[row * blocks * 16..(row + 1) * blocks * 16];
let ss = &scales[row * blocks..(row + 1) * blocks];
acc += unsafe { int4_dot_row_avx512vnni(&act, ps, ss, &ascales, &act_sum8) };
}
std::hint::black_box(acc);
new_runs.push(t.elapsed().as_nanos() as u64);
}
let ps = &packed[0..blocks * 16];
let ss = &scales[0..blocks];
let act_sum8 = activation_block_sums8(&activation_deint, blocks);
let old0 = unsafe { int4_dot_row_avx512vnni_old(&activation, ps, ss, &ascales) };
let new0 =
unsafe { int4_dot_row_avx512vnni(&activation_deint, ps, ss, &ascales, &act_sum8) };
assert!(
(old0 - new0).abs() <= 1e-4 * old0.abs().max(1.0),
"old {old0} new {new0}"
);
let old_ms = median(old_runs) as f64 / 1e6;
let new_ms = median(new_runs) as f64 / 1e6;
eprintln!(
"INT4 UNPACK BEFORE/AFTER (K={k} N={n} block=32, median of {reps}):\n\
before (unpacklo/unpackhi + inserti64x4): {old_ms:>8.3} ms\n\
after (deinterleave-once + permutex2var): {new_ms:>8.3} ms\n\
speedup: {:.3}x ({:+.1}%)",
old_ms / new_ms,
(old_ms / new_ms - 1.0) * 100.0,
);
}
#[cfg(target_arch = "x86_64")]
#[test]
fn int4_dot_row_avx512vnni_matches_scalar() {
if !(std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("avx512f")
&& std::arch::is_x86_feature_detected!("avx512bw")
&& std::arch::is_x86_feature_detected!("avx512vnni")
&& std::arch::is_x86_feature_detected!("avx512vl"))
{
return;
}
for blocks in [1usize, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 16, 17, 18] {
let activation: Vec<i8> = (0..blocks * 32)
.map(|i| (((i * 37 + 11) % 255) as i32 - 127) as i8)
.collect();
let packed: Vec<u8> = (0..blocks * 16)
.map(|i| ((i * 53 + 7) % 256) as u8)
.collect();
let scales: Vec<f32> = (0..blocks)
.map(|i| ((i * 13 % 17) + 1) as f32 / 100.0)
.collect();
let activation_scales: Vec<f32> = (0..blocks)
.map(|i| ((i * 7 % 11) + 1) as f32 / 50.0)
.collect();
let scalar = int4_dot_row_scalar(&activation, &packed, &scales, &activation_scales);
let activation_deint = deinterleave_activation_int4(&activation);
let act_sum8 = activation_block_sums8(&activation_deint, blocks);
let wide = unsafe {
int4_dot_row_avx512vnni(
&activation_deint,
&packed,
&scales,
&activation_scales,
&act_sum8,
)
};
assert!(
(wide - scalar).abs() <= 1e-4 * scalar.abs().max(1.0),
"blocks={blocks}: avx512 int4 dot {wide} != scalar {scalar}",
);
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn int4_dot_row_avx512vnni_multiaccumulator_preserves_argmax_vs_scalar() {
if !(std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("avx512f")
&& std::arch::is_x86_feature_detected!("avx512bw")
&& std::arch::is_x86_feature_detected!("avx512vnni")
&& std::arch::is_x86_feature_detected!("avx512vl"))
{
return;
}
for blocks in [6usize, 7, 10, 11, 12, 13, 16, 17, 18] {
let n = 8usize;
let activation: Vec<i8> = (0..blocks * 32)
.map(|i| (((i * 29 + 3) % 255) as i32 - 127) as i8)
.collect();
let activation_deint = deinterleave_activation_int4(&activation);
let act_sum8 = activation_block_sums8(&activation_deint, blocks);
let activation_scales: Vec<f32> = (0..blocks)
.map(|i| ((i * 5 % 13) + 1) as f32 / 40.0)
.collect();
let base_packed: Vec<u8> = (0..blocks * 16)
.map(|i| ((i * 53 + 7) % 256) as u8)
.collect();
let base_scales: Vec<f32> = (0..blocks)
.map(|i| ((i * 13 % 17) + 1) as f32 / 100.0)
.collect();
let mut scalar_out = vec![0.0f32; n];
let mut wide_out = vec![0.0f32; n];
for row in 0..n {
let mut packed = base_packed.clone();
let idx = row % packed.len();
packed[idx] = packed[idx].wrapping_add(1);
scalar_out[row] =
int4_dot_row_scalar(&activation, &packed, &base_scales, &activation_scales);
wide_out[row] = unsafe {
int4_dot_row_avx512vnni(
&activation_deint,
&packed,
&base_scales,
&activation_scales,
&act_sum8,
)
};
}
assert_eq!(
argmax(&scalar_out),
argmax(&wide_out),
"blocks={blocks}: multi-accumulator avx512 flipped argmax vs scalar\n\
scalar={scalar_out:?}\nwide={wide_out:?}",
);
for row in 0..n {
assert!(
(wide_out[row] - scalar_out[row]).abs()
<= 1e-4 * scalar_out[row].abs().max(1.0),
"blocks={blocks} row={row}: {} != {}",
wide_out[row],
scalar_out[row],
);
}
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn int4_dot_row_avxvnni_matches_scalar() {
if !(std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("avxvnni"))
{
return;
}
for blocks in [1usize, 2, 3, 4, 5, 8, 9] {
let activation: Vec<i8> = (0..blocks * 32)
.map(|i| (((i * 41 + 5) % 255) as i32 - 127) as i8)
.collect();
let packed: Vec<u8> = (0..blocks * 16)
.map(|i| ((i * 47 + 3) % 256) as u8)
.collect();
let scales: Vec<f32> = (0..blocks)
.map(|i| ((i * 13 % 17) + 1) as f32 / 100.0)
.collect();
let activation_scales: Vec<f32> = (0..blocks)
.map(|i| ((i * 7 % 11) + 1) as f32 / 50.0)
.collect();
let scalar = int4_dot_row_scalar(&activation, &packed, &scales, &activation_scales);
let activation_deint = deinterleave_activation_int4(&activation);
let wide = unsafe {
int4_dot_row_avxvnni(&activation_deint, &packed, &scales, &activation_scales)
};
assert!(
(wide - scalar).abs() <= 1e-4 * scalar.abs().max(1.0),
"blocks={blocks}: avxvnni int4 dot {wide} != scalar {scalar}",
);
}
}
#[test]
fn deinterleave_activation_int4_is_a_block_permutation() {
let activation: Vec<i8> = (0..96i32).map(|i| (i - 48) as i8).collect();
let deint = deinterleave_activation_int4(&activation);
assert_eq!(deint.len(), activation.len());
for block in 0..activation.len() / 32 {
for i in 0..16 {
assert_eq!(deint[block * 32 + i], activation[block * 32 + 2 * i]);
assert_eq!(
deint[block * 32 + 16 + i],
activation[block * 32 + 2 * i + 1]
);
}
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn dot_u8_i8_avx512vnni_matches_scalar() {
if !(std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("avx512f")
&& std::arch::is_x86_feature_detected!("avx512vnni")
&& std::arch::is_x86_feature_detected!("avx512vl"))
{
return;
}
for len in [0usize, 1, 7, 31, 32, 33, 63, 64, 65, 96, 129, 200] {
let activation: Vec<u8> = (0..len).map(|i| ((i * 29 + 7) % 255) as u8).collect();
let weight: Vec<i8> = (0..len).map(|i| ((i * 17 % 31) as i8) - 15).collect();
let scalar = dot_u8_i8_scalar(&activation, &weight);
let wide = unsafe { dot_u8_i8_avx512vnni(&activation, &weight) };
assert_eq!(wide, scalar, "len={len}: avx512 u8xi8 dot mismatch");
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn dot_u8_i8_avx2_matches_scalar() {
if !std::arch::is_x86_feature_detected!("avx2") {
return;
}
let mut state = 0x243f_6a88_85a3_08d3u64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for len in [
0usize, 1, 2, 3, 7, 8, 15, 16, 17, 31, 32, 33, 48, 63, 64, 65, 96, 127, 128, 129, 200,
255, 256, 257, 512, 1000,
] {
let a_max = vec![255u8; len];
let w_pos = vec![127i8; len];
let w_neg = vec![-128i8; len];
let a_zero = vec![0u8; len];
for (act, wgt) in [
(&a_max, &w_pos),
(&a_max, &w_neg),
(&a_zero, &w_neg),
(&a_max, &a_max.iter().map(|_| -1i8).collect::<Vec<_>>()),
] {
let scalar = dot_u8_i8_scalar(act, wgt);
let simd = unsafe { dot_u8_i8_avx2(act, wgt) };
assert_eq!(simd, scalar, "len={len}: adversarial avx2 dot mismatch");
}
let activation: Vec<u8> = (0..len).map(|_| (next() & 0xff) as u8).collect();
let weight: Vec<i8> = (0..len).map(|_| (next() & 0xff) as u8 as i8).collect();
let scalar = dot_u8_i8_scalar(&activation, &weight);
let simd = unsafe { dot_u8_i8_avx2(&activation, &weight) };
assert_eq!(simd, scalar, "len={len}: random avx2 dot mismatch");
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn int8_row_avx2_matches_scalar() {
if !std::arch::is_x86_feature_detected!("avx2") {
return;
}
let (k, n, block_size) = (131usize, 7usize, 32usize);
let padded_k = k.div_ceil(block_size) * block_size;
let k_blocks = padded_k / block_size;
let mut state = 0x9e37_79b9_7f4a_7c15u64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let activation: Vec<u8> = (0..padded_k).map(|_| (next() & 0xff) as u8).collect();
let activation_scales: Vec<f32> = (0..k_blocks)
.map(|_| (next() & 0xff) as f32 / 512.0 + 0.01)
.collect();
let values: Vec<i8> = (0..n * padded_k)
.map(|_| (next() & 0xff) as u8 as i8)
.collect();
let scales: Vec<f32> = (0..n * k_blocks)
.map(|_| (next() & 0xff) as f32 / 512.0 + 0.01)
.collect();
let block_sums: Vec<i32> = values
.chunks_exact(block_size)
.map(|b| b.iter().map(|&w| w as i32).sum())
.collect();
let weight = Int8Weight {
values,
scales,
block_sums,
};
let mut scalar_out = vec![0.0f32; n];
let mut avx2_out = vec![0.0f32; n];
int8_row(
&activation,
&activation_scales,
&weight,
&mut scalar_out,
k_blocks,
padded_k,
block_size,
DotKernel::Scalar,
false,
);
int8_row(
&activation,
&activation_scales,
&weight,
&mut avx2_out,
k_blocks,
padded_k,
block_size,
DotKernel::Avx2,
false,
);
assert_eq!(avx2_out, scalar_out, "Avx2 int8_row diverged from Scalar");
}
#[cfg(target_arch = "aarch64")]
#[test]
fn dot_u8_i8_neon_matches_scalar() {
let mut state = 0x243f_6a88_85a3_08d3u64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for len in [
0usize, 1, 2, 3, 7, 8, 15, 16, 17, 31, 32, 33, 48, 63, 64, 65, 96, 127, 128, 129, 200,
255, 256, 257, 512, 1000,
] {
let a_max = vec![255u8; len];
let w_pos = vec![127i8; len];
let w_neg = vec![-128i8; len];
let a_zero = vec![0u8; len];
for (act, wgt) in [
(&a_max, &w_pos),
(&a_max, &w_neg),
(&a_zero, &w_neg),
(&a_max, &a_max.iter().map(|_| -1i8).collect::<Vec<_>>()),
] {
let scalar = dot_u8_i8_scalar(act, wgt);
let simd = unsafe { dot_u8_i8_neon(act, wgt) };
assert_eq!(simd, scalar, "len={len}: adversarial neon dot mismatch");
}
let activation: Vec<u8> = (0..len).map(|_| (next() & 0xff) as u8).collect();
let weight: Vec<i8> = (0..len).map(|_| (next() & 0xff) as u8 as i8).collect();
let scalar = dot_u8_i8_scalar(&activation, &weight);
let simd = unsafe { dot_u8_i8_neon(&activation, &weight) };
assert_eq!(simd, scalar, "len={len}: random neon dot mismatch");
}
}
#[cfg(target_arch = "aarch64")]
#[test]
fn selected_dot_kernel_is_neon_on_aarch64() {
assert_eq!(
selected_dot_kernel(),
DotKernel::Neon,
"aarch64 must select the NEON int8 dot, never Scalar"
);
let activation: Vec<u8> = (0..128).map(|i| ((i * 29 + 7) % 255) as u8).collect();
let weight: Vec<i8> = (0..128).map(|i| ((i * 17 % 31) as i8) - 15).collect();
let scalar = dot_u8_i8(&activation, &weight, DotKernel::Scalar);
assert_eq!(dot_u8_i8(&activation, &weight, DotKernel::Neon), scalar);
}
#[cfg(target_arch = "aarch64")]
#[test]
fn int8_row_neon_matches_scalar() {
let (k, n, block_size) = (131usize, 7usize, 32usize);
let padded_k = k.div_ceil(block_size) * block_size;
let k_blocks = padded_k / block_size;
let mut state = 0x9e37_79b9_7f4a_7c15u64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let activation: Vec<u8> = (0..padded_k).map(|_| (next() & 0xff) as u8).collect();
let activation_scales: Vec<f32> = (0..k_blocks)
.map(|_| (next() & 0xff) as f32 / 512.0 + 0.01)
.collect();
let values: Vec<i8> = (0..n * padded_k)
.map(|_| (next() & 0xff) as u8 as i8)
.collect();
let scales: Vec<f32> = (0..n * k_blocks)
.map(|_| (next() & 0xff) as f32 / 512.0 + 0.01)
.collect();
let block_sums: Vec<i32> = values
.chunks_exact(block_size)
.map(|b| b.iter().map(|&w| w as i32).sum())
.collect();
let weight = Int8Weight {
values,
scales,
block_sums,
};
let mut scalar_out = vec![0.0f32; n];
let mut neon_out = vec![0.0f32; n];
int8_row(
&activation,
&activation_scales,
&weight,
&mut scalar_out,
k_blocks,
padded_k,
block_size,
DotKernel::Scalar,
false,
);
int8_row(
&activation,
&activation_scales,
&weight,
&mut neon_out,
k_blocks,
padded_k,
block_size,
DotKernel::Neon,
false,
);
assert_eq!(neon_out, scalar_out, "Neon int8_row diverged from Scalar");
}
#[cfg(target_arch = "x86_64")]
#[test]
fn dot_u8_i16_avx512_matches_serial_reference() {
if !have_avx512bw() {
return;
}
for &(group, len) in &[
(32usize, 32usize),
(32, 33),
(32, 48),
(32, 96),
(32, 130),
(16, 48),
(64, 200),
(128, 128),
] {
let weight: Vec<u8> = (0..len).map(|i| ((i * 53 + 11) % 256) as u8).collect();
let activation: Vec<i16> = (0..len)
.map(|i| (((i * 1103) % 65535) as i32 - 32767) as i16)
.collect();
let n_groups = len.div_ceil(group);
let group_scales: Vec<f32> = (0..n_groups).map(|g| 0.01 * (g as f32 + 1.0)).collect();
let expected: f32 = (0..n_groups)
.map(|g| {
let start = g * group;
let end = (start + group).min(len);
let dot: i32 = (start..end)
.map(|i| weight[i] as i32 * activation[i] as i32)
.sum();
group_scales[g] * dot as f32
})
.sum();
let got =
unsafe { block_dot_u8_i16_avx512bw(&weight, &activation, &group_scales, group) };
assert!(
(got - expected).abs() <= 1e-3 * expected.abs().max(1.0),
"group={group} len={len}: avx512bw block dot {got} != reference {expected}",
);
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn gemv_nk_u8_i16_avx512_preserves_argmax_on_massive_activation_channel() {
if !have_avx512bw() {
return;
}
let (n, k, block_size) = (2usize, 128usize, 128usize);
let mut values = vec![128u8; n * k];
for value in values.iter_mut().take(k).skip(1) {
*value = 132; }
values[k] = 129; let scales = vec![1.0f32; n];
let scaled_zero_points = vec![128.0f32; n];
let mut activation = vec![1.0f32; k];
activation[0] = 300.0;
let dequantized: Vec<f32> = values.iter().map(|&v| v as f32 - 128.0).collect();
let oracle = reference(&activation, &dequantized, 1, k, n);
let oracle_argmax = argmax(&oracle);
let amax = activation.iter().fold(0.0f32, |m, &v| m.max(v.abs()));
let i8_scale = amax / 127.0;
let a_int8: Vec<f32> = activation
.iter()
.map(|&v| (v / i8_scale).round().clamp(-127.0, 127.0) * i8_scale)
.collect();
let int8_out = reference(&a_int8, &dequantized, 1, k, n);
assert_ne!(
argmax(&int8_out),
oracle_argmax,
"test is vacuous: int8-activation did not flip the argmax",
);
let mut out = vec![0.0f32; n];
gemv_nk_u8_i16(
&activation,
&values,
&scales,
&scaled_zero_points,
&mut out,
k,
n,
block_size,
);
assert_eq!(
argmax(&out),
oracle_argmax,
"avx512bw int16 flipped the argmax (oracle={oracle:?}, got={out:?})",
);
}
#[test]
fn matmulnbits_direct_int4_parallel_partial_k_matches_serial() {
let (k, n, block_size) = (77usize, 1025usize, 32usize);
let blocks = k.div_ceil(block_size);
let activations: Vec<f32> = (0..k)
.map(|i| ((i * 23 % 53) as f32 - 26.0) / 17.0)
.collect();
let packed_weight = PackedInt4Weight {
values: (0..n * blocks * block_size / 2)
.map(|i| ((i * 29 + 7) % 256) as u8)
.collect(),
scales: (0..n * blocks)
.map(|i| ((i * 13 % 17) + 1) as f32 / 100.0)
.collect(),
};
let mut serial = vec![0.0; n];
let mut parallel = vec![0.0; n];
let dot_kernel = selected_dot_kernel();
rayon::ThreadPoolBuilder::new()
.num_threads(1)
.build()
.unwrap()
.install(|| {
int4_matmul_m1(&activations, &packed_weight, &mut serial, k, n, dot_kernel);
});
rayon::ThreadPoolBuilder::new()
.num_threads(4)
.build()
.unwrap()
.install(|| {
int4_matmul_m1(
&activations,
&packed_weight,
&mut parallel,
k,
n,
dot_kernel,
);
});
assert_eq!(parallel, serial);
}
#[test]
fn matmulnbits_parallel_n_partition_matches_serial() {
let (k, n, block_size) = (1025usize, 1025usize, 32usize);
let padded_k = k.div_ceil(block_size) * block_size;
let activations: Vec<f32> = (0..k)
.map(|i| ((i * 23 % 53) as f32 - 26.0) / 17.0)
.collect();
let values: Vec<i8> = (0..n * padded_k)
.map(|i| ((i * 11 % 16) as i8) - 8)
.collect();
let block_sums = values
.chunks_exact(block_size)
.map(|block| block.iter().map(|&value| value as i32).sum())
.collect();
let weight = Int8Weight {
values,
scales: vec![0.01; n * k.div_ceil(block_size)],
block_sums,
};
let mut serial = vec![0.0; n];
let mut parallel = vec![0.0; n];
rayon::ThreadPoolBuilder::new()
.num_threads(1)
.build()
.unwrap()
.install(|| {
int8_matmul(
&activations,
&weight,
&mut serial,
1,
k,
n,
block_size,
DotKernel::Scalar,
);
});
rayon::ThreadPoolBuilder::new()
.num_threads(4)
.build()
.unwrap()
.install(|| {
int8_matmul(
&activations,
&weight,
&mut parallel,
1,
k,
n,
block_size,
DotKernel::Scalar,
);
});
assert_eq!(parallel, serial);
}
#[test]
fn matmulnbits_partition_scales_with_pool_size_and_work() {
let chunk = |threads, n, k| {
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap()
.install(|| output_chunk_len(n, k))
};
assert_eq!(chunk(1, 4864, 896), 4864);
assert_eq!(chunk(24, 16, 32), 16);
assert_eq!(chunk(24, 896, 896), 36);
assert_eq!(chunk(48, 896, 896), 36);
assert_eq!(chunk(96, 896, 896), 896);
assert_eq!(chunk(96, 4864, 896), 4864);
assert_eq!(chunk(96, 151_936, 896), 1583);
}
#[test]
fn decode_thread_count_defaults_invalid_values_and_clamps() {
assert_eq!(resolve_decode_threads(None, 96), Some(8));
assert_eq!(resolve_decode_threads(None, 4), Some(3));
assert_eq!(resolve_decode_threads(None, 8), Some(4));
assert_eq!(resolve_decode_threads(None, 1), Some(1));
assert_eq!(resolve_decode_threads(Some(""), 96), Some(8));
assert_eq!(resolve_decode_threads(Some("0"), 8), None);
assert_eq!(resolve_decode_threads(Some("4"), 96), Some(4));
assert_eq!(resolve_decode_threads(Some("1000"), 96), Some(96));
assert_eq!(resolve_decode_threads(Some("abc"), 96), Some(8));
assert_eq!(resolve_decode_threads(Some("-4"), 4), Some(3));
assert_eq!(resolve_decode_threads(Some("4"), 0), None);
}
#[test]
fn persistent_decode_thread_default_is_half_the_logical_cpus() {
assert_eq!(default_persistent_threads(96), Some(48));
assert_eq!(default_persistent_threads(8), Some(4));
assert_eq!(default_persistent_threads(4), Some(2));
assert_eq!(default_persistent_threads(2), Some(1));
assert_eq!(default_persistent_threads(1), Some(1));
assert_eq!(default_persistent_threads(0), None);
assert_ne!(default_persistent_threads(96), default_decode_threads(96));
}
#[test]
fn persistent_decode_threads_honor_env_and_opt_out() {
assert_eq!(resolve_persistent_decode_threads(None, 96), Some(48));
assert_eq!(resolve_persistent_decode_threads(Some(""), 96), Some(48));
assert_eq!(resolve_persistent_decode_threads(Some("0"), 96), None);
assert_eq!(resolve_persistent_decode_threads(Some("32"), 96), Some(32));
assert_eq!(resolve_persistent_decode_threads(Some("1"), 96), Some(1));
assert_eq!(
resolve_persistent_decode_threads(Some("1000"), 96),
Some(96)
);
assert_eq!(resolve_persistent_decode_threads(Some("abc"), 96), Some(48));
assert_eq!(resolve_persistent_decode_threads(Some("-4"), 8), Some(4));
assert_eq!(resolve_persistent_decode_threads(Some("8"), 0), None);
}
#[test]
fn rayon_global_threads_bound_only_by_an_explicit_budget() {
assert_eq!(resolve_rayon_global_threads(None, None, 96), None);
assert_eq!(resolve_rayon_global_threads(None, Some(""), 96), None);
assert_eq!(resolve_rayon_global_threads(Some(8), None, 96), Some(8));
assert_eq!(
resolve_rayon_global_threads(Some(8), Some("2"), 96),
Some(8)
);
assert_eq!(resolve_rayon_global_threads(Some(1000), None, 96), Some(96));
assert_eq!(resolve_rayon_global_threads(None, Some("8"), 96), Some(8));
assert_eq!(resolve_rayon_global_threads(None, Some(" 8 "), 96), Some(8));
assert_eq!(resolve_rayon_global_threads(None, Some("0"), 96), None);
assert_eq!(resolve_rayon_global_threads(None, Some("abc"), 96), None);
assert_eq!(resolve_rayon_global_threads(Some(0), Some("8"), 96), None);
assert_eq!(resolve_rayon_global_threads(Some(8), None, 0), None);
}
#[test]
fn explicit_budget_precedes_env_for_every_decode_pool() {
assert_eq!(
resolve_decode_threads_with_override(Some(6), Some("2"), 96),
Some(6)
);
assert_eq!(
resolve_persistent_decode_threads_with_override(Some(8), Some("32"), 96),
Some(8)
);
assert_eq!(
resolve_dense_decode_threads_with_override(Some(12), Some("0"), 96),
Some(12)
);
assert_eq!(
resolve_persistent_decode_threads_with_override(None, None, 96),
Some(48),
"the uncapped automatic default must remain unchanged"
);
}
#[test]
fn dense_decode_thread_default_scales_and_clamps() {
assert_eq!(default_dense_decode_threads(96), Some(24));
assert_eq!(default_dense_decode_threads(128), Some(32)); assert_eq!(default_dense_decode_threads(64), Some(16));
assert_eq!(default_dense_decode_threads(48), Some(12));
assert_eq!(default_dense_decode_threads(16), Some(8)); assert_eq!(default_dense_decode_threads(8), Some(8)); assert_eq!(default_dense_decode_threads(4), Some(4)); assert_eq!(default_dense_decode_threads(1), Some(1));
assert_eq!(default_dense_decode_threads(0), None);
assert_ne!(default_dense_decode_threads(96), default_decode_threads(96));
assert_ne!(
default_dense_decode_threads(96),
default_persistent_threads(96)
);
}
#[test]
fn dense_decode_threads_honor_env_and_opt_out() {
assert_eq!(resolve_dense_decode_threads(None, 96), Some(24));
assert_eq!(resolve_dense_decode_threads(Some(""), 96), Some(24));
assert_eq!(resolve_dense_decode_threads(Some("0"), 96), None);
assert_eq!(resolve_dense_decode_threads(Some("20"), 96), Some(20));
assert_eq!(resolve_dense_decode_threads(Some("1000"), 96), Some(96));
assert_eq!(resolve_dense_decode_threads(Some("1"), 96), Some(1));
assert_eq!(resolve_dense_decode_threads(Some("abc"), 96), Some(24));
assert_eq!(resolve_dense_decode_threads(Some("8"), 0), None);
}
#[test]
fn dense_decode_pool_scope_runs_and_clears_residency() {
let sum = with_decode_pool_scope(false, || (0..1_000u64).sum::<u64>());
assert_eq!(sum, 499_500);
assert!(
!IN_DECODE_POOL.with(Cell::get),
"dense scope must not leak the residency flag to the caller"
);
assert!(spmd_decode_active().is_none());
}
#[test]
fn decode_thread_pool_supports_global_pool_opt_out() {
assert!(build_decode_pool(None).unwrap().is_none());
let pool = build_decode_pool(Some(3)).unwrap().unwrap();
assert_eq!(pool.install(rayon::current_num_threads), 3);
}
#[test]
fn decode_residency_guard_sets_and_restores_flag() {
assert!(!IN_DECODE_POOL.with(Cell::get));
{
let _outer = DecodeResidencyGuard::enter();
assert!(IN_DECODE_POOL.with(Cell::get));
{
let _inner = DecodeResidencyGuard::enter();
assert!(IN_DECODE_POOL.with(Cell::get));
}
assert!(IN_DECODE_POOL.with(Cell::get));
}
assert!(!IN_DECODE_POOL.with(Cell::get));
}
#[test]
fn decode_residency_guard_clears_on_panic() {
assert!(!IN_DECODE_POOL.with(Cell::get));
let result = std::panic::catch_unwind(|| {
let _guard = DecodeResidencyGuard::enter();
assert!(IN_DECODE_POOL.with(Cell::get));
panic!("decode forward panicked");
});
assert!(result.is_err());
assert!(
!IN_DECODE_POOL.with(Cell::get),
"residency flag must be cleared after a panicking forward unwinds"
);
}
#[test]
fn with_decode_pool_runs_inline_when_resident() {
let _guard = DecodeResidencyGuard::enter();
let caller = std::thread::current().id();
let ran_on = with_decode_pool(|| std::thread::current().id()).unwrap();
assert_eq!(
ran_on, caller,
"resident with_decode_pool must run inline on the caller thread"
);
}
#[test]
fn with_decode_pool_scope_marks_residency_when_pool_active() {
let pool_active = DECODE_POOL
.get_or_init(|| build_decode_pool(configured_decode_threads()))
.as_ref()
.ok()
.and_then(Option::as_ref)
.is_some();
let (flag_inside, inline_same_thread) = with_decode_pool_scope(true, || {
let worker = std::thread::current().id();
let inner = with_decode_pool(|| std::thread::current().id()).unwrap();
(IN_DECODE_POOL.with(Cell::get), inner == worker)
});
if pool_active {
assert!(flag_inside, "scope must set residency flag inside the pool");
assert!(
inline_same_thread,
"inner with_decode_pool must run inline on the scope worker"
);
}
assert!(!IN_DECODE_POOL.with(Cell::get));
}
#[test]
fn matmulnbits_symmetric_block32_matches_independent_dequantization() {
let (m, k, n, block_size) = (3, 64, 8, 32);
let a: Vec<f32> = (0..m * k)
.map(|i| ((i * 17 % 29) as f32 - 14.0) / 11.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 13 % 31) as f32 - 15.0) / 9.0)
.collect();
let (packed, scales, _, dequantized) = quantize(&weights, n, k, block_size, false);
let (graph, node) = model_node(
&[m, k],
&[n, 2, 16],
&[n, 2],
None,
&[m, n],
k,
n,
block_size,
);
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let a = Owned::f32(&[m, k], &a);
let b = Owned::u8(&[n, 2, 16], &packed);
let scales = Owned::f32(&[n, 2], &scales);
let mut y = Owned::zeros_f32(&[m, n]);
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
assert_close(&y.to_f32(), &reference(&a.to_f32(), &dequantized, m, k, n));
}
#[test]
fn matmulnbits_f16_bf16_inputs_match_widened_f32_for_decode_and_prefill() {
let (k, n, block_size) = (64usize, 9usize, 32usize);
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 13 % 31) as f32 - 15.0) / 9.0)
.collect();
let (packed, scales, _, _) = quantize(&weights, n, k, block_size, false);
let bias_values: Vec<f32> = (0..n).map(|i| (i as f32 - 4.0) / 17.0).collect();
for dtype in [DataType::Float16, DataType::BFloat16] {
for m in [1usize, 3usize] {
let a_values: Vec<f32> = (0..m * k)
.map(|i| ((i * 17 % 43) as f32 - 21.0) / 13.0)
.collect();
let low_a = match dtype {
DataType::Float16 => Owned::f16(&[m, k], &a_values),
DataType::BFloat16 => Owned::bf16(&[m, k], &a_values),
_ => unreachable!(),
};
let low_scales = match dtype {
DataType::Float16 => Owned::f16(&[n, 2], &scales),
DataType::BFloat16 => Owned::bf16(&[n, 2], &scales),
_ => unreachable!(),
};
let low_bias = match dtype {
DataType::Float16 => Owned::f16(&[n], &bias_values),
DataType::BFloat16 => Owned::bf16(&[n], &bias_values),
_ => unreachable!(),
};
let widened = |owned: &Owned| match dtype {
DataType::Float16 => owned.to_f16_as_f32(),
DataType::BFloat16 => owned.to_bf16_as_f32(),
_ => unreachable!(),
};
let f32_a = Owned::f32(&[m, k], &widened(&low_a));
let f32_scales = Owned::f32(&[n, 2], &widened(&low_scales));
let f32_bias = Owned::f32(&[n], &widened(&low_bias));
let b = Owned::u8(&[n, 2, 16], &packed);
let absent_zp = TensorView::absent(DataType::Uint8);
let absent_gidx = TensorView::absent(DataType::Int32);
let mut low_y = Owned::zeros(dtype, &[m, n]);
accuracy4_kernel(k, n, block_size)
.execute(
&[
low_a.view(),
b.view(),
low_scales.view(),
absent_zp,
absent_gidx,
low_bias.view(),
],
&mut [low_y.view_mut()],
)
.unwrap();
let mut f32_y = Owned::zeros_f32(&[m, n]);
accuracy4_kernel(k, n, block_size)
.execute(
&[
f32_a.view(),
b.view(),
f32_scales.view(),
absent_zp,
absent_gidx,
f32_bias.view(),
],
&mut [f32_y.view_mut()],
)
.unwrap();
let actual = widened(&low_y);
let reference = f32_y.to_f32();
let narrowed_reference: Vec<f32> = reference
.iter()
.map(|&value| match dtype {
DataType::Float16 => half::f16::from_f32(value).to_f32(),
DataType::BFloat16 => half::bf16::from_f32(value).to_f32(),
_ => unreachable!(),
})
.collect();
assert_eq!(
actual, narrowed_reference,
"{dtype:?} M={m} must compute in f32 and narrow only at output"
);
let tolerance: f32 = match dtype {
DataType::Float16 => 2e-2,
DataType::BFloat16 => 1.5e-1,
_ => unreachable!(),
};
for (index, (&actual, &reference)) in actual.iter().zip(&reference).enumerate() {
assert!(
(actual - reference).abs() <= tolerance.max(tolerance * reference.abs()),
"{dtype:?} M={m} index {index}: actual={actual}, widened f32={reference}"
);
}
}
}
}
#[test]
fn matmulnbits_2bit_symmetric_block32_matches_dequantized_f32_reference() {
let (m, k, n, block_size) = (3, 45, 7, 32);
let a_values: Vec<f32> = (0..m * k)
.map(|i| ((i * 17 % 43) as f32 - 21.0) / 13.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 19 % 47) as f32 - 23.0) / 12.0)
.collect();
let (packed, scales) = quantize_symmetric_2bit(&weights, n, k, block_size);
let dequantized = dequantize_2bit_reference(&packed, &scales, n, k, block_size);
let blocks = k.div_ceil(block_size);
let (graph, node) = model_node(
&[m, k],
&[n, blocks, block_size / 4],
&[n, blocks],
None,
&[m, n],
k,
n,
block_size,
);
let mut graph = graph;
graph
.node_mut(node)
.attributes
.insert("bits".into(), Attribute::Int(2));
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.expect("CPU EP must register bits=2 MatMulNBits");
let a = Owned::f32(&[m, k], &a_values);
let b = Owned::u8(&[n, blocks, block_size / 4], &packed);
let scales = Owned::f32(&[n, blocks], &scales);
let mut y = Owned::zeros_f32(&[m, n]);
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
assert_close(&y.to_f32(), &reference(&a_values, &dequantized, m, k, n));
}
#[test]
fn matmulnbits_2bit_unpacks_low_bits_first() {
let k = 32;
let (graph, node) = model_node(&[1, k], &[1, 1, 8], &[1], None, &[1, 1], k, 1, 32);
let mut graph = graph;
graph
.node_mut(node)
.attributes
.insert("bits".into(), Attribute::Int(2));
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let mut activation = vec![0.0; k];
activation[..4].copy_from_slice(&[1.0, 10.0, 100.0, 1000.0]);
let mut packed = vec![0xaa; 8];
packed[0] = 0b11_10_01_00;
let a = Owned::f32(&[1, k], &activation);
let b = Owned::u8(&[1, 1, 8], &packed);
let scales = Owned::f32(&[1], &[1.0]);
let mut y = Owned::zeros_f32(&[1, 1]);
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
assert_eq!(y.to_f32(), vec![988.0]); }
#[test]
fn matmulnbits_asymmetric_block16_batched_non_square() {
let (m, k, n, block_size) = (6, 48, 5, 16);
let a: Vec<f32> = (0..m * k)
.map(|i| ((i * 7 % 23) as f32 - 5.0) / 8.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 19 % 37) as f32 - 9.0) / 10.0)
.collect();
let (packed, scales, zero_points, dequantized) = quantize(&weights, n, k, block_size, true);
let zero_points = zero_points.unwrap();
let (graph, node) = model_node(
&[2, 3, k],
&[n, 3, 8],
&[n * 3],
Some(&[n, 2]),
&[2, 3, n],
k,
n,
block_size,
);
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let a = Owned::f32(&[2, 3, k], &a);
let b = Owned::u8(&[n, 3, 8], &packed);
let scales = Owned::f32(&[n * 3], &scales);
let zero_points = Owned::u8(&[n, 2], &zero_points);
let mut y = Owned::zeros_f32(&[2, 3, n]);
kernel
.execute(
&[a.view(), b.view(), scales.view(), zero_points.view()],
&mut [y.view_mut()],
)
.unwrap();
assert_close(&y.to_f32(), &reference(&a.to_f32(), &dequantized, m, k, n));
}
#[test]
fn matmulnbits_prepacked_m1_block32_symmetric_reuses_weight_for_new_activations() {
let (k, n, block_size) = (35, 7, 32);
let a1_values: Vec<f32> = (0..k)
.map(|i| ((i * 11 % 41) as f32 - 20.0) / 13.0)
.collect();
let a2_values: Vec<f32> = a1_values
.iter()
.enumerate()
.map(|(i, &value)| value * -0.5 + i as f32 / 17.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 11 % 41) as f32 - 20.0) / 13.0)
.collect();
let (packed, scales, _, _) = quantize(&weights, n, k, block_size, false);
let dequantized = dequantize_reference(&packed, &scales, None, n, k, block_size);
let mut kernel = test_kernel(k, n, block_size);
kernel.set_constant_inputs(&[false, true, true]);
let b = Owned::u8(&[n, 2, 16], &packed);
let scales = Owned::f32(&[n, 2], &scales);
let a1 = Owned::f32(&[1, k], &a1_values);
let mut y1 = Owned::zeros_f32(&[1, n]);
kernel
.execute(&[a1.view(), b.view(), scales.view()], &mut [y1.view_mut()])
.unwrap();
assert_close(&y1.to_f32(), &reference(&a1_values, &dequantized, 1, k, n));
let cached_ptr = prepack_cache_ptr(&kernel);
assert!(
cached_ptr.is_some(),
"M=1 constant B must populate a prepacked weight cache"
);
let a2 = Owned::f32(&[1, k], &a2_values);
let mut y2 = Owned::zeros_f32(&[1, n]);
kernel
.execute(&[a2.view(), b.view(), scales.view()], &mut [y2.view_mut()])
.unwrap();
assert_eq!(
prepack_cache_ptr(&kernel),
cached_ptr,
"prepacked weight cache must be reused (stable) across activations"
);
assert_close(&y2.to_f32(), &reference(&a2_values, &dequantized, 1, k, n));
assert_ne!(y1.to_f32(), y2.to_f32());
}
#[test]
fn matmulnbits_prepacked_m1_block128_explicit_zp_partial_block_matches_reference() {
let (k, n, block_size) = (141, 7, 128);
let a_values: Vec<f32> = (0..k)
.map(|i| ((i * 11 % 41) as f32 - 20.0) / 13.0)
.collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 23 % 47) as f32 - 19.0) / 12.0)
.collect();
let (packed, scales, zero_points, _) = quantize(&weights, n, k, block_size, true);
let zero_points = zero_points.unwrap();
let dequantized =
dequantize_reference(&packed, &scales, Some(&zero_points), n, k, block_size);
let mut kernel = test_kernel(k, n, block_size);
kernel.set_constant_inputs(&[false, true, true, true]);
let a = Owned::f32(&[1, k], &a_values);
let b = Owned::u8(&[n, 2, 64], &packed);
let scales = Owned::f32(&[n, 2], &scales);
let zero_points = Owned::u8(&[n, 1], &zero_points);
let mut y = Owned::zeros_f32(&[1, n]);
kernel
.execute(
&[a.view(), b.view(), scales.view(), zero_points.view()],
&mut [y.view_mut()],
)
.unwrap();
assert_close(&y.to_f32(), &reference(&a_values, &dequantized, 1, k, n));
assert!(
prepack_cache_populated(&kernel),
"M=1 constant B/scales/zero-points must take a prepacked path"
);
}
#[test]
fn matmulnbits_m1_dynamic_b_falls_back_without_populating_prepack_cache() {
let (k, n, block_size) = (35, 5, 32);
let a_values: Vec<f32> = (0..k).map(|i| ((i * 5 % 29) as f32 - 14.0) / 9.0).collect();
let weights: Vec<f32> = (0..n * k)
.map(|i| ((i * 7 % 31) as f32 - 15.0) / 10.0)
.collect();
let (packed, scales, _, _) = quantize(&weights, n, k, block_size, false);
let dequantized = dequantize_reference(&packed, &scales, None, n, k, block_size);
let mut kernel = test_kernel(k, n, block_size);
kernel.set_constant_inputs(&[false, false, true]);
let a = Owned::f32(&[1, k], &a_values);
let b = Owned::u8(&[n, 2, 16], &packed);
let scales = Owned::f32(&[n, 2], &scales);
let mut y = Owned::zeros_f32(&[1, n]);
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
assert_close(&y.to_f32(), &reference(&a_values, &dequantized, 1, k, n));
assert!(
kernel.weight_nk.get().is_none(),
"dynamic B must use the fallback rather than populate the prepack cache"
);
}
#[test]
fn matmulnbits_unpacks_low_nibble_before_high_nibble() {
let k = 16;
let (graph, node) = model_node(&[1, k], &[1, 1, 8], &[1], None, &[1, 1], k, 1, 16);
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let mut activation = vec![0.0; k];
activation[0] = 1.0;
activation[1] = 10.0;
let mut packed = vec![0x88; 8];
packed[0] = 0xe1;
let a = Owned::f32(&[1, k], &activation);
let b = Owned::u8(&[1, 1, 8], &packed);
let scales = Owned::f32(&[1], &[1.0]);
let mut y = Owned::zeros_f32(&[1, 1]);
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
assert_eq!(y.to_f32(), vec![53.0]); }
#[test]
fn matmulnbits_honors_non_contiguous_group_indices() {
let k = 32;
let mut graph = Graph::new();
graph.opset_imports.insert("com.microsoft".into(), 1);
let a_value = graph.create_named_value("A", DataType::Float32, static_shape([1, k]));
let b_value = graph.create_named_value("B", DataType::Uint8, static_shape([1, 2, 8]));
let scales_value =
graph.create_named_value("scales", DataType::Float32, static_shape([1, 2]));
let g_idx_value = graph.create_named_value("g_idx", DataType::Int32, static_shape([k]));
for value in [a_value, b_value, scales_value, g_idx_value] {
graph.add_input(value);
}
let output = graph.create_named_value("Y", DataType::Float32, static_shape([1, 1]));
let mut node = Node::new(
NodeId(0),
"MatMulNBits",
vec![
Some(a_value),
Some(b_value),
Some(scales_value),
None,
Some(g_idx_value),
],
vec![output],
);
node.domain = "com.microsoft".into();
node.attributes.insert("K".into(), Attribute::Int(k as i64));
node.attributes.insert("N".into(), Attribute::Int(1));
node.attributes.insert("bits".into(), Attribute::Int(4));
node.attributes
.insert("block_size".into(), Attribute::Int(16));
let node = graph.insert_node(node);
graph.add_output(output);
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.unwrap();
let mut activation = vec![1.0; k];
activation[16..].fill(2.0);
let a = Owned::f32(&[1, k], &activation);
let b = Owned::u8(&[1, 2, 8], &[0x99; 16]);
let scales = Owned::f32(&[1, 2], &[1.0, 2.0]);
let groups: Vec<i32> = (0..k).map(|i| if i < 16 { 1 } else { 0 }).collect();
let groups = Owned::i32(&[k], &groups);
let absent_zp = TensorView::absent(DataType::Uint8);
let mut y = Owned::zeros_f32(&[1, 1]);
kernel
.execute(
&[a.view(), b.view(), scales.view(), absent_zp, groups.view()],
&mut [y.view_mut()],
)
.unwrap();
assert_eq!(y.to_f32(), vec![64.0]);
}
#[test]
fn matmulnbits_rejects_unsupported_bit_width() {
let (graph, node) = model_node(&[1, 16], &[1, 1, 8], &[1], None, &[1, 1], 16, 1, 16);
let mut graph = graph;
graph
.node_mut(node)
.attributes
.insert("bits".into(), Attribute::Int(3));
let model = Model::new(&graph);
let error = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.err()
.expect("bits=3 must be rejected");
assert!(format!("{error}").contains("supports bits in {2, 4, 8}"));
}
#[test]
fn matmulnbits_factory_accepts_bits8() {
let (graph, node) = model_node(&[1, 16], &[1, 1, 16], &[1], None, &[1, 1], 16, 1, 16);
let mut graph = graph;
graph
.node_mut(node)
.attributes
.insert("bits".into(), Attribute::Int(8));
let model = Model::new(&graph);
CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.expect("bits=8 must be accepted");
}
#[test]
fn matmulnbits_defaults_missing_bits_to_int4() {
let k = 16;
let (graph, node) = model_node(&[1, k], &[1, 1, 8], &[1], None, &[1, 1], k, 1, 16);
let mut graph = graph;
graph.node_mut(node).attributes.remove("bits");
let model = Model::new(&graph);
let kernel = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.expect("missing bits must default to 4");
let mut activation = vec![0.0; k];
activation[0] = 1.0;
activation[1] = 10.0;
let mut packed = vec![0x88; 8];
packed[0] = 0xe1;
let a = Owned::f32(&[1, k], &activation);
let b = Owned::u8(&[1, 1, 8], &packed);
let scales = Owned::f32(&[1], &[1.0]);
let mut y = Owned::zeros_f32(&[1, 1]);
kernel
.execute(&[a.view(), b.view(), scales.view()], &mut [y.view_mut()])
.unwrap();
assert_eq!(y.to_f32(), vec![53.0]);
}
#[test]
fn matmulnbits_rejects_prepacked_weight_layout() {
let (graph, node) = model_node(&[1, 16], &[1, 1, 8], &[1], None, &[1, 1], 16, 1, 16);
let mut graph = graph;
graph
.node_mut(node)
.attributes
.insert("weight_prepacked".into(), Attribute::Int(1));
let model = Model::new(&graph);
let error = CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 1)
.err()
.expect("prepacked weights must be rejected");
let message = format!("{error}");
assert!(message.contains("weight_prepacked=1"));
assert!(message.contains("standard (non-prepacked) layout"));
}
#[cfg(feature = "mlas")]
fn mlas_close(actual: &[f32], expected: &[f32], tol: f32, ctx: &str) {
assert_eq!(actual.len(), expected.len());
for (i, (a, e)) in actual.iter().zip(expected).enumerate() {
let diff = (a - e).abs();
let rel = diff / e.abs().max(1.0);
assert!(
diff <= tol || rel <= tol,
"{ctx}: index {i} mlas={a} ref={e} diff={diff}"
);
}
}
#[cfg(feature = "mlas")]
fn pseudo(n: usize, seed: f32) -> Vec<f32> {
(0..n)
.map(|i| ((i as f32 * 0.017 + seed).sin()) * 1.5)
.collect()
}
#[cfg(feature = "mlas")]
#[test]
fn matmulnbits_mlas_matches_dequant_reference() {
let (n, k) = (96usize, 256usize);
for &block_size in &[32usize, 64, 128] {
let k_blocks = k.div_ceil(block_size);
let blob = block_size / 2;
let weights_nk = pseudo(n * k, 0.3);
for &asymmetric in &[false, true] {
let (packed, scales, zps, _dq) =
quantize(&weights_nk, n, k, block_size, asymmetric);
let ref_weights =
dequantize_reference(&packed, &scales, zps.as_deref(), n, k, block_size);
let b = Owned::u8(&[n, k_blocks, blob], &packed);
let scales_t = Owned::f32(&[n, k_blocks], &scales);
let zp_owned = zps
.as_ref()
.map(|z| Owned::u8(&[n, k_blocks.div_ceil(2)], z));
for &accuracy_level in &[0i64, 4] {
let comp = if accuracy_level == 4 {
mlas_sys::SQNBitComputeType::Int8
} else {
mlas_sys::SQNBitComputeType::Fp32
};
let kernel = MatMulNBitsKernel {
accuracy_level,
..test_kernel(k, n, block_size)
};
let zp_view = zp_owned.as_ref().map(|z| z.view());
let Some(packed_weight) = kernel
.build_mlas_packed(&b.view(), &scales_t.view(), zp_view.as_ref(), comp)
.unwrap()
else {
eprintln!(
"MLAS SQNBit int4 blk={block_size} {comp:?} unavailable; skipping"
);
continue;
};
for &m in &[1usize, 5] {
let a = pseudo(m * k, 0.8);
for bias in [None, Some(pseudo(n, 0.1))] {
let mut out = vec![0.0f32; m * n];
mlas_sys::sqnbit_gemm(
&packed_weight,
m,
&a,
bias.as_deref(),
&mut out,
true,
);
let mut expected = reference(&a, &ref_weights, m, k, n);
if let Some(bias) = &bias {
for row in expected.chunks_exact_mut(n) {
for (v, b) in row.iter_mut().zip(bias) {
*v += b;
}
}
}
let tol = if accuracy_level == 4 { 6e-2 } else { 2e-3 };
mlas_close(
&out,
&expected,
tol,
&format!(
"blk{block_size} asym{asymmetric} acc{accuracy_level} m{m} bias{}",
bias.is_some()
),
);
}
}
}
}
}
}
#[cfg(feature = "mlas")]
#[test]
fn matmulnbits_try_mlas_falls_back_for_gidx_and_bits2() {
let (n, k, block_size) = (2usize, 32usize, 32usize);
let k_blocks = k.div_ceil(block_size);
let a = vec![0.5f32; k];
let mut result = vec![0.0f32; n];
let kernel = test_kernel(k, n, block_size);
let b = Owned::u8(
&[n, k_blocks, block_size / 2],
&vec![0x88; n * k_blocks * block_size / 2],
);
let scales = Owned::f32(&[n, k_blocks], &vec![1.0; n * k_blocks]);
let g_idx: Vec<i32> = (0..k).map(|i| (i / block_size) as i32).collect();
let g_idx = Owned::i32(&[k], &g_idx);
assert_eq!(
kernel
.try_mlas_sqnbit(
&b.view(),
&scales.view(),
None,
Some(&g_idx.view()),
false,
&a,
1,
None,
&mut result,
)
.unwrap(),
None,
"g_idx present must fall back",
);
let blob2 = block_size / 4;
let kernel2 = MatMulNBitsKernel {
bits: 2,
..test_kernel(k, n, block_size)
};
let b2 = Owned::u8(&[n, k_blocks, blob2], &vec![0x55; n * k_blocks * blob2]);
assert_eq!(
kernel2
.try_mlas_sqnbit(
&b2.view(),
&scales.view(),
None,
None,
false,
&a,
1,
None,
&mut result,
)
.unwrap(),
None,
"bits==2 must fall back",
);
}
#[cfg(feature = "mlas")]
#[test]
fn matmulnbits_resolve_decode_min_parses_or_defaults() {
assert_eq!(default_sqnbit_decode_min(96), 16);
assert_eq!(default_sqnbit_decode_min(4), 6);
assert_eq!(default_sqnbit_decode_min(8), 8);
assert_eq!(resolve_decode_min(None, 96), 16);
assert_eq!(resolve_decode_min(Some(""), 96), 16);
assert_eq!(resolve_decode_min(Some("abc"), 96), 16);
assert_eq!(resolve_decode_min(Some("32"), 96), 32);
assert_eq!(resolve_decode_min(Some(" 8 "), 96), 8);
assert_eq!(resolve_decode_min(Some("1"), 96), 1);
}
#[cfg(feature = "mlas")]
fn backend_env_lock() -> &'static std::sync::Mutex<()> {
static LOCK: OnceLock<std::sync::Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| std::sync::Mutex::new(()))
}
const SPMD_PARITY_CHILD_ENV: &str = "NXRT_SPMD_PARITY_CHILD";
const SPMD_PARITY_MARKER: &str = "NXRT_SPMD_PARITY_BYTES=";
fn real_int4_decode_fixture_bytes() -> Vec<u8> {
let (n, k, block_size) = (1024usize, 1024usize, 32usize);
let blocks = k / block_size;
let packed = PackedInt4Weight {
values: (0..n * blocks * (block_size / 2))
.map(|index| {
let low = ((index * 13 + 3) & 0xf) as u8;
let high = ((index * 7 + 11) & 0xf) as u8;
low | (high << 4)
})
.collect(),
scales: (0..n * blocks)
.map(|index| 0.000_5 + (index % 29) as f32 * 0.000_031_25)
.collect(),
};
let mut activation: Vec<f32> = (0..k)
.map(|index| ((index * 37 % 257) as f32 - 128.0) * 0.007_812_5)
.collect();
let dot_kernel = selected_dot_kernel();
let mut bytes = Vec::with_capacity(6 * n * std::mem::size_of::<f32>());
with_decode_pool_scope(true, || {
for op in 0..6usize {
let mut output = vec![0.0f32; n];
int4_matmul_m1(&activation, &packed, &mut output, k, n, dot_kernel);
for value in &output {
bytes.extend_from_slice(&value.to_bits().to_le_bytes());
}
for (index, value) in activation.iter_mut().enumerate() {
*value = output[index] * 0.125
+ ((op * 17 + index * 5) % 31) as f32 * 0.000_976_562_5;
}
}
});
bytes
}
fn parity_worker_count() -> usize {
let available = available_parallelism().max(1);
let host_odd = if available.is_multiple_of(2) {
available.saturating_sub(1)
} else {
available
};
host_odd.clamp(1, 15)
}
fn parity_child_output(persistent: bool) -> Vec<u8> {
parity_child_output_mode(if persistent {
SpmdParityMode::Forced
} else {
SpmdParityMode::Off
})
}
#[derive(Clone, Copy)]
enum SpmdParityMode {
Off,
Forced,
Auto,
}
impl SpmdParityMode {
fn child_tag(self) -> &'static str {
match self {
Self::Off => "off",
Self::Forced => "on",
Self::Auto => "auto",
}
}
}
fn parity_child_output_mode(mode: SpmdParityMode) -> Vec<u8> {
let workers = parity_worker_count().to_string();
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command
.arg("--exact")
.arg("kernels::matmul_nbits::tests::spmd_real_int4_parity_subprocess")
.arg("--nocapture")
.arg("--test-threads=1")
.env(SPMD_PARITY_CHILD_ENV, mode.child_tag())
.env(DECODE_THREADS_ENV, &workers)
.env("RAYON_NUM_THREADS", &workers)
.env_remove(crate::decode_affinity::DECODE_AFFINITY_ENV);
match mode {
SpmdParityMode::Forced => {
command.env(crate::decode_spmd::PERSISTENT_POOL_ENV, "1");
}
SpmdParityMode::Off => {
command.env(crate::decode_spmd::PERSISTENT_POOL_ENV, "0");
}
SpmdParityMode::Auto => {
command.env_remove(crate::decode_spmd::PERSISTENT_POOL_ENV);
}
}
let persistent = matches!(mode, SpmdParityMode::Forced);
let output = command.output().expect("run SPMD parity child process");
assert!(
output.status.success(),
"SPMD parity child failed (persistent={persistent}):\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("child output is UTF-8");
let encoded = stdout
.lines()
.find_map(|line| {
line.find(SPMD_PARITY_MARKER)
.map(|index| &line[index + SPMD_PARITY_MARKER.len()..])
})
.expect("child emitted parity bytes");
assert_eq!(encoded.len() % 2, 0);
encoded
.as_bytes()
.chunks_exact(2)
.map(|pair| {
let pair = std::str::from_utf8(pair).unwrap();
u8::from_str_radix(pair, 16).unwrap()
})
.collect()
}
#[test]
fn spmd_real_int4_parity_subprocess() {
let Ok(mode) = std::env::var(SPMD_PARITY_CHILD_ENV) else {
return;
};
let persistent = mode == "on";
let auto = mode == "auto";
assert_eq!(
crate::decode_spmd::pools().is_some(),
persistent || auto,
"the forced/auto children must build the persistent pool and the off child must not"
);
SPMD_TEST_DISPATCHES.store(0, std::sync::atomic::Ordering::Relaxed);
let bytes = real_int4_decode_fixture_bytes();
if persistent || auto {
assert!(
SPMD_TEST_DISPATCHES.load(std::sync::atomic::Ordering::Relaxed) >= 6,
"persistent/auto parity child did not route every real int4 op through SPMD"
);
}
if persistent {
let pool = crate::decode_spmd::pools().expect("ON child built the persistent pool");
if pool.node_count() == 1 {
let workers = pool.total_workers();
assert_eq!(
workers,
parity_worker_count(),
"single-node persistent pool must use the requested (unclamped) worker count"
);
if workers > 1 {
assert!(
!workers.is_multiple_of(2),
"parity must run at an odd worker count to cover uneven row sharding"
);
}
}
}
let encoded: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect();
println!("{SPMD_PARITY_MARKER}{encoded}");
}
#[test]
fn spmd_real_multi_op_int4_is_bit_identical_at_odd_worker_count() {
let baseline = parity_child_output(false);
let persistent = parity_child_output(true);
assert_eq!(
persistent, baseline,
"odd-worker persistent SPMD output must be byte-identical to flag-OFF \
across every sequential packed-int4 MatMulNBits op"
);
}
#[test]
fn spmd_auto_calibrated_decode_is_bit_identical_to_flat() {
let baseline = parity_child_output_mode(SpmdParityMode::Off);
let auto = parity_child_output_mode(SpmdParityMode::Auto);
assert_eq!(
auto, baseline,
"auto-calibrated decode output must be byte-identical to the flat baseline"
);
}
#[cfg(feature = "mlas")]
const MLAS_SHARD_PARITY_CHILD_ENV: &str = "NXRT_MLAS_SHARD_PARITY_CHILD";
#[cfg(feature = "mlas")]
const MLAS_SHARD_PARITY_MARKER: &str = "NXRT_MLAS_SHARD_PARITY_BYTES=";
#[cfg(feature = "mlas")]
fn mlas_shard_parity_child_output(no_shard: bool) -> Vec<f32> {
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command
.arg("--exact")
.arg("kernels::matmul_nbits::tests::mlas_sharded_decode_parity_subprocess")
.arg("--nocapture")
.arg("--test-threads=1")
.env(MLAS_SHARD_PARITY_CHILD_ENV, "1")
.env(DECODE_THREADS_ENV, "3")
.env("RAYON_NUM_THREADS", "3")
.env(crate::decode_spmd::PERSISTENT_POOL_ENV, "1")
.env_remove(crate::decode_affinity::DECODE_AFFINITY_ENV);
if no_shard {
command.env("ONNX_GENAI_CPU_MM_MLAS_NO_SHARD", "1");
} else {
command.env_remove("ONNX_GENAI_CPU_MM_MLAS_NO_SHARD");
}
let output = command
.output()
.expect("run MLAS-shard parity child process");
assert!(
output.status.success(),
"MLAS-shard parity child failed (no_shard={no_shard}):\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("child output is UTF-8");
let encoded = stdout
.lines()
.find_map(|line| {
line.find(MLAS_SHARD_PARITY_MARKER)
.map(|index| &line[index + MLAS_SHARD_PARITY_MARKER.len()..])
})
.expect("child emitted MLAS-shard parity bytes");
assert_eq!(encoded.len() % 8, 0);
encoded
.as_bytes()
.chunks_exact(8)
.map(|hex| {
let hex = std::str::from_utf8(hex).unwrap();
f32::from_bits(u32::from_str_radix(hex, 16).unwrap())
})
.collect()
}
#[cfg(feature = "mlas")]
const MLAS_SHARD_PARITY_N: usize = 176;
#[cfg(feature = "mlas")]
const MLAS_SHARD_PARITY_CONFIGS: &[(usize, usize)] =
&[(256, 128), (512, 32), (256, 64), (256, 32), (128, 32)];
#[cfg(feature = "mlas")]
#[test]
fn mlas_sharded_decode_parity_subprocess() {
if std::env::var(MLAS_SHARD_PARITY_CHILD_ENV).is_err() {
return;
}
let n = MLAS_SHARD_PARITY_N;
let pool = crate::decode_spmd::pools().expect("forced persistent SPMD pool");
let segments = pool.output_column_segments(n, MLAS_SQNBIT_DECODE_SHARD_ALIGN);
assert_eq!(
segments.len(),
3,
"child requested three persistent workers"
);
assert_eq!(segments.iter().map(|&(_, len)| len).sum::<usize>(), n);
assert!(
segments.windows(2).any(|pair| pair[0].1 != pair[1].1),
"N={n} must create uneven worker output-column segments: {segments:?}"
);
let activation = pseudo(
MLAS_SHARD_PARITY_CONFIGS
.iter()
.map(|&(k, _)| k)
.max()
.unwrap(),
0.8,
);
let mut encoded = String::new();
for &(k, block_size) in MLAS_SHARD_PARITY_CONFIGS {
let blocks = k.div_ceil(block_size);
let weights_nk = pseudo(n * k, 0.3);
let (packed_bytes, scales, _zps, _dq) = quantize(&weights_nk, n, k, block_size, false);
let b = Owned::u8(&[n, blocks, block_size / 2], &packed_bytes);
let scales_t = Owned::f32(&[n, blocks], &scales);
let kernel = test_kernel(k, n, block_size);
let mut output = vec![0.0f32; n];
let served = with_decode_pool_scope(true, || {
assert!(
spmd_decode_active().is_some(),
"the actual MLAS call must run inside the persistent SPMD scope"
);
kernel
.try_mlas_sqnbit(
&b.view(),
&scales_t.view(),
None,
None,
true,
&activation[..k],
1,
None,
&mut output,
)
.unwrap()
});
assert_eq!(
served,
Some(()),
"MLAS CompFp32 must serve this decode (k={k} blk={block_size})"
);
if mlas_no_shard() {
assert!(
kernel.mlas_shards.get().is_none(),
"NO_SHARD must select the full-width MLAS call (k={k} blk={block_size})"
);
} else {
let shards = kernel
.mlas_shards
.get()
.expect("the cached sharded MLAS route must be populated")
.as_ref()
.expect("MLAS packed every worker shard");
assert_eq!(shards.len(), segments.len());
assert!(
shards.iter().flatten().count() > 1,
"the cached route must contain multiple real MLAS shards \
(k={k} blk={block_size})"
);
}
for value in &output {
encoded.push_str(&format!("{:08x}", value.to_bits()));
}
}
println!("{MLAS_SHARD_PARITY_MARKER}{encoded}");
}
#[cfg(feature = "mlas")]
#[test]
fn mlas_sharded_decode_matches_no_shard_full_width() {
let sharded = mlas_shard_parity_child_output(false);
let full_width = mlas_shard_parity_child_output(true);
assert_eq!(
sharded.len(),
full_width.len(),
"cached SPMD MLAS shards vs NO_SHARD full-width decode: length mismatch"
);
let mismatches: Vec<_> = sharded
.iter()
.zip(&full_width)
.enumerate()
.filter(|(_, (a, b))| a.to_bits() != b.to_bits())
.map(|(i, (a, b))| (i, a.to_bits(), b.to_bits()))
.collect();
let max_ulp = sharded
.iter()
.zip(&full_width)
.map(|(a, b)| (a.to_bits() as i64 - b.to_bits() as i64).unsigned_abs())
.max()
.unwrap_or(0);
assert!(
mismatches.is_empty(),
"aligned SPMD MLAS-shard decode must be bit-identical to NO_SHARD full-width \
(max_ulp={max_ulp}); if this fails, MLAS_SQNBIT_DECODE_SHARD_ALIGN is not \
keeping N-tiles whole. mismatching (index, sharded_bits, full_bits): {mismatches:?}"
);
}
#[cfg(feature = "mlas")]
const MLAS_PREFILL_PARITY_CHILD_ENV: &str = "NXRT_MLAS_PREFILL_PARITY_CHILD";
#[cfg(feature = "mlas")]
const MLAS_PREFILL_PARITY_MARKER: &str = "NXRT_MLAS_PREFILL_PARITY_BYTES=";
#[cfg(feature = "mlas")]
const MLAS_PREFILL_PARITY_M: usize = 7;
#[cfg(feature = "mlas")]
fn mlas_prefill_parity_child_output(serial: bool) -> Vec<f32> {
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command
.arg("--exact")
.arg("kernels::matmul_nbits::tests::mlas_prefill_dispatch_parity_subprocess")
.arg("--nocapture")
.arg("--test-threads=1")
.env(MLAS_PREFILL_PARITY_CHILD_ENV, "1")
.env(DECODE_THREADS_ENV, "3")
.env("RAYON_NUM_THREADS", "6")
.env(crate::decode_spmd::PERSISTENT_POOL_ENV, "1")
.env_remove(crate::decode_affinity::DECODE_AFFINITY_ENV);
if serial {
command.env("ONNX_GENAI_CPU_MM_MLAS_PREFILL_SERIAL", "1");
} else {
command.env_remove("ONNX_GENAI_CPU_MM_MLAS_PREFILL_SERIAL");
}
let output = command
.output()
.expect("run MLAS prefill parity child process");
assert!(
output.status.success(),
"MLAS prefill parity child failed (serial={serial}):\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("child output is UTF-8");
let encoded = stdout
.lines()
.find_map(|line| {
line.find(MLAS_PREFILL_PARITY_MARKER)
.map(|index| &line[index + MLAS_PREFILL_PARITY_MARKER.len()..])
})
.expect("child emitted MLAS prefill parity bytes");
assert_eq!(encoded.len() % 8, 0);
encoded
.as_bytes()
.chunks_exact(8)
.map(|hex| {
let hex = std::str::from_utf8(hex).unwrap();
f32::from_bits(u32::from_str_radix(hex, 16).unwrap())
})
.collect()
}
#[cfg(feature = "mlas")]
#[test]
fn mlas_prefill_dispatch_parity_subprocess() {
if std::env::var(MLAS_PREFILL_PARITY_CHILD_ENV).is_err() {
return;
}
let n = MLAS_SHARD_PARITY_N;
let m = MLAS_PREFILL_PARITY_M;
let pool = crate::decode_spmd::pools().expect("forced persistent SPMD pool");
let segments = pool.output_column_segments(n, MLAS_SQNBIT_DECODE_SHARD_ALIGN);
assert!(
segments.iter().filter(|&&(_, len)| len > 0).count() > 1,
"prefill parity needs multiple real N shards to exercise the parallel \
dispatch: {segments:?}"
);
let max_k = MLAS_SHARD_PARITY_CONFIGS
.iter()
.map(|&(k, _)| k)
.max()
.unwrap();
let activation = pseudo(m * max_k, 0.8);
let mut encoded = String::new();
for &(k, block_size) in MLAS_SHARD_PARITY_CONFIGS {
let blocks = k.div_ceil(block_size);
let weights_nk = pseudo(n * k, 0.3);
let (packed_bytes, scales, _zps, _dq) = quantize(&weights_nk, n, k, block_size, false);
let b = Owned::u8(&[n, blocks, block_size / 2], &packed_bytes);
let scales_t = Owned::f32(&[n, blocks], &scales);
let kernel = test_kernel(k, n, block_size);
let mut output = vec![0.0f32; m * n];
let mut activations = vec![0.0f32; m * k];
for row in 0..m {
activations[row * k..(row + 1) * k]
.copy_from_slice(&activation[row * max_k..row * max_k + k]);
}
let served = kernel
.try_mlas_sqnbit(
&b.view(),
&scales_t.view(),
None,
None,
true,
&activations,
m,
None,
&mut output,
)
.unwrap();
assert_eq!(
served,
Some(()),
"MLAS CompFp32 must serve this prefill (k={k} blk={block_size})"
);
let shards = kernel
.mlas_shards
.get()
.expect("the cached sharded MLAS route must be populated")
.as_ref()
.expect("MLAS packed every worker shard");
assert!(
shards.iter().flatten().count() > 1,
"the cached route must contain multiple real MLAS shards \
(k={k} blk={block_size})"
);
for value in &output {
encoded.push_str(&format!("{:08x}", value.to_bits()));
}
}
println!("{MLAS_PREFILL_PARITY_MARKER}{encoded}");
}
#[cfg(feature = "mlas")]
#[test]
fn mlas_prefill_parallel_dispatch_matches_serial() {
let parallel = mlas_prefill_parity_child_output(false);
let serial = mlas_prefill_parity_child_output(true);
assert_eq!(
parallel.len(),
serial.len(),
"parallel vs serial prefill dispatch: length mismatch"
);
let mismatches: Vec<_> = parallel
.iter()
.zip(&serial)
.enumerate()
.filter(|(_, (a, b))| a.to_bits() != b.to_bits())
.map(|(i, (a, b))| (i, a.to_bits(), b.to_bits()))
.collect();
let max_ulp = parallel
.iter()
.zip(&serial)
.map(|(a, b)| (a.to_bits() as i64 - b.to_bits() as i64).unsigned_abs())
.max()
.unwrap_or(0);
assert!(
mismatches.is_empty(),
"parallel prefill dispatch must be bit-identical to the serial \
multithread=true loop (max_ulp={max_ulp}); mismatching \
(index, parallel_bits, serial_bits): {mismatches:?}"
);
}
#[cfg(feature = "mlas")]
#[test]
#[ignore]
fn search_drift_configs() {
fn split(n: usize, w: usize) -> Vec<(usize, usize)> {
let base = n / w;
let rem = n % w;
let mut segs = Vec::new();
let mut off = 0;
for worker in 0..w {
let len = base + usize::from(worker < rem);
segs.push((off, len));
off += len;
}
segs
}
for &n in &[97usize, 129, 130, 131, 176, 177, 191, 193, 200, 255, 257] {
for &w in &[2usize, 3, 4, 5, 6, 7, 8] {
if w > n {
continue;
}
for &(k, block_size) in &[
(256usize, 128usize),
(512, 32),
(256, 64),
(256, 32),
(128, 32),
] {
for &asym in &[false, true] {
let blocks = k.div_ceil(block_size);
let blob = block_size / 2;
let weights_nk = pseudo(n * k, 0.3);
let (packed, scales, zps, _dq) =
quantize(&weights_nk, n, k, block_size, asym);
let activation = pseudo(k, 0.8);
let make = |start: usize, len: usize| {
let pb = &packed[start * blocks * blob..(start + len) * blocks * blob];
let sc = &scales[start * blocks..(start + len) * blocks];
let zp = zps.as_deref().map(|z| {
let row = blocks.div_ceil(2);
&z[start * row..(start + len) * row]
});
mlas_sys::SQNBitPackedB::new(
len,
k,
4,
block_size,
mlas_sys::SQNBitComputeType::Fp32,
pb,
sc,
zp,
)
};
let Some(full) = make(0, n) else { continue };
let mut c_full = vec![0.0f32; n];
mlas_sys::sqnbit_gemm(&full, 1, &activation, None, &mut c_full, false);
let segs = split(n, w);
let mut c_shard = vec![0.0f32; n];
for &(start, len) in &segs {
if len == 0 {
continue;
}
let packed_shard = make(start, len).unwrap();
let mut out = vec![0.0f32; len];
mlas_sys::sqnbit_gemm(
&packed_shard,
1,
&activation,
None,
&mut out,
false,
);
c_shard[start..start + len].copy_from_slice(&out);
}
let max_ulp = c_full
.iter()
.zip(&c_shard)
.map(|(a, b)| (a.to_bits() as i64 - b.to_bits() as i64).unsigned_abs())
.max()
.unwrap_or(0);
if max_ulp > 0 {
let interior: Vec<usize> =
segs.iter().map(|&(s, _)| s).skip(1).collect();
println!(
"DRIFT n={n} w={w} k={k} blk={block_size} asym={asym} \
max_ulp={max_ulp} boundaries={interior:?}"
);
}
}
}
}
}
}
const AFFINITY_DEFER_CHILD_ENV: &str = "NXRT_AFFINITY_DEFER_CHILD";
const AFFINITY_DEFER_MARKER: &str = "NXRT_AFFINITY_DEFER=";
#[test]
fn affinity_defer_routing_child() {
let Ok(scenario) = std::env::var(AFFINITY_DEFER_CHILD_ENV) else {
return;
};
match scenario.as_str() {
"auto_off" | "auto_node" | "auto_compact" => {
assert!(
crate::decode_spmd::pools().is_none(),
"Auto default + explicit affinity ({scenario}) must defer to the flat \
path and build no persistent SPMD pool"
);
}
"forced_off" => {
assert!(
crate::decode_spmd::pools().is_some(),
"Forced persistent pool must ignore the affinity defer and build SPMD"
);
}
"auto_malformed" => {
assert!(
crate::decode_spmd::pools().is_none(),
"Auto default + malformed affinity must defer to the flat path"
);
assert!(
crate::decode_affinity::plan_decode_affinity(4).is_err(),
"malformed affinity must still surface an error on the deferred flat path"
);
}
other => panic!("unknown affinity-defer scenario `{other}`"),
}
crate::decode_spmd::shutdown_pools();
println!("{AFFINITY_DEFER_MARKER}ok");
}
const STATUS_ACCESS_VIOLATION: i32 = -1_073_741_819;
const AFFINITY_DEFER_CHILD_MAX_ATTEMPTS: u32 = 3;
fn is_environmental_access_violation_crash(
success: bool,
exit_code: Option<i32>,
stdout: &str,
stderr: &str,
) -> bool {
if success {
return false;
}
if stdout.contains(&format!("{AFFINITY_DEFER_MARKER}ok")) {
return false;
}
if stderr.contains("panicked at") || stderr.contains("assertion") {
return false;
}
exit_code == Some(STATUS_ACCESS_VIOLATION)
}
fn run_affinity_defer_child(scenario: &str, affinity: &str, forced: bool) {
let workers = parity_worker_count().to_string();
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command
.arg("--exact")
.arg("kernels::matmul_nbits::tests::affinity_defer_routing_child")
.arg("--nocapture")
.arg("--test-threads=1")
.env(AFFINITY_DEFER_CHILD_ENV, scenario)
.env(crate::decode_affinity::DECODE_AFFINITY_ENV, affinity)
.env(DECODE_THREADS_ENV, &workers)
.env("RAYON_NUM_THREADS", &workers);
if forced {
command.env(crate::decode_spmd::PERSISTENT_POOL_ENV, "1");
} else {
command.env_remove(crate::decode_spmd::PERSISTENT_POOL_ENV);
}
for attempt in 1..=AFFINITY_DEFER_CHILD_MAX_ATTEMPTS {
let output = command.output().expect("run affinity-defer child process");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if is_environmental_access_violation_crash(
output.status.success(),
output.status.code(),
&stdout,
&stderr,
) && attempt < AFFINITY_DEFER_CHILD_MAX_ATTEMPTS
{
eprintln!(
"note: retrying affinity-defer child (scenario={scenario}) after \
environmental STATUS_ACCESS_VIOLATION crash, attempt {attempt}"
);
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
}
assert!(
output.status.success(),
"affinity-defer child failed (scenario={scenario}):\nstdout:\n{stdout}\n\
stderr:\n{stderr}"
);
assert!(
stdout.contains(&format!("{AFFINITY_DEFER_MARKER}ok")),
"affinity-defer child did not confirm scenario {scenario}:\n{stdout}"
);
return;
}
}
#[test]
fn auto_default_with_explicit_affinity_defers_to_flat() {
run_affinity_defer_child("auto_off", "off", false);
run_affinity_defer_child("auto_node", "node:0", false);
run_affinity_defer_child("auto_compact", "compact", false);
}
#[test]
fn forced_persistent_pool_ignores_explicit_affinity() {
run_affinity_defer_child("forced_off", "off", true);
}
#[test]
fn auto_default_malformed_affinity_still_errors_on_flat_path() {
run_affinity_defer_child("auto_malformed", "not-a-real-mode", false);
}
#[test]
fn access_violation_crash_signature_classifier() {
let marker_ok = format!("{AFFINITY_DEFER_MARKER}ok");
assert!(is_environmental_access_violation_crash(
false,
Some(STATUS_ACCESS_VIOLATION),
"",
"",
));
assert!(!is_environmental_access_violation_crash(
true,
Some(STATUS_ACCESS_VIOLATION),
&marker_ok,
"",
));
assert!(!is_environmental_access_violation_crash(
false,
Some(STATUS_ACCESS_VIOLATION),
"",
"thread 'main' panicked at src/foo.rs:1:1:\nassertion failed",
));
assert!(!is_environmental_access_violation_crash(
false,
Some(1),
"",
""
));
assert!(!is_environmental_access_violation_crash(
false, None, "", ""
));
assert!(!is_environmental_access_violation_crash(
false,
Some(STATUS_ACCESS_VIOLATION),
&marker_ok,
"",
));
}
#[cfg(feature = "mlas")]
#[test]
fn matmulnbits_try_mlas_gates_decode_by_m_threshold() {
let (n, k, block_size) = (32usize, 64usize, 32usize);
let k_blocks = k.div_ceil(block_size);
let blob = block_size / 2;
let weights_nk = pseudo(n * k, 0.3);
let (packed_bytes, scales, _zps, _dq) = quantize(&weights_nk, n, k, block_size, false);
if mlas_sys::SQNBitPackedB::new(
n,
k,
4,
block_size,
mlas_sys::SQNBitComputeType::Int8,
&packed_bytes,
&scales,
None,
)
.is_none()
{
eprintln!("MLAS SQNBit int4 kernel unavailable; skipping M-gate test");
return;
}
let kernel = accuracy4_kernel(k, n, block_size);
let b = Owned::u8(&[n, k_blocks, blob], &packed_bytes);
let scales_t = Owned::f32(&[n, k_blocks], &scales);
let at = default_sqnbit_decode_min(available_parallelism());
let below = at - 1;
let _guard = backend_env_lock().lock().unwrap();
let previous = std::env::var("NXRT_CPU_GEMM_BACKEND").ok();
unsafe { std::env::set_var("NXRT_CPU_GEMM_BACKEND", "mlas") };
let call = |m: usize| {
let a = pseudo(m * k, 0.8);
let mut result = vec![0.0f32; m * n];
kernel
.try_mlas_sqnbit(
&b.view(),
&scales_t.view(),
None,
None,
false,
&a,
m,
None,
&mut result,
)
.unwrap()
};
let decode = call(below);
let prefill = call(at);
unsafe {
match &previous {
Some(value) => std::env::set_var("NXRT_CPU_GEMM_BACKEND", value),
None => std::env::remove_var("NXRT_CPU_GEMM_BACKEND"),
}
}
assert_eq!(
decode, None,
"m={below} (< {at}) must fall back to the hand int4 path",
);
assert_eq!(prefill, Some(()), "m={at} must route to MLAS SQNBit",);
}
#[cfg(feature = "mlas")]
#[test]
fn matmulnbits_try_mlas_serves_slow_dequant_decode() {
let (n, k, block_size) = (32usize, 64usize, 32usize);
let k_blocks = k.div_ceil(block_size);
let blob = block_size / 2;
let weights_nk = pseudo(n * k, 0.3);
let (packed_bytes, scales, _zps, dq) = quantize(&weights_nk, n, k, block_size, false);
if mlas_sys::SQNBitPackedB::new(
n,
k,
4,
block_size,
mlas_sys::SQNBitComputeType::Fp32,
&packed_bytes,
&scales,
None,
)
.is_none()
{
eprintln!("MLAS SQNBit int4 CompFp32 kernel unavailable; skipping slow-decode test");
return;
}
let kernel = test_kernel(k, n, block_size);
let b = Owned::u8(&[n, k_blocks, blob], &packed_bytes);
let scales_t = Owned::f32(&[n, k_blocks], &scales);
let _guard = backend_env_lock().lock().unwrap();
let previous = std::env::var("NXRT_CPU_GEMM_BACKEND").ok();
unsafe { std::env::set_var("NXRT_CPU_GEMM_BACKEND", "mlas") };
let a = pseudo(k, 0.8);
let mut result = vec![0.0f32; n];
let served = kernel
.try_mlas_sqnbit(
&b.view(),
&scales_t.view(),
None,
None,
false,
&a,
1,
None,
&mut result,
)
.unwrap();
unsafe {
match &previous {
Some(value) => std::env::set_var("NXRT_CPU_GEMM_BACKEND", value),
None => std::env::remove_var("NXRT_CPU_GEMM_BACKEND"),
}
}
assert_eq!(
served,
Some(()),
"m=1 bits=4 accuracy_level=0 (slow hand dequant GEMV) must route to MLAS SQNBit",
);
let expected = reference(&a, &dq, 1, k, n);
mlas_close(&result, &expected, 2e-3, "slow-dequant m1 CompFp32");
}
#[cfg(feature = "mlas")]
#[test]
fn matmulnbits_try_mlas_routes_acclevel0_without_mlas_backend() {
let (n, k, block_size) = (32usize, 64usize, 32usize);
let k_blocks = k.div_ceil(block_size);
let blob = block_size / 2;
let weights_nk = pseudo(n * k, 0.3);
let (packed_bytes, scales, _zps, dq) = quantize(&weights_nk, n, k, block_size, false);
if mlas_sys::SQNBitPackedB::new(
n,
k,
4,
block_size,
mlas_sys::SQNBitComputeType::Fp32,
&packed_bytes,
&scales,
None,
)
.is_none()
{
eprintln!(
"MLAS SQNBit int4 CompFp32 kernel unavailable; skipping acc0-default-backend test"
);
return;
}
let b = Owned::u8(&[n, k_blocks, blob], &packed_bytes);
let scales_t = Owned::f32(&[n, k_blocks], &scales);
let a = pseudo(k, 0.8);
let _guard = backend_env_lock().lock().unwrap();
let previous = std::env::var("NXRT_CPU_GEMM_BACKEND").ok();
unsafe { std::env::set_var("NXRT_CPU_GEMM_BACKEND", "generic") };
assert_ne!(
crate::backend::CpuBackend::auto_detect(),
crate::backend::CpuBackend::Mlas,
"test precondition: backend must not resolve to MLAS",
);
let call = |kernel: &MatMulNBitsKernel| {
let mut result = vec![0.0f32; n];
let served = kernel
.try_mlas_sqnbit(
&b.view(),
&scales_t.view(),
None,
None,
false,
&a,
1,
None,
&mut result,
)
.unwrap();
(served, result)
};
let (acc0_served, acc0_result) = call(&test_kernel(k, n, block_size));
let (acc4_served, _) = call(&accuracy4_kernel(k, n, block_size));
unsafe {
match &previous {
Some(value) => std::env::set_var("NXRT_CPU_GEMM_BACKEND", value),
None => std::env::remove_var("NXRT_CPU_GEMM_BACKEND"),
}
}
assert_eq!(
acc0_served,
Some(()),
"accuracy_level=0 must route to MLAS SQNBit even when the backend is not MLAS",
);
let expected = reference(&a, &dq, 1, k, n);
mlas_close(
&acc0_result,
&expected,
2e-3,
"acc0 default-backend CompFp32",
);
assert_eq!(
acc4_served, None,
"accuracy_level=4 decode must stay on the fast hand path when the backend is not MLAS",
);
}
#[cfg(feature = "mlas")]
#[test]
#[ignore = "perf probe; run explicitly with --ignored --nocapture"]
fn matmulnbits_mlas_perf() {
use std::time::Instant;
fn time<F: FnMut() + Send>(threads: usize, mut run: F) -> f64 {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap();
pool.install(|| {
for _ in 0..20 {
run();
}
let iters = 200u32;
let start = Instant::now();
for _ in 0..iters {
run();
}
start.elapsed().as_secs_f64() * 1e6 / iters as f64
})
}
let block_size = 32usize;
let dot_kernel = selected_dot_kernel();
for &(k, n) in &[(2048usize, 2048usize), (4096, 11008)] {
let k_blocks = k.div_ceil(block_size);
let blob = block_size / 2;
let weights_nk = pseudo(n * k, 0.3);
let (packed_bytes, scales, _zps, _dq) = quantize(&weights_nk, n, k, block_size, false);
let kernel = accuracy4_kernel(k, n, block_size);
let b = Owned::u8(&[n, k_blocks, blob], &packed_bytes);
let scales_t = Owned::f32(&[n, k_blocks], &scales);
let int8_weight = kernel
.prepack_int8_weight(&b.view(), &scales_t.view(), None)
.unwrap();
let int4_weight = PackedInt4Weight {
values: packed_bytes.clone(),
scales: scales.clone(),
};
let mlas_packed = mlas_sys::SQNBitPackedB::new(
n,
k,
4,
block_size,
mlas_sys::SQNBitComputeType::Int8,
&packed_bytes,
&scales,
None,
)
.expect("MLAS SQNBit int4 must be available for the perf probe");
for &m in &[1usize, 32] {
let a = pseudo(m * k, 0.8);
for threads in [1usize, 8] {
let hand_us = if m == 1 {
time(threads, || {
let mut out = vec![0.0f32; n];
int4_matmul_m1(&a, &int4_weight, &mut out, k, n, dot_kernel);
})
} else {
time(threads, || {
let mut out = vec![0.0f32; m * n];
int8_matmul(
&a,
&int8_weight,
&mut out,
m,
k,
n,
block_size,
dot_kernel,
);
})
};
let mlas_us = time(threads, || {
let mut out = vec![0.0f32; m * n];
mlas_sys::sqnbit_gemm(&mlas_packed, m, &a, None, &mut out, true);
});
eprintln!(
"int4 K={k} N={n} M={m} {threads}t: hand={hand_us:.1}us mlas={mlas_us:.1}us \
speedup={:.2}x",
hand_us / mlas_us
);
}
}
}
}
#[cfg(feature = "mlas")]
#[test]
#[ignore = "perf probe; run explicitly with --ignored --nocapture"]
fn int4_gemv_decode_microbench() {
use std::time::Instant;
fn median_ns<F: FnMut() + Send>(threads: usize, mut run: F) -> f64 {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap();
pool.install(|| {
for _ in 0..30 {
run();
}
let mut samples = [0.0f64; 5];
for sample in samples.iter_mut() {
let iters = 300u32;
let start = Instant::now();
for _ in 0..iters {
run();
}
*sample = start.elapsed().as_secs_f64() * 1e9 / iters as f64;
}
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
samples[2]
})
}
let block_size = 32usize;
let dot_kernel = selected_dot_kernel();
let shapes: &[(&str, usize, usize)] = &[
("qkv_proj", 1024, 4096),
("o_proj", 2048, 1024),
("gate_proj", 1024, 3072),
("up_proj", 1024, 3072),
("down_proj", 3072, 1024),
("lm_head", 1024, 151936),
];
eprintln!(
"int4 GEMV M=1 microbench (Qwen3-0.6B shapes), dot_kernel={dot_kernel:?}, median-of-5"
);
for &(label, k, n) in shapes {
let weights_nk = pseudo(n * k, 0.3);
let (packed_bytes, scales, _zps, _dq) = quantize(&weights_nk, n, k, block_size, false);
let int4_weight = PackedInt4Weight {
values: packed_bytes.clone(),
scales: scales.clone(),
};
let mlas_packed = mlas_sys::SQNBitPackedB::new(
n,
k,
4,
block_size,
mlas_sys::SQNBitComputeType::Int8,
&packed_bytes,
&scales,
None,
)
.expect("MLAS SQNBit int4 must be available for the perf probe");
let a = pseudo(k, 0.8);
let weight_bytes = (n as f64) * (k as f64) / 2.0;
let flops = 2.0 * (n as f64) * (k as f64);
for threads in [1usize, 32] {
let hand_ns = median_ns(threads, || {
let mut out = vec![0.0f32; n];
int4_matmul_m1(&a, &int4_weight, &mut out, k, n, dot_kernel);
});
let mlas_ns = median_ns(threads, || {
let mut out = vec![0.0f32; n];
mlas_sys::sqnbit_gemm(&mlas_packed, 1, &a, None, &mut out, true);
});
let hand_gbs = weight_bytes / hand_ns;
let mlas_gbs = weight_bytes / mlas_ns;
let hand_gflops = flops / hand_ns;
let mlas_gflops = flops / mlas_ns;
eprintln!(
"{label:10} K={k:6} N={n:6} {threads:2}t: \
hand {hand_ns:8.0}ns {hand_gbs:6.1}GB/s {hand_gflops:6.1}GF | \
mlas {mlas_ns:8.0}ns {mlas_gbs:6.1}GB/s {mlas_gflops:6.1}GF | \
ratio(hand/mlas)={:.2}x",
hand_ns / mlas_ns
);
}
}
}
#[cfg(feature = "mlas")]
#[test]
#[ignore = "perf probe; run explicitly with --ignored --nocapture"]
fn matmulnbits_mlas_decode_step() {
use std::time::Instant;
let layers = 28usize;
let ops: &[(usize, usize, usize)] = &[
(3584, 4608, layers), (3584, 3584, layers), (3584, 18944, layers), (3584, 18944, layers), (18944, 3584, layers), (3584, 152064, 1), ];
let block_size = 32usize;
let dot_kernel = selected_dot_kernel();
struct Weights {
k: usize,
n: usize,
int4: PackedInt4Weight,
mlas_int8: mlas_sys::SQNBitPackedB,
mlas_fp32: mlas_sys::SQNBitPackedB,
}
let mut built: Vec<Weights> = Vec::new();
let mut weight_bytes = 0u64;
for (shape_index, &(k, n, count)) in ops.iter().enumerate() {
for instance in 0..count {
let seed = 0.3 + shape_index as f32 * 0.11 + instance as f32 * 0.001;
let weights_nk = pseudo(n * k, seed);
let (packed_bytes, scales, _zps, _dq) =
quantize(&weights_nk, n, k, block_size, false);
let make = |comp| {
mlas_sys::SQNBitPackedB::new(
n,
k,
4,
block_size,
comp,
&packed_bytes,
&scales,
None,
)
};
let (Some(mlas_int8), Some(mlas_fp32)) = (
make(mlas_sys::SQNBitComputeType::Int8),
make(mlas_sys::SQNBitComputeType::Fp32),
) else {
eprintln!("MLAS SQNBit int4 kernel unavailable; skipping decode-step probe");
return;
};
weight_bytes += (n as u64) * (k as u64) / 2;
built.push(Weights {
k,
n,
int4: PackedInt4Weight {
values: packed_bytes,
scales,
},
mlas_int8,
mlas_fp32,
});
}
}
let threads = configured_decode_threads()
.or_else(|| default_decode_threads(available_parallelism()))
.unwrap_or(1);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap();
let run_hand = || {
for w in &built {
let a = vec![0.03f32; w.k];
let mut out = vec![0.0f32; w.n];
int4_matmul_m1(&a, &w.int4, &mut out, w.k, w.n, dot_kernel);
}
};
let run_mlas_int8 = || {
for w in &built {
let a = vec![0.03f32; w.k];
let mut out = vec![0.0f32; w.n];
mlas_sys::sqnbit_gemm(&w.mlas_int8, 1, &a, None, &mut out, true);
}
};
let run_mlas_fp32 = || {
for w in &built {
let a = vec![0.03f32; w.k];
let mut out = vec![0.0f32; w.n];
mlas_sys::sqnbit_gemm(&w.mlas_fp32, 1, &a, None, &mut out, true);
}
};
let step = |label: &str, run: &(dyn Fn() + Sync)| {
pool.install(|| {
for _ in 0..3 {
run();
}
let iters = 20u32;
let start = Instant::now();
for _ in 0..iters {
run();
}
let per_step = start.elapsed().as_secs_f64() / iters as f64;
let gbs = weight_bytes as f64 / per_step / 1e9;
eprintln!(
"decode-step {label}: {:.2} ms/step {:.2} tok/s {:.1} GB/s ({threads}t)",
per_step * 1e3,
1.0 / per_step,
gbs,
);
});
};
eprintln!(
"decode-step probe: {} weight bytes/token, {threads} decode threads",
weight_bytes
);
step("hand", &run_hand);
step("mlas-int8", &run_mlas_int8);
step("mlas-fp32", &run_mlas_fp32);
}
}