pub mod imatrix_input;
pub mod policy;
pub mod recipe;
#[cfg(test)]
mod recipe_golden;
use std::collections::BTreeMap;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use clap::Parser;
use frink_gguf::{GgmlType, GgufFile, GgufValue, GgufWriter, TensorPlan};
use rayon::prelude::*;
use imatrix_input::{imatrix_for_tensor, imatrix_metadata, load_imatrix};
use policy::{allows_quantization, disposition, parse_target, Disposition, Target};
use recipe::{ModelShape, Recipe};
const GGML_QNT_VERSION: u32 = 2;
const ROWS_PER_TASK: usize = 64;
#[derive(Parser, Debug)]
pub struct QuantizeArgs {
pub input: PathBuf,
pub output: Option<PathBuf>,
#[arg(long = "type", default_value = "Q8_0")]
pub ty: String,
#[arg(long)]
pub pure: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub imatrix: Option<PathBuf>,
}
pub(crate) struct Planned {
pub(crate) name: String,
pub(crate) shape: Vec<u64>,
source_dtype: GgmlType,
out_dtype: GgmlType,
source_bytes: usize,
out_bytes: usize,
copy_reason: Option<&'static str>,
}
pub fn run(args: QuantizeArgs) -> Result<()> {
let target = parse_target(&args.ty).map_err(|e| anyhow::anyhow!("{e}"))?;
let file =
GgufFile::open(&args.input).with_context(|| format!("opening {}", args.input.display()))?;
if file
.metadata_u64(frink_gguf::sharded::SPLIT_COUNT_KEY)
.is_some_and(|n| n > 1)
{
bail!(
"{} is one shard of a split GGUF. `frink quantize` writes a single file and has no \
--keep-split; merge the shards first, or quantize the unsplit source.",
args.input.display()
);
}
let output = args
.output
.clone()
.unwrap_or_else(|| default_output_path(&args.input, target));
if !args.dry_run {
if output == args.input {
bail!("output would overwrite the input ({})", output.display());
}
if output.exists() && !args.force {
bail!(
"{} already exists (pass --force to overwrite)",
output.display()
);
}
}
let planned = plan(&file, target, args.pure)?;
let imatrix = match &args.imatrix {
Some(p) => Some(load_imatrix(p)?),
None => None,
};
let mut metadata: BTreeMap<String, GgufValue> = file
.metadata
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
metadata.insert(
"general.file_type".to_string(),
GgufValue::U32(target.llama_ftype()),
);
metadata.insert(
"general.quantization_version".to_string(),
GgufValue::U32(GGML_QNT_VERSION),
);
if let (Some(im), Some(p)) = (&imatrix, &args.imatrix) {
imatrix_metadata(&mut metadata, im, p);
}
let src_total: u64 = planned.iter().map(|p| p.source_bytes as u64).sum();
let out_total: u64 = planned.iter().map(|p| p.out_bytes as u64).sum();
let n_quantized = planned.iter().filter(|p| p.copy_reason.is_none()).count();
println!(
"quantize: {} -> {}",
args.input.display(),
if args.dry_run {
"(dry run, nothing written)".to_string()
} else {
output.display().to_string()
}
);
println!(" target: {}", target.name());
for (i, p) in planned.iter().enumerate() {
match p.copy_reason {
Some(reason) => println!(
" [{:>4}/{}] {:<44} {:?} {:>10} B copy ({reason})",
i + 1,
planned.len(),
p.name,
p.source_dtype,
p.source_bytes
),
None => println!(
" [{:>4}/{}] {:<44} {:?} {:>10} B -> {:?} {:>10} B",
i + 1,
planned.len(),
p.name,
p.source_dtype,
p.source_bytes,
p.out_dtype,
p.out_bytes
),
}
}
println!(
" {n_quantized}/{} tensors quantized; {:.2} MiB -> {:.2} MiB ({:.2}x)",
planned.len(),
src_total as f64 / (1024.0 * 1024.0),
out_total as f64 / (1024.0 * 1024.0),
src_total as f64 / out_total.max(1) as f64,
);
if args.dry_run {
return Ok(());
}
let plan_entries: Vec<TensorPlan> = planned
.iter()
.map(|p| TensorPlan {
name: p.name.clone(),
shape: p.shape.clone(),
dtype: p.out_dtype,
byte_len: p.out_bytes,
})
.collect();
let out_file =
std::fs::File::create(&output).with_context(|| format!("creating {}", output.display()))?;
let mut writer = GgufWriter::create(
BufWriter::with_capacity(4 << 20, out_file),
&metadata,
plan_entries,
)?;
for p in &planned {
let src = file.tensor_bytes(&p.name)?;
if p.copy_reason.is_some() {
writer.write_tensor(&p.name, src)?;
} else {
let qw = match &imatrix {
Some(im) => imatrix_for_tensor(im, p)?,
None => None,
};
let encoded = encode_tensor(p, src, qw)?;
writer.write_tensor(&p.name, &encoded)?;
}
}
writer.finish()?.into_inner()?;
println!("wrote {}", output.display());
Ok(())
}
fn default_output_path(input: &Path, target: Target) -> PathBuf {
let stem = input
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "ggml-model".to_string());
input.with_file_name(format!("{stem}-{}.gguf", target.name()))
}
fn plan(file: &GgufFile, target: Target, pure: bool) -> Result<Vec<Planned>> {
let mut out = Vec::with_capacity(file.tensors.len());
let chosen_types = if pure {
BTreeMap::new()
} else {
let tensors: Vec<(String, Vec<u64>)> = file
.tensors
.iter()
.map(|t| (t.name.clone(), t.shape.clone()))
.collect();
Recipe::resolve_all(
target,
ModelShape::from_header(file),
&tensors,
|name, shape| allows_quantization(name, shape).is_none(),
)
};
for t in &file.tensors {
let source_bytes = t.byte_len().ok_or_else(|| {
anyhow::anyhow!(
"tensor '{}' has dtype {:?}, whose block layout this build does not know, so it \
cannot even be copied through",
t.name,
t.dtype
)
})?;
let copy_through = |reason: &'static str| Planned {
name: t.name.clone(),
shape: t.shape.clone(),
source_dtype: t.dtype,
out_dtype: t.dtype,
source_bytes,
out_bytes: source_bytes,
copy_reason: Some(reason),
};
if let Some(reason) = allows_quantization(&t.name, &t.shape) {
out.push(copy_through(reason));
continue;
}
let chosen = if pure {
target.ggml_type()
} else {
*chosen_types
.get(&t.name)
.expect("resolve_all and allows_quantization disagree about a tensor")
};
match disposition(t.dtype, chosen) {
Disposition::Copy(reason) => out.push(copy_through(reason)),
Disposition::Quantize(ty) => {
if !matches!(t.dtype, GgmlType::F32 | GgmlType::F16 | GgmlType::BF16) {
bail!(
"tensor '{}' is {:?}. `frink quantize` reads F32/F16/BF16 sources only: \
re-quantizing an already-quantized tensor stacks a second rounding on \
the first, and the result is worse than quantizing the original \
checkpoint once. Convert from the original weights instead.",
t.name,
t.dtype
);
}
let (block_bytes, block_elems) = ty.block_layout();
let n_cols = t.shape[0] as usize;
if block_elems == 0 || !n_cols.is_multiple_of(block_elems) {
bail!(
"tensor '{}' has {n_cols} columns, which is not a multiple of {:?}'s \
block size ({block_elems}). {}",
t.name,
ty,
target.fallback_note()
);
}
let n_elements = t.element_count().ok_or_else(|| {
anyhow::anyhow!("tensor '{}' declares an unrepresentable shape", t.name)
})?;
out.push(Planned {
name: t.name.clone(),
shape: t.shape.clone(),
source_dtype: t.dtype,
out_dtype: ty,
source_bytes,
out_bytes: n_elements / block_elems * block_bytes,
copy_reason: None,
});
}
}
}
Ok(out)
}
fn encode_tensor(p: &Planned, src: &[u8], imatrix: Option<&[f32]>) -> Result<Vec<u8>> {
let n_cols = p.shape[0] as usize;
let n_rows = (p.shape.iter().product::<u64>() as usize)
.checked_div(n_cols)
.unwrap_or(0);
let ne1 = p.shape.get(1).copied().unwrap_or(1).max(1) as usize;
let src_row_bytes = source_bytes_per_element(p.source_dtype) * n_cols;
let out_row_bytes = p.out_bytes / n_rows.max(1);
let groups: Vec<Vec<u8>> = (0..n_rows)
.collect::<Vec<_>>()
.par_chunks(ROWS_PER_TASK)
.map(|rows| {
let mut buf = Vec::with_capacity(rows.len() * out_row_bytes);
let mut scratch = vec![0f32; n_cols];
for &r in rows {
let row = &src[r * src_row_bytes..(r + 1) * src_row_bytes];
decode_source_row(p.source_dtype, row, &mut scratch)?;
let qw = imatrix.map(|im| {
let mat = r / ne1;
&im[mat * n_cols..(mat + 1) * n_cols]
});
encode_row(p.out_dtype, &scratch, qw, &mut buf)?.ok_or_else(|| {
anyhow::anyhow!(
"tensor '{}' row length {n_cols} is not a whole number of {:?} blocks",
p.name,
p.out_dtype
)
})?;
}
Ok(buf)
})
.collect::<Result<Vec<Vec<u8>>>>()?;
let mut out = Vec::with_capacity(p.out_bytes);
for g in groups {
out.extend_from_slice(&g);
}
debug_assert_eq!(out.len(), p.out_bytes);
Ok(out)
}
fn encode_row(
ty: GgmlType,
row: &[f32],
qw: Option<&[f32]>,
out: &mut Vec<u8>,
) -> Result<Option<()>> {
Ok(match ty {
GgmlType::Q8_0 => frink_quant::encode_row_q8_0(row, out),
GgmlType::Q4K => frink_quant::encode_row_q4_k(row, qw, out),
GgmlType::Q5K => frink_quant::encode_row_q5_k(row, qw, out),
GgmlType::Q6K => frink_quant::encode_row_q6_k(row, qw, out),
other => bail!(
"the mix chose {other:?} for a tensor and `frink quantize` has no encoder for it. \
This is a bug in the recipe table, not in the checkpoint: `plan` refuses an \
unwritable type before any byte is written."
),
})
}
fn source_bytes_per_element(dtype: GgmlType) -> usize {
match dtype {
GgmlType::F32 => 4,
GgmlType::F16 | GgmlType::BF16 => 2,
other => unreachable!("source dtype {other:?} reached the encoder"),
}
}
fn decode_source_row(dtype: GgmlType, row: &[u8], out: &mut [f32]) -> Result<()> {
match dtype {
GgmlType::F32 => {
for (o, c) in out.iter_mut().zip(row.as_chunks::<4>().0) {
*o = f32::from_le_bytes(*c);
}
}
GgmlType::F16 => {
for (o, c) in out.iter_mut().zip(row.as_chunks::<2>().0) {
*o = half::f16::from_le_bytes(*c).to_f32();
}
}
GgmlType::BF16 => {
for (o, c) in out.iter_mut().zip(row.as_chunks::<2>().0) {
*o = f32::from_bits(u32::from(u16::from_le_bytes(*c)) << 16);
}
}
other => bail!("source dtype {other:?} reached the encoder"),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"frink-quantize-{}-{}-{:?}",
tag,
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn args(input: &Path, output: &Path, ty: &str) -> QuantizeArgs {
QuantizeArgs {
input: input.to_path_buf(),
output: Some(output.to_path_buf()),
ty: ty.into(),
pure: false,
dry_run: false,
force: true,
imatrix: None,
}
}
fn write_f16_source(path: &Path, n_cols: usize) -> Vec<f32> {
let n = n_cols;
let values: Vec<f32> = (0..n * 2)
.map(|i| ((i as f32) * 0.037).sin() * 0.8)
.collect();
let f16_bytes = |vals: &[f32]| -> Vec<u8> {
vals.iter()
.flat_map(|v| half::f16::from_f32(*v).to_le_bytes())
.collect()
};
let mut metadata = BTreeMap::new();
metadata.insert(
"general.architecture".to_string(),
GgufValue::String("llama".into()),
);
metadata.insert("general.file_type".to_string(), GgufValue::U32(1));
let w = f16_bytes(&values);
let one_d = f16_bytes(&values[..n]);
let norm = f16_bytes(&values);
let gate = f16_bytes(&values);
let head = f16_bytes(&values);
let plan = vec![
TensorPlan {
name: "blk.0.attn_q.weight".into(),
shape: vec![n as u64, 2],
dtype: GgmlType::F16,
byte_len: w.len(),
},
TensorPlan {
name: "blk.0.attn_q.bias".into(),
shape: vec![n as u64],
dtype: GgmlType::F16,
byte_len: one_d.len(),
},
TensorPlan {
name: "blk.0.attn_norm.weight".into(),
shape: vec![n as u64, 2],
dtype: GgmlType::F16,
byte_len: norm.len(),
},
TensorPlan {
name: "blk.0.ffn_gate_inp.weight".into(),
shape: vec![n as u64, 2],
dtype: GgmlType::F16,
byte_len: gate.len(),
},
TensorPlan {
name: "output.weight".into(),
shape: vec![n as u64, 2],
dtype: GgmlType::F16,
byte_len: head.len(),
},
];
let f = std::fs::File::create(path).unwrap();
let mut wr = GgufWriter::create(BufWriter::new(f), &metadata, plan).unwrap();
wr.write_tensor("blk.0.attn_q.weight", &w).unwrap();
wr.write_tensor("blk.0.attn_q.bias", &one_d).unwrap();
wr.write_tensor("blk.0.attn_norm.weight", &norm).unwrap();
wr.write_tensor("blk.0.ffn_gate_inp.weight", &gate).unwrap();
wr.write_tensor("output.weight", &head).unwrap();
wr.finish().unwrap().into_inner().unwrap();
values
}
#[test]
fn an_f16_gguf_round_trips_through_quantize_and_reads_back_as_q8_0() {
let dir = tmp_dir("roundtrip");
let src = dir.join("src.gguf");
let dst = dir.join("dst.gguf");
let values = write_f16_source(&src, 64);
run(args(&src, &dst, "Q8_0")).unwrap();
let out = GgufFile::open(&dst).unwrap();
assert_eq!(out.metadata_u64("general.file_type"), Some(7));
assert_eq!(out.metadata_u64("general.quantization_version"), Some(2));
assert_eq!(out.metadata_str("general.architecture"), Some("llama"));
let q = out.find_tensor("blk.0.attn_q.weight").unwrap();
assert_eq!(q.dtype, GgmlType::Q8_0);
assert_eq!(q.shape, vec![64, 2]);
for kept in [
"blk.0.attn_q.bias",
"blk.0.attn_norm.weight",
"blk.0.ffn_gate_inp.weight",
] {
assert_eq!(
out.find_tensor(kept).unwrap().dtype,
GgmlType::F16,
"{kept} should have been kept at source precision"
);
}
let back =
frink_quant::dequant_q8_0(out.tensor_bytes("blk.0.attn_q.weight").unwrap()).unwrap();
assert_eq!(back.len(), values.len());
for (i, (&want, &have)) in values.iter().zip(back.iter()).enumerate() {
assert!((want - have).abs() < 0.01, "element {i}: {want} -> {have}");
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn the_quantized_tensor_bytes_are_what_the_encoder_produces_for_those_rows() {
let dir = tmp_dir("bytes");
let src = dir.join("src.gguf");
let dst = dir.join("dst.gguf");
let values = write_f16_source(&src, 64);
run(args(&src, &dst, "q8_0")).unwrap();
let mut want = Vec::new();
let f16_roundtrip: Vec<f32> = values
.iter()
.map(|v| half::f16::from_f32(*v).to_f32())
.collect();
for row in f16_roundtrip.chunks(64) {
frink_quant::encode_row_q8_0(row, &mut want).unwrap();
}
let out = GgufFile::open(&dst).unwrap();
assert_eq!(out.tensor_bytes("blk.0.attn_q.weight").unwrap(), &want[..]);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn asking_for_a_target_with_no_encoder_refuses_before_touching_the_filesystem() {
let dir = tmp_dir("refuse");
let src = dir.join("src.gguf");
let dst = dir.join("dst.gguf");
write_f16_source(&src, 64);
let err = run(args(&src, &dst, "Q3_K_M")).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("cannot WRITE Q3_K_M"), "{msg}");
assert!(msg.contains("Q8_0"), "{msg}");
assert!(!dst.exists(), "a refused run must not leave a file behind");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn an_already_quantized_source_tensor_is_refused_by_name() {
let dir = tmp_dir("requant");
let src = dir.join("src.gguf");
let bytes = vec![0u8; 144 * 2]; let plan = vec![TensorPlan {
name: "blk.0.attn_q.weight".into(),
shape: vec![256, 2],
dtype: GgmlType::Q4K,
byte_len: bytes.len(),
}];
let f = std::fs::File::create(&src).unwrap();
let mut wr = GgufWriter::create(BufWriter::new(f), &BTreeMap::new(), plan).unwrap();
wr.write_tensor("blk.0.attn_q.weight", &bytes).unwrap();
wr.finish().unwrap().into_inner().unwrap();
let err = run(QuantizeArgs {
dry_run: true,
..args(&src, &dir.join("dst.gguf"), "Q8_0")
})
.unwrap_err();
assert!(
err.to_string().contains("F32/F16/BF16 sources only"),
"{err}"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_row_length_that_is_not_a_multiple_of_the_block_size_is_refused() {
let dir = tmp_dir("ragged");
let src = dir.join("src.gguf");
let bytes = vec![0u8; 33 * 2 * 2];
let plan = vec![TensorPlan {
name: "blk.0.attn_q.weight".into(),
shape: vec![33, 2],
dtype: GgmlType::F16,
byte_len: bytes.len(),
}];
let f = std::fs::File::create(&src).unwrap();
let mut wr = GgufWriter::create(BufWriter::new(f), &BTreeMap::new(), plan).unwrap();
wr.write_tensor("blk.0.attn_q.weight", &bytes).unwrap();
wr.finish().unwrap().into_inner().unwrap();
let err = run(QuantizeArgs {
dry_run: true,
..args(&src, &dir.join("dst.gguf"), "Q8_0")
})
.unwrap_err();
assert!(err.to_string().contains("not a multiple of"), "{err}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn an_existing_output_is_not_overwritten_without_force() {
let dir = tmp_dir("clobber");
let src = dir.join("src.gguf");
let dst = dir.join("dst.gguf");
write_f16_source(&src, 64);
std::fs::write(&dst, b"precious").unwrap();
let err = run(QuantizeArgs {
force: false,
..args(&src, &dst, "Q8_0")
})
.unwrap_err();
assert!(err.to_string().contains("--force"), "{err}");
assert_eq!(std::fs::read(&dst).unwrap(), b"precious");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn the_q4_k_m_mix_promotes_the_output_head_and_pure_does_not() {
let dir = tmp_dir("mix");
let src = dir.join("src.gguf");
write_f16_source(&src, 256);
let mixed = dir.join("mixed.gguf");
run(args(&src, &mixed, "q4_k_m")).unwrap();
let pure = dir.join("pure.gguf");
run(QuantizeArgs {
pure: true,
..args(&src, &pure, "q4_k_m")
})
.unwrap();
let m = GgufFile::open(&mixed).unwrap();
let p = GgufFile::open(&pure).unwrap();
assert_eq!(m.find_tensor("output.weight").unwrap().dtype, GgmlType::Q6K);
assert_eq!(p.find_tensor("output.weight").unwrap().dtype, GgmlType::Q4K);
for f in [&m, &p] {
assert_eq!(
f.find_tensor("blk.0.attn_q.weight").unwrap().dtype,
GgmlType::Q4K
);
}
assert_eq!(m.metadata_u64("general.file_type"), Some(15));
assert_eq!(p.metadata_u64("general.file_type"), Some(15));
assert_ne!(
std::fs::read(&mixed).unwrap(),
std::fs::read(&pure).unwrap()
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn an_f16_gguf_round_trips_through_quantize_and_reads_back_as_q4_k() {
let dir = tmp_dir("q4k");
let src = dir.join("src.gguf");
let dst = dir.join("dst.gguf");
let values = write_f16_source(&src, 256);
run(QuantizeArgs {
pure: true,
..args(&src, &dst, "q4_k_s")
})
.unwrap();
let out = GgufFile::open(&dst).unwrap();
assert_eq!(out.metadata_u64("general.file_type"), Some(14));
let q = out.find_tensor("blk.0.attn_q.weight").unwrap();
assert_eq!(q.dtype, GgmlType::Q4K);
assert_eq!(q.shape, vec![256, 2]);
for kept in [
"blk.0.attn_q.bias",
"blk.0.attn_norm.weight",
"blk.0.ffn_gate_inp.weight",
] {
assert_eq!(
out.find_tensor(kept).unwrap().dtype,
GgmlType::F16,
"{kept} should have been kept at source precision"
);
}
let mut want = Vec::new();
let f16_roundtrip: Vec<f32> = values
.iter()
.map(|v| half::f16::from_f32(*v).to_f32())
.collect();
for row in f16_roundtrip.chunks(256) {
frink_quant::encode_row_q4_k(row, None, &mut want).unwrap();
}
assert_eq!(out.tensor_bytes("blk.0.attn_q.weight").unwrap(), &want[..]);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_row_too_narrow_for_a_super_block_is_refused_and_names_llama_cpps_fallback() {
let dir = tmp_dir("narrow");
let src = dir.join("src.gguf");
write_f16_source(&src, 64);
let err = run(QuantizeArgs {
pure: true,
dry_run: true,
..args(&src, &dir.join("dst.gguf"), "q4_k_s")
})
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("not a multiple of Q4K's block size (256)"),
"{msg}"
);
assert!(msg.contains("Q4_K -> Q5_0"), "{msg}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn an_imatrix_reaches_the_encoder_and_is_recorded_in_the_metadata() {
let dir = tmp_dir("imatrix");
let src = dir.join("src.gguf");
let values = write_f16_source(&src, 256);
let im_path = dir.join("im.gguf");
let mut stats = BTreeMap::new();
stats.insert(
"blk.0.attn_q.weight".to_string(),
crate::imatrix::file::Stats {
values: (0..256).map(|i| 1.0 + (i % 7) as f32).collect(),
counts: vec![4],
},
);
crate::imatrix::file::write(
&im_path,
crate::imatrix::file::OutputFormat::Gguf,
&stats,
&["calib.txt".to_string()],
2,
512,
)
.unwrap();
let plain = dir.join("plain.gguf");
run(QuantizeArgs {
pure: true,
..args(&src, &plain, "q4_k_s")
})
.unwrap();
let weighted = dir.join("weighted.gguf");
run(QuantizeArgs {
pure: true,
imatrix: Some(im_path.clone()),
..args(&src, &weighted, "q4_k_s")
})
.unwrap();
let qw: Vec<f32> = (0..256).map(|i| (1.0 + (i % 7) as f32) / 4.0).collect();
let mut want = Vec::new();
for row in values.chunks(256) {
let f16_row: Vec<f32> = row
.iter()
.map(|v| half::f16::from_f32(*v).to_f32())
.collect();
frink_quant::encode_row_q4_k(&f16_row, Some(&qw), &mut want).unwrap();
}
let w = GgufFile::open(&weighted).unwrap();
let p = GgufFile::open(&plain).unwrap();
assert_eq!(w.tensor_bytes("blk.0.attn_q.weight").unwrap(), &want[..]);
assert_ne!(
w.tensor_bytes("blk.0.attn_q.weight").unwrap(),
p.tensor_bytes("blk.0.attn_q.weight").unwrap()
);
assert_eq!(w.metadata_u64("quantize.imatrix.entries_count"), Some(1));
assert_eq!(w.metadata_u64("quantize.imatrix.chunks_count"), Some(2));
assert_eq!(
w.metadata_str("quantize.imatrix.dataset"),
Some("calib.txt")
);
assert!(p.metadata_str("quantize.imatrix.file").is_none());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn the_default_output_name_carries_the_target_and_sits_beside_the_input() {
assert_eq!(
default_output_path(Path::new("/m/Llama-3.2-1B-F16.gguf"), Target::Q8_0),
PathBuf::from("/m/Llama-3.2-1B-F16-Q8_0.gguf")
);
}
}