use burn::tensor::{Int, Tensor, backend::Backend};
pub const DEFAULT_Q4_GROUP_SIZE: usize = 32;
pub fn dequantize_q4<B: Backend>(
packed: Tensor<B, 2, Int>,
scales: Tensor<B, 2>,
group_size: usize,
) -> Tensor<B, 2> {
let [rows, packed_cols] = packed.dims();
let half = group_size / 2;
assert_eq!(
packed_cols % half,
0,
"packed width {packed_cols} not a multiple of half group {half}"
);
let groups_per_row = packed_cols / half;
let cols = groups_per_row * group_size;
let [s_rows, s_cols] = scales.dims();
assert_eq!(
(s_rows, s_cols),
(rows, groups_per_row),
"scales shape [{s_rows}, {s_cols}] does not match [rows, cols/group] = [{rows}, {groups_per_row}]"
);
let nibbles = packed.reshape([rows, groups_per_row, half]);
let lo = nibbles.clone().remainder_scalar(16); let hi = nibbles.div_scalar(16);
let nibbles = Tensor::cat(vec![lo, hi], 2).float();
let scales = scales
.unsqueeze_dim::<3>(2)
.expand([rows, groups_per_row, group_size]);
let w = (nibbles - 8.0) * scales;
w.reshape([rows, cols])
}
#[cfg(test)]
mod tests {
use super::*;
use burn::tensor::TensorData;
type B = burn::backend::NdArray<f32>;
fn reference_block(bytes: &[u8; 16], scale: f32) -> [f32; 32] {
let mut out = [0.0f32; 32];
for j in 0..16 {
out[j] = ((bytes[j] & 0x0F) as f32 - 8.0) * scale;
out[j + 16] = ((bytes[j] >> 4) as f32 - 8.0) * scale;
}
out
}
#[test]
fn dequantize_matches_scalar_reference() {
let device = Default::default();
let packed_bytes: Vec<i32> = (0..64).map(|i| ((i * 37 + 11) % 256) as i32).collect();
let scales: Vec<f32> = vec![0.5, -1.25, 2.0, 0.75];
let packed = Tensor::<B, 2, Int>::from_data(
TensorData::new(packed_bytes.clone(), [2, 32]),
&device,
);
let scales_t = Tensor::<B, 2>::from_data(TensorData::new(scales.clone(), [2, 2]), &device);
let w = dequantize_q4(packed, scales_t, DEFAULT_Q4_GROUP_SIZE);
let got: Vec<f32> = w.into_data().to_vec().unwrap();
let mut expected = Vec::with_capacity(128);
for row in 0..2 {
for g in 0..2 {
let mut block = [0u8; 16];
for j in 0..16 {
block[j] = packed_bytes[row * 32 + g * 16 + j] as u8;
}
expected.extend_from_slice(&reference_block(&block, scales[row * 2 + g]));
}
}
assert_eq!(got.len(), expected.len());
for (g, e) in got.iter().zip(expected.iter()) {
assert!((g - e).abs() < 1e-6, "got {g}, expected {e}");
}
}
#[test]
fn all_eights_pack_to_zero_weights() {
let device = Default::default();
let packed = Tensor::<B, 2, Int>::from_data(
TensorData::new(vec![0x88i32; 16], [1, 16]),
&device,
);
let scales = Tensor::<B, 2>::from_data(TensorData::new(vec![3.5f32], [1, 1]), &device);
let w = dequantize_q4(packed, scales, DEFAULT_Q4_GROUP_SIZE);
let got: Vec<f32> = w.into_data().to_vec().unwrap();
assert_eq!(got, vec![0.0; 32]);
}
}