use kopitiam_core::{DType, Error, Result};
use crate::half::f16_to_f32;
pub(crate) fn dequantize(dtype: DType, bytes: &[u8]) -> Result<Vec<f32>> {
let block_bytes = dtype.block_bytes();
let block_size = dtype.block_size();
let num_blocks = bytes.len() / block_bytes;
let mut out = Vec::with_capacity(num_blocks * block_size);
let decode_block: fn(&[u8], &mut Vec<f32>) = match dtype {
DType::Q4_0 => dequant_q4_0,
DType::Q4_1 => dequant_q4_1,
DType::Q5_0 => dequant_q5_0,
DType::Q5_1 => dequant_q5_1,
DType::Q8_0 => dequant_q8_0,
_ => return Err(Error::UnsupportedDType { op: "dequantize", dtype }),
};
for block in bytes.chunks_exact(block_bytes) {
decode_block(block, &mut out);
}
Ok(out)
}
fn read_f16(bytes: &[u8], offset: usize) -> f32 {
f16_to_f32(u16::from_le_bytes([bytes[offset], bytes[offset + 1]]))
}
fn dequant_q4_0(block: &[u8], out: &mut Vec<f32>) {
let d = read_f16(block, 0);
let qs = &block[2..18];
let mut values = [0f32; 32];
for (j, &byte) in qs.iter().enumerate() {
values[j] = (i32::from(byte & 0x0F) - 8) as f32 * d;
values[j + 16] = (i32::from(byte >> 4) - 8) as f32 * d;
}
out.extend_from_slice(&values);
}
fn dequant_q4_1(block: &[u8], out: &mut Vec<f32>) {
let d = read_f16(block, 0);
let m = read_f16(block, 2);
let qs = &block[4..20];
let mut values = [0f32; 32];
for (j, &byte) in qs.iter().enumerate() {
values[j] = f32::from(byte & 0x0F) * d + m;
values[j + 16] = f32::from(byte >> 4) * d + m;
}
out.extend_from_slice(&values);
}
fn dequant_q5_0(block: &[u8], out: &mut Vec<f32>) {
let d = read_f16(block, 0);
let qh = u32::from_le_bytes([block[2], block[3], block[4], block[5]]);
let qs = &block[6..22];
let mut values = [0f32; 32];
for (j, &byte) in qs.iter().enumerate() {
let high_0 = (((qh >> j) << 4) & 0x10) as u8;
let high_1 = ((qh >> (j + 12)) & 0x10) as u8;
values[j] = (i32::from((byte & 0x0F) | high_0) - 16) as f32 * d;
values[j + 16] = (i32::from((byte >> 4) | high_1) - 16) as f32 * d;
}
out.extend_from_slice(&values);
}
fn dequant_q5_1(block: &[u8], out: &mut Vec<f32>) {
let d = read_f16(block, 0);
let m = read_f16(block, 2);
let qh = u32::from_le_bytes([block[4], block[5], block[6], block[7]]);
let qs = &block[8..24];
let mut values = [0f32; 32];
for (j, &byte) in qs.iter().enumerate() {
let high_0 = (((qh >> j) << 4) & 0x10) as u8;
let high_1 = ((qh >> (j + 12)) & 0x10) as u8;
values[j] = f32::from((byte & 0x0F) | high_0) * d + m;
values[j + 16] = f32::from((byte >> 4) | high_1) * d + m;
}
out.extend_from_slice(&values);
}
fn dequant_q8_0(block: &[u8], out: &mut Vec<f32>) {
let d = read_f16(block, 0);
let qs = &block[2..34];
out.extend(qs.iter().map(|&b| f32::from(b as i8) * d));
}
pub(crate) fn quantize_row_q8_0(x: &[f32]) -> (Vec<f32>, Vec<i8>) {
debug_assert!(x.len().is_multiple_of(32), "quantize_row_q8_0 requires a whole number of 32-element blocks");
let num_blocks = x.len() / 32;
let mut scales = Vec::with_capacity(num_blocks);
let mut q = vec![0i8; x.len()];
for (b, chunk) in x.chunks_exact(32).enumerate() {
let amax = chunk.iter().fold(0f32, |m, &v| m.max(v.abs()));
let d = amax / 127.0;
let id = if d != 0.0 { 1.0 / d } else { 0.0 };
for (j, &v) in chunk.iter().enumerate() {
q[b * 32 + j] = (v * id).round().clamp(-127.0, 127.0) as i8;
}
scales.push(d);
}
(scales, q)
}
pub(crate) fn q4_0_dot_q8_0(weight_block: &[u8], x_q: &[i8], x_scale: f32) -> f32 {
debug_assert_eq!(weight_block.len(), DType::Q4_0.block_bytes());
debug_assert_eq!(x_q.len(), 32);
let d_w = read_f16(weight_block, 0);
let qs = &weight_block[2..18];
let mut acc: i32 = 0;
for (j, &byte) in qs.iter().enumerate() {
let w_lo = i32::from(byte & 0x0F) - 8;
let w_hi = i32::from(byte >> 4) - 8;
acc += w_lo * i32::from(x_q[j]);
acc += w_hi * i32::from(x_q[j + 16]);
}
acc as f32 * d_w * x_scale
}
pub(crate) fn q8_0_dot_q8_0(weight_block: &[u8], x_q: &[i8], x_scale: f32) -> f32 {
debug_assert_eq!(weight_block.len(), DType::Q8_0.block_bytes());
debug_assert_eq!(x_q.len(), 32);
let d_w = read_f16(weight_block, 0);
let qs = &weight_block[2..34];
let mut acc: i32 = 0;
for (j, &byte) in qs.iter().enumerate() {
acc += i32::from(byte as i8) * i32::from(x_q[j]);
}
acc as f32 * d_w * x_scale
}
#[cfg(test)]
mod tests {
use super::*;
fn q4_0_block(d_bits: u16, values: [i8; 32]) -> Vec<u8> {
let mut block = vec![0u8; 18];
block[0..2].copy_from_slice(&d_bits.to_le_bytes());
for j in 0..16 {
let low = (values[j] + 8) as u8 & 0x0F;
let high = (values[j + 16] + 8) as u8 & 0x0F;
block[2 + j] = low | (high << 4);
}
block
}
#[test]
fn q4_0_decodes_known_nibbles_with_the_i_and_i_plus_16_byte_sharing_rule() {
let mut values = [0i8; 32];
for (j, v) in values.iter_mut().enumerate() {
*v = ((j % 16) as i8) - 8;
}
let block = q4_0_block(0x4000, values);
let mut out = Vec::new();
dequant_q4_0(&block, &mut out);
assert_eq!(out.len(), 32);
for j in 0..32 {
assert_eq!(out[j], f32::from(values[j]) * 2.0, "mismatch at index {j}");
}
let byte0 = block[2];
assert_eq!((byte0 & 0x0F) as i32 - 8, i32::from(values[0]));
assert_eq!((byte0 >> 4) as i32 - 8, i32::from(values[16]));
}
#[test]
fn q4_0_all_zero_block_decodes_to_all_zero() {
let block = q4_0_block(0x4000, [0i8; 32]); let mut out = Vec::new();
dequant_q4_0(&block, &mut out);
assert!(out.iter().all(|&v| v == 0.0));
}
fn q4_1_block(d_bits: u16, m_bits: u16, values: [u8; 32]) -> Vec<u8> {
let mut block = vec![0u8; 20];
block[0..2].copy_from_slice(&d_bits.to_le_bytes());
block[2..4].copy_from_slice(&m_bits.to_le_bytes());
for j in 0..16 {
block[4 + j] = (values[j] & 0x0F) | ((values[j + 16] & 0x0F) << 4);
}
block
}
#[test]
fn q4_1_decodes_with_scale_and_min() {
let mut values = [0u8; 32];
for (j, v) in values.iter_mut().enumerate() {
*v = (j % 16) as u8;
}
let block = q4_1_block(0x3C00, 0x4900, values);
let mut out = Vec::new();
dequant_q4_1(&block, &mut out);
for j in 0..32 {
let expected = f32::from(values[j]) * 1.0 + 10.0;
assert_eq!(out[j], expected, "mismatch at index {j}");
}
}
fn q5_0_block(d_bits: u16, values: [i8; 32]) -> Vec<u8> {
let mut block = vec![0u8; 22];
block[0..2].copy_from_slice(&d_bits.to_le_bytes());
let mut qh: u32 = 0;
for j in 0..16 {
let raw_low = (values[j] + 16) as u8; let raw_high = (values[j + 16] + 16) as u8;
if raw_low & 0x10 != 0 {
qh |= 1 << j;
}
if raw_high & 0x10 != 0 {
qh |= 1 << (j + 16);
}
}
block[2..6].copy_from_slice(&qh.to_le_bytes());
for j in 0..16 {
let raw_low = ((values[j] + 16) as u8) & 0x0F;
let raw_high = ((values[j + 16] + 16) as u8) & 0x0F;
block[6 + j] = raw_low | (raw_high << 4);
}
block
}
#[test]
fn q5_0_decodes_the_5th_bit_from_the_qh_field() {
let mut values = [0i8; 32];
for (j, v) in values.iter_mut().enumerate() {
*v = ((j % 32) as i8) - 16;
}
let block = q5_0_block(0x3C00, values);
let mut out = Vec::new();
dequant_q5_0(&block, &mut out);
for j in 0..32 {
assert_eq!(out[j], f32::from(values[j]), "mismatch at index {j}");
}
}
fn q5_1_block(d_bits: u16, m_bits: u16, values: [u8; 32]) -> Vec<u8> {
let mut block = vec![0u8; 24];
block[0..2].copy_from_slice(&d_bits.to_le_bytes());
block[2..4].copy_from_slice(&m_bits.to_le_bytes());
let mut qh: u32 = 0;
for j in 0..16 {
if values[j] & 0x10 != 0 {
qh |= 1 << j;
}
if values[j + 16] & 0x10 != 0 {
qh |= 1 << (j + 16); }
}
block[4..8].copy_from_slice(&qh.to_le_bytes());
for j in 0..16 {
block[8 + j] = (values[j] & 0x0F) | ((values[j + 16] & 0x0F) << 4);
}
block
}
#[test]
fn q5_1_decodes_with_scale_min_and_5th_bit() {
let mut values = [0u8; 32];
for (j, v) in values.iter_mut().enumerate() {
*v = (j % 32) as u8;
}
let block = q5_1_block(0x3C00, 0x3800, values); let mut out = Vec::new();
dequant_q5_1(&block, &mut out);
for j in 0..32 {
let expected = f32::from(values[j]) * 1.0 + 0.5;
assert_eq!(out[j], expected, "mismatch at index {j}");
}
}
fn q8_0_block(d_bits: u16, values: [i8; 32]) -> Vec<u8> {
let mut block = vec![0u8; 34];
block[0..2].copy_from_slice(&d_bits.to_le_bytes());
for (j, &v) in values.iter().enumerate() {
block[2 + j] = v as u8;
}
block
}
#[test]
fn q8_0_decodes_signed_bytes_scaled() {
let mut values = [0i8; 32];
for (j, v) in values.iter_mut().enumerate() {
*v = (j as i8) - 16; }
let block = q8_0_block(0x4000, values); let mut out = Vec::new();
dequant_q8_0(&block, &mut out);
for j in 0..32 {
assert_eq!(out[j], f32::from(values[j]) * 2.0, "mismatch at index {j}");
}
}
#[test]
fn q8_0_extreme_values_do_not_overflow_i8() {
let block = q8_0_block(0x3C00, [i8::MIN, i8::MAX].repeat(16).try_into().unwrap());
let mut out = Vec::new();
dequant_q8_0(&block, &mut out);
assert_eq!(out[0], f32::from(i8::MIN));
assert_eq!(out[1], f32::from(i8::MAX));
}
#[test]
fn dequantize_dispatches_by_dtype_and_handles_multiple_blocks() {
let block_a = q8_0_block(0x3C00, [1i8; 32]); let block_b = q8_0_block(0x4000, [2i8; 32]); let mut bytes = block_a;
bytes.extend(block_b);
let out = dequantize(DType::Q8_0, &bytes).unwrap();
assert_eq!(out.len(), 64);
assert!(out[0..32].iter().all(|&v| v == 1.0));
assert!(out[32..64].iter().all(|&v| v == 4.0));
}
#[test]
fn quantize_row_q8_0_round_trips_within_one_quantization_step() {
let x: Vec<f32> = (0..32).map(|j| (j as f32 - 16.0) * 0.37).collect();
let (scales, q) = quantize_row_q8_0(&x);
assert_eq!(scales.len(), 1);
let amax = x.iter().fold(0f32, |m, &v| m.max(v.abs()));
let d = amax / 127.0;
assert_eq!(scales[0], d);
for (j, &orig) in x.iter().enumerate() {
let decoded = f32::from(q[j]) * d;
assert!((decoded - orig).abs() <= d / 2.0 + 1e-6, "index {j}: {decoded} vs {orig} (d={d})");
}
}
#[test]
fn quantize_row_q8_0_all_zero_block_has_zero_scale_and_zero_values() {
let (scales, q) = quantize_row_q8_0(&[0.0f32; 32]);
assert_eq!(scales, vec![0.0]);
assert!(q.iter().all(|&v| v == 0));
}
#[test]
fn quantize_row_q8_0_handles_multiple_blocks_independently() {
let mut x = vec![1.0f32; 32];
x.extend(vec![100.0f32; 32]);
let (scales, q) = quantize_row_q8_0(&x);
assert_eq!(scales.len(), 2);
assert_ne!(scales[0], scales[1]);
assert_eq!(q[0], 127);
assert_eq!(q[32], 127);
}
#[test]
fn quantize_row_q8_0_extreme_magnitude_does_not_overflow_i8() {
let x = [f32::MAX, -f32::MAX].repeat(16);
let (_scales, q) = quantize_row_q8_0(&x);
assert!(q.iter().all(|&v| v == 127 || v == -127));
}
#[test]
fn q4_0_dot_q8_0_matches_dequantize_then_dot() {
let mut w_values = [0i8; 32];
for (j, v) in w_values.iter_mut().enumerate() {
*v = ((j % 16) as i8) - 8;
}
let w_block = q4_0_block(0x3C00, w_values);
let x: Vec<f32> = (0..32).map(|j| ((j as f32) - 16.0) * 0.5).collect();
let (x_scales, x_q) = quantize_row_q8_0(&x);
let fused = q4_0_dot_q8_0(&w_block, &x_q, x_scales[0]);
let mut w_decoded = Vec::new();
dequant_q4_0(&w_block, &mut w_decoded);
let x_decoded: Vec<f32> = x_q.iter().map(|&q| f32::from(q) * x_scales[0]).collect();
let reference: f32 = w_decoded.iter().zip(&x_decoded).map(|(a, b)| a * b).sum();
assert!((fused - reference).abs() < 1e-3, "fused={fused}, reference={reference}");
}
#[test]
fn q8_0_dot_q8_0_matches_dequantize_then_dot() {
let mut w_values = [0i8; 32];
for (j, v) in w_values.iter_mut().enumerate() {
*v = ((j as i32 * 7) % 256 - 128) as i8;
}
let w_block = q8_0_block(0x3800, w_values);
let x: Vec<f32> = (0..32).map(|j| (j as f32 - 16.0) * 0.5).collect();
let (x_scales, x_q) = quantize_row_q8_0(&x);
let fused = q8_0_dot_q8_0(&w_block, &x_q, x_scales[0]);
let mut w_decoded = Vec::new();
dequant_q8_0(&w_block, &mut w_decoded);
let x_decoded: Vec<f32> = x_q.iter().map(|&q| f32::from(q) * x_scales[0]).collect();
let reference: f32 = w_decoded.iter().zip(&x_decoded).map(|(a, b)| a * b).sum();
assert!((fused - reference).abs() < 1e-3, "fused={fused}, reference={reference}");
}
#[test]
fn q4_0_dot_q8_0_of_an_all_zero_weight_block_is_zero() {
let w_block = q4_0_block(0x3C00, [0i8; 32]); let x = vec![5.0f32; 32];
let (x_scales, x_q) = quantize_row_q8_0(&x);
assert_eq!(q4_0_dot_q8_0(&w_block, &x_q, x_scales[0]), 0.0);
}
#[test]
fn q4_0_nibble_zero_decodes_to_minus_eight_not_zero() {
let w_block = q4_0_block(0x3C00, [-8i8; 32]); let x = vec![5.0f32; 32];
let (x_scales, x_q) = quantize_row_q8_0(&x);
let dot = q4_0_dot_q8_0(&w_block, &x_q, x_scales[0]);
assert!((dot - -1280.0).abs() < 1e-2, "expected 32 * -8 * 5 = -1280, got {dot}");
}
}