use anyhow::{Context, Result, bail, ensure};
use inferencelayer::gguf_write::{GgufWriter, WVal, encode_ggml_im};
fn type_id(name: &str) -> Result<u32> {
Ok(match name.to_ascii_lowercase().as_str() {
"f32" => 0,
"f16" => 1,
"q4_0" => 2,
"q5_0" => 6,
"q8_0" => 8,
"q4_k" => 12,
"q5_k" => 13,
"q6_k" => 14,
"iq4_nl" => 20,
"iq4_xs" => 23,
"iq2_xs" => 17,
"iq3_xxs" => 18,
"iq3_s" => 21,
other => bail!("unknown type {other}"),
})
}
fn type_name(id: u32) -> &'static str {
match id {
0 => "F32",
1 => "F16",
2 => "Q4_0",
6 => "Q5_0",
8 => "Q8_0",
12 => "Q4_K",
13 => "Q5_K",
14 => "Q6_K",
17 => "IQ2_XS",
18 => "IQ3_XXS",
21 => "IQ3_S",
20 => "IQ4_NL",
23 => "IQ4_XS",
_ => "?",
}
}
fn preset_type(preset: &str, gguf_name: &str) -> Result<u32> {
Ok(match preset {
"f16" => 1,
"q8_0" => 8,
"q4_0" => 2,
"q5_0" => 6,
"q4_k_s" => 12,
"q6_k" => 14,
"iq4_xs" => 23,
"iq3_xxs" => 18,
"iq2_xs" => 17,
"q5_k_m" => {
if gguf_name.contains("attn_v") || gguf_name.contains("ffn_down") {
14
} else {
13
}
}
"q4_k_m" => {
if gguf_name.contains("attn_v") || gguf_name.contains("ffn_down") {
14
} else {
12
}
}
other => bail!("unknown preset {other}"),
})
}
fn fallback(ty: u32, cols: usize) -> u32 {
if matches!(ty, 12 | 13 | 14 | 17 | 18 | 21 | 23) && cols % 256 != 0 {
match ty {
14 => 8,
17 | 18 | 21 | 23 => 20,
_ => 6,
}
} else {
ty
}
}
fn main() -> Result<()> {
let mut model = String::new();
let mut out = String::new();
let mut preset = "q4_k_m".to_string();
let mut overrides: Vec<(String, u32)> = Vec::new();
let mut imatrix_path = String::new();
let mut tok_pre = "gpt-2".to_string();
let mut it = std::env::args().skip(1);
while let Some(a) = it.next() {
match a.as_str() {
"--model" => model = it.next().context("--model value")?,
"--out" => out = it.next().context("--out value")?,
"--preset" => preset = it.next().context("--preset value")?,
"--tokenizer-pre" => tok_pre = it.next().context("--tokenizer-pre value")?,
"--imatrix" => imatrix_path = it.next().context("--imatrix value")?,
"--tensor-type" => {
let kv = it.next().context("--tensor-type value")?;
let (k, v) = kv.split_once('=').context("--tensor-type substr=type")?;
overrides.push((k.to_string(), type_id(v)?));
}
other => bail!("unknown argument {other}"),
}
}
ensure!(!model.is_empty() && !out.is_empty(), "--model and --out required");
if preset.contains(',') {
let presets: Vec<String> = preset.split(',').map(|x| x.trim().to_string()).collect();
let base = std::path::Path::new(&out);
let stem = base.file_stem().and_then(|x| x.to_str()).unwrap_or("out");
let dir = base.parent().unwrap_or(std::path::Path::new("."));
for p in &presets {
let po = dir.join(format!("{stem}-{p}.gguf"));
let mut args = vec![
std::env::args().next().unwrap_or_default(),
"--model".into(), model.clone(),
"--out".into(), po.to_string_lossy().into_owned(),
"--preset".into(), p.clone(),
"--tokenizer-pre".into(), tok_pre.clone(),
];
if !imatrix_path.is_empty() {
args.push("--imatrix".into());
args.push(imatrix_path.clone());
}
for (k, v) in &overrides {
args.push("--tensor-type".into());
args.push(format!("{k}={}", type_name(*v).to_ascii_lowercase()));
}
let st = std::process::Command::new(std::env::current_exe()?)
.args(&args[1..])
.status()?;
ensure!(st.success(), "preset {p} failed");
}
return Ok(());
}
let imatrix: std::collections::HashMap<String, Vec<f32>> = if imatrix_path.is_empty() {
Default::default()
} else {
serde_json::from_slice(&std::fs::read(&imatrix_path).context("read --imatrix")?)
.context("parse --imatrix json")?
};
let dir = std::path::Path::new(&model);
let cfg_raw: serde_json::Value =
serde_json::from_slice(&std::fs::read(dir.join("config.json"))?)?;
let cfg = inferencelayer::weights::Lfm2Config::from_json(&std::fs::read(
dir.join("config.json"),
)?)?;
use inferencelayer::weights::Arch;
let (arch_name, is_llama) = match cfg.arch {
Arch::Llama => ("llama", true),
Arch::Qwen2 => ("qwen2", false),
other => bail!("gguf-quantize handles llama/qwen2 checkpoints (got {other:?})"),
};
let st = inferencelayer::weights::lazyst_open_for_tools(dir)?;
let (h, hd, nh, nkv) = (cfg.hidden, cfg.head_dim, cfg.n_heads, cfg.n_kv_heads);
let mut g = GgufWriter::new();
g.kv("general.architecture", WVal::Str(arch_name.into()));
g.kv("general.name", WVal::Str("inferencelayer".into()));
let k = |s: &str| format!("{arch_name}.{s}");
g.kv(&k("embedding_length"), WVal::U32(h as u32));
g.kv(&k("block_count"), WVal::U32(cfg.n_layers as u32));
g.kv(&k("attention.head_count"), WVal::U32(nh as u32));
g.kv(&k("attention.head_count_kv"), WVal::U32(nkv as u32));
g.kv(&k("feed_forward_length"), WVal::U32(cfg.intermediate as u32));
g.kv(&k("attention.layer_norm_rms_epsilon"), WVal::F32(cfg.eps));
g.kv(&k("rope.freq_base"), WVal::F32(cfg.rope_theta));
g.kv(&k("rope.dimension_count"), WVal::U32(if cfg.rotary_dim > 0 { cfg.rotary_dim } else { hd } as u32));
g.kv(&k("attention.key_length"), WVal::U32(hd as u32));
g.kv(&k("attention.value_length"), WVal::U32(hd as u32));
g.kv(
&k("context_length"),
WVal::U32(cfg_raw["max_position_embeddings"].as_u64().unwrap_or(4096) as u32),
);
let tj: serde_json::Value =
serde_json::from_slice(&std::fs::read(dir.join("tokenizer.json"))?)
.context("tokenizer.json")?;
let vocab = tj["model"]["vocab"]
.as_object()
.context("tokenizer.json model.vocab")?;
let mut tokens: Vec<(u32, String)> = vocab
.iter()
.map(|(t, id)| (id.as_u64().unwrap() as u32, t.clone()))
.collect();
let mut special_ids = std::collections::HashSet::new();
if let Some(added) = tj["added_tokens"].as_array() {
for a in added {
let id = a["id"].as_u64().unwrap() as u32;
if a["special"].as_bool().unwrap_or(false) {
special_ids.insert(id);
}
if !vocab.contains_key(a["content"].as_str().unwrap_or("")) {
tokens.push((id, a["content"].as_str().unwrap_or("").to_string()));
}
}
}
tokens.sort_by_key(|(id, _)| *id);
for (i, (id, _)) in tokens.iter().enumerate() {
ensure!(*id as usize == i, "non-dense vocab at {i} (id {id})");
}
let mut token_type: Vec<i32> = tokens
.iter()
.map(|(id, _)| if special_ids.contains(id) { 3 } else { 1 })
.collect();
let mut token_strs: Vec<String> = tokens.iter().map(|(_, t)| t.clone()).collect();
let n_emb_rows = st.shape("model.embed_tokens.weight")?[0];
for i in token_strs.len()..n_emb_rows {
token_strs.push(format!("[PAD{i}]"));
token_type.push(5); }
let merges: Vec<String> = tj["model"]["merges"]
.as_array()
.map(|a| {
a.iter()
.map(|m| {
if let Some(s) = m.as_str() {
s.to_string() } else {
let p = m.as_array().unwrap();
format!("{} {}", p[0].as_str().unwrap(), p[1].as_str().unwrap())
}
})
.collect()
})
.unwrap_or_default();
g.kv("tokenizer.ggml.model", WVal::Str("gpt2".into()));
g.kv("tokenizer.ggml.pre", WVal::Str(tok_pre));
g.kv("tokenizer.ggml.tokens", WVal::StrArray(token_strs));
g.kv("tokenizer.ggml.token_type", WVal::I32Array(token_type));
g.kv("tokenizer.ggml.merges", WVal::StrArray(merges));
if let Some(b) = cfg_raw["bos_token_id"].as_u64() {
g.kv("tokenizer.ggml.bos_token_id", WVal::U32(b as u32));
}
if let Some(e) = cfg_raw["eos_token_id"].as_u64() {
g.kv("tokenizer.ggml.eos_token_id", WVal::U32(e as u32));
}
if let Ok(tc) = std::fs::read(std::path::Path::new(&model).join("tokenizer_config.json")) {
if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&tc) {
if let Some(t) = v["chat_template"].as_str() {
g.kv("tokenizer.chat_template", WVal::Str(t.to_string()));
eprintln!(" embedded chat_template ({} chars)", t.len());
}
}
}
let permute = |w: &[f32], n_head: usize| -> Vec<f32> {
let mut outv = vec![0f32; w.len()];
for hh in 0..n_head {
for f in 0..hd {
let (a, b) = (f / (hd / 2), f % (hd / 2));
let gr = 2 * b + a;
outv[(hh * hd + gr) * h..(hh * hd + gr) * h + h]
.copy_from_slice(&w[(hh * hd + f) * h..(hh * hd + f) * h + h]);
}
}
outv
};
let pick = |gguf_name: &str, cols: usize| -> Result<u32> {
let mut ty = preset_type(&preset, gguf_name)?;
for (pat, t) in &overrides {
if gguf_name.contains(pat.as_str()) {
ty = *t;
}
}
Ok(fallback(ty, cols))
};
let mut n_im_used = 0usize;
let mut add_w = |g: &mut GgufWriter, gguf_name: &str, w: &[f32], rows: usize, cols: usize| -> Result<()> {
let ty = pick(gguf_name, cols)?;
let im = imatrix.get(gguf_name).map(|v| v.as_slice());
if im.is_some() {
n_im_used += 1;
}
let bytes = encode_ggml_im(w, cols, ty, im)?;
eprintln!(
" {gguf_name:34} [{rows}x{cols}] {}{}",
type_name(ty),
if im.is_some() { " +imatrix" } else { "" }
);
g.tensor(gguf_name, &[rows, cols], ty, bytes);
Ok(())
};
let f32_tensor = |g: &mut GgufWriter, gguf_name: &str, v: &[f32]| {
let mut b = Vec::with_capacity(v.len() * 4);
for x in v {
b.extend_from_slice(&x.to_le_bytes());
}
g.tensor(gguf_name, &[v.len()], 0, b);
};
let tens = |name: &str| -> Result<Vec<f32>> { st.tensor_f32(name) };
let shape = |name: &str| -> Result<Vec<usize>> { Ok(st.shape(name)?.to_vec()) };
let emb = tens("model.embed_tokens.weight")?;
let esh = shape("model.embed_tokens.weight")?;
add_w(&mut g, "token_embd.weight", &emb, esh[0], esh[1])?;
if st.has("lm_head.weight") {
let hw = tens("lm_head.weight")?;
let hs = shape("lm_head.weight")?;
add_w(&mut g, "output.weight", &hw, hs[0], hs[1])?;
}
f32_tensor(&mut g, "output_norm.weight", &tens("model.norm.weight")?);
for i in 0..cfg.n_layers {
let p = format!("model.layers.{i}");
let b = format!("blk.{i}");
f32_tensor(&mut g, &format!("{b}.attn_norm.weight"), &tens(&format!("{p}.input_layernorm.weight"))?);
f32_tensor(&mut g, &format!("{b}.ffn_norm.weight"), &tens(&format!("{p}.post_attention_layernorm.weight"))?);
for (hf, gg, nheads, perm) in [
("self_attn.q_proj", "attn_q", nh, is_llama),
("self_attn.k_proj", "attn_k", nkv, is_llama),
("self_attn.v_proj", "attn_v", nkv, false),
("self_attn.o_proj", "attn_output", 0, false),
("mlp.gate_proj", "ffn_gate", 0, false),
("mlp.up_proj", "ffn_up", 0, false),
("mlp.down_proj", "ffn_down", 0, false),
] {
let w = tens(&format!("{p}.{hf}.weight"))?;
let sh = shape(&format!("{p}.{hf}.weight"))?;
let w = if perm { permute(&w, nheads) } else { w };
add_w(&mut g, &format!("{b}.{gg}.weight"), &w, sh[0], sh[1])?;
}
if !is_llama {
for (hf, gg) in [
("self_attn.q_proj", "attn_q"),
("self_attn.k_proj", "attn_k"),
("self_attn.v_proj", "attn_v"),
] {
let bias = tens(&format!("{p}.{hf}.bias"))?;
f32_tensor(&mut g, &format!("{b}.{gg}.bias"), &bias);
}
}
}
g.write_to(std::path::Path::new(&out))?;
let sz = std::fs::metadata(&out)?.len();
println!(
"wrote {out} ({:.1} MB, preset {preset}, {} overrides, {} imatrix-weighted tensors)",
sz as f64 / 1e6,
overrides.len(),
n_im_used
);
Ok(())
}