use half::f16;
use rayon::prelude::*;
use super::common::{best_index_int8, GROUP_MAX_EPS};
pub const QK4_NL: usize = 32;
pub const BLOCK_BYTES: usize = 2 + QK4_NL / 2;
const KVALUES_IQ4NL: [i8; 16] = [
-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113,
];
fn quantize_block_iq4_nl(
x: &[f32], out: &mut [u8], values: &[i8], quant_weights: Option<&[f32]>, ntry: i32,
) {
debug_assert_eq!(x.len(), QK4_NL);
debug_assert_eq!(out.len(), BLOCK_BYTES);
let super_block_size = QK4_NL;
let block_size = QK4_NL;
let mut sigma2 = 0.0f32;
for &v in x.iter() {
sigma2 = v.mul_add(v, sigma2);
}
sigma2 *= 2.0 / (super_block_size as f32);
let mut q4 = [0u8; QK4_NL / 2];
let mut weight = [0.0f32; QK4_NL];
let mut l_buf = [0u8; QK4_NL];
let xb = x;
if let Some(qw) = quant_weights {
for j in 0..block_size {
weight[j] = qw[j] * (sigma2 + xb[j] * xb[j]).sqrt();
}
} else {
for j in 0..block_size {
weight[j] = xb[j] * xb[j];
}
}
let mut amax = 0.0f32;
let mut max = 0.0f32;
for j in 0..block_size {
let ax = xb[j].abs();
if ax > amax {
amax = ax;
max = xb[j];
}
}
let mut scale0 = 0.0f32;
let all_zero = amax < GROUP_MAX_EPS;
if !all_zero {
let mut d = if ntry > 0 {
-max / (values[0] as f32)
} else {
max / (values[0] as f32)
};
let mut id = 1.0 / d;
let mut sumqx = 0.0f32;
let mut sumq2 = 0.0f32;
for j in 0..block_size {
let al = id * xb[j];
let l = best_index_int8(values, al);
l_buf[j] = l as u8;
let q = values[l] as f32;
let w = weight[j];
sumqx = (w * q).mul_add(xb[j], sumqx);
sumq2 = (w * q).mul_add(q, sumq2);
}
d = if sumq2 > 0.0 { sumqx / sumq2 } else { 0.0 };
let mut best = d * sumqx;
let lo = -ntry;
let hi = ntry;
if lo <= hi {
for itry in lo..=hi {
id = ((itry as f32) + (values[0] as f32)) / max;
let mut sumqx_t = 0.0f32;
let mut sumq2_t = 0.0f32;
for j in 0..block_size {
let al = id * xb[j];
let l = best_index_int8(values, al);
let q = values[l] as f32;
let w = weight[j];
sumqx_t = (w * q).mul_add(xb[j], sumqx_t);
sumq2_t = (w * q).mul_add(q, sumq2_t);
}
if sumq2_t > 0.0 && sumqx_t * sumqx_t > best * sumq2_t {
d = sumqx_t / sumq2_t;
best = d * sumqx_t;
}
}
}
scale0 = d;
}
let d_final = scale0;
let dh = f16::from_f32(d_final);
if ntry > 0 {
let id = if scale0 != 0.0 { 1.0 / scale0 } else { 0.0 };
for j in 0..super_block_size {
l_buf[j] = best_index_int8(values, id * x[j]) as u8;
}
}
for j in 0..16 {
q4[j] = l_buf[j] | (l_buf[16 + j] << 4);
}
out[..2].copy_from_slice(&dh.to_le_bytes());
out[2..].copy_from_slice(&q4);
}
pub fn quantize(src: &[f32], n_per_row: usize, imatrix: Option<&[f32]>) -> Vec<u8> {
assert!(
n_per_row % QK4_NL == 0,
"n_per_row {} not multiple of QK4_NL {}",
n_per_row,
QK4_NL
);
assert!(
src.len() % n_per_row == 0,
"src len {} not multiple of n_per_row {}",
src.len(),
n_per_row
);
if let Some(im) = imatrix {
assert_eq!(
im.len(),
n_per_row,
"imatrix len {} must equal n_per_row {} (per-row weights)",
im.len(),
n_per_row,
);
}
let ntry: i32 = 7;
let nblock_per_row = n_per_row / QK4_NL;
let nrows = src.len() / n_per_row;
let row_bytes = nblock_per_row * BLOCK_BYTES;
let mut out = vec![0u8; nrows * row_bytes];
out.par_chunks_exact_mut(row_bytes)
.enumerate()
.for_each(|(row, row_dst)| {
let row_src = &src[row * n_per_row..(row + 1) * n_per_row];
for ibl in 0..nblock_per_row {
let xb = &row_src[ibl * QK4_NL..(ibl + 1) * QK4_NL];
let qw_block = imatrix.map(|im| &im[ibl * QK4_NL..(ibl + 1) * QK4_NL]);
let blk_out = &mut row_dst[ibl * BLOCK_BYTES..(ibl + 1) * BLOCK_BYTES];
quantize_block_iq4_nl(xb, blk_out, &KVALUES_IQ4NL, qw_block, ntry);
}
});
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn fixture_path(name: &str) -> PathBuf {
let manifest =
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set by cargo test");
PathBuf::from(manifest)
.join("tests/fixtures/ggml_quants")
.join(name)
}
fn read_f32s(name: &str) -> Vec<f32> {
let bytes = fs::read(fixture_path(name)).expect("read fixture");
assert!(
bytes.len() % 4 == 0,
"fixture {} not a multiple of 4 bytes",
name
);
let mut out = Vec::with_capacity(bytes.len() / 4);
for chunk in bytes.chunks_exact(4) {
out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
}
out
}
fn read_bytes(name: &str) -> Vec<u8> {
fs::read(fixture_path(name)).expect("read fixture")
}
fn make_imatrix(n: usize, seed: u32) -> Vec<f32> {
let mut state = seed;
(0..n)
.map(|_| {
state = state.wrapping_add(0x6D2B79F5);
let mut t = state;
t = (t ^ (t >> 15)).wrapping_mul(t | 1);
t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
let u = t ^ (t >> 14);
let v = (u as f32 / u32::MAX as f32) * 2.0 - 1.0;
v.abs() + 1e-3
})
.collect()
}
#[test]
fn byte_cmp_noim() {
let input = read_f32s("iq4_nl_64_noim_input.bin");
let expected = read_bytes("iq4_nl_64_noim_expected.bin");
let got = quantize(&input, 64, None);
assert_eq!(got.len(), expected.len(), "IQ4_NL noim length mismatch");
assert_eq!(got, expected, "IQ4_NL noim byte-cmp failed");
}
#[test]
fn byte_cmp_im() {
let input = read_f32s("iq4_nl_64_im_input.bin");
let expected = read_bytes("iq4_nl_64_im_expected.bin");
let imatrix = make_imatrix(64, 2);
let got = quantize(&input, 64, Some(&imatrix));
assert_eq!(got.len(), expected.len(), "IQ4_NL im length mismatch");
assert_eq!(got, expected, "IQ4_NL im byte-cmp failed");
}
#[test]
fn iq4_nl_all_zero_block_writes_0x88_qs_2026_05_21() {
let input = vec![0.0f32; 32];
let got = quantize(&input, 32, None);
assert_eq!(got.len(), 18, "IQ4_NL block is 18 bytes (2 f16 d + 16 qs)");
assert_eq!(got[0], 0, "d[0] = 0x00");
assert_eq!(got[1], 0, "d[1] = 0x00");
for i in 2..18 {
assert_eq!(
got[i],
0x88,
"qs[{}] should be 0x88 (packed L=8 from best_index_int8(0)), got 0x{:02x}",
i - 2,
got[i]
);
}
}
}