use crate::q8_0_shader::{BLOCK_BYTES, BLOCK_ELEMS};
pub fn pack_words(bytes: &[u8]) -> Vec<u32> {
let mut words = Vec::with_capacity(bytes.len().div_ceil(4));
for chunk in bytes.chunks(4) {
let mut w = [0u8; 4];
w[..chunk.len()].copy_from_slice(chunk);
words.push(u32::from_le_bytes(w));
}
words
}
#[inline]
fn weight_byte(words: &[u32], k: usize) -> u32 {
(words[k >> 2] >> ((k & 3) * 8)) & 0xff
}
pub fn f16_to_f32(h: u32) -> f32 {
let sign = h >> 15;
let exp = (h >> 10) & 0x1f;
let mant = h & 0x3ff;
let mant_hi = mant << 13;
let normal = f32::from_bits(((exp + 112) << 23) | mant_hi);
let subnormal = mant as f32 * f32::from_bits(0x3380_0000);
let inf_or_nan = f32::from_bits(0x7f80_0000 | mant_hi);
let magnitude = if exp == 0 {
subnormal
} else if exp == 31 {
inf_or_nan
} else {
normal
};
if sign != 0 {
-magnitude
} else {
magnitude
}
}
pub fn matvec_reference(
weight_words: &[u32],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Vec<f32> {
let mut out = vec![0f32; rows];
for (row, y) in out.iter_mut().enumerate() {
let row_base = row * row_bytes;
let mut acc = 0f32;
for b in 0..n_blocks_per_row {
let off = row_base + b * BLOCK_BYTES;
let lo = weight_byte(weight_words, off);
let hi = weight_byte(weight_words, off + 1);
let scale = f16_to_f32(lo | (hi << 8));
let x_base = b * BLOCK_ELEMS;
let q_base = off + 2;
for j in 0..BLOCK_ELEMS {
let q_byte = weight_byte(weight_words, q_base + j);
let biased = (q_byte ^ 128).wrapping_sub(128);
let q = (biased as i32) as f32;
acc += scale * q * x[x_base + j];
}
}
*y = acc;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn pseudo_random(seed: u64, n: usize) -> Vec<f32> {
let mut s = seed | 1;
(0..n)
.map(|_| {
s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((s >> 33) as u32 as f32 / u32::MAX as f32) * 2.0 - 1.0
})
.collect()
}
fn build_rows(rows: usize, cols: usize, seed: u64) -> (Vec<u8>, Vec<Vec<f32>>) {
let mut bytes = Vec::new();
let mut dense = Vec::new();
for r in 0..rows {
let row = pseudo_random(seed + r as u64 * 7919, cols);
bytes.extend(ferrox_quant::quantize_q8_0(&row));
dense.push(row);
}
(bytes, dense)
}
#[test]
fn f16_decode_matches_half_crate_on_every_bit_pattern() {
let mut checked = 0u32;
for bits in 0..=u16::MAX {
let want = half::f16::from_bits(bits).to_f32();
let got = f16_to_f32(bits as u32);
if want.is_nan() {
assert!(got.is_nan(), "0x{bits:04x}: expected NaN, got {got}");
} else {
assert_eq!(
got.to_bits(),
want.to_bits(),
"0x{bits:04x}: {got} != {want}"
);
}
checked += 1;
}
assert_eq!(checked, 65_536);
}
#[test]
fn pack_words_zero_pads_a_partial_tail() {
assert_eq!(pack_words(&[1, 2, 3, 4]), vec![0x0403_0201]);
assert_eq!(pack_words(&[1, 2, 3]), vec![0x0003_0201]);
assert_eq!(pack_words(&[]), Vec::<u32>::new());
assert_eq!(pack_words(&[0u8; BLOCK_BYTES]).len(), 9);
}
#[test]
fn reference_matches_ferrox_quant_dequant_then_dot() {
for (rows, blocks) in [(1usize, 1usize), (5, 3), (64, 8), (7, 2)] {
let cols = blocks * BLOCK_ELEMS;
let (bytes, _) = build_rows(rows, cols, 0xfe11 + rows as u64);
let row_bytes = blocks * BLOCK_BYTES;
assert_eq!(bytes.len(), rows * row_bytes);
let x = pseudo_random(0xa5a5, cols);
let got = matvec_reference(&pack_words(&bytes), &x, rows, row_bytes, blocks);
for (r, g) in got.iter().enumerate() {
let dequantized =
ferrox_quant::dequant_q8_0(&bytes[r * row_bytes..(r + 1) * row_bytes]).unwrap();
let want: f32 = dequantized
.iter()
.zip(&x)
.fold(0f32, |acc, (w, xv)| acc + w * xv);
assert_eq!(
g.to_bits(),
want.to_bits(),
"rows={rows} blocks={blocks} row={r}: {g} != {want}"
);
}
}
}
#[test]
fn reference_is_correct_when_rows_are_not_word_aligned() {
let blocks = 3;
let cols = blocks * BLOCK_ELEMS;
let row_bytes = blocks * BLOCK_BYTES;
assert_eq!(row_bytes % 4, 2, "this test needs a misaligned row stride");
let rows = 9;
let (bytes, _) = build_rows(rows, cols, 0x0dd0);
let x = pseudo_random(0x1234, cols);
let got = matvec_reference(&pack_words(&bytes), &x, rows, row_bytes, blocks);
for (r, g) in got.iter().enumerate() {
let dequantized =
ferrox_quant::dequant_q8_0(&bytes[r * row_bytes..(r + 1) * row_bytes]).unwrap();
let want: f32 = dequantized
.iter()
.zip(&x)
.fold(0f32, |acc, (w, xv)| acc + w * xv);
assert_eq!(g.to_bits(), want.to_bits(), "row {r}");
}
}
#[test]
fn sign_extension_covers_the_whole_int8_range() {
for v in 0..=255u32 {
let got = ((v ^ 128).wrapping_sub(128) as i32) as f32;
let want = (v as u8 as i8) as f32;
assert_eq!(got, want, "byte {v}");
}
}
}