use rayon::prelude::*;
use std::ops::Range;
use std::sync::Arc;
use ferrox_gguf::GgmlType;
use crate::tensor::Tensor;
pub mod gpu_backend;
pub mod lora;
mod repack_cache;
#[cfg(any(feature = "cuda", feature = "metal", feature = "vulkan"))]
use gpu_backend::BackendDispatch;
use gpu_backend::{with_gpu_backend_caps, with_gpu_backends, BackendCaps, Cuda, Metal};
pub use lora::{LoraDelta, LoraScale, LoraShapeError, LoraStack};
pub use repack_cache::MapId;
use repack_cache::{
get_or_repack_q4_0x4, get_or_repack_q4k, get_or_repack_q5k, get_or_repack_q6k,
get_or_repack_q8x4,
};
pub enum WeightBytes {
Owned(Vec<u8>),
Mapped {
mmap: Arc<memmap2::Mmap>,
range: Range<usize>,
},
Shared {
buf: Arc<Vec<u8>>,
range: Range<usize>,
},
}
impl WeightBytes {
pub fn as_slice(&self) -> &[u8] {
match self {
WeightBytes::Owned(v) => v,
WeightBytes::Mapped { mmap, range } => &mmap[range.clone()],
WeightBytes::Shared { buf, range } => &buf[range.clone()],
}
}
pub fn len(&self) -> usize {
self.as_slice().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn map_id(&self) -> Option<MapId> {
match self {
WeightBytes::Mapped { mmap, range } => Some(MapId::of(mmap, range.start)),
WeightBytes::Owned(_) | WeightBytes::Shared { .. } => None,
}
}
pub fn is_mapped(&self) -> bool {
matches!(self, WeightBytes::Mapped { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QuantKind {
Q8_0,
Q4_0,
Q4K,
Q5K,
Q6K,
Q2K,
Q3K,
Q4_1,
Q5_0,
Q5_1,
Q8_1,
IQ4NL,
IQ4XS,
IQ1S,
IQ2XXS,
IQ3XXS,
IQ2XS,
IQ2S,
IQ3S,
IQ1M,
Mxfp4Gguf,
}
impl QuantKind {
pub const ALL: &'static [QuantKind] = &[
QuantKind::Q8_0,
QuantKind::Q4_0,
QuantKind::Q4K,
QuantKind::Q5K,
QuantKind::Q6K,
QuantKind::Q2K,
QuantKind::Q3K,
QuantKind::Q4_1,
QuantKind::Q5_0,
QuantKind::Q5_1,
QuantKind::Q8_1,
QuantKind::IQ4NL,
QuantKind::IQ4XS,
QuantKind::IQ1S,
QuantKind::IQ2XXS,
QuantKind::IQ3XXS,
QuantKind::IQ2XS,
QuantKind::IQ2S,
QuantKind::IQ3S,
QuantKind::IQ1M,
QuantKind::Mxfp4Gguf,
];
pub fn name(self) -> &'static str {
match self {
QuantKind::Q8_0 => "Q8_0",
QuantKind::Q4_0 => "Q4_0",
QuantKind::Q4K => "Q4_K",
QuantKind::Q5K => "Q5_K",
QuantKind::Q6K => "Q6_K",
QuantKind::Q2K => "Q2_K",
QuantKind::Q3K => "Q3_K",
QuantKind::Q4_1 => "Q4_1",
QuantKind::Q5_0 => "Q5_0",
QuantKind::Q5_1 => "Q5_1",
QuantKind::Q8_1 => "Q8_1",
QuantKind::IQ4NL => "IQ4_NL",
QuantKind::IQ4XS => "IQ4_XS",
QuantKind::IQ1S => "IQ1_S",
QuantKind::IQ2XXS => "IQ2_XXS",
QuantKind::IQ3XXS => "IQ3_XXS",
QuantKind::IQ2XS => "IQ2_XS",
QuantKind::IQ2S => "IQ2_S",
QuantKind::IQ3S => "IQ3_S",
QuantKind::IQ1M => "IQ1_M",
QuantKind::Mxfp4Gguf => "MXFP4",
}
}
}
pub fn metal_matvec_kind_name(kind: QuantKind) -> Option<&'static str> {
Metal::matvec_kernel(kind)
}
pub fn metal_mul_mm_kind_supported(kind: QuantKind) -> bool {
Metal::gemm_supported(kind)
}
pub fn quant_kind_for(dtype: GgmlType) -> Option<QuantKind> {
match dtype {
GgmlType::Q8_0 => Some(QuantKind::Q8_0),
GgmlType::Q4_0 => Some(QuantKind::Q4_0),
GgmlType::Q4K => Some(QuantKind::Q4K),
GgmlType::Q5K => Some(QuantKind::Q5K),
GgmlType::Q6K => Some(QuantKind::Q6K),
GgmlType::Q2K => Some(QuantKind::Q2K),
GgmlType::Q3K => Some(QuantKind::Q3K),
GgmlType::Q4_1 => Some(QuantKind::Q4_1),
GgmlType::Q5_0 => Some(QuantKind::Q5_0),
GgmlType::Q5_1 => Some(QuantKind::Q5_1),
GgmlType::Q8_1 => Some(QuantKind::Q8_1),
GgmlType::IQ4NL => Some(QuantKind::IQ4NL),
GgmlType::IQ4XS => Some(QuantKind::IQ4XS),
GgmlType::IQ2XS => Some(QuantKind::IQ2XS),
GgmlType::IQ2S => Some(QuantKind::IQ2S),
GgmlType::IQ3S => Some(QuantKind::IQ3S),
GgmlType::IQ1M => Some(QuantKind::IQ1M),
GgmlType::IQ1S => Some(QuantKind::IQ1S),
GgmlType::IQ2XXS => Some(QuantKind::IQ2XXS),
GgmlType::IQ3XXS => Some(QuantKind::IQ3XXS),
GgmlType::MXFP4 => Some(QuantKind::Mxfp4Gguf),
_ => None,
}
}
pub fn cuda_mul_mm_kind_supported(kind: QuantKind) -> bool {
Cuda::gemm_supported(kind)
}
pub fn cuda_matvec_kind_supported(kind: QuantKind) -> bool {
Cuda::matvec_kernel(kind).is_some()
}
pub fn cpu_int_dot_kind_supported(kind: QuantKind, cols: usize) -> bool {
match kind {
QuantKind::Q8_0 | QuantKind::Q4_0 => cols.is_multiple_of(32),
QuantKind::Q4K | QuantKind::Q5K | QuantKind::Q6K => cols.is_multiple_of(256),
_ => false,
}
}
pub fn active_backend() -> crate::kernel_registry::Backend {
#[allow(unused_macros)]
macro_rules! first_enabled {
($b:ty) => {
if <$b as BackendDispatch>::dense_enabled() {
return <$b as BackendCaps>::ID;
}
};
}
with_gpu_backends!(first_enabled);
crate::kernel_registry::Backend::Cpu
}
const MIN_TASK_MACS: usize = 1 << 16;
#[cfg(feature = "metal")]
pub fn metal_dense_enabled() -> bool {
Metal::dense_enabled()
}
#[cfg(feature = "cuda")]
pub fn cuda_dense_enabled() -> bool {
Cuda::dense_enabled()
}
pub fn cpu_int_dot_enabled() -> bool {
#[cfg(test)]
{
match INT_DOT_TEST_OVERRIDE.load(std::sync::atomic::Ordering::Acquire) {
0 => return false,
1 => return true,
_ => {}
}
}
use std::sync::OnceLock;
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
matches!(
std::env::var("FERROX_CPU_INT_DOT").ok().as_deref(),
Some("1") | Some("true") | Some("on")
)
})
}
#[cfg(test)]
static INT_DOT_TEST_OVERRIDE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1);
pub unsafe fn default_cpu_int_dot_on() {
if std::env::var_os("FERROX_CPU_INT_DOT").is_none() && int_dot_is_a_win_here() {
unsafe { std::env::set_var("FERROX_CPU_INT_DOT", "1") };
}
}
fn int_dot_is_a_win_here() -> bool {
let tier = int_dot_tier_here();
tier.matvec || tier.batch_gemm
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum IntDotShape {
Matvec,
BatchGemm,
}
pub fn cpu_int_dot_for(shape: IntDotShape) -> bool {
cpu_int_dot_enabled() && int_dot_tier_here().covers(shape)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct IntDotTier {
matvec: bool,
batch_gemm: bool,
}
impl IntDotTier {
fn covers(self, shape: IntDotShape) -> bool {
match shape {
IntDotShape::Matvec => self.matvec,
IntDotShape::BatchGemm => self.batch_gemm,
}
}
}
#[inline]
fn q5k_batch_takes_kx8(interleave: usize) -> bool {
cfg!(target_arch = "aarch64") || ferrox_quant::q5_kx8_gemm_uses_acts_x4(interleave)
}
fn int_dot_tier_here() -> IntDotTier {
#[cfg(target_arch = "aarch64")]
{
IntDotTier {
matvec: true,
batch_gemm: ferrox_quant::batch_gemm_is_accelerated(
ferrox_quant::preferred_interleave(),
),
}
}
#[cfg(target_arch = "x86_64")]
{
IntDotTier {
matvec: false,
batch_gemm: ferrox_quant::batch_gemm_is_accelerated(
ferrox_quant::preferred_interleave(),
),
}
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
{
IntDotTier {
matvec: false,
batch_gemm: false,
}
}
}
pub enum BatchActs {
Q8 {
acts: Vec<ferrox_quant::Q8Activations>,
tiles: Vec<ferrox_quant::Q8ActsX4>,
cols: usize,
},
Q8K {
acts: Vec<ferrox_quant::Q8KActivations>,
tiles: Vec<ferrox_quant::Q8KActsX4>,
cols: usize,
},
}
const _: () = {
assert!(ferrox_quant::Q4_KX8_GEMM_NC == ferrox_quant::Q8K_ACTS_X4_NC);
assert!(ferrox_quant::Q5_KX8_GEMM_NC == ferrox_quant::Q8K_ACTS_X4_NC);
};
pub enum WeightMatrix {
F32(Tensor),
Quantized {
data: WeightBytes,
rows: usize,
cols: usize,
kind: QuantKind,
},
Mxfp4 {
packed: WeightBytes,
scale: WeightBytes,
rows: usize,
cols: usize,
},
Adapted {
base: Box<WeightMatrix>,
lora: LoraStack,
},
}
impl WeightMatrix {
pub fn attach_lora(&mut self, delta: LoraDelta) {
assert_eq!(delta.rows(), self.rows(), "LoRA delta rows");
assert_eq!(delta.cols(), self.cols(), "LoRA delta cols");
if let WeightMatrix::Adapted { lora, .. } = self {
lora.push(delta);
return;
}
let placeholder = WeightMatrix::F32(Tensor::new(Vec::new(), vec![0, 0]));
let base = std::mem::replace(self, placeholder);
*self = WeightMatrix::Adapted {
base: Box::new(base),
lora: LoraStack::new(delta),
};
}
pub fn lora(&self) -> Option<&LoraStack> {
match self {
WeightMatrix::Adapted { lora, .. } => Some(lora),
_ => None,
}
}
pub fn base(&self) -> &WeightMatrix {
match self {
WeightMatrix::Adapted { base, .. } => base,
_ => self,
}
}
pub fn bytes_len(&self) -> usize {
match self {
WeightMatrix::Quantized { data, .. } => data.len(),
WeightMatrix::Adapted { base, .. } => base.bytes_len(),
_ => 0,
}
}
pub fn bytes_eq(&self, other: &WeightMatrix) -> bool {
match (self.base(), other.base()) {
(WeightMatrix::Quantized { data: a, .. }, WeightMatrix::Quantized { data: b, .. }) => {
a.as_slice() == b.as_slice()
}
_ => false,
}
}
pub fn rows(&self) -> usize {
match self {
WeightMatrix::F32(t) => t.rows(),
WeightMatrix::Quantized { rows, .. } => *rows,
WeightMatrix::Mxfp4 { rows, .. } => *rows,
WeightMatrix::Adapted { base, .. } => base.rows(),
}
}
pub fn quant_kind(&self) -> Option<QuantKind> {
match self {
WeightMatrix::Quantized { kind, .. } => Some(*kind),
WeightMatrix::F32(_) | WeightMatrix::Mxfp4 { .. } => None,
WeightMatrix::Adapted { base, .. } => base.quant_kind(),
}
}
pub fn cols(&self) -> usize {
match self {
WeightMatrix::F32(t) => t.cols(),
WeightMatrix::Quantized { cols, .. } => *cols,
WeightMatrix::Mxfp4 { cols, .. } => *cols,
WeightMatrix::Adapted { base, .. } => base.cols(),
}
}
fn block_bytes_per_row(&self, kind: QuantKind, cols: usize) -> usize {
match kind {
QuantKind::Q8_0 => {
(cols / ferrox_quant::Q8_0_BLOCK_ELEMS) * ferrox_quant::Q8_0_BLOCK_BYTES
}
QuantKind::Q4_0 => {
(cols / ferrox_quant::Q4_0_BLOCK_ELEMS) * ferrox_quant::Q4_0_BLOCK_BYTES
}
QuantKind::Q4K => {
(cols / ferrox_quant::Q4_K_BLOCK_ELEMS) * ferrox_quant::Q4_K_BLOCK_BYTES
}
QuantKind::Q5K => {
(cols / ferrox_quant::Q5_K_BLOCK_ELEMS) * ferrox_quant::Q5_K_BLOCK_BYTES
}
QuantKind::Q6K => {
(cols / ferrox_quant::Q6_K_BLOCK_ELEMS) * ferrox_quant::Q6_K_BLOCK_BYTES
}
QuantKind::Q2K => {
(cols / ferrox_quant::Q2_K_BLOCK_ELEMS) * ferrox_quant::Q2_K_BLOCK_BYTES
}
QuantKind::Q3K => {
(cols / ferrox_quant::Q3_K_BLOCK_ELEMS) * ferrox_quant::Q3_K_BLOCK_BYTES
}
QuantKind::Q4_1 => {
(cols / ferrox_quant::Q4_1_BLOCK_ELEMS) * ferrox_quant::Q4_1_BLOCK_BYTES
}
QuantKind::Q5_0 => {
(cols / ferrox_quant::Q5_0_BLOCK_ELEMS) * ferrox_quant::Q5_0_BLOCK_BYTES
}
QuantKind::Q5_1 => {
(cols / ferrox_quant::Q5_1_BLOCK_ELEMS) * ferrox_quant::Q5_1_BLOCK_BYTES
}
QuantKind::Q8_1 => {
(cols / ferrox_quant::Q8_1_BLOCK_ELEMS) * ferrox_quant::Q8_1_BLOCK_BYTES
}
QuantKind::IQ4NL => {
(cols / ferrox_quant::IQ4_NL_BLOCK_ELEMS) * ferrox_quant::IQ4_NL_BLOCK_BYTES
}
QuantKind::IQ4XS => {
(cols / ferrox_quant::IQ4_XS_BLOCK_ELEMS) * ferrox_quant::IQ4_XS_BLOCK_BYTES
}
QuantKind::IQ1S => {
(cols / ferrox_quant::IQ1_S_BLOCK_ELEMS) * ferrox_quant::IQ1_S_BLOCK_BYTES
}
QuantKind::IQ2XXS => {
(cols / ferrox_quant::IQ2_XXS_BLOCK_ELEMS) * ferrox_quant::IQ2_XXS_BLOCK_BYTES
}
QuantKind::IQ3XXS => {
(cols / ferrox_quant::IQ3_XXS_BLOCK_ELEMS) * ferrox_quant::IQ3_XXS_BLOCK_BYTES
}
QuantKind::IQ2XS => {
(cols / ferrox_quant::IQ2_XS_BLOCK_ELEMS) * ferrox_quant::IQ2_XS_BLOCK_BYTES
}
QuantKind::IQ2S => {
(cols / ferrox_quant::IQ2_S_BLOCK_ELEMS) * ferrox_quant::IQ2_S_BLOCK_BYTES
}
QuantKind::IQ3S => {
(cols / ferrox_quant::IQ3_S_BLOCK_ELEMS) * ferrox_quant::IQ3_S_BLOCK_BYTES
}
QuantKind::IQ1M => {
(cols / ferrox_quant::IQ1_M_BLOCK_ELEMS) * ferrox_quant::IQ1_M_BLOCK_BYTES
}
QuantKind::Mxfp4Gguf => {
(cols / ferrox_quant::MXFP4_GGUF_BLOCK_ELEMS) * ferrox_quant::MXFP4_GGUF_BLOCK_BYTES
}
}
}
fn min_rows_per_task(rows: usize) -> usize {
let threads = crate::par::num_threads();
let by_threads = (rows / (threads * 4)).max(8.min(rows.max(1)));
let per_row = crate::par::macs_per_row();
if per_row == 0 {
return by_threads;
}
let need = MIN_TASK_MACS.div_ceil(per_row.max(1));
by_threads.max(need.min(rows.max(1)))
}
fn par_chunked_groups(
n_groups: usize,
group_rows: usize,
n_tiles: usize,
tile_batch: usize,
body: impl Fn(usize, usize, usize) + Sync,
) {
if n_groups == 0 || n_tiles == 0 {
return;
}
let nth = crate::par::num_threads();
const CHUNK_ELEMS: usize = 16;
let g_per_chunk = (CHUNK_ELEMS / group_rows).max(1);
let t_per_chunk = (CHUNK_ELEMS / tile_batch).max(1);
let mut nchunk_g = n_groups.div_ceil(g_per_chunk);
let mut nchunk_t = n_tiles.div_ceil(t_per_chunk);
if nchunk_g * nchunk_t < nth * 4 {
if n_groups * group_rows > n_tiles * tile_batch {
nchunk_g = nth.min(n_groups);
nchunk_t = 1;
} else {
nchunk_g = 1;
nchunk_t = nth.min(n_tiles);
}
}
let dg = n_groups.div_ceil(nchunk_g);
let dt = n_tiles.div_ceil(nchunk_t);
crate::par::indices(nchunk_g * nchunk_t, 1, |chunk| {
let g0 = (chunk % nchunk_g) * dg;
let g1 = (g0 + dg).min(n_groups);
let t0 = (chunk / nchunk_g) * dt;
let t1 = (t0 + dt).min(n_tiles);
for g in g0..g1 {
body(g, t0, t1);
}
});
}
fn q8_acts<'a>(
shared: Option<&'a BatchActs>,
x_batch: &[f32],
batch_size: usize,
cols: usize,
owned: &'a mut Vec<ferrox_quant::Q8Activations>,
) -> (
&'a [ferrox_quant::Q8Activations],
&'a [ferrox_quant::Q8ActsX4],
) {
if let Some(BatchActs::Q8 {
acts,
tiles,
cols: c,
}) = shared
{
if acts.len() == batch_size && *c == cols {
return (acts, tiles);
}
}
*owned = (0..batch_size)
.into_par_iter()
.map(|b| ferrox_quant::quantize_activations_q8(&x_batch[b * cols..(b + 1) * cols]))
.collect();
(owned, &[])
}
fn q8k_acts<'a>(
shared: Option<&'a BatchActs>,
x_batch: &[f32],
batch_size: usize,
cols: usize,
owned: &'a mut Vec<ferrox_quant::Q8KActivations>,
) -> (
&'a [ferrox_quant::Q8KActivations],
&'a [ferrox_quant::Q8KActsX4],
) {
if let Some(BatchActs::Q8K {
acts,
tiles,
cols: c,
}) = shared
{
if acts.len() == batch_size && *c == cols {
return (acts, tiles);
}
}
*owned = (0..batch_size)
.into_par_iter()
.map(|b| ferrox_quant::quantize_activations_q8_k(&x_batch[b * cols..(b + 1) * cols]))
.collect();
(owned, &[])
}
fn prefer_serial_matvec(rows: usize, cols: usize) -> bool {
rows.saturating_mul(cols) < 256_000
}
fn dot(kind: QuantKind, row: &[u8], x: &[f32]) -> f32 {
match kind {
QuantKind::Q8_0 => ferrox_quant::dot_q8_0_f32(row, x),
QuantKind::Q4_0 => ferrox_quant::dot_q4_0_f32(row, x),
QuantKind::Q4K => ferrox_quant::dot_q4_k_f32(row, x),
QuantKind::Q5K => ferrox_quant::dot_q5_k_f32(row, x),
QuantKind::Q6K => ferrox_quant::dot_q6_k_f32(row, x),
QuantKind::Q2K => ferrox_quant::dot_q2_k_f32(row, x),
QuantKind::Q3K => ferrox_quant::dot_q3_k_f32(row, x),
QuantKind::Q4_1 => ferrox_quant::dot_q4_1_f32(row, x),
QuantKind::Q5_0 => ferrox_quant::dot_q5_0_f32(row, x),
QuantKind::Q5_1 => ferrox_quant::dot_q5_1_f32(row, x),
QuantKind::Q8_1 => ferrox_quant::dot_q8_1_f32(row, x),
QuantKind::IQ4NL => ferrox_quant::dot_iq4_nl_f32(row, x),
QuantKind::IQ4XS => ferrox_quant::dot_iq4_xs_f32(row, x),
QuantKind::IQ1S => ferrox_quant::dot_iq1_s_f32(row, x),
QuantKind::IQ2XXS => ferrox_quant::dot_iq2_xxs_f32(row, x),
QuantKind::IQ3XXS => ferrox_quant::dot_iq3_xxs_f32(row, x),
QuantKind::IQ2XS => ferrox_quant::dot_iq2_xs_f32(row, x),
QuantKind::IQ2S => ferrox_quant::dot_iq2_s_f32(row, x),
QuantKind::IQ3S => ferrox_quant::dot_iq3_s_f32(row, x),
QuantKind::IQ1M => ferrox_quant::dot_iq1_m_f32(row, x),
QuantKind::Mxfp4Gguf => ferrox_quant::dot_mxfp4_gguf_f32(row, x),
}
}
fn dequant(kind: QuantKind, bytes: &[u8]) -> Vec<f32> {
let out = match kind {
QuantKind::Q8_0 => ferrox_quant::dequant_q8_0(bytes),
QuantKind::Q4_0 => ferrox_quant::dequant_q4_0(bytes),
QuantKind::Q4K => ferrox_quant::dequant_q4_k(bytes),
QuantKind::Q5K => ferrox_quant::dequant_q5_k(bytes),
QuantKind::Q6K => ferrox_quant::dequant_q6_k(bytes),
QuantKind::Q2K => ferrox_quant::dequant_q2_k(bytes),
QuantKind::Q3K => ferrox_quant::dequant_q3_k(bytes),
QuantKind::Q4_1 => ferrox_quant::dequant_q4_1(bytes),
QuantKind::Q5_0 => ferrox_quant::dequant_q5_0(bytes),
QuantKind::Q5_1 => ferrox_quant::dequant_q5_1(bytes),
QuantKind::Q8_1 => ferrox_quant::dequant_q8_1(bytes),
QuantKind::IQ4NL => ferrox_quant::dequant_iq4_nl(bytes),
QuantKind::IQ4XS => ferrox_quant::dequant_iq4_xs(bytes),
QuantKind::IQ1S => ferrox_quant::dequant_iq1_s(bytes),
QuantKind::IQ2XXS => ferrox_quant::dequant_iq2_xxs(bytes),
QuantKind::IQ3XXS => ferrox_quant::dequant_iq3_xxs(bytes),
QuantKind::IQ2XS => ferrox_quant::dequant_iq2_xs(bytes),
QuantKind::IQ2S => ferrox_quant::dequant_iq2_s(bytes),
QuantKind::IQ3S => ferrox_quant::dequant_iq3_s(bytes),
QuantKind::IQ1M => ferrox_quant::dequant_iq1_m(bytes),
QuantKind::Mxfp4Gguf => ferrox_quant::dequant_mxfp4_gguf(bytes),
};
out.expect("row byte length is block-aligned by construction (block_bytes_per_row)")
}
pub fn dequant_row(&self, r: usize) -> Vec<f32> {
assert!(r < self.rows(), "row {r} out of range ({})", self.rows());
match self {
WeightMatrix::F32(t) => t.row(r).to_vec(),
WeightMatrix::Quantized {
data, cols, kind, ..
} => {
let row_bytes = self.block_bytes_per_row(*kind, *cols);
let bytes = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
let out = Self::dequant(*kind, bytes);
debug_assert_eq!(out.len(), *cols);
out
}
WeightMatrix::Mxfp4 {
packed,
scale,
cols,
..
} => {
let packed_per_row = cols / 2;
let scales_per_row = cols / ferrox_quant::MXFP4_GROUP_SIZE;
let p = &packed.as_slice()[r * packed_per_row..(r + 1) * packed_per_row];
let sc = &scale.as_slice()[r * scales_per_row..(r + 1) * scales_per_row];
ferrox_quant::dequant_mxfp4_row(p, sc)
.expect("row slices are group-aligned by construction")
}
WeightMatrix::Adapted { base, lora } => {
let mut row = base.dequant_row(r);
lora.add_row_to(r, &mut row);
row
}
}
}
#[cfg(feature = "metal")]
pub fn mul_mm_sg_launch(&self) -> Option<ferrox_metal::gpu::MulMmSgLaunch<'_>> {
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = self
else {
return None;
};
let kind_name = match kind {
QuantKind::Q8_0 => "Q8_0",
QuantKind::Q4_0 => "Q4_0",
QuantKind::Q5_0 => "Q5_0",
QuantKind::Q4K => "Q4_K",
QuantKind::Q5K => "Q5_K",
QuantKind::Q6K => "Q6_K",
QuantKind::IQ4XS => "IQ4_XS",
_ => return None,
};
let (fn_name, block_bytes, block_elems) = ferrox_metal::gpu::mul_mm_sg_meta(kind_name)?;
Some(ferrox_metal::gpu::MulMmSgLaunch {
weights: data.as_slice(),
rows: *rows,
row_bytes: self.block_bytes_per_row(*kind, *cols),
fn_name,
block_bytes,
block_elems,
})
}
#[cfg(feature = "cuda")]
pub fn cuda_mul_mm_view(&self) -> Option<ferrox_cuda::prefill::MulMmWeights<'_>> {
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = self
else {
return None;
};
if !cuda_mul_mm_kind_supported(*kind) {
return None;
}
let mm_kind = ferrox_cuda::mul_mm::kind_by_name(kind.name())?;
Some(ferrox_cuda::prefill::MulMmWeights {
kind: mm_kind,
data: data.as_slice(),
rows: *rows,
cols: *cols,
row_bytes: self.block_bytes_per_row(*kind, *cols),
})
}
#[cfg(any(feature = "metal", feature = "cuda"))]
pub fn prefers_gpu_batch(&self) -> bool {
!matches!(
self.base(),
WeightMatrix::Quantized {
kind: QuantKind::IQ4NL
| QuantKind::IQ1S
| QuantKind::IQ2XXS
| QuantKind::IQ3XXS
| QuantKind::IQ2XS
| QuantKind::IQ2S
| QuantKind::IQ3S
| QuantKind::IQ1M,
..
}
)
}
pub fn apply(&self, x: &[f32]) -> Vec<f32> {
assert_eq!(
x.len(),
self.cols(),
"activation length must match matrix column count"
);
crate::activation_tap::observe(self, x, 1);
if let WeightMatrix::Adapted { base, lora } = self {
let mut out = base.apply(x);
lora.add_to(x, &mut out);
return out;
}
#[cfg(feature = "cuda")]
{
if cuda_dense_enabled() {
if let Some(out) = self.apply_gpu(x) {
return out;
}
}
}
#[cfg(feature = "metal")]
{
if metal_dense_enabled() {
if let Some(out) = self.apply_gpu(x) {
return out;
}
}
}
self.apply_cpu(x)
}
pub fn apply_softcapped(&self, x: &[f32], softcap: f32) -> Vec<f32> {
#[cfg(feature = "metal")]
if metal_dense_enabled() {
if let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = self
{
if let Some(kind_name) = Metal::matvec_kernel(*kind) {
crate::activation_tap::observe(self, x, 1);
let row_bytes = self.block_bytes_per_row(*kind, *cols);
let epilogue = ferrox_metal::gpu::MatvecEpilogue {
softcap: Some(softcap),
};
match ferrox_metal::gpu::launch_matvec_kind_with(
kind_name,
data.as_slice(),
x,
*rows,
row_bytes,
epilogue,
) {
Some(Ok(out)) => return out,
Some(Err(e)) => {
eprintln!(
"ferrox: Metal softcapped matvec failed, falling back to CPU: {e}"
);
}
None => {}
}
}
}
}
let mut out = self.apply(x);
crate::matmul::softcap_inplace(&mut out, softcap);
out
}
pub fn apply_three(a: &Self, b: &Self, c: &Self, x: &[f32]) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
#[cfg(feature = "metal")]
let gpu = metal_dense_enabled();
#[cfg(not(feature = "metal"))]
let gpu = false;
#[cfg(feature = "cuda")]
let gpu = gpu || cuda_dense_enabled();
if gpu {
return (a.apply(x), b.apply(x), c.apply(x));
}
crate::par::join3(|| a.apply(x), || b.apply(x), || c.apply(x))
}
pub fn apply_cpu(&self, x: &[f32]) -> Vec<f32> {
assert_eq!(
x.len(),
self.cols(),
"activation length must match matrix column count"
);
crate::par::with_op_work(self.rows(), x.len(), || self.apply_cpu_inner(x))
}
fn apply_cpu_inner(&self, x: &[f32]) -> Vec<f32> {
match self {
WeightMatrix::F32(t) => {
let xt = Tensor::new(x.to_vec(), vec![1, x.len()]);
crate::matmul::matmul_f32(&xt, t).data
}
WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} => {
let row_bytes = self.block_bytes_per_row(*kind, *cols);
let mut out = vec![0f32; *rows];
if cpu_int_dot_for(IntDotShape::Matvec) {
match *kind {
QuantKind::Q8_0 if x.len().is_multiple_of(32) => {
let act = ferrox_quant::quantize_activations_q8(x);
let n_groups = *rows / ferrox_quant::Q8_0X4_NROWS;
let serial = Self::prefer_serial_matvec(*rows, *cols);
let interleave = ferrox_quant::q8_0x4_interleave();
if n_groups > 0 {
let packed = get_or_repack_q8x4(data, *rows, *cols);
if serial {
for (g, chunk) in out[..n_groups * ferrox_quant::Q8_0X4_NROWS]
.chunks_mut(ferrox_quant::Q8_0X4_NROWS)
.enumerate()
{
ferrox_quant::gemv_q8_0x4_group(
&packed, g, &act, *cols, interleave, chunk,
);
}
} else {
crate::par::chunks_mut(
&mut out[..n_groups * ferrox_quant::Q8_0X4_NROWS],
ferrox_quant::Q8_0X4_NROWS,
Self::min_rows_per_task(n_groups).max(1),
|g, chunk| {
ferrox_quant::gemv_q8_0x4_group(
&packed, g, &act, *cols, interleave, chunk,
);
},
);
}
let data_slice = data.as_slice();
let tail_len = *rows - n_groups * ferrox_quant::Q8_0X4_NROWS;
if tail_len > 0 {
let tail = &mut out[n_groups * ferrox_quant::Q8_0X4_NROWS..];
if serial || Self::prefer_serial_matvec(tail_len, *cols) {
for (i, o) in tail.iter_mut().enumerate() {
let r = n_groups * ferrox_quant::Q8_0X4_NROWS + i;
let row =
&data_slice[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q8_0_q8(row, &act);
}
} else {
let min_len = Self::min_rows_per_task(tail_len);
crate::par::items_mut(tail, min_len, |i, o| {
let r = n_groups * ferrox_quant::Q8_0X4_NROWS + i;
let row =
&data_slice[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q8_0_q8(row, &act);
});
}
}
return out;
}
if serial {
for (r, o) in out.iter_mut().enumerate() {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q8_0_q8(row, &act);
}
} else {
crate::par::items_mut(
&mut out,
Self::min_rows_per_task(*rows),
|r, o| {
let row =
&data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q8_0_q8(row, &act);
},
);
}
return out;
}
QuantKind::Q4_0 if x.len().is_multiple_of(32) => {
let act = ferrox_quant::quantize_activations_q8(x);
let n_groups = *rows / ferrox_quant::Q4_0X4_NROWS;
let serial = Self::prefer_serial_matvec(*rows, *cols);
let interleave = ferrox_quant::q4_0x4_interleave();
if n_groups > 0 {
let packed = get_or_repack_q4_0x4(data, *rows, *cols);
if serial {
for (g, chunk) in out[..n_groups * ferrox_quant::Q4_0X4_NROWS]
.chunks_mut(ferrox_quant::Q4_0X4_NROWS)
.enumerate()
{
ferrox_quant::gemv_q4_0x4_group(
&packed, g, &act, *cols, interleave, chunk,
);
}
} else {
crate::par::chunks_mut(
&mut out[..n_groups * ferrox_quant::Q4_0X4_NROWS],
ferrox_quant::Q4_0X4_NROWS,
Self::min_rows_per_task(n_groups).max(1),
|g, chunk| {
ferrox_quant::gemv_q4_0x4_group(
&packed, g, &act, *cols, interleave, chunk,
);
},
);
}
let data_slice = data.as_slice();
let tail_len = *rows - n_groups * ferrox_quant::Q4_0X4_NROWS;
if tail_len > 0 {
let tail = &mut out[n_groups * ferrox_quant::Q4_0X4_NROWS..];
if serial || Self::prefer_serial_matvec(tail_len, *cols) {
for (i, o) in tail.iter_mut().enumerate() {
let r = n_groups * ferrox_quant::Q4_0X4_NROWS + i;
let row =
&data_slice[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q4_0_q8(row, &act);
}
} else {
let min_len = Self::min_rows_per_task(tail_len);
crate::par::items_mut(tail, min_len, |i, o| {
let r = n_groups * ferrox_quant::Q4_0X4_NROWS + i;
let row =
&data_slice[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q4_0_q8(row, &act);
});
}
}
return out;
}
if serial {
for (r, o) in out.iter_mut().enumerate() {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q4_0_q8(row, &act);
}
} else {
crate::par::items_mut(
&mut out,
Self::min_rows_per_task(*rows),
|r, o| {
let row =
&data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q4_0_q8(row, &act);
},
);
}
return out;
}
QuantKind::Q4K if x.len().is_multiple_of(256) => {
let act = ferrox_quant::quantize_activations_q8_k(x);
let n_groups = *rows / ferrox_quant::Q4_KX8_NROWS;
if n_groups > 0 {
let interleave = ferrox_quant::q4_kx8_interleave();
let packed = get_or_repack_q4k(data, *rows, *cols);
crate::par::chunks_mut(
&mut out[..n_groups * ferrox_quant::Q4_KX8_NROWS],
ferrox_quant::Q4_KX8_NROWS,
Self::min_rows_per_task(n_groups).max(1),
|g, chunk| {
ferrox_quant::gemv_q4_kx8_group(
&packed, g, &act, *cols, interleave, chunk,
);
},
);
let data_slice = data.as_slice();
crate::par::items_mut(
&mut out[n_groups * ferrox_quant::Q4_KX8_NROWS..],
Self::min_rows_per_task(
*rows - n_groups * ferrox_quant::Q4_KX8_NROWS,
),
|i, o| {
let r = n_groups * ferrox_quant::Q4_KX8_NROWS + i;
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q4_k_q8(row, &act);
},
);
return out;
}
crate::par::items_mut(
&mut out,
Self::min_rows_per_task(*rows),
|r, o| {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q4_k_q8(row, &act);
},
);
return out;
}
QuantKind::Q5K if x.len().is_multiple_of(256) => {
let act = ferrox_quant::quantize_activations_q8_k(x);
let n_groups = *rows / ferrox_quant::Q5_KX8_NROWS;
if n_groups > 0 {
let interleave = ferrox_quant::q5_kx8_interleave();
let packed = get_or_repack_q5k(data, *rows, *cols);
crate::par::chunks_mut(
&mut out[..n_groups * ferrox_quant::Q5_KX8_NROWS],
ferrox_quant::Q5_KX8_NROWS,
Self::min_rows_per_task(n_groups).max(1),
|g, chunk| {
ferrox_quant::gemv_q5_kx8_group(
&packed, g, &act, *cols, interleave, chunk,
);
},
);
let data_slice = data.as_slice();
crate::par::items_mut(
&mut out[n_groups * ferrox_quant::Q5_KX8_NROWS..],
Self::min_rows_per_task(
*rows - n_groups * ferrox_quant::Q5_KX8_NROWS,
),
|i, o| {
let r = n_groups * ferrox_quant::Q5_KX8_NROWS + i;
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q5_k_q8(row, &act);
},
);
return out;
}
crate::par::items_mut(
&mut out,
Self::min_rows_per_task(*rows),
|r, o| {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q5_k_q8(row, &act);
},
);
return out;
}
QuantKind::IQ4XS if x.len().is_multiple_of(256) => {
let act = ferrox_quant::quantize_activations_q8_k(x);
crate::par::items_mut(
&mut out,
Self::min_rows_per_task(*rows),
|r, o| {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_iq4_xs_q8_k(row, &act);
},
);
return out;
}
QuantKind::Q6K if x.len().is_multiple_of(256) => {
let act = ferrox_quant::quantize_activations_q8_k(x);
let n_groups = *rows / ferrox_quant::Q6_KX8_NROWS;
if n_groups > 0 {
let interleave = ferrox_quant::q6_kx8_interleave();
let packed = get_or_repack_q6k(data, *rows, *cols);
crate::par::chunks_mut(
&mut out[..n_groups * ferrox_quant::Q6_KX8_NROWS],
ferrox_quant::Q6_KX8_NROWS,
Self::min_rows_per_task(n_groups).max(1),
|g, out8| {
ferrox_quant::gemv_q6_kx8_group(
&packed, g, &act, *cols, interleave, out8,
);
},
);
crate::par::items_mut(
&mut out[n_groups * ferrox_quant::Q6_KX8_NROWS..],
Self::min_rows_per_task(
*rows - n_groups * ferrox_quant::Q6_KX8_NROWS,
),
|i, o| {
let r = n_groups * ferrox_quant::Q6_KX8_NROWS + i;
let row =
&data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q6_k_q8(row, &act);
},
);
return out;
}
crate::par::items_mut(
&mut out,
Self::min_rows_per_task(*rows),
|r, o| {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = ferrox_quant::dot_q6_k_q8(row, &act);
},
);
return out;
}
_ => {}
}
}
crate::par::items_mut(&mut out, Self::min_rows_per_task(*rows), |r, o| {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
*o = Self::dot(*kind, row, x);
});
out
}
WeightMatrix::Mxfp4 {
packed,
scale,
rows,
cols,
} => {
let packed_row_bytes = cols / 2;
let scale_row_bytes = cols / ferrox_quant::MXFP4_GROUP_SIZE;
let mut out = vec![0f32; *rows];
crate::par::items_mut(&mut out, Self::min_rows_per_task(*rows), |r, o| {
let prow = &packed.as_slice()[r * packed_row_bytes..(r + 1) * packed_row_bytes];
let srow = &scale.as_slice()[r * scale_row_bytes..(r + 1) * scale_row_bytes];
*o = ferrox_quant::dot_mxfp4_row_f32(prow, srow, x);
});
out
}
WeightMatrix::Adapted { base, lora } => {
let mut out = base.apply_cpu_inner(x);
lora.add_to(x, &mut out);
out
}
}
}
pub fn apply_cpu_q8(&self, act: &ferrox_quant::Q8Activations) -> Option<Vec<f32>> {
crate::par::with_op_work(self.rows(), self.cols(), || self.apply_cpu_q8_inner(act))
}
fn apply_cpu_q8_inner(&self, act: &ferrox_quant::Q8Activations) -> Option<Vec<f32>> {
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = self
else {
return None;
};
if !matches!(*kind, QuantKind::Q8_0 | QuantKind::Q4_0)
|| !cpu_int_dot_for(IntDotShape::Matvec)
{
return None;
}
if act.q.len() != *cols || !cols.is_multiple_of(32) {
return None;
}
let row_bytes = self.block_bytes_per_row(*kind, *cols);
let mut out = vec![0f32; *rows];
let kind = *kind;
let bytes = data.as_slice();
if matches!(kind, QuantKind::Q8_0) {
let n_groups = *rows / ferrox_quant::Q8_0X4_NROWS;
if n_groups > 0 {
let packed = get_or_repack_q8x4(data, *rows, *cols);
let serial = Self::prefer_serial_matvec(*rows, *cols);
let interleave = ferrox_quant::q8_0x4_interleave();
let body = |g: usize, chunk: &mut [f32]| {
ferrox_quant::gemv_q8_0x4_group(&packed, g, act, *cols, interleave, chunk);
};
if serial {
for (g, chunk) in out[..n_groups * ferrox_quant::Q8_0X4_NROWS]
.chunks_mut(ferrox_quant::Q8_0X4_NROWS)
.enumerate()
{
body(g, chunk);
}
} else {
crate::par::chunks_mut(
&mut out[..n_groups * ferrox_quant::Q8_0X4_NROWS],
ferrox_quant::Q8_0X4_NROWS,
Self::min_rows_per_task(n_groups).max(1),
|g, chunk| body(g, chunk),
);
}
let tail_len = *rows - n_groups * ferrox_quant::Q8_0X4_NROWS;
if tail_len > 0 {
let tail = &mut out[n_groups * ferrox_quant::Q8_0X4_NROWS..];
if serial || Self::prefer_serial_matvec(tail_len, *cols) {
for (i, o) in tail.iter_mut().enumerate() {
let r = n_groups * ferrox_quant::Q8_0X4_NROWS + i;
*o = ferrox_quant::dot_q8_0_q8(
&bytes[r * row_bytes..(r + 1) * row_bytes],
act,
);
}
} else {
let min_len = Self::min_rows_per_task(tail_len);
crate::par::items_mut(tail, min_len, |i, o| {
let r = n_groups * ferrox_quant::Q8_0X4_NROWS + i;
*o = ferrox_quant::dot_q8_0_q8(
&bytes[r * row_bytes..(r + 1) * row_bytes],
act,
);
});
}
}
return Some(out);
}
}
if matches!(kind, QuantKind::Q4_0) {
let n_groups = *rows / ferrox_quant::Q4_0X4_NROWS;
if n_groups > 0 {
let packed = get_or_repack_q4_0x4(data, *rows, *cols);
let serial = Self::prefer_serial_matvec(*rows, *cols);
let interleave = ferrox_quant::q4_0x4_interleave();
let body = |g: usize, chunk: &mut [f32]| {
ferrox_quant::gemv_q4_0x4_group(&packed, g, act, *cols, interleave, chunk);
};
if serial {
for (g, chunk) in out[..n_groups * ferrox_quant::Q4_0X4_NROWS]
.chunks_mut(ferrox_quant::Q4_0X4_NROWS)
.enumerate()
{
body(g, chunk);
}
} else {
crate::par::chunks_mut(
&mut out[..n_groups * ferrox_quant::Q4_0X4_NROWS],
ferrox_quant::Q4_0X4_NROWS,
Self::min_rows_per_task(n_groups).max(1),
|g, chunk| body(g, chunk),
);
}
let tail_len = *rows - n_groups * ferrox_quant::Q4_0X4_NROWS;
if tail_len > 0 {
let tail = &mut out[n_groups * ferrox_quant::Q4_0X4_NROWS..];
if serial || Self::prefer_serial_matvec(tail_len, *cols) {
for (i, o) in tail.iter_mut().enumerate() {
let r = n_groups * ferrox_quant::Q4_0X4_NROWS + i;
*o = ferrox_quant::dot_q4_0_q8(
&bytes[r * row_bytes..(r + 1) * row_bytes],
act,
);
}
} else {
let min_len = Self::min_rows_per_task(tail_len);
crate::par::items_mut(tail, min_len, |i, o| {
let r = n_groups * ferrox_quant::Q4_0X4_NROWS + i;
*o = ferrox_quant::dot_q4_0_q8(
&bytes[r * row_bytes..(r + 1) * row_bytes],
act,
);
});
}
}
return Some(out);
}
}
if Self::prefer_serial_matvec(*rows, *cols) {
for (r, o) in out.iter_mut().enumerate() {
let row = &bytes[r * row_bytes..(r + 1) * row_bytes];
*o = match kind {
QuantKind::Q8_0 => ferrox_quant::dot_q8_0_q8(row, act),
QuantKind::Q4_0 => ferrox_quant::dot_q4_0_q8(row, act),
_ => unreachable!(),
};
}
return Some(out);
}
crate::par::items_mut(&mut out, Self::min_rows_per_task(*rows), |r, o| {
let row = &bytes[r * row_bytes..(r + 1) * row_bytes];
*o = match kind {
QuantKind::Q8_0 => ferrox_quant::dot_q8_0_q8(row, act),
QuantKind::Q4_0 => ferrox_quant::dot_q4_0_q8(row, act),
_ => unreachable!(),
};
});
Some(out)
}
pub fn dot_pair_cpu_q8(
&self,
row: usize,
act: &ferrox_quant::Q8Activations,
) -> Option<(f32, f32)> {
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = self
else {
return None;
};
if !matches!(*kind, QuantKind::Q8_0 | QuantKind::Q4_0)
|| !cpu_int_dot_for(IntDotShape::Matvec)
{
return None;
}
if act.q.len() != *cols || !cols.is_multiple_of(32) || row + 1 >= *rows {
return None;
}
let row_bytes = self.block_bytes_per_row(*kind, *cols);
let bytes = data.as_slice();
let r0 = &bytes[row * row_bytes..(row + 1) * row_bytes];
let r1 = &bytes[(row + 1) * row_bytes..(row + 2) * row_bytes];
Some(match *kind {
QuantKind::Q4_0 => ferrox_quant::dot_q4_0_q8_2row(r0, r1, act),
QuantKind::Q8_0 => (
ferrox_quant::dot_q8_0_q8(r0, act),
ferrox_quant::dot_q8_0_q8(r1, act),
),
_ => unreachable!(),
})
}
pub fn dot_row_cpu_q8(&self, row: usize, act: &ferrox_quant::Q8Activations) -> Option<f32> {
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = self
else {
return None;
};
if row >= *rows
|| !matches!(*kind, QuantKind::Q8_0 | QuantKind::Q4_0)
|| !cpu_int_dot_for(IntDotShape::Matvec)
|| act.q.len() != *cols
|| !cols.is_multiple_of(32)
{
return None;
}
let row_bytes = self.block_bytes_per_row(*kind, *cols);
let bytes = &data.as_slice()[row * row_bytes..(row + 1) * row_bytes];
Some(match *kind {
QuantKind::Q8_0 => ferrox_quant::dot_q8_0_q8(bytes, act),
QuantKind::Q4_0 => ferrox_quant::dot_q4_0_q8(bytes, act),
_ => unreachable!(),
})
}
pub fn apply_batch(&self, x_batch: &[f32], batch_size: usize) -> Vec<f32> {
self.apply_batch_with_acts(x_batch, batch_size, None)
}
pub fn quantize_batch_acts(&self, x_batch: &[f32], batch_size: usize) -> Option<BatchActs> {
if let WeightMatrix::Adapted { base, .. } = self {
return base.quantize_batch_acts(x_batch, batch_size);
}
#[cfg(feature = "metal")]
{
if metal_dense_enabled()
&& matches!(
self,
WeightMatrix::Quantized { kind, .. } if Self::metal_kind_supported(*kind)
)
{
return None;
}
}
#[cfg(feature = "cuda")]
{
if cuda_dense_enabled() && matches!(self, WeightMatrix::Quantized { .. }) {
return None;
}
}
let WeightMatrix::Quantized { cols, kind, .. } = self else {
return None;
};
if !cpu_int_dot_for(IntDotShape::BatchGemm) || x_batch.len() != batch_size * cols {
return None;
}
let cols = *cols;
match kind {
QuantKind::Q8_0 | QuantKind::Q4_0 if cols.is_multiple_of(32) => {
let acts: Vec<_> = (0..batch_size)
.into_par_iter()
.map(|b| {
ferrox_quant::quantize_activations_q8(&x_batch[b * cols..(b + 1) * cols])
})
.collect();
let tiles =
if ferrox_quant::q8_0x4_gemm_uses_acts_x4(ferrox_quant::q8_0x4_interleave()) {
acts.par_chunks(ferrox_quant::Q8K_ACTS_X4_NC)
.map(|chunk| ferrox_quant::prepare_q8_acts_x4(chunk, cols))
.collect()
} else {
Vec::new()
};
Some(BatchActs::Q8 { acts, tiles, cols })
}
QuantKind::Q4K | QuantKind::Q5K | QuantKind::Q6K if cols.is_multiple_of(256) => {
let acts: Vec<_> = (0..batch_size)
.into_par_iter()
.map(|b| {
ferrox_quant::quantize_activations_q8_k(&x_batch[b * cols..(b + 1) * cols])
})
.collect();
let tiles =
if ferrox_quant::q4_kx8_gemm_uses_acts_x4(ferrox_quant::q4_kx8_interleave()) {
acts.par_chunks(ferrox_quant::Q8K_ACTS_X4_NC)
.map(|chunk| ferrox_quant::prepare_q8_k_acts_x4(chunk, cols))
.collect()
} else {
Vec::new()
};
Some(BatchActs::Q8K { acts, tiles, cols })
}
_ => None,
}
}
pub fn apply_batch_with_acts(
&self,
x_batch: &[f32],
batch_size: usize,
shared: Option<&BatchActs>,
) -> Vec<f32> {
let cols = self.cols();
assert_eq!(
x_batch.len(),
batch_size * cols,
"x_batch length must be batch_size * cols"
);
if batch_size == 0 {
return Vec::new();
}
crate::activation_tap::observe(self, x_batch, batch_size);
if let WeightMatrix::Adapted { base, lora } = self {
let mut out = base.apply_batch_with_acts(x_batch, batch_size, shared);
lora.add_batch_to(x_batch, batch_size, &mut out);
return out;
}
#[derive(Clone, Copy)]
struct BatchOut(*mut f32);
unsafe impl Send for BatchOut {}
unsafe impl Sync for BatchOut {}
impl BatchOut {
#[inline]
unsafe fn set(self, idx: usize, v: f32) {
*self.0.add(idx) = v;
}
}
#[cfg(feature = "metal")]
{
if metal_dense_enabled()
&& matches!(
self,
WeightMatrix::Quantized { kind, .. } if Self::metal_kind_supported(*kind)
)
{
if let Some(out) = self.apply_gpu_batch(x_batch, batch_size) {
return out;
}
crate::kernel_registry::miss(
crate::kernel_registry::Lookup::new(
crate::kernel_registry::Backend::Metal,
crate::kernel_registry::op::GEMM_PREFILL,
self.quant_kind(),
),
"N x apply (one command buffer each)",
);
let rows = self.rows();
let mut out = vec![0f32; batch_size * rows];
for b in 0..batch_size {
let y = self.apply(&x_batch[b * cols..(b + 1) * cols]);
out[b * rows..(b + 1) * rows].copy_from_slice(&y);
}
return out;
} else if metal_dense_enabled() {
let look = crate::kernel_registry::Lookup::new(
crate::kernel_registry::Backend::Metal,
crate::kernel_registry::op::GEMM_PREFILL,
self.quant_kind(),
);
if self.quant_kind().is_some() {
crate::kernel_registry::miss(look, "CPU apply_batch");
} else {
crate::kernel_registry::miss_by_design(look, "CPU f32 GEMM");
}
}
}
#[cfg(feature = "cuda")]
{
if cuda_dense_enabled() {
if let WeightMatrix::Quantized { data, kind, .. } = self {
if cuda_mul_mm_kind_supported(*kind)
&& ferrox_cuda::mul_mm::worth_a_gemm(batch_size)
{
let mm_kind = ferrox_cuda::mul_mm::kind_by_name(kind.name())
.expect("cuda_mul_mm_kind_supported agreed");
let row_bytes = self.block_bytes_per_row(*kind, cols);
match ferrox_cuda::mul_mm_launch::launch_mul_mm(
mm_kind,
data.as_slice(),
x_batch,
self.rows(),
cols,
batch_size,
row_bytes,
) {
Ok(out) => return out,
Err(_) => {
crate::kernel_registry::miss(
crate::kernel_registry::Lookup::new(
crate::kernel_registry::Backend::Cuda,
crate::kernel_registry::op::GEMM_PREFILL,
self.quant_kind(),
),
"N x matvec (the GEMM launch failed)",
);
}
}
}
}
}
if cuda_dense_enabled()
&& matches!(self, WeightMatrix::Quantized { .. })
&& self.apply_gpu(&x_batch[..cols]).is_some()
{
let rows = self.rows();
let mut out = vec![0f32; batch_size * rows];
for b in 0..batch_size {
match self.apply_gpu(&x_batch[b * cols..(b + 1) * cols]) {
Some(y) => out[b * rows..(b + 1) * rows].copy_from_slice(&y),
None => {
let y = self.apply(&x_batch[b * cols..(b + 1) * cols]);
out[b * rows..(b + 1) * rows].copy_from_slice(&y);
}
}
}
return out;
}
}
match self {
WeightMatrix::F32(t) => {
let xt = Tensor::new(x_batch.to_vec(), vec![batch_size, cols]);
crate::matmul::matmul_f32(&xt, t).data
}
WeightMatrix::Quantized {
data,
rows,
cols: _,
kind,
} => {
let row_bytes = self.block_bytes_per_row(*kind, cols);
let mut out = vec![0f32; batch_size * rows];
let out_w = BatchOut(out.as_mut_ptr());
if cpu_int_dot_for(IntDotShape::BatchGemm) {
match *kind {
QuantKind::Q8_0 if cols.is_multiple_of(32) => {
let mut acts_owned = Vec::new();
let (acts, shared_tiles) =
Self::q8_acts(shared, x_batch, batch_size, cols, &mut acts_owned);
let n_groups = *rows / ferrox_quant::Q8_0X4_NROWS;
if n_groups > 0 {
let packed = get_or_repack_q8x4(data, *rows, cols);
let nrows_g = ferrox_quant::Q8_0X4_NROWS;
let interleave = ferrox_quant::q8_0x4_interleave();
if ferrox_quant::q8_0x4_gemm_uses_acts_x4(interleave) {
let nc = ferrox_quant::Q8K_ACTS_X4_NC;
let tiles_owned: Vec<ferrox_quant::Q8ActsX4>;
let act_tiles: &[ferrox_quant::Q8ActsX4] =
if shared_tiles.is_empty() {
tiles_owned = acts
.par_chunks(nc)
.map(|chunk| {
ferrox_quant::prepare_q8_acts_x4(chunk, cols)
})
.collect();
&tiles_owned
} else {
shared_tiles
};
let accel = ferrox_quant::AccelX4::detect();
Self::par_chunked_groups(
n_groups,
nrows_g,
act_tiles.len(),
nc,
|g, t0, t1| {
let mut tmp = [0f32;
ferrox_quant::Q8_0X4_NROWS
* ferrox_quant::Q8K_ACTS_X4_NC];
for (t, tile) in act_tiles[t0..t1].iter().enumerate() {
let t = t0 + t;
let n = tile.na;
let tmp = &mut tmp[..nrows_g * n];
ferrox_quant::gemm_q8_0x4_group_x4_on(
&packed, g, tile, cols, interleave, accel, tmp,
);
for j in 0..n {
let col = (t * nc + j) * rows + g * nrows_g;
for r in 0..nrows_g {
unsafe {
out_w.set(col + r, tmp[r * n + j]);
}
}
}
}
},
);
} else {
let span = ferrox_quant::Q8_0X4_GEMM_NC;
let n_tiles = batch_size.div_ceil(span);
Self::par_chunked_groups(
n_groups,
nrows_g,
n_tiles,
span,
|g, t0, t1| {
let b0 = t0 * span;
let b1 = (t1 * span).min(batch_size);
let n = b1 - b0;
let mut group = vec![0f32; nrows_g * n];
ferrox_quant::gemm_q8_0x4_group(
&packed,
g,
&acts[b0..b1],
cols,
interleave,
&mut group,
);
for (bi, b) in (b0..b1).enumerate() {
for r in 0..nrows_g {
unsafe {
out_w.set(
b * rows + g * nrows_g + r,
group[r * n + bi],
);
}
}
}
},
);
}
let data_slice = data.as_slice();
let tail = *rows - n_groups * ferrox_quant::Q8_0X4_NROWS;
crate::par::indices(tail, Self::min_rows_per_task(tail), |i| {
let r = n_groups * ferrox_quant::Q8_0X4_NROWS + i;
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
for (b, act) in acts.iter().enumerate() {
unsafe {
out_w.set(
b * rows + r,
ferrox_quant::dot_q8_0_q8(row, act),
);
}
}
});
} else {
crate::par::indices(*rows, Self::min_rows_per_task(*rows), |r| {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
for (b, act) in acts.iter().enumerate() {
unsafe {
out_w.set(
b * rows + r,
ferrox_quant::dot_q8_0_q8(row, act),
);
}
}
});
}
return out;
}
QuantKind::Q4_0 if cols.is_multiple_of(32) => {
let mut acts_owned = Vec::new();
let (acts, shared_tiles) =
Self::q8_acts(shared, x_batch, batch_size, cols, &mut acts_owned);
let n_groups = *rows / ferrox_quant::Q4_0X4_NROWS;
if n_groups > 0 {
let packed = get_or_repack_q4_0x4(data, *rows, cols);
let nrows_g = ferrox_quant::Q4_0X4_NROWS;
let interleave = ferrox_quant::q4_0x4_interleave();
if ferrox_quant::q4_0x4_gemm_uses_acts_x4(interleave) {
let nc = ferrox_quant::Q8K_ACTS_X4_NC;
let tiles_owned: Vec<ferrox_quant::Q8ActsX4>;
let act_tiles: &[ferrox_quant::Q8ActsX4] =
if shared_tiles.is_empty() {
tiles_owned = acts
.par_chunks(nc)
.map(|chunk| {
ferrox_quant::prepare_q8_acts_x4(chunk, cols)
})
.collect();
&tiles_owned
} else {
shared_tiles
};
let accel = ferrox_quant::AccelX4::detect();
Self::par_chunked_groups(
n_groups,
nrows_g,
act_tiles.len(),
nc,
|g, t0, t1| {
let mut tmp = [0f32;
ferrox_quant::Q4_0X4_NROWS
* ferrox_quant::Q8K_ACTS_X4_NC];
for (t, tile) in act_tiles[t0..t1].iter().enumerate() {
let t = t0 + t;
let n = tile.na;
let tmp = &mut tmp[..nrows_g * n];
ferrox_quant::gemm_q4_0x4_group_x4_on(
&packed, g, tile, cols, interleave, accel, tmp,
);
for j in 0..n {
let col = (t * nc + j) * rows + g * nrows_g;
for r in 0..nrows_g {
unsafe {
out_w.set(col + r, tmp[r * n + j]);
}
}
}
}
},
);
} else {
let span = ferrox_quant::Q8_0X4_GEMM_NC;
let n_tiles = batch_size.div_ceil(span);
Self::par_chunked_groups(
n_groups,
nrows_g,
n_tiles,
span,
|g, t0, t1| {
let b0 = t0 * span;
let b1 = (t1 * span).min(batch_size);
let n = b1 - b0;
let mut group = vec![0f32; nrows_g * n];
ferrox_quant::gemm_q4_0x4_group(
&packed,
g,
&acts[b0..b1],
cols,
interleave,
&mut group,
);
for (bi, b) in (b0..b1).enumerate() {
for r in 0..nrows_g {
unsafe {
out_w.set(
b * rows + g * nrows_g + r,
group[r * n + bi],
);
}
}
}
},
);
}
let data_slice = data.as_slice();
let tail = *rows - n_groups * ferrox_quant::Q4_0X4_NROWS;
crate::par::indices(tail, Self::min_rows_per_task(tail), |i| {
let r = n_groups * ferrox_quant::Q4_0X4_NROWS + i;
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
for (b, act) in acts.iter().enumerate() {
unsafe {
out_w.set(
b * rows + r,
ferrox_quant::dot_q4_0_q8(row, act),
);
}
}
});
} else {
crate::par::indices(*rows, Self::min_rows_per_task(*rows), |r| {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
for (b, act) in acts.iter().enumerate() {
unsafe {
out_w.set(
b * rows + r,
ferrox_quant::dot_q4_0_q8(row, act),
);
}
}
});
}
return out;
}
QuantKind::Q4K if cols.is_multiple_of(256) => {
let mut acts_owned = Vec::new();
let (acts, shared_tiles) =
Self::q8k_acts(shared, x_batch, batch_size, cols, &mut acts_owned);
let n_groups = *rows / ferrox_quant::Q4_KX8_NROWS;
if n_groups > 0 {
let interleave = ferrox_quant::q4_kx8_interleave();
let packed = get_or_repack_q4k(data, *rows, cols);
let nc = ferrox_quant::Q4_KX8_GEMM_NC;
let tiles_owned: Vec<ferrox_quant::Q8KActsX4>;
let act_tiles: &[ferrox_quant::Q8KActsX4] =
if !ferrox_quant::q4_kx8_gemm_uses_acts_x4(interleave) {
&[]
} else if !shared_tiles.is_empty() {
shared_tiles
} else {
tiles_owned = acts
.par_chunks(nc)
.map(|chunk| {
ferrox_quant::prepare_q8_k_acts_x4(chunk, cols)
})
.collect();
&tiles_owned
};
let accel = ferrox_quant::AccelX4::detect();
let n_tiles = batch_size.div_ceil(nc);
Self::par_chunked_groups(
n_groups,
ferrox_quant::Q4_KX8_NROWS,
n_tiles,
nc,
|g, t0, t1| {
let mut tile = [0f32;
ferrox_quant::Q4_KX8_NROWS
* ferrox_quant::Q4_KX8_GEMM_NC];
for t in t0..t1 {
let chunk =
&acts[t * nc..((t + 1) * nc).min(batch_size)];
let n = chunk.len();
let tile = &mut tile[..ferrox_quant::Q4_KX8_NROWS * n];
if act_tiles.is_empty() {
ferrox_quant::gemm_q4_kx8_group(
&packed, g, chunk, cols, interleave, tile,
);
} else {
ferrox_quant::gemm_q4_kx8_group_x4_on(
&packed,
g,
&act_tiles[t],
cols,
interleave,
accel,
tile,
);
}
for j in 0..n {
let col = (t * nc + j) * rows
+ g * ferrox_quant::Q4_KX8_NROWS;
for r in 0..ferrox_quant::Q4_KX8_NROWS {
unsafe {
out_w.set(col + r, tile[r * n + j]);
}
}
}
}
},
);
let data_slice = data.as_slice();
let tail = *rows - n_groups * ferrox_quant::Q4_KX8_NROWS;
crate::par::indices(tail, Self::min_rows_per_task(tail), |i| {
let r = n_groups * ferrox_quant::Q4_KX8_NROWS + i;
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
for (b, act) in acts.iter().enumerate() {
unsafe {
out_w.set(
b * rows + r,
ferrox_quant::dot_q4_k_q8(row, act),
);
}
}
});
} else {
crate::par::indices(*rows, Self::min_rows_per_task(*rows), |r| {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
for (b, act) in acts.iter().enumerate() {
unsafe {
out_w.set(
b * rows + r,
ferrox_quant::dot_q4_k_q8(row, act),
);
}
}
});
}
return out;
}
QuantKind::Q5K if cols.is_multiple_of(256) => {
let mut acts_owned = Vec::new();
let (acts, shared_tiles) =
Self::q8k_acts(shared, x_batch, batch_size, cols, &mut acts_owned);
let interleave = ferrox_quant::q5_kx8_interleave();
let use_kx8 = q5k_batch_takes_kx8(interleave);
let n_groups = if use_kx8 {
*rows / ferrox_quant::Q5_KX8_NROWS
} else {
0
};
if n_groups > 0 {
let packed = get_or_repack_q5k(data, *rows, cols);
let nc = ferrox_quant::Q5_KX8_GEMM_NC;
let tiles_owned: Vec<ferrox_quant::Q8KActsX4>;
let act_tiles: &[ferrox_quant::Q8KActsX4] =
if !ferrox_quant::q5_kx8_gemm_uses_acts_x4(interleave) {
&[]
} else if !shared_tiles.is_empty() {
shared_tiles
} else {
tiles_owned = acts
.par_chunks(nc)
.map(|chunk| {
ferrox_quant::prepare_q8_k_acts_x4(chunk, cols)
})
.collect();
&tiles_owned
};
let accel = ferrox_quant::AccelX4::detect();
let n_tiles = batch_size.div_ceil(nc);
Self::par_chunked_groups(
n_groups,
ferrox_quant::Q5_KX8_NROWS,
n_tiles,
nc,
|g, t0, t1| {
let mut tile = [0f32;
ferrox_quant::Q5_KX8_NROWS
* ferrox_quant::Q5_KX8_GEMM_NC];
for t in t0..t1 {
let chunk =
&acts[t * nc..((t + 1) * nc).min(batch_size)];
let n = chunk.len();
let tile = &mut tile[..ferrox_quant::Q5_KX8_NROWS * n];
if act_tiles.is_empty() {
ferrox_quant::gemm_q5_kx8_group(
&packed, g, chunk, cols, interleave, tile,
);
} else {
ferrox_quant::gemm_q5_kx8_group_x4_on(
&packed,
g,
&act_tiles[t],
cols,
interleave,
accel,
tile,
);
}
for j in 0..n {
let col = (t * nc + j) * rows
+ g * ferrox_quant::Q5_KX8_NROWS;
for r in 0..ferrox_quant::Q5_KX8_NROWS {
unsafe {
out_w.set(col + r, tile[r * n + j]);
}
}
}
}
},
);
let data_slice = data.as_slice();
let tail = *rows - n_groups * ferrox_quant::Q5_KX8_NROWS;
crate::par::indices(tail, Self::min_rows_per_task(tail), |i| {
let r = n_groups * ferrox_quant::Q5_KX8_NROWS + i;
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
for (b, act) in acts.iter().enumerate() {
unsafe {
out_w.set(
b * rows + r,
ferrox_quant::dot_q5_k_q8(row, act),
);
}
}
});
} else {
let data_slice = data.as_slice();
crate::par::indices(*rows, Self::min_rows_per_task(*rows), |r| {
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
let nc = ferrox_quant::Q5_K_GEMM_NC;
for (t, chunk) in acts.chunks(nc).enumerate() {
let n = chunk.len();
let mut tmp = [0f32; ferrox_quant::Q5_K_GEMM_NC];
ferrox_quant::gemm_q5_k_q8_row(row, chunk, &mut tmp[..n]);
for (j, v) in tmp[..n].iter().enumerate() {
unsafe {
out_w.set((t * nc + j) * rows + r, *v);
}
}
}
});
}
return out;
}
QuantKind::Q6K if cols.is_multiple_of(256) => {
let mut acts_owned = Vec::new();
let (acts, shared_tiles) =
Self::q8k_acts(shared, x_batch, batch_size, cols, &mut acts_owned);
let interleave = ferrox_quant::q6_kx8_interleave();
let use_kx8 = ferrox_quant::q6_kx8_gemm_uses_acts_x4(interleave);
let n_groups = if use_kx8 {
*rows / ferrox_quant::Q6_KX8_NROWS
} else {
0
};
if n_groups > 0 {
let packed = get_or_repack_q6k(data, *rows, cols);
let nc = ferrox_quant::Q8K_ACTS_X4_NC;
let tiles_owned: Vec<ferrox_quant::Q8KActsX4>;
let act_tiles: &[ferrox_quant::Q8KActsX4] =
if shared_tiles.is_empty() {
tiles_owned = acts
.par_chunks(nc)
.map(|chunk| {
ferrox_quant::prepare_q8_k_acts_x4(chunk, cols)
})
.collect();
&tiles_owned
} else {
shared_tiles
};
let accel = ferrox_quant::AccelX4::detect();
let n_tiles = batch_size.div_ceil(nc);
Self::par_chunked_groups(
n_groups,
ferrox_quant::Q6_KX8_NROWS,
n_tiles,
nc,
|g, t0, t1| {
let mut tile = [0f32;
ferrox_quant::Q6_KX8_NROWS
* ferrox_quant::Q8K_ACTS_X4_NC];
for t in t0..t1 {
let chunk =
&acts[t * nc..((t + 1) * nc).min(batch_size)];
let n = chunk.len();
let tile = &mut tile[..ferrox_quant::Q6_KX8_NROWS * n];
ferrox_quant::gemm_q6_kx8_group_x4_on(
&packed,
g,
&act_tiles[t],
cols,
interleave,
accel,
tile,
);
for j in 0..n {
let col = (t * nc + j) * rows
+ g * ferrox_quant::Q6_KX8_NROWS;
for r in 0..ferrox_quant::Q6_KX8_NROWS {
unsafe {
out_w.set(col + r, tile[r * n + j]);
}
}
}
}
},
);
let data_slice = data.as_slice();
let tail = *rows - n_groups * ferrox_quant::Q6_KX8_NROWS;
crate::par::indices(tail, Self::min_rows_per_task(tail), |i| {
let r = n_groups * ferrox_quant::Q6_KX8_NROWS + i;
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
for (b, act) in acts.iter().enumerate() {
unsafe {
out_w.set(
b * rows + r,
ferrox_quant::dot_q6_k_q8(row, act),
);
}
}
});
} else {
let data_slice = data.as_slice();
crate::par::indices(*rows, Self::min_rows_per_task(*rows), |r| {
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
let nc = ferrox_quant::Q6_K_GEMM_NC;
for (t, chunk) in acts.chunks(nc).enumerate() {
let mut tmp = [0f32; ferrox_quant::Q6_K_GEMM_NC];
let n = chunk.len();
ferrox_quant::gemm_q6_k_q8_row(row, chunk, &mut tmp[..n]);
for (j, v) in tmp[..n].iter().enumerate() {
unsafe {
out_w.set((t * nc + j) * rows + r, *v);
}
}
}
});
}
return out;
}
QuantKind::IQ4XS if cols.is_multiple_of(256) => {
let mut acts_owned = Vec::new();
let (acts, _) =
Self::q8k_acts(shared, x_batch, batch_size, cols, &mut acts_owned);
let data_slice = data.as_slice();
crate::par::indices(*rows, Self::min_rows_per_task(*rows), |r| {
let row = &data_slice[r * row_bytes..(r + 1) * row_bytes];
for (b, act) in acts.iter().enumerate() {
unsafe {
out_w.set(
b * rows + r,
ferrox_quant::dot_iq4_xs_q8_k(row, act),
);
}
}
});
return out;
}
QuantKind::Q5K | QuantKind::Q6K => {}
_ => {}
}
}
crate::par::indices(*rows, Self::min_rows_per_task(*rows), |r| {
let row = &data.as_slice()[r * row_bytes..(r + 1) * row_bytes];
for b in 0..batch_size {
let x = &x_batch[b * cols..(b + 1) * cols];
unsafe {
out_w.set(b * rows + r, Self::dot(*kind, row, x));
}
}
});
out
}
WeightMatrix::Mxfp4 {
packed,
scale,
rows,
cols: _,
} => {
let packed_row_bytes = cols / 2;
let scale_row_bytes = cols / ferrox_quant::MXFP4_GROUP_SIZE;
let mut out = vec![0f32; batch_size * rows];
let out_w = BatchOut(out.as_mut_ptr());
crate::par::indices(*rows, Self::min_rows_per_task(*rows), |r| {
let prow = &packed.as_slice()[r * packed_row_bytes..(r + 1) * packed_row_bytes];
let srow = &scale.as_slice()[r * scale_row_bytes..(r + 1) * scale_row_bytes];
for b in 0..batch_size {
let x = &x_batch[b * cols..(b + 1) * cols];
unsafe {
out_w.set(b * rows + r, ferrox_quant::dot_mxfp4_row_f32(prow, srow, x));
}
}
});
out
}
WeightMatrix::Adapted { .. } => unreachable!("handled before dispatch"),
}
}
pub fn resident_bytes(&self) -> usize {
match self {
WeightMatrix::F32(t) => t.len() * 4,
WeightMatrix::Quantized { data, .. } => data.len(),
WeightMatrix::Mxfp4 { packed, scale, .. } => packed.len() + scale.len(),
WeightMatrix::Adapted { base, lora } => base.resident_bytes() + lora.resident_bytes(),
}
}
#[cfg(any(feature = "cuda", feature = "metal", feature = "vulkan"))]
pub fn apply_gpu(&self, x: &[f32]) -> Option<Vec<f32>> {
assert_eq!(
x.len(),
self.cols(),
"activation length must match matrix column count"
);
if let WeightMatrix::Adapted { base, lora } = self {
let mut out = base.apply_gpu(x)?;
lora.add_to(x, &mut out);
return Some(out);
}
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = self
else {
let backend = active_backend();
if backend.is_accelerator() {
crate::kernel_registry::miss_by_design(
crate::kernel_registry::Lookup::new(
backend,
crate::kernel_registry::op::MATVEC,
None,
),
"host GEMV",
);
}
return None;
};
let row_bytes = self.block_bytes_per_row(*kind, *cols);
#[allow(unused_macros)]
macro_rules! try_matvec {
($b:ty) => {
if let Some(result) = <$b as BackendDispatch>::launch_matvec(
*kind,
data.as_slice(),
x,
*rows,
row_bytes,
) {
match result {
Ok(out) => return Some(out),
Err(e) => {
eprintln!(
"ferrox: {} matvec dispatch failed, {}: {e}",
<$b as BackendCaps>::NAME,
<$b as BackendDispatch>::MATVEC_FALLBACK
);
}
}
}
};
}
with_gpu_backends!(try_matvec);
let backend = active_backend();
if backend.is_accelerator() {
crate::kernel_registry::miss(
crate::kernel_registry::Lookup::new(
backend,
crate::kernel_registry::op::MATVEC,
Some(*kind),
),
"CPU apply_cpu",
);
}
None
}
#[cfg(any(feature = "cuda", feature = "metal"))]
pub fn apply_gpu_multi(mats: &[&WeightMatrix], x: &[f32]) -> Option<Vec<Vec<f32>>> {
if mats.is_empty() {
return None;
}
assert_eq!(
x.len(),
mats[0].cols(),
"activation length must match matrix column count"
);
if mats.iter().any(|m| m.lora().is_some()) {
let bases: Vec<&WeightMatrix> = mats.iter().map(|m| m.base()).collect();
let mut outs = Self::apply_gpu_multi(&bases, x)?;
for (m, out) in mats.iter().zip(outs.iter_mut()) {
if let Some(lora) = m.lora() {
lora.add_to(x, out);
}
}
return Some(outs);
}
#[cfg(feature = "cuda")]
if cuda_dense_enabled() {
let mut launches = Vec::with_capacity(mats.len());
for m in mats {
assert_eq!(m.cols(), mats[0].cols());
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = m
else {
return None;
};
let (kernel_src, module_name, fn_name) =
ferrox_cuda::gpu::matvec_launch_meta(kind.name())?;
let row_bytes = m.block_bytes_per_row(*kind, *cols);
let n_blocks_per_row = row_bytes / Self::block_bytes_for_kind(*kind);
launches.push(ferrox_cuda::gpu::MatvecLaunch {
kernel_src,
module_name,
fn_name,
weights: data.as_slice(),
rows: *rows,
row_bytes,
n_blocks_per_row,
});
}
match ferrox_cuda::gpu::launch_matvec_multi(x, &launches) {
Ok(outs) => return Some(outs),
Err(e) => {
eprintln!("ferrox: CUDA multi-matvec failed, trying next backend: {e}");
}
}
}
#[cfg(feature = "metal")]
if metal_dense_enabled() {
let mut launches = Vec::with_capacity(mats.len());
let mut held: Vec<(&[u8], usize, usize, &'static str)> = Vec::with_capacity(mats.len());
for m in mats {
assert_eq!(m.cols(), mats[0].cols());
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = m
else {
return None;
};
let kind_name = match kind {
QuantKind::Q8_0 => "Q8_0",
QuantKind::Q4_0 => "Q4_0",
QuantKind::Q4K => "Q4_K",
QuantKind::Q5K => "Q5_K",
QuantKind::Q6K => "Q6_K",
QuantKind::IQ4XS => "IQ4_XS",
_ => return None,
};
let row_bytes = m.block_bytes_per_row(*kind, *cols);
held.push((data.as_slice(), *rows, row_bytes, kind_name));
}
for (weights, rows, row_bytes, kind_name) in &held {
let (src, fn_name, block_bytes, block_elems, rows_per_tg) =
ferrox_metal::gpu::matvec_launch_meta(kind_name)?;
launches.push(ferrox_metal::gpu::MatvecLaunch {
kernel_src: src,
fn_name,
block_bytes,
block_elems,
weights,
rows: *rows,
row_bytes: *row_bytes,
rows_per_tg,
});
}
match ferrox_metal::gpu::launch_matvec_fused(x, &launches) {
Ok(outs) => return Some(outs),
Err(e) => {
eprintln!("ferrox: Metal fused matvec failed, falling back to CPU: {e}");
}
}
}
None
}
#[cfg(any(feature = "cuda", feature = "metal"))]
pub fn apply_gpu_dense_ffn_swiglu(
gate: &WeightMatrix,
up: &WeightMatrix,
down: &WeightMatrix,
x: &[f32],
) -> Option<Vec<f32>> {
#[cfg(feature = "cuda")]
{
if cuda_dense_enabled() {
fn cuda_launch(m: &WeightMatrix) -> Option<ferrox_cuda::gpu::MatvecLaunch<'_>> {
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = m
else {
return None;
};
let (kernel_src, module_name, fn_name) =
ferrox_cuda::gpu::matvec_launch_meta(kind.name())?;
let row_bytes = m.block_bytes_per_row(*kind, *cols);
let n_blocks_per_row = row_bytes / WeightMatrix::block_bytes_for_kind(*kind);
Some(ferrox_cuda::gpu::MatvecLaunch {
kernel_src,
module_name,
fn_name,
weights: data.as_slice(),
rows: *rows,
row_bytes,
n_blocks_per_row,
})
}
if let (Some(g), Some(u), Some(d)) =
(cuda_launch(gate), cuda_launch(up), cuda_launch(down))
{
assert_eq!(gate.cols(), x.len());
assert_eq!(up.cols(), x.len());
assert_eq!(down.cols(), gate.rows());
match ferrox_cuda::gpu::launch_dense_ffn_swiglu(&g, &u, &d, x) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!("ferrox: CUDA dense FFN fuse failed, trying next: {e}");
}
}
}
}
}
#[cfg(feature = "metal")]
{
if metal_dense_enabled() {
fn metal_launch(m: &WeightMatrix) -> Option<ferrox_metal::gpu::MatvecLaunch<'_>> {
let WeightMatrix::Quantized {
data,
rows,
cols: _,
kind,
} = m
else {
return None;
};
let kind_name = match kind {
QuantKind::Q8_0 => "Q8_0",
QuantKind::Q4_0 => "Q4_0",
QuantKind::Q4K => "Q4_K",
QuantKind::Q5K => "Q5_K",
QuantKind::Q6K => "Q6_K",
QuantKind::IQ4XS => "IQ4_XS",
_ => return None,
};
let (src, fn_name, block_bytes, block_elems, rows_per_tg) =
ferrox_metal::gpu::matvec_launch_meta(kind_name)?;
let row_bytes = data.as_slice().len().checked_div(*rows).unwrap_or(0);
Some(ferrox_metal::gpu::MatvecLaunch {
kernel_src: src,
fn_name,
block_bytes,
block_elems,
weights: data.as_slice(),
rows: *rows,
row_bytes,
rows_per_tg,
})
}
if let (Some(g), Some(u), Some(d)) =
(metal_launch(gate), metal_launch(up), metal_launch(down))
{
assert_eq!(gate.cols(), x.len());
assert_eq!(up.cols(), x.len());
assert_eq!(down.cols(), gate.rows());
match ferrox_metal::gpu::launch_dense_ffn_swiglu(&g, &u, &d, x) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!("ferrox: Metal dense FFN fuse failed, falling back: {e}");
}
}
}
}
}
None
}
#[cfg(feature = "metal")]
pub fn apply_gpu_batch(&self, x_batch: &[f32], batch_size: usize) -> Option<Vec<f32>> {
if !metal_dense_enabled() || batch_size == 0 {
return None;
}
if let WeightMatrix::Adapted { base, lora } = self {
let mut out = base.apply_gpu_batch(x_batch, batch_size)?;
lora.add_batch_to(x_batch, batch_size, &mut out);
return Some(out);
}
let WeightMatrix::Quantized {
data,
rows,
cols,
kind,
} = self
else {
return None;
};
let Some(kind_name) = metal_matvec_kind_name(*kind) else {
crate::kernel_registry::miss(
crate::kernel_registry::Lookup::new(
crate::kernel_registry::Backend::Metal,
crate::kernel_registry::op::GEMM_PREFILL,
Some(*kind),
),
"CPU apply_batch",
);
return None;
};
let (src, fn_name, block_bytes, block_elems, rows_per_tg) =
ferrox_metal::gpu::matvec_launch_meta(kind_name)?;
let row_bytes = self.block_bytes_per_row(*kind, *cols);
let use_mul_mm = batch_size >= 4;
if use_mul_mm {
if !metal_mul_mm_kind_supported(*kind) {
crate::kernel_registry::miss(
crate::kernel_registry::Lookup::new(
crate::kernel_registry::Backend::Metal,
crate::kernel_registry::op::GEMM_PREFILL,
Some(*kind),
),
"Metal N x matvec batch",
);
}
match kind {
QuantKind::Q4_0 => {
match ferrox_metal::gpu::launch_q4_0_mul_mm_sg(
data.as_slice(),
x_batch,
*rows,
row_bytes,
batch_size,
) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!(
"ferrox: Metal Q4_0 simdgroup mul_mm failed, batched fallback: {e}"
);
}
}
match ferrox_metal::gpu::launch_q4_0_mul_mm(
data.as_slice(),
x_batch,
*rows,
row_bytes,
batch_size,
) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!("ferrox: Metal Q4_0 mul_mm failed, matvec fallback: {e}");
}
}
}
QuantKind::Q8_0 => {
match ferrox_metal::gpu::launch_q8_0_mul_mm_sg(
data.as_slice(),
x_batch,
*rows,
row_bytes,
batch_size,
) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!(
"ferrox: Metal Q8_0 simdgroup mul_mm failed, matvec fallback: {e}"
);
}
}
}
QuantKind::Q5K => {
match ferrox_metal::gpu::launch_q5_k_mul_mm_sg(
data.as_slice(),
x_batch,
*rows,
row_bytes,
batch_size,
) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!(
"ferrox: Metal Q5_K simdgroup mul_mm failed, matvec fallback: {e}"
);
}
}
}
QuantKind::IQ4XS => {
match ferrox_metal::gpu::launch_iq4_xs_mul_mm_sg(
data.as_slice(),
x_batch,
*rows,
row_bytes,
batch_size,
) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!(
"ferrox: Metal IQ4_XS simdgroup mul_mm failed, matvec fallback: {e}"
);
}
}
}
QuantKind::Q4K => {
match ferrox_metal::gpu::launch_q4_k_mul_mm_sg(
data.as_slice(),
x_batch,
*rows,
row_bytes,
batch_size,
) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!(
"ferrox: Metal Q4_K simdgroup mul_mm failed, batched-matvec fallback: {e}"
);
}
}
match ferrox_metal::gpu::launch_q4_k_mul_mm(
data.as_slice(),
x_batch,
*rows,
row_bytes,
batch_size,
) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!(
"ferrox: Metal Q4_K mul_mm (MUL_MM path) failed, matvec fallback: {e}"
);
}
}
}
QuantKind::Q6K => {
match ferrox_metal::gpu::launch_q6_k_mul_mm_sg(
data.as_slice(),
x_batch,
*rows,
row_bytes,
batch_size,
) {
Ok(out) => return Some(out),
Err(e) => {
eprintln!(
"ferrox: Metal Q6_K simdgroup mul_mm failed, matvec fallback: {e}"
);
}
}
}
_ => {}
}
}
let launch = ferrox_metal::gpu::MatvecLaunch {
kernel_src: src,
fn_name,
block_bytes,
block_elems,
weights: data.as_slice(),
rows: *rows,
row_bytes,
rows_per_tg,
};
match ferrox_metal::gpu::launch_matvec_batch(&launch, x_batch, batch_size) {
Ok(out) => Some(out),
Err(e) => {
eprintln!("ferrox: Metal batch matvec failed, falling back: {e}");
None
}
}
}
#[cfg(feature = "metal")]
fn metal_kind_supported(kind: QuantKind) -> bool {
metal_matvec_kind_name(kind).is_some()
}
#[track_caller]
pub fn probe_kernels(&self, role: &'static str) {
if !crate::kernel_registry::enabled() {
return;
}
self.probe_kernels_into(
crate::kernel_registry::global(),
role,
std::panic::Location::caller(),
);
}
pub fn probe_kernels_into(
&self,
reg: &crate::kernel_registry::Registry,
role: &'static str,
loc: &'static std::panic::Location<'static>,
) {
self.probe_kernels_for(reg, active_backend(), role, loc)
}
pub fn probe_kernels_for(
&self,
reg: &crate::kernel_registry::Registry,
backend: crate::kernel_registry::Backend,
role: &'static str,
loc: &'static std::panic::Location<'static>,
) {
use crate::kernel_registry::{op, Backend, Lookup, Outcome};
let kind = self.quant_kind();
let cols = self.cols();
let look = |op: &'static str| Lookup {
backend,
op,
role,
kind,
};
let (matvec, gemm, gemm_fallback) = {
let mut found = (false, false, "");
macro_rules! caps_of {
($b:ty) => {
if backend == <$b as BackendCaps>::ID {
found = (
kind.is_some_and(|k| <$b as BackendCaps>::matvec_kernel(k).is_some()),
kind.is_some_and(<$b as BackendCaps>::gemm_supported),
<$b as BackendCaps>::GEMM_FALLBACK,
);
}
};
}
with_gpu_backend_caps!(caps_of);
found
};
if backend.is_accelerator() {
reg.record_build_at(
loc,
look(op::MATVEC),
match kind {
_ if matvec => Outcome::Hit,
Some(_) => Outcome::slow_path("CPU apply_cpu"),
None => Outcome::by_design("host GEMV"),
},
);
reg.record_build_at(
loc,
look(op::GEMM_PREFILL),
match (gemm, matvec, kind) {
(true, ..) => Outcome::Hit,
(false, true, _) => Outcome::slow_path(gemm_fallback),
(false, false, Some(_)) => Outcome::slow_path("CPU apply_batch"),
(false, false, None) => Outcome::by_design("CPU f32 GEMM"),
},
);
}
if !matvec || !gemm {
let int_dot = cpu_int_dot_for(IntDotShape::Matvec)
&& kind.is_some_and(|k| cpu_int_dot_kind_supported(k, cols));
reg.record_build_at(
loc,
Lookup {
backend: Backend::Cpu,
op: op::MATVEC,
role,
kind,
},
match kind {
_ if int_dot => Outcome::Hit,
Some(_) => Outcome::slow_path("f32 dequant-dot"),
None => Outcome::by_design("f32 GEMM"),
},
);
}
}
#[cfg(any(feature = "cuda", feature = "vulkan"))]
pub(crate) fn block_bytes_for_kind(kind: QuantKind) -> usize {
match kind {
QuantKind::Q8_0 => ferrox_quant::Q8_0_BLOCK_BYTES,
QuantKind::Q4_0 => ferrox_quant::Q4_0_BLOCK_BYTES,
QuantKind::Q5_0 => ferrox_quant::Q5_0_BLOCK_BYTES,
QuantKind::Q4K => ferrox_quant::Q4_K_BLOCK_BYTES,
QuantKind::Q5K => ferrox_quant::Q5_K_BLOCK_BYTES,
QuantKind::Q6K => ferrox_quant::Q6_K_BLOCK_BYTES,
QuantKind::Q2K => ferrox_quant::Q2_K_BLOCK_BYTES,
QuantKind::Q3K => ferrox_quant::Q3_K_BLOCK_BYTES,
QuantKind::IQ4NL => ferrox_quant::IQ4_NL_BLOCK_BYTES,
QuantKind::IQ4XS => ferrox_quant::IQ4_XS_BLOCK_BYTES,
QuantKind::Mxfp4Gguf => ferrox_quant::MXFP4_GGUF_BLOCK_BYTES,
_ => unreachable!(
"apply_gpu only calls this for the CUDA/Vulkan-dispatchable kinds, not {kind:?}"
),
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn the_task_floor_demands_more_rows_of_a_narrower_matrix() {
if crate::par::policy::pinned().is_some() {
return; }
let rows = 4096usize;
let narrow = crate::par::with_op_work(rows, 64, || WeightMatrix::min_rows_per_task(rows));
let wider = crate::par::with_op_work(rows, 256, || WeightMatrix::min_rows_per_task(rows));
assert_eq!(narrow, MIN_TASK_MACS.div_ceil(64));
assert!(
narrow > wider,
"a 64-wide row carries a quarter of a 256-wide row's work, so a \
task must hold four times as many of them: {narrow} vs {wider}"
);
}
#[test]
fn the_four_dtypes_the_duplicated_tables_disagreed_about_all_map() {
assert_eq!(quant_kind_for(GgmlType::IQ1S), Some(QuantKind::IQ1S));
assert_eq!(quant_kind_for(GgmlType::IQ2XXS), Some(QuantKind::IQ2XXS));
assert_eq!(quant_kind_for(GgmlType::IQ3XXS), Some(QuantKind::IQ3XXS));
assert_eq!(quant_kind_for(GgmlType::MXFP4), Some(QuantKind::Mxfp4Gguf));
}
#[test]
fn every_dtype_with_a_gpu_kernel_is_reachable_through_the_map() {
let mapped: Vec<QuantKind> = [
GgmlType::Q8_0,
GgmlType::Q4_0,
GgmlType::Q4K,
GgmlType::Q5K,
GgmlType::Q6K,
GgmlType::IQ4XS,
]
.into_iter()
.map(|d| quant_kind_for(d).expect("a dtype with a GPU kernel must map"))
.collect();
for kind in mapped {
assert!(
metal_mul_mm_kind_supported(kind) || cuda_matvec_kind_supported(kind),
"{kind:?} was listed as having a GPU kernel"
);
}
}
#[cfg(feature = "cuda")]
#[test]
fn every_cuda_matvec_kind_has_a_launcher() {
use super::gpu_backend::cuda_matvec_launch;
for &kind in QuantKind::ALL {
assert_eq!(
cuda_matvec_kind_supported(kind),
cuda_matvec_launch(kind).is_some(),
"{kind:?}: the capability table and the launch table disagree"
);
}
}
#[cfg(any(feature = "cuda", feature = "vulkan"))]
#[test]
fn every_cuda_or_vulkan_matvec_kind_has_a_block_size() {
use super::gpu_backend::{BackendCaps, Cuda, Vulkan};
for &kind in QuantKind::ALL {
if Cuda::matvec_kernel(kind).is_none() && Vulkan::matvec_kernel(kind).is_none() {
continue;
}
let block_bytes = WeightMatrix::block_bytes_for_kind(kind);
assert!(
block_bytes > 0,
"{kind:?}: a claimed matvec kind needs a real block size"
);
}
}
#[cfg(any(feature = "cuda", feature = "vulkan"))]
#[test]
fn the_two_block_size_functions_agree_for_every_gpu_kind() {
use super::gpu_backend::{BackendCaps, Cuda, Vulkan};
for &kind in QuantKind::ALL {
if Cuda::matvec_kernel(kind).is_none() && Vulkan::matvec_kernel(kind).is_none() {
continue;
}
let block_bytes = WeightMatrix::block_bytes_for_kind(kind);
let mm = ferrox_cuda::mul_mm::kind_by_name(kind.name())
.unwrap_or_else(|| panic!("{kind:?}: claims a GPU matvec with no mul_mm row"));
assert_eq!(
block_bytes, mm.block_bytes,
"{kind:?}: ferrox-core's block size is not the one the kernel strides by"
);
let probe = WeightMatrix::Quantized {
data: WeightBytes::Owned(Vec::new()),
rows: 1,
cols: mm.block_elems,
kind,
};
for blocks in 3..=5usize {
let cols = mm.block_elems * blocks;
let row_bytes = probe.block_bytes_per_row(kind, cols);
assert_eq!(
row_bytes,
blocks * block_bytes,
"{kind:?}: block_bytes_per_row({cols}) is not {blocks} x {block_bytes}"
);
assert_eq!(
row_bytes / block_bytes,
blocks,
"{kind:?}: the n_blocks_per_row the matvec seam derives is wrong"
);
}
}
}
#[test]
fn an_unquantized_dtype_maps_to_nothing() {
assert_eq!(quant_kind_for(GgmlType::F32), None);
assert_eq!(quant_kind_for(GgmlType::F16), None);
}
use super::*;
pub(super) struct ForceIntDot {
_lock: std::sync::MutexGuard<'static, ()>,
}
impl ForceIntDot {
pub(super) fn new(on: bool) -> Self {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let lock = LOCK.lock().unwrap_or_else(|e| e.into_inner());
INT_DOT_TEST_OVERRIDE.store(i8::from(on), std::sync::atomic::Ordering::Release);
Self { _lock: lock }
}
}
impl Drop for ForceIntDot {
fn drop(&mut self) {
INT_DOT_TEST_OVERRIDE.store(-1, std::sync::atomic::Ordering::Release);
}
}
#[test]
fn force_int_dot_moves_the_getter_and_restores_it() {
{
let _g = ForceIntDot::new(true);
assert!(cpu_int_dot_enabled(), "forcing on must enable int dot");
}
{
let _g = ForceIntDot::new(false);
assert!(!cpu_int_dot_enabled(), "forcing off must disable int dot");
}
assert_eq!(
INT_DOT_TEST_OVERRIDE.load(std::sync::atomic::Ordering::Acquire),
-1,
"the guard must clear the override on drop"
);
}
#[test]
fn dequant_row_matches_full_dequant_per_row() {
let rows = 3;
let cols = 64;
let f32_data: Vec<f32> = (0..rows * cols).map(|i| (i as f32) * 0.1 - 5.0).collect();
let m = WeightMatrix::F32(Tensor::new(f32_data.clone(), vec![rows, cols]));
for r in 0..rows {
assert_eq!(m.dequant_row(r), &f32_data[r * cols..(r + 1) * cols]);
}
let mut packed = Vec::new();
for r in 0..rows {
packed.extend(make_q8_0_row(&f32_data[r * cols..(r + 1) * cols]));
}
let row_bytes = packed.len() / rows;
let q = WeightMatrix::Quantized {
data: WeightBytes::Owned(packed.clone()),
rows,
cols,
kind: QuantKind::Q8_0,
};
for r in 0..rows {
let expected =
ferrox_quant::dequant_q8_0(&packed[r * row_bytes..(r + 1) * row_bytes]).unwrap();
assert_eq!(q.dequant_row(r), expected, "Q8_0 row {r}");
}
let cols = 64;
let packed: Vec<u8> = pseudo_bytes(7, rows * cols / 2);
let scales: Vec<u8> = pseudo_bytes(11, rows * cols / 32);
let m = WeightMatrix::Mxfp4 {
packed: WeightBytes::Owned(packed.clone()),
scale: WeightBytes::Owned(scales.clone()),
rows,
cols,
};
for r in 0..rows {
let expected = ferrox_quant::dequant_mxfp4_row(
&packed[r * cols / 2..(r + 1) * cols / 2],
&scales[r * cols / 32..(r + 1) * cols / 32],
)
.unwrap();
assert_eq!(m.dequant_row(r), expected, "Mxfp4 row {r}");
}
}
#[test]
fn dequant_row_agrees_with_fused_dot_on_the_same_row() {
let rows = 4;
let cols = 64;
let f32_data: Vec<f32> = (0..rows * cols)
.map(|i| ((i as f32) * 0.13).sin())
.collect();
let mut packed = Vec::new();
for r in 0..rows {
packed.extend(make_q8_0_row(&f32_data[r * cols..(r + 1) * cols]));
}
let q = WeightMatrix::Quantized {
data: WeightBytes::Owned(packed),
rows,
cols,
kind: QuantKind::Q8_0,
};
let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.031).cos()).collect();
let applied = q.apply(&x);
let bound = |row: &[f32]| {
if !cpu_int_dot_for(IntDotShape::Matvec) {
return 1e-4;
}
let amax = x.iter().fold(0f32, |m, v| m.max(v.abs()));
let l1: f32 = row.iter().map(|w| w.abs()).sum();
(amax / 127.0 / 2.0) * l1
};
for (r, &got) in applied.iter().enumerate() {
let row = q.dequant_row(r);
let via_row: f32 = row.iter().zip(&x).map(|(a, b)| a * b).sum();
let bound = bound(&row);
assert!(
(got - via_row).abs() < bound,
"row {r}: apply={got} via dequant_row={via_row} (bound {bound:e})"
);
}
}
fn make_q8_0_row(values: &[f32]) -> Vec<u8> {
ferrox_quant::quantize_q8_0(values)
}
fn pseudo_bytes(seed: u32, len: usize) -> Vec<u8> {
let mut state = seed.wrapping_mul(2654435761).wrapping_add(1);
(0..len)
.map(|_| {
state = state.wrapping_mul(1103515245).wrapping_add(12345);
(state >> 16) as u8
})
.collect()
}
fn pseudo_mxfp4_scale_bytes(seed: u32, len: usize) -> Vec<u8> {
pseudo_bytes(seed, len)
.into_iter()
.map(|b| b % 180)
.collect()
}
#[test]
fn f32_and_mxfp4_paths_agree() {
let rows = 2;
let cols = 64; let packed = pseudo_bytes(1, rows * (cols / 2));
let scale = pseudo_mxfp4_scale_bytes(2, rows * (cols / ferrox_quant::MXFP4_GROUP_SIZE));
let x: Vec<f32> = (0..cols).map(|i| (i as f32) * 0.01 - 0.3).collect();
let mut f32_weights = Vec::with_capacity(rows * cols);
for r in 0..rows {
let prow = &packed[r * (cols / 2)..(r + 1) * (cols / 2)];
let srow = &scale[r * (cols / ferrox_quant::MXFP4_GROUP_SIZE)
..(r + 1) * (cols / ferrox_quant::MXFP4_GROUP_SIZE)];
f32_weights.extend(ferrox_quant::dequant_mxfp4_row(prow, srow).unwrap());
}
let f32_matrix = WeightMatrix::F32(Tensor::new(f32_weights, vec![rows, cols]));
let f32_out = f32_matrix.apply(&x);
let mxfp4_matrix = WeightMatrix::Mxfp4 {
packed: WeightBytes::Owned(packed),
scale: WeightBytes::Owned(scale),
rows,
cols,
};
let mxfp4_out = mxfp4_matrix.apply(&x);
assert_eq!(f32_out.len(), rows);
assert_eq!(mxfp4_out.len(), rows);
for (f, m) in f32_out.iter().zip(mxfp4_out.iter()) {
assert!((f - m).abs() < 1e-3, "f32={f} mxfp4={m}");
}
}
#[test]
fn mxfp4_apply_batch_matches_sequential_apply_calls() {
let rows = 3;
let cols = 64;
let packed = pseudo_bytes(3, rows * (cols / 2));
let scale = pseudo_mxfp4_scale_bytes(4, rows * (cols / ferrox_quant::MXFP4_GROUP_SIZE));
let matrix = WeightMatrix::Mxfp4 {
packed: WeightBytes::Owned(packed),
scale: WeightBytes::Owned(scale),
rows,
cols,
};
let batch_size = 4;
let x_batch: Vec<f32> = (0..batch_size * cols)
.map(|i| ((i % 13) as f32) * 0.02 - 0.15)
.collect();
let batched = matrix.apply_batch(&x_batch, batch_size);
assert_eq!(batched.len(), batch_size * rows);
for b in 0..batch_size {
let x = &x_batch[b * cols..(b + 1) * cols];
let sequential = matrix.apply(x);
let from_batch = &batched[b * rows..(b + 1) * rows];
assert_eq!(
sequential, from_batch,
"batch row {b} disagrees with sequential apply()"
);
}
}
#[test]
fn mxfp4_resident_bytes_matches_the_packed_plus_scale_byte_count_not_eager_f32() {
let rows = 2;
let cols = 64;
let packed = pseudo_bytes(5, rows * (cols / 2));
let scale = pseudo_mxfp4_scale_bytes(6, rows * (cols / ferrox_quant::MXFP4_GROUP_SIZE));
let packed_len = packed.len();
let scale_len = scale.len();
let matrix = WeightMatrix::Mxfp4 {
packed: WeightBytes::Owned(packed),
scale: WeightBytes::Owned(scale),
rows,
cols,
};
assert_eq!(matrix.resident_bytes(), packed_len + scale_len);
let eager_f32_bytes = rows * cols * 4;
assert!(
matrix.resident_bytes() * 4 < eager_f32_bytes,
"expected MXFP4 resident bytes well under 1/4 of eager f32: got {} vs {}",
matrix.resident_bytes(),
eager_f32_bytes
);
}
#[test]
fn f32_and_quantized_paths_agree_within_quant_error() {
let weights: Vec<f32> = (0..32).map(|i| ((i as f32) - 16.0) * 0.2).collect();
let x: Vec<f32> = (0..32).map(|i| (i as f32) * 0.05 - 0.8).collect();
let f32_matrix = WeightMatrix::F32(Tensor::new(weights.clone(), vec![1, 32]));
let f32_out = f32_matrix.apply(&x);
let packed = make_q8_0_row(&weights);
let quant_matrix = WeightMatrix::Quantized {
data: WeightBytes::Owned(packed),
rows: 1,
cols: 32,
kind: QuantKind::Q8_0,
};
let quant_out = quant_matrix.apply(&x);
assert_eq!(f32_out.len(), 1);
assert_eq!(quant_out.len(), 1);
assert!(
(f32_out[0] - quant_out[0]).abs() < 0.05,
"f32={} quant={}",
f32_out[0],
quant_out[0]
);
}
#[test]
fn quantized_resident_bytes_is_smaller_than_f32() {
let weights = vec![0.1f32; 64]; let f32_matrix = WeightMatrix::F32(Tensor::new(weights.clone(), vec![2, 32]));
let mut packed = Vec::new();
for chunk in weights.chunks(32) {
packed.extend(ferrox_quant::quantize_q8_0(chunk));
}
let quant_matrix = WeightMatrix::Quantized {
data: WeightBytes::Owned(packed),
rows: 2,
cols: 32,
kind: QuantKind::Q8_0,
};
assert_eq!(f32_matrix.resident_bytes(), 64 * 4); assert_eq!(quant_matrix.resident_bytes(), 2 * 34); assert!(quant_matrix.resident_bytes() < f32_matrix.resident_bytes());
let ratio = f32_matrix.resident_bytes() as f32 / quant_matrix.resident_bytes() as f32;
assert!(ratio > 3.5, "expected ~4x reduction, got {ratio}x");
}
#[test]
fn rows_and_cols_report_correctly_for_both_variants() {
let f32_matrix = WeightMatrix::F32(Tensor::new(vec![0.0; 6], vec![2, 3]));
assert_eq!(f32_matrix.rows(), 2);
assert_eq!(f32_matrix.cols(), 3);
let quant_matrix = WeightMatrix::Quantized {
data: WeightBytes::Owned(vec![0u8; 34]),
rows: 1,
cols: 32,
kind: QuantKind::Q8_0,
};
assert_eq!(quant_matrix.rows(), 1);
assert_eq!(quant_matrix.cols(), 32);
}
#[test]
#[should_panic]
fn apply_panics_on_activation_length_mismatch() {
let f32_matrix = WeightMatrix::F32(Tensor::new(vec![0.0; 6], vec![2, 3]));
f32_matrix.apply(&[1.0, 2.0]); }
#[test]
fn apply_batch_with_batch_size_one_matches_apply() {
let _int_dot = ForceIntDot::new(false);
let weights: Vec<f32> = (0..32).map(|i| (i as f32 - 16.0) * 0.13).collect();
let x: Vec<f32> = (0..32).map(|i| (i as f32) * 0.02 - 0.3).collect();
let f32_matrix = WeightMatrix::F32(Tensor::new(weights.clone(), vec![1, 32]));
let single = f32_matrix.apply(&x);
let batched = f32_matrix.apply_batch(&x, 1);
assert_eq!(single, batched);
let packed = ferrox_quant::quantize_q8_0(&weights);
let quant_matrix = WeightMatrix::Quantized {
data: WeightBytes::Owned(packed),
rows: 1,
cols: 32,
kind: QuantKind::Q8_0,
};
let single_q = quant_matrix.apply(&x);
let batched_q = quant_matrix.apply_batch(&x, 1);
assert_eq!(single_q, batched_q);
}
#[test]
fn apply_batch_matches_sequential_apply_calls_for_each_row_f32() {
let rows = 3;
let cols = 32;
let weights: Vec<f32> = (0..rows * cols)
.map(|i| ((i % 17) as f32 - 8.0) * 0.05)
.collect();
let matrix = WeightMatrix::F32(Tensor::new(weights, vec![rows, cols]));
let batch_size = 4;
let x_batch: Vec<f32> = (0..batch_size * cols)
.map(|i| ((i % 13) as f32) * 0.03 - 0.2)
.collect();
let batched = matrix.apply_batch(&x_batch, batch_size);
assert_eq!(batched.len(), batch_size * rows);
for b in 0..batch_size {
let x = &x_batch[b * cols..(b + 1) * cols];
let sequential = matrix.apply(x);
let from_batch = &batched[b * rows..(b + 1) * rows];
assert_eq!(
sequential, from_batch,
"batch row {b} disagrees with sequential apply()"
);
}
}
#[test]
fn apply_batch_matches_sequential_apply_calls_for_each_row_quantized() {
let rows = 3;
let cols = 32;
let weights: Vec<f32> = (0..rows * cols)
.map(|i| ((i % 19) as f32 - 9.0) * 0.07)
.collect();
let mut packed = Vec::new();
for row in weights.chunks(cols) {
packed.extend(ferrox_quant::quantize_q8_0(row));
}
let matrix = WeightMatrix::Quantized {
data: WeightBytes::Owned(packed),
rows,
cols,
kind: QuantKind::Q8_0,
};
let batch_size = 5;
let x_batch: Vec<f32> = (0..batch_size * cols)
.map(|i| ((i % 11) as f32) * 0.04 - 0.25)
.collect();
let batched = matrix.apply_batch(&x_batch, batch_size);
assert_eq!(batched.len(), batch_size * rows);
for b in 0..batch_size {
let x = &x_batch[b * cols..(b + 1) * cols];
let sequential = matrix.apply(x);
let from_batch = &batched[b * rows..(b + 1) * rows];
assert_batch_row_matches(QuantKind::Q8_0, "", b, &sequential, from_batch);
}
}
pub(super) fn f16_le(x: f32) -> [u8; 2] {
let bits = x.to_bits();
let exp = ((bits >> 23) & 0xff) as i32 - 127 + 15;
let mant = (bits >> 13) & 0x3ff;
(((exp as u16) << 10) | mant as u16).to_le_bytes()
}
fn assert_batch_row_matches(
kind: QuantKind,
ctx: &str,
b: usize,
sequential: &[f32],
from_batch: &[f32],
) {
let scale = sequential
.iter()
.fold(0.0f32, |a, v| a.max(v.abs()))
.max(1.0);
let mixed = cpu_int_dot_for(IntDotShape::Matvec) != cpu_int_dot_for(IntDotShape::BatchGemm);
let bound = if cfg!(any(feature = "metal", feature = "cuda")) {
5e-3
} else if mixed {
6e-2
} else {
1e-4
};
for (r, (s, got)) in sequential.iter().zip(from_batch.iter()).enumerate() {
let err = (s - got).abs() / scale;
assert!(
err < bound,
"{kind:?} {ctx} batch {b} row {r}: apply()={s} apply_batch={got} \
(err {err:e} of row scale {scale}, bound {bound:e})"
);
}
}
fn synth_quant_matrix(kind: QuantKind, rows: usize, cols: usize) -> WeightMatrix {
let mut state = 0x1234_5678u32;
let mut next = move || {
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
(state >> 24) as u8
};
let mut data = Vec::new();
match kind {
QuantKind::Q8_0 | QuantKind::Q4_0 => {
let qs = if kind == QuantKind::Q8_0 { 32 } else { 16 };
for _ in 0..rows * (cols / 32) {
data.extend_from_slice(&f16_le(0.02 + f32::from(next()) * 0.0004));
for _ in 0..qs {
data.push(next());
}
}
}
QuantKind::Q4K | QuantKind::Q5K => {
let body = if kind == QuantKind::Q4K {
12 + 128
} else {
12 + 32 + 128
};
for _ in 0..rows * (cols / 256) {
data.extend_from_slice(&f16_le(0.01 + f32::from(next()) * 0.0002));
data.extend_from_slice(&f16_le(0.005 + f32::from(next()) * 0.0001));
for _ in 0..body {
data.push(next());
}
}
}
QuantKind::Q6K => {
for _ in 0..rows * (cols / 256) {
for _ in 0..128 + 64 + 16 {
data.push(next());
}
data.extend_from_slice(&f16_le(0.01 + f32::from(next()) * 0.0002));
}
}
_ => unreachable!("synth_quant_matrix: unsupported kind"),
}
WeightMatrix::Quantized {
data: WeightBytes::Owned(data),
rows,
cols,
kind,
}
}
fn assert_apply_batch_matches_apply(
kind: QuantKind,
rows: usize,
cols: usize,
batch_size: usize,
seed: usize,
) {
let x_batch: Vec<f32> = (0..batch_size * cols)
.map(|i| (((i * 31 + seed) % 97) as f32) * 0.021 - 1.0)
.collect();
let matrix = synth_quant_matrix(kind, rows, cols);
let batched = matrix.apply_batch(&x_batch, batch_size);
assert_eq!(batched.len(), batch_size * rows);
let ctx = format!(
"rows {rows} cols {cols} batch_size {batch_size} int_dot {}",
cpu_int_dot_for(IntDotShape::BatchGemm)
);
for b in 0..batch_size {
let x = &x_batch[b * cols..(b + 1) * cols];
let sequential = matrix.apply(x);
let from_batch = &batched[b * rows..(b + 1) * rows];
assert_batch_row_matches(kind, &ctx, b, &sequential, from_batch);
}
}
const BATCH_SHAPE_KINDS: [QuantKind; 5] = [
QuantKind::Q8_0,
QuantKind::Q4_0,
QuantKind::Q4K,
QuantKind::Q5K,
QuantKind::Q6K,
];
#[test]
fn apply_batch_matches_apply_across_kinds_with_groups_and_tail() {
for int_dot in [false, true] {
let _g = ForceIntDot::new(int_dot);
for kind in BATCH_SHAPE_KINDS {
assert_apply_batch_matches_apply(kind, 19, 512, 6, 7);
}
}
}
#[test]
fn apply_batch_matches_apply_for_sub_tile_shapes() {
for int_dot in [false, true] {
let _g = ForceIntDot::new(int_dot);
for kind in BATCH_SHAPE_KINDS {
for rows in [1, 2, 3, 5, 7] {
for batch_size in [1, 2, 5] {
assert_apply_batch_matches_apply(kind, rows, 256, batch_size, 13);
}
}
}
}
}
#[test]
fn int_dot_batch_matches_dequant_dot_reference() {
let _g = ForceIntDot::new(true);
assert!(cpu_int_dot_enabled(), "forcing on must enable int dot");
if !cpu_int_dot_for(IntDotShape::BatchGemm) {
return;
}
for kind in BATCH_SHAPE_KINDS {
for rows in [1, 3, 5, 7, 8, 9, 19] {
for cols in [256, 512] {
for batch_size in [1, 3, 4, 9] {
assert_int_dot_matches_dequant_dot(kind, rows, cols, batch_size, 23);
}
}
}
}
for kind in [QuantKind::Q8_0, QuantKind::Q4_0] {
for rows in [1, 3, 4, 5, 11] {
for batch_size in [1, 3, 4, 9] {
assert_int_dot_matches_dequant_dot(kind, rows, 32, batch_size, 29);
}
}
}
}
fn assert_int_dot_matches_dequant_dot(
kind: QuantKind,
rows: usize,
cols: usize,
batch_size: usize,
seed: usize,
) {
let x_batch: Vec<f32> = (0..batch_size * cols)
.map(|i| (((i * 37 + seed) % 89) as f32) * 0.019 - 0.8)
.collect();
let matrix = synth_quant_matrix(kind, rows, cols);
let got = matrix.apply_batch(&x_batch, batch_size);
assert_eq!(got.len(), batch_size * rows);
let mut want = vec![0f32; batch_size * rows];
for r in 0..rows {
let w = matrix.dequant_row(r);
assert_eq!(w.len(), cols);
for b in 0..batch_size {
let x = &x_batch[b * cols..(b + 1) * cols];
want[b * rows + r] = w.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
}
}
let rms = (want.iter().map(|v| v * v).sum::<f32>() / want.len() as f32).sqrt();
for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
let err = (g - w).abs();
assert!(
err < 0.12 * rms.max(1e-3),
"{kind:?} rows {rows} cols {cols} batch_size {batch_size} [flat {i}]: \
int-dot={g} dequant-dot={w} (err {err}, rms {rms})"
);
}
}
#[test]
fn apply_batch_chunked_grid_matches_apply() {
for int_dot in [false, true] {
let _g = ForceIntDot::new(int_dot);
for kind in BATCH_SHAPE_KINDS {
assert_apply_batch_matches_apply(kind, 259, 512, 66, 5);
}
}
}
#[test]
fn apply_batch_with_shared_acts_matches_apply_batch() {
let _int_dot = ForceIntDot::new(true);
let rows = 19;
let cols = 512;
let batch_size = 6;
let x_batch: Vec<f32> = (0..batch_size * cols)
.map(|i| (((i * 29 + 11) % 89) as f32) * 0.023 - 1.0)
.collect();
for kind in [
QuantKind::Q8_0,
QuantKind::Q4_0,
QuantKind::Q4K,
QuantKind::Q6K,
] {
let matrix = synth_quant_matrix(kind, rows, cols);
let baseline = matrix.apply_batch(&x_batch, batch_size);
let shared = matrix.quantize_batch_acts(&x_batch, batch_size);
let with_shared = matrix.apply_batch_with_acts(&x_batch, batch_size, shared.as_ref());
assert_eq!(
baseline, with_shared,
"{kind:?}: shared acts changed the result"
);
let wrong = match kind {
QuantKind::Q8_0 | QuantKind::Q4_0 => BatchActs::Q8K {
acts: Vec::new(),
tiles: Vec::new(),
cols,
},
_ => BatchActs::Q8 {
acts: Vec::new(),
tiles: Vec::new(),
cols,
},
};
let with_wrong = matrix.apply_batch_with_acts(&x_batch, batch_size, Some(&wrong));
assert_eq!(
baseline, with_wrong,
"{kind:?}: mismatched shared acts were not ignored"
);
}
}
#[test]
fn shared_acts_are_reused_only_at_the_matching_length_and_width() {
let cols = 512;
let batch_size = 7;
let x_batch: Vec<f32> = (0..batch_size * cols)
.map(|i| (((i * 37 + 5) % 83) as f32) * 0.019 - 0.9)
.collect();
let acts: Vec<_> = (0..batch_size)
.map(|b| ferrox_quant::quantize_activations_q8_k(&x_batch[b * cols..(b + 1) * cols]))
.collect();
let tiles: Vec<_> = acts
.chunks(ferrox_quant::Q8K_ACTS_X4_NC)
.map(|c| ferrox_quant::prepare_q8_k_acts_x4(c, cols))
.collect();
let n_tiles = tiles.len();
let shared = BatchActs::Q8K { acts, tiles, cols };
let mut owned = Vec::new();
let (got, quads) =
WeightMatrix::q8k_acts(Some(&shared), &x_batch, batch_size, cols, &mut owned);
assert_eq!(got.len(), batch_size);
assert_eq!(
quads.len(),
n_tiles,
"matching batch did not reuse its quads"
);
assert!(owned.is_empty(), "matching batch was re-quantized anyway");
let mut owned = Vec::new();
let (got, quads) =
WeightMatrix::q8k_acts(Some(&shared), &x_batch, batch_size, 256, &mut owned);
assert!(quads.is_empty(), "quads from another width were accepted");
assert_eq!(got.len(), batch_size);
assert_eq!(got[0].n_blocks(), 1, "fallback did not quantize at 256");
let mut owned = Vec::new();
let (got, quads) =
WeightMatrix::q8k_acts(Some(&shared), &x_batch[..cols], 1, cols, &mut owned);
assert!(quads.is_empty(), "quads for another batch were accepted");
assert_eq!(got.len(), 1);
let acts: Vec<_> = (0..batch_size)
.map(|b| ferrox_quant::quantize_activations_q8(&x_batch[b * cols..(b + 1) * cols]))
.collect();
let tiles: Vec<_> = acts
.chunks(ferrox_quant::Q8K_ACTS_X4_NC)
.map(|c| ferrox_quant::prepare_q8_acts_x4(c, cols))
.collect();
let n_tiles = tiles.len();
let shared = BatchActs::Q8 { acts, tiles, cols };
let mut owned = Vec::new();
let (got, quads) =
WeightMatrix::q8_acts(Some(&shared), &x_batch, batch_size, cols, &mut owned);
assert_eq!(got.len(), batch_size);
assert_eq!(
quads.len(),
n_tiles,
"matching batch did not reuse its quads"
);
let mut owned = Vec::new();
let (got, quads) =
WeightMatrix::q8_acts(Some(&shared), &x_batch, batch_size, 256, &mut owned);
assert!(quads.is_empty(), "quads from another width were accepted");
assert_eq!(got[0].n_blocks(), 8, "fallback did not quantize at 256");
}
#[test]
fn shared_quads_are_what_each_consumer_would_have_built_itself() {
let _int_dot = ForceIntDot::new(true);
if !cpu_int_dot_for(IntDotShape::BatchGemm) {
return;
}
let rows = 24;
let cols = 512;
let batch_size = 7;
let x_batch: Vec<f32> = (0..batch_size * cols)
.map(|i| (((i * 37 + 5) % 83) as f32) * 0.019 - 0.9)
.collect();
for (donor, consumers) in [
(QuantKind::Q4K, &[QuantKind::Q5K, QuantKind::Q6K][..]),
(QuantKind::Q8_0, &[QuantKind::Q4_0][..]),
] {
let shared = synth_quant_matrix(donor, rows, cols)
.quantize_batch_acts(&x_batch, batch_size)
.expect("INT_DOT is on and this kind/width is eligible");
for kind in consumers {
let matrix = synth_quant_matrix(*kind, rows, cols);
let baseline = matrix.apply_batch(&x_batch, batch_size);
let shared_out = matrix.apply_batch_with_acts(&x_batch, batch_size, Some(&shared));
assert_eq!(
baseline, shared_out,
"{kind:?} consuming {donor:?} quads changed the result"
);
}
}
}
#[test]
fn apply_batch_with_zero_batch_size_returns_empty() {
let matrix = WeightMatrix::F32(Tensor::new(vec![0.0; 6], vec![2, 3]));
let out = matrix.apply_batch(&[], 0);
assert!(out.is_empty());
}
#[cfg(any(feature = "cuda", feature = "metal", feature = "vulkan"))]
mod gpu_dispatch {
use super::*;
#[test]
fn apply_gpu_returns_none_for_f32() {
let matrix = WeightMatrix::F32(Tensor::new(vec![0.0; 6], vec![2, 3]));
assert!(matrix.apply_gpu(&[0.0, 0.0, 0.0]).is_none());
}
#[test]
fn apply_gpu_returns_none_for_mxfp4() {
let matrix = WeightMatrix::Mxfp4 {
packed: WeightBytes::Owned(vec![0u8; 32]),
scale: WeightBytes::Owned(vec![0u8; 2]),
rows: 1,
cols: 64,
};
assert!(matrix.apply_gpu(&vec![0.0; 64]).is_none());
}
#[test]
fn apply_gpu_returns_none_for_an_unsupported_quant_kind() {
let matrix = WeightMatrix::Quantized {
data: WeightBytes::Owned(vec![0u8; ferrox_quant::Q4_1_BLOCK_BYTES]),
rows: 1,
cols: ferrox_quant::Q4_1_BLOCK_ELEMS,
kind: QuantKind::Q4_1,
};
assert!(matrix
.apply_gpu(&[0.0; ferrox_quant::Q4_1_BLOCK_ELEMS])
.is_none());
}
#[test]
#[ignore = "requires real GPU hardware (CUDA or Metal) -- run with --ignored"]
fn apply_gpu_matches_apply_for_q8_0_on_real_hardware() {
let weights: Vec<f32> = (0..64).map(|i| ((i as f32) - 32.0) * 0.05).collect();
let x: Vec<f32> = (0..64).map(|i| (i as f32) * 0.01 - 0.3).collect();
let packed = ferrox_quant::quantize_q8_0(&weights);
let matrix = WeightMatrix::Quantized {
data: WeightBytes::Owned(packed),
rows: 1,
cols: 64,
kind: QuantKind::Q8_0,
};
let cpu = matrix.apply_cpu(&x);
let gpu = matrix
.apply_gpu(&x)
.expect("Q8_0 must dispatch to a real GPU kernel");
assert_eq!(cpu.len(), gpu.len());
for (c, g) in cpu.iter().zip(gpu.iter()) {
assert!((c - g).abs() < 1e-2, "cpu={c} gpu={g}");
}
}
}
fn shaped(kind: QuantKind, rows: usize, cols: usize) -> WeightMatrix {
let per_row = match kind {
QuantKind::Q8_0 => cols / 32 * 34,
_ => cols,
};
WeightMatrix::Quantized {
data: WeightBytes::Owned(vec![0u8; rows * per_row.max(1)]),
rows,
cols,
kind,
}
}
#[test]
fn quant_kind_all_lists_every_variant_exactly_once() {
let mut names: Vec<&str> = QuantKind::ALL.iter().map(|k| k.name()).collect();
let total = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total, "QuantKind::ALL has a duplicate");
assert_eq!(
total, 21,
"a QuantKind variant was added without updating ALL"
);
}
#[test]
fn every_metal_matvec_kind_also_has_a_metal_gemm() {
for &k in QuantKind::ALL {
assert_eq!(
metal_matvec_kind_name(k).is_some(),
metal_mul_mm_kind_supported(k),
"{}: matvec and mul_mm kernel tables disagree -- one of the two \
is a silent slow path",
k.name()
);
}
}
#[test]
fn metal_kind_names_match_the_quant_kind_names() {
for &k in QuantKind::ALL {
if let Some(name) = metal_matvec_kind_name(k) {
assert_eq!(name, k.name());
}
}
}
#[test]
fn a_deliberately_unsupported_kind_trips_the_registry() {
use crate::kernel_registry::{Backend, Outcome};
let reg = crate::kernel_registry::Registry::new();
let loc = std::panic::Location::caller();
shaped(QuantKind::Q4K, 64, 256).probe_kernels_for(®, Backend::Metal, "ffn_down", loc);
shaped(QuantKind::IQ2XXS, 64, 256).probe_kernels_for(®, Backend::Metal, "ffn_up", loc);
let report = reg.seal();
let violations = &report.violations;
assert_eq!(
violations.len(),
2,
"expected matvec + gemm misses for IQ2_XXS only, got: {:?}",
report
.entries
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
);
assert!(
violations
.iter()
.all(|v| v.key.kind == Some(QuantKind::IQ2XXS)),
"Q4_K must not be flagged"
);
assert!(
violations.iter().any(|v| matches!(
v.outcome,
Outcome::Miss { fallback, .. } if fallback == "CPU apply_batch"
)),
"the report must name the fallback that will actually run"
);
let rendered = report.render_violations();
assert!(rendered.contains("IQ2_XXS"), "{rendered}");
assert!(rendered.contains("weight_matrix.rs"), "{rendered}");
assert!(
report.entries.iter().any(|e| e.key.backend == Backend::Cpu
&& e.key.kind == Some(QuantKind::IQ2XXS)
&& matches!(e.outcome, Outcome::Miss { fallback, .. } if fallback == "f32 dequant-dot")),
"{:?}",
report.entries.iter().map(|e| e.to_string()).collect::<Vec<_>>()
);
}
#[test]
fn a_fully_supported_model_seals_clean() {
use crate::kernel_registry::Backend;
let reg = crate::kernel_registry::Registry::new();
let loc = std::panic::Location::caller();
for kind in [QuantKind::Q4K, QuantKind::Q6K, QuantKind::Q8_0] {
shaped(kind, 64, 256).probe_kernels_for(®, Backend::Metal, "ffn_down", loc);
}
let report = reg.seal();
assert!(report.violations.is_empty(), "{}", report.render());
}
#[test]
fn a_kind_cuda_cannot_run_is_recorded_as_leaving_the_gpu() {
use crate::kernel_registry::{op, Backend, Outcome};
let reg = crate::kernel_registry::Registry::new();
let loc = std::panic::Location::caller();
shaped(QuantKind::Q4_1, 64, 256).probe_kernels_for(®, Backend::Cuda, "ffn_down", loc);
let report = reg.seal();
assert!(
report.entries.iter().any(|e| e.key.backend == Backend::Cuda
&& e.key.op == op::GEMM_PREFILL
&& matches!(
e.outcome,
Outcome::Miss { fallback, .. } if fallback == "CPU apply_batch"
)),
"{}",
report.render()
);
}
#[test]
fn a_cuda_kind_with_a_matvec_also_has_a_gemm() {
for kind in QuantKind::ALL {
if cuda_matvec_kind_supported(*kind) {
assert!(
cuda_mul_mm_kind_supported(*kind),
"{kind:?} can be decoded on CUDA but not prefilled there, \
which decomposes a prefill into one matvec launch per position"
);
}
}
}
#[test]
fn an_f32_weight_is_recorded_without_being_a_violation() {
use crate::kernel_registry::Backend;
let reg = crate::kernel_registry::Registry::new();
let m = WeightMatrix::F32(Tensor::new(vec![0.0; 64 * 32], vec![64, 32]));
m.probe_kernels_for(
®,
Backend::Metal,
"moe_router",
std::panic::Location::caller(),
);
let report = reg.seal();
assert!(!report.misses.is_empty());
assert!(report.violations.is_empty(), "{}", report.render());
}
}
#[cfg(test)]
mod int_dot_default_tests {
use super::{IntDotShape, IntDotTier};
#[test]
fn the_matvec_half_is_taken_only_where_its_kernels_are() {
assert_eq!(
super::int_dot_tier_here().matvec,
cfg!(target_arch = "aarch64"),
"the matvec half is aarch64's (i8mm, interleave-8 NEON) and nowhere else; \
x86 measured 4x to 8.8x slower with it on"
);
}
#[test]
fn the_batch_half_is_taken_only_where_a_simd_gemm_answers_for_it() {
assert_eq!(
super::int_dot_tier_here().batch_gemm,
ferrox_quant::batch_gemm_is_accelerated(ferrox_quant::preferred_interleave())
&& cfg!(any(target_arch = "aarch64", target_arch = "x86_64")),
"the batch half must agree with the kernel probe, not with a written-down list"
);
}
#[test]
fn the_q5k_batch_path_is_taken_wherever_its_simd_gemm_exists() {
let interleave = ferrox_quant::q5_kx8_interleave();
if ferrox_quant::q5_kx8_gemm_uses_acts_x4(interleave) {
assert!(super::q5k_batch_takes_kx8(interleave));
}
assert!(super::q5k_batch_takes_kx8(interleave) || !cfg!(target_arch = "aarch64"));
}
#[test]
fn the_batch_probe_is_wider_than_the_interleave_8_one() {
for width in [4usize, 8] {
assert!(
ferrox_quant::batch_gemm_is_accelerated(width)
|| !ferrox_quant::interleaved_gemm_is_accelerated(width),
"the batch probe must answer yes wherever the interleave-8 one does"
);
}
#[cfg(target_arch = "aarch64")]
if std::arch::is_aarch64_feature_detected!("dotprod") {
assert!(
ferrox_quant::batch_gemm_is_accelerated(4),
"a dotprod host runs the width-4 sdot GEMM for Q4_K/Q5_K/Q8_0/Q4_0"
);
assert!(
!ferrox_quant::interleaved_gemm_is_accelerated(4),
"the interleave-8 predicate is about the quad kernels only"
);
}
}
#[test]
fn the_default_is_on_when_either_half_is_a_win() {
let tier = super::int_dot_tier_here();
assert_eq!(
super::int_dot_is_a_win_here(),
tier.matvec || tier.batch_gemm
);
}
#[test]
fn covers_answers_per_shape_rather_than_per_host() {
let matvec_only = IntDotTier {
matvec: true,
batch_gemm: false,
};
let batch_only = IntDotTier {
matvec: false,
batch_gemm: true,
};
assert!(matvec_only.covers(IntDotShape::Matvec));
assert!(!matvec_only.covers(IntDotShape::BatchGemm));
assert!(!batch_only.covers(IntDotShape::Matvec));
assert!(batch_only.covers(IntDotShape::BatchGemm));
}
}