use std::path::Path;
use mlx_native::gguf::GgufFile;
use super::accumulator::{Accumulator, AccumulatorRegistry};
use super::error::ImatrixError;
use super::gguf_writer::{KV_KEY_CHUNK_COUNT, KV_KEY_CHUNK_SIZE, KV_KEY_TYPE, KV_VALUE_TYPE};
#[derive(Debug)]
pub struct LoadedImatrix {
pub source_path: String,
pub datasets: Vec<String>,
pub chunk_count: u32,
pub chunk_size: u32,
pub registry: AccumulatorRegistry,
}
impl LoadedImatrix {
pub fn load_from_path(path: &Path) -> Result<Self, ImatrixError> {
let source_path = path.display().to_string();
let gguf = GgufFile::open(path).map_err(|e| ImatrixError::Parse {
detail: format!("{source_path}: {e}"),
})?;
let actual_type = gguf.metadata_string(KV_KEY_TYPE).unwrap_or("");
if actual_type != KV_VALUE_TYPE {
return Err(ImatrixError::NotAnImatrix {
path: source_path,
actual: actual_type.to_string(),
});
}
let chunk_count =
gguf.metadata_u32(KV_KEY_CHUNK_COUNT)
.ok_or_else(|| ImatrixError::MissingKv {
path: source_path.clone(),
key: KV_KEY_CHUNK_COUNT,
})?;
let chunk_size =
gguf.metadata_u32(KV_KEY_CHUNK_SIZE)
.ok_or_else(|| ImatrixError::MissingKv {
path: source_path.clone(),
key: KV_KEY_CHUNK_SIZE,
})?;
let datasets: Vec<String> = match gguf.metadata("imatrix.datasets") {
Some(_) => extract_string_array(&gguf, "imatrix.datasets").unwrap_or_default(),
None => Vec::new(),
};
use std::collections::BTreeMap;
let mut halves: BTreeMap<String, (bool, bool)> = BTreeMap::new();
let mut registry = AccumulatorRegistry::new();
let names: Vec<String> = gguf.tensor_names().iter().map(|s| s.to_string()).collect();
for name in &names {
let stripped = match name.strip_suffix(".in_sum2") {
Some(s) => s,
None => continue,
};
halves
.entry(stripped.to_string())
.or_insert((false, false))
.0 = true;
let info = gguf
.tensor_info(name)
.expect("name from tensor_names is valid");
let (n_per_row, n_mat) = shape_to_n_per_row_and_n_mat(&info.shape);
let acc = registry.register(stripped, n_per_row, n_mat)?;
let payload = read_f32_tensor(path, &gguf, info).map_err(ImatrixError::from)?;
if payload.len() != acc.values.len() {
return Err(ImatrixError::Parse {
detail: format!(
"{source_path}: {name} payload length {} doesn't match \
expected n_per_row*n_mat={}",
payload.len(),
acc.values.len(),
),
});
}
acc.values = payload;
}
for name in &names {
let stripped = match name.strip_suffix(".counts") {
Some(s) => s,
None => continue,
};
halves
.entry(stripped.to_string())
.or_insert((false, false))
.1 = true;
let info = gguf
.tensor_info(name)
.expect("name from tensor_names is valid");
let n_mat = match info.shape.as_slice() {
[m, 1] => *m,
[m] => *m,
_ => {
return Err(ImatrixError::Parse {
detail: format!(
"{source_path}: counts tensor `{name}` has unexpected shape \
{:?}; expected [n_mat, 1] or [n_mat]",
info.shape
),
});
}
};
let acc =
registry
.get_mut(stripped)
.ok_or_else(|| ImatrixError::MismatchedTensorPair {
path: source_path.clone(),
name: stripped.to_string(),
})?;
if acc.n_mat != n_mat {
return Err(ImatrixError::Parse {
detail: format!(
"{source_path}: counts/in_sum2 disagree on n_mat for `{stripped}`: \
in_sum2={} vs counts={}",
acc.n_mat, n_mat
),
});
}
let payload = read_f32_tensor(path, &gguf, info).map_err(ImatrixError::from)?;
if payload.len() != n_mat {
return Err(ImatrixError::Parse {
detail: format!(
"{source_path}: counts tensor `{name}` payload length {} != n_mat={}",
payload.len(),
n_mat
),
});
}
acc.counts = payload.iter().map(|&v| v.round() as i64).collect();
}
for (stripped, (has_sum2, has_counts)) in halves.iter() {
if !(*has_sum2 && *has_counts) {
return Err(ImatrixError::MismatchedTensorPair {
path: source_path,
name: stripped.clone(),
});
}
}
Ok(LoadedImatrix {
source_path,
datasets,
chunk_count,
chunk_size,
registry,
})
}
pub fn accumulator(&self, weight_name: &str) -> Option<&Accumulator> {
self.registry.get(weight_name)
}
pub fn tensor_pair_count(&self) -> usize {
self.registry.len()
}
}
fn shape_to_n_per_row_and_n_mat(shape: &[usize]) -> (usize, usize) {
match shape {
[n_mat, n_per_row] => (*n_per_row, *n_mat),
[n_per_row] => (*n_per_row, 1),
_ => (shape.iter().product(), 1),
}
}
fn extract_string_array(_gguf: &GgufFile, _key: &str) -> Option<Vec<String>> {
None
}
fn read_f32_tensor(
path: &Path,
gguf: &GgufFile,
info: &mlx_native::gguf::TensorInfo,
) -> std::io::Result<Vec<f32>> {
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
let mut f = File::open(path)?;
let abs_offset = gguf.tensor_data_offset() + info.offset as u64;
f.seek(SeekFrom::Start(abs_offset))?;
let mut buf = vec![0u8; info.byte_len];
f.read_exact(&mut buf)?;
let n = info.byte_len / 4;
let mut out = Vec::with_capacity(n);
for chunk in buf.chunks_exact(4) {
let v = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
out.push(v);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::super::accumulator::AccumulatorRegistry;
use super::super::gguf_writer::write_imatrix;
use super::*;
use std::io::{Cursor, Write};
#[test]
fn round_trip_imatrix_file() {
let mut reg = AccumulatorRegistry::new();
let acc = reg.register("blk.0.attn_q.weight", 4, 1).unwrap();
acc.absorb_dense(&[1.0, 2.0, 3.0, 4.0]).unwrap();
acc.absorb_dense(&[2.0, 2.0, 2.0, 2.0]).unwrap();
let expected_values = acc.values.clone();
let expected_counts = acc.counts.clone();
let buf = Cursor::new(Vec::new());
let inner = write_imatrix(buf, ®, &["cdv3".to_string()], 1, 512).unwrap();
let bytes = inner.into_inner();
let tmp = tempfile::NamedTempFile::new().unwrap();
{
let mut f = std::fs::File::create(tmp.path()).unwrap();
f.write_all(&bytes).unwrap();
f.flush().unwrap();
}
let loaded = LoadedImatrix::load_from_path(tmp.path()).unwrap();
assert_eq!(loaded.chunk_count, 1);
assert_eq!(loaded.chunk_size, 512);
assert_eq!(loaded.tensor_pair_count(), 1);
let acc = loaded.accumulator("blk.0.attn_q.weight").unwrap();
assert_eq!(acc.n_per_row, 4);
assert_eq!(acc.n_mat, 1);
assert_eq!(acc.values, expected_values);
assert_eq!(acc.counts, expected_counts);
}
#[test]
fn round_trip_moe_imatrix_file() {
let mut reg = AccumulatorRegistry::new();
let acc = reg.register("blk.0.ffn_gate_exps.weight", 3, 4).unwrap();
acc.absorb_moe(0, &[1.0, 2.0, 3.0]).unwrap();
acc.absorb_moe(2, &[1.0, 1.0, 1.0]).unwrap();
acc.absorb_moe(2, &[2.0, 2.0, 2.0]).unwrap();
let expected_values = acc.values.clone();
let expected_counts = acc.counts.clone();
let buf = Cursor::new(Vec::new());
let inner = write_imatrix(buf, ®, &["cdv3".to_string()], 1, 512).unwrap();
let bytes = inner.into_inner();
let tmp = tempfile::NamedTempFile::new().unwrap();
{
let mut f = std::fs::File::create(tmp.path()).unwrap();
f.write_all(&bytes).unwrap();
f.flush().unwrap();
}
let loaded = LoadedImatrix::load_from_path(tmp.path()).unwrap();
let acc = loaded.accumulator("blk.0.ffn_gate_exps.weight").unwrap();
assert_eq!(acc.n_per_row, 3);
assert_eq!(acc.n_mat, 4);
assert_eq!(acc.values, expected_values);
assert_eq!(acc.counts, expected_counts);
}
#[test]
fn rejects_non_imatrix_gguf() {
use crate::backends::gguf::types::MetaValue;
use crate::backends::gguf::writer::GgufWriter;
use crate::quantize::ggml_quants::GgmlType;
let buf = Cursor::new(Vec::new());
let mut w = GgufWriter::new(buf);
w.write_header(0, 1).unwrap();
w.write_metadata_kv(
"general.type",
&MetaValue::String("not_imatrix".to_string()),
)
.unwrap();
w.pad_to_alignment().unwrap();
w.finalize().unwrap();
let bytes = w.into_inner().into_inner();
let tmp = tempfile::NamedTempFile::new().unwrap();
{
let mut f = std::fs::File::create(tmp.path()).unwrap();
f.write_all(&bytes).unwrap();
f.flush().unwrap();
}
let err = LoadedImatrix::load_from_path(tmp.path()).unwrap_err();
match err {
ImatrixError::NotAnImatrix { actual, .. } => {
assert_eq!(actual, "not_imatrix");
}
other => panic!("expected NotAnImatrix, got {other:?}"),
}
let _ = GgmlType::F32;
}
#[test]
fn rejects_missing_chunk_count() {
use crate::backends::gguf::types::MetaValue;
use crate::backends::gguf::writer::GgufWriter;
let buf = Cursor::new(Vec::new());
let mut w = GgufWriter::new(buf);
w.write_header(0, 2).unwrap();
w.write_metadata_kv("general.type", &MetaValue::String("imatrix".to_string()))
.unwrap();
w.write_metadata_kv("imatrix.chunk_size", &MetaValue::U32(512))
.unwrap();
w.pad_to_alignment().unwrap();
w.finalize().unwrap();
let bytes = w.into_inner().into_inner();
let tmp = tempfile::NamedTempFile::new().unwrap();
{
let mut f = std::fs::File::create(tmp.path()).unwrap();
f.write_all(&bytes).unwrap();
f.flush().unwrap();
}
let err = LoadedImatrix::load_from_path(tmp.path()).unwrap_err();
match err {
ImatrixError::MissingKv { key, .. } => assert_eq!(key, "imatrix.chunk_count"),
other => panic!("expected MissingKv, got {other:?}"),
}
}
#[test]
fn imatrix_real_load_round_trip_byte_cmp() {
let Some(ref_path) = std::env::var_os("HF2Q_IMATRIX_REAL_REF") else {
eprintln!(
"skip: HF2Q_IMATRIX_REAL_REF not set — provide path to a \
real .imatrix.gguf produced by stock llama-imatrix to run \
the byte-cmp acceptance gate"
);
return;
};
let ref_path = std::path::PathBuf::from(ref_path);
eprintln!("loading reference imatrix: {}", ref_path.display());
use crate::quantize::imatrix::ImatrixData;
let data1 = ImatrixData::load_from_path(&ref_path)
.expect("hf2q load_from_path must parse stock llama-imatrix output");
let loaded1 = &data1.loaded;
eprintln!(
" loaded {} tensor pairs, datasets={:?}, chunk_count={}, chunk_size={}",
loaded1.registry.len(),
loaded1.datasets,
loaded1.chunk_count,
loaded1.chunk_size,
);
assert!(
!loaded1.registry.is_empty(),
"reference imatrix must have at least one tensor"
);
assert!(
loaded1.chunk_count > 0,
"reference imatrix must have non-zero chunk_count"
);
let tmp = tempfile::NamedTempFile::new().unwrap();
data1
.write_gguf(tmp.path(), &loaded1.datasets)
.expect("hf2q write_gguf must succeed on data derived from stock ref");
let data2 = ImatrixData::load_from_path(tmp.path())
.expect("hf2q load_from_path must parse hf2q's own writer output");
let loaded2 = &data2.loaded;
assert_eq!(loaded2.datasets, loaded1.datasets);
assert_eq!(loaded2.chunk_count, loaded1.chunk_count);
assert_eq!(loaded2.chunk_size, loaded1.chunk_size);
assert_eq!(
loaded2.registry.len(),
loaded1.registry.len(),
"tensor count must match across round-trip"
);
for ((name1, acc1), (name2, acc2)) in loaded1.registry.iter().zip(loaded2.registry.iter()) {
assert_eq!(name1, name2, "tensor name mismatch");
assert_eq!(
acc1.n_per_row, acc2.n_per_row,
"tensor `{name1}` n_per_row mismatch"
);
assert_eq!(acc1.n_mat, acc2.n_mat, "tensor `{name1}` n_mat mismatch");
assert_eq!(
acc1.values, acc2.values,
"tensor `{name1}` in_sum2 payload (values) mismatch"
);
assert_eq!(
acc1.counts, acc2.counts,
"tensor `{name1}` counts payload mismatch"
);
}
eprintln!(
"byte-cmp PASS: hf2q load+write+reload matches stock llama-imatrix output \
({} tensor pairs, {} chunks)",
loaded1.registry.len(),
loaded1.chunk_count
);
}
#[test]
fn imatrix_risk2_cpu_vs_metal_numeric_diff() {
let (Some(cpu_path), Some(metal_path)) = (
std::env::var_os("HF2Q_IMATRIX_CPU_REF"),
std::env::var_os("HF2Q_IMATRIX_METAL_REF"),
) else {
eprintln!(
"skip: set HF2Q_IMATRIX_CPU_REF + HF2Q_IMATRIX_METAL_REF to run \
the Risk 2 numeric-divergence spike"
);
return;
};
use crate::quantize::imatrix::ImatrixData;
let cpu = ImatrixData::load_from_path(&std::path::PathBuf::from(&cpu_path))
.expect("load CPU ref");
let metal = ImatrixData::load_from_path(&std::path::PathBuf::from(&metal_path))
.expect("load Metal ref");
assert_eq!(
cpu.loaded.registry.len(),
metal.loaded.registry.len(),
"CPU + Metal must enumerate the same tensor set"
);
let mut worst_abs: f64 = 0.0;
let mut worst_rel: f64 = 0.0;
let mut worst_abs_tensor = String::new();
let mut worst_rel_tensor = String::new();
let mut total_elements: u64 = 0;
let mut nonzero_diff_elements: u64 = 0;
let mut rel_errs: Vec<f64> = Vec::new();
for ((name_c, acc_c), (name_m, acc_m)) in
cpu.loaded.registry.iter().zip(metal.loaded.registry.iter())
{
assert_eq!(name_c, name_m, "tensor name mismatch");
assert_eq!(
acc_c.values.len(),
acc_m.values.len(),
"tensor `{name_c}` values len differs"
);
for (v_c, v_m) in acc_c.values.iter().zip(acc_m.values.iter()) {
total_elements += 1;
let a = f64::from(*v_c);
let b = f64::from(*v_m);
let abs_err = (a - b).abs();
if abs_err > 0.0 {
nonzero_diff_elements += 1;
}
if abs_err > worst_abs {
worst_abs = abs_err;
worst_abs_tensor = name_c.to_string();
}
let denom = a.abs().max(b.abs()).max(1e-30);
let rel_err = abs_err / denom;
if rel_err > worst_rel {
worst_rel = rel_err;
worst_rel_tensor = name_c.to_string();
}
rel_errs.push(rel_err);
}
}
rel_errs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = rel_errs.len();
let p50 = rel_errs[n / 2];
let p90 = rel_errs[(n as f64 * 0.90) as usize];
let p99 = rel_errs[(n as f64 * 0.99) as usize];
let p999 = rel_errs[(n as f64 * 0.999) as usize];
let mean: f64 = rel_errs.iter().sum::<f64>() / n as f64;
eprintln!(
"Risk 2 numeric divergence (CPU vs Metal stock llama-imatrix):\n \
tensors compared: {}\n \
elements compared: {}\n \
elements with non-zero diff: {} ({:.3}%)\n \
worst abs error: {:.6e} on tensor `{}`\n \
worst rel error: {:.6e} on tensor `{}`\n \
rel-error distribution: mean {:.6e} p50 {:.6e} p90 {:.6e} p99 {:.6e} p99.9 {:.6e}",
cpu.loaded.registry.len(),
total_elements,
nonzero_diff_elements,
100.0 * nonzero_diff_elements as f64 / total_elements as f64,
worst_abs,
worst_abs_tensor,
worst_rel,
worst_rel_tensor,
mean,
p50,
p90,
p99,
p999,
);
}
}