use crate::k_quants::{GgmlType, QK_K};
use fuel_ir::quantized::GgmlDType;
use half::f16;
pub const QK4_NL: usize = 32;
pub const KVALUES_IQ4NL: [i8; 16] = [
-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113,
];
#[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct BlockIq4Nl {
pub(crate) d: f16,
pub(crate) qs: [u8; QK4_NL / 2],
}
const _: () = assert!(std::mem::size_of::<BlockIq4Nl>() == 2 + QK4_NL / 2);
#[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct BlockIq4Xs {
pub(crate) d: f16,
pub(crate) scales_h: u16,
pub(crate) scales_l: [u8; QK_K / 64],
pub(crate) qs: [u8; QK_K / 2],
}
const _: () = assert!(std::mem::size_of::<BlockIq4Xs>() == 2 + 2 + QK_K / 64 + QK_K / 2);
fn nearest_code(v: f32) -> u8 {
let mut best = 0u8;
let mut best_err = f32::INFINITY;
for (i, &k) in KVALUES_IQ4NL.iter().enumerate() {
let err = (v - f32::from(k)).abs();
if err < best_err {
best_err = err;
best = i as u8;
}
}
best
}
fn block_scale(xs: &[f32]) -> f32 {
let mut amax = 0f32;
let mut max = 0f32;
for &x in xs {
if amax < x.abs() {
amax = x.abs();
max = x;
}
}
max / f32::from(KVALUES_IQ4NL[0])
}
fn pack32(xs: &[f32], d: f32, qs: &mut [u8]) {
let id = if d != 0.0 { 1.0 / d } else { 0.0 };
for j in 0..16 {
qs[j] = nearest_code(xs[j] * id) | (nearest_code(xs[j + 16] * id) << 4);
}
}
impl GgmlType for BlockIq4Nl {
const DTYPE: GgmlDType = GgmlDType::Iq4Nl;
const BLCK_SIZE: usize = QK4_NL;
fn to_float(xs: &[Self], ys: &mut [f32]) {
for (b, y) in xs.iter().zip(ys.as_chunks_mut::<QK4_NL>().0.iter_mut()) {
let d = b.d.to_f32();
for j in 0..QK4_NL / 2 {
y[j] = d * f32::from(KVALUES_IQ4NL[(b.qs[j] & 0xf) as usize]);
y[j + QK4_NL / 2] = d * f32::from(KVALUES_IQ4NL[(b.qs[j] >> 4) as usize]);
}
}
}
fn from_float(xs: &[f32], ys: &mut [Self]) {
for (x, y) in xs.as_chunks::<QK4_NL>().0.iter().zip(ys.iter_mut()) {
let d = block_scale(x);
y.d = f16::from_f32(d);
pack32(x, y.d.to_f32(), &mut y.qs);
}
}
}
impl GgmlType for BlockIq4Xs {
const DTYPE: GgmlDType = GgmlDType::Iq4Xs;
const BLCK_SIZE: usize = QK_K;
fn to_float(xs: &[Self], ys: &mut [f32]) {
for (b, y) in xs.iter().zip(ys.as_chunks_mut::<QK_K>().0.iter_mut()) {
let d = b.d.to_f32();
for ib in 0..QK_K / 32 {
let ls = ((b.scales_l[ib / 2] >> (4 * (ib % 2))) & 0xf) as i32
| ((((b.scales_h >> (2 * ib)) & 3) as i32) << 4);
let dl = d * (ls - 32) as f32;
let qs = &b.qs[ib * 16..ib * 16 + 16];
let out = &mut y[ib * 32..ib * 32 + 32];
for j in 0..16 {
out[j] = dl * f32::from(KVALUES_IQ4NL[(qs[j] & 0xf) as usize]);
out[j + 16] = dl * f32::from(KVALUES_IQ4NL[(qs[j] >> 4) as usize]);
}
}
}
}
fn from_float(xs: &[f32], ys: &mut [Self]) {
for (x, y) in xs.as_chunks::<QK_K>().0.iter().zip(ys.iter_mut()) {
let mut sub = [0f32; QK_K / 32];
let mut dmax = 0f32;
for (ib, s) in sub.iter_mut().enumerate() {
*s = block_scale(&x[ib * 32..ib * 32 + 32]);
dmax = dmax.max(s.abs());
}
let d = f16::from_f32(dmax / 31.0);
let df = d.to_f32();
y.d = d;
y.scales_h = 0;
y.scales_l = [0; QK_K / 64];
for (ib, &s) in sub.iter().enumerate() {
let ls = if df != 0.0 {
((s / df).round() as i32).clamp(-32, 31) + 32
} else {
32
} as u16;
y.scales_l[ib / 2] |= ((ls & 0xf) as u8) << (4 * (ib % 2));
y.scales_h |= (ls >> 4) << (2 * ib);
let dl = df * (i32::from(ls) - 32) as f32;
pack32(
&x[ib * 32..ib * 32 + 32],
dl,
&mut y.qs[ib * 16..ib * 16 + 16],
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codebook_matches_ggml_common_h() {
let printed: [i8; 16] = [
-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113,
];
assert_eq!(KVALUES_IQ4NL, printed);
}
#[test]
fn iq4_nl_dequantizes_as_the_ggml_formula() {
let mut qs = [0u8; 16];
for (j, q) in qs.iter_mut().enumerate() {
*q = j as u8 | ((15 - j as u8) << 4);
}
let blk = BlockIq4Nl {
d: f16::from_f32(0.5),
qs,
};
let mut y = [0f32; 32];
BlockIq4Nl::to_float(&[blk], &mut y);
assert_eq!(y[0], -63.5);
assert_eq!(y[15], 56.5);
assert_eq!(y[16], 56.5); assert_eq!(y[31], -63.5); assert_eq!(y[8], 0.5); }
#[test]
fn iq4_xs_dequantizes_as_the_ggml_formula() {
let mut blk = BlockIq4Xs::zeros();
blk.d = f16::from_f32(2.0);
blk.scales_l[0] = 0xE1;
blk.scales_h = 0b0000_0110; blk.qs[0] = 0x80; blk.qs[16] = 0x0F; let mut y = vec![0f32; QK_K];
BlockIq4Xs::to_float(&[blk], &mut y);
assert_eq!(y[0], 2.0 * -127.0);
assert_eq!(y[16], 2.0 * 1.0);
assert_eq!(y[32], -4.0 * 113.0);
assert_eq!(y[48], -4.0 * -127.0);
}
fn wave(n: usize) -> Vec<f32> {
(0..n)
.map(|i| ((i as f32) * 0.37).sin() * 3.0 + ((i as f32) * 0.11).cos())
.collect()
}
#[test]
fn quantize_then_dequantize_stays_within_codebook_resolution() {
for sign in [1.0f32, -1.0] {
let x: Vec<f32> = wave(QK_K).into_iter().map(|v| v * sign).collect();
let amax = x.iter().fold(0f32, |m, v| m.max(v.abs()));
let mut nl = vec![BlockIq4Nl::zeros(); QK_K / QK4_NL];
BlockIq4Nl::from_float(&x, &mut nl);
let mut y = vec![0f32; QK_K];
BlockIq4Nl::to_float(&nl, &mut y);
let nl_err = x
.iter()
.zip(&y)
.fold(0f32, |m, (a, b)| m.max((a - b).abs()));
let mut xs = vec![BlockIq4Xs::zeros()];
BlockIq4Xs::from_float(&x, &mut xs);
BlockIq4Xs::to_float(&xs, &mut y);
let xs_err = x
.iter()
.zip(&y)
.fold(0f32, |m, (a, b)| m.max((a - b).abs()));
assert!(nl_err <= 0.14 * amax, "iq4_nl err {nl_err} vs amax {amax}");
assert!(xs_err <= 0.20 * amax, "iq4_xs err {xs_err} vs amax {amax}");
assert!(amax > 1.0);
}
}
#[test]
fn all_zero_input_quantizes_to_zero() {
let x = vec![0f32; QK_K];
let mut xs = vec![BlockIq4Xs::zeros()];
BlockIq4Xs::from_float(&x, &mut xs);
let mut y = vec![1f32; QK_K];
BlockIq4Xs::to_float(&xs, &mut y);
assert!(y.iter().all(|v| *v == 0.0));
}
#[test]
fn matmul_matches_the_dequantized_dot() {
let x = wave(QK_K);
let a: Vec<f32> = wave(QK_K).iter().map(|v| v * 0.5 + 0.1).collect();
let mut w = vec![BlockIq4Nl::zeros(); QK_K / QK4_NL];
BlockIq4Nl::from_float(&x, &mut w);
let mut wf = vec![0f32; QK_K];
BlockIq4Nl::to_float(&w, &mut wf);
let want: f32 = wf.iter().zip(&a).map(|(p, r)| p * r).sum();
let mut got = [0f32; 1];
crate::k_quants::matmul((1, QK_K, 1), &a, &w, &mut got).expect("matmul");
assert!(
(got[0] - want).abs() <= 1e-3 * want.abs().max(1.0),
"{} vs {want}",
got[0]
);
}
}