use anyhow::{Context, anyhow};
use cortiq_core::CmfModel;
use cortiq_core::format::{CmfHeader, TensorSpec};
use cortiq_core::types::{ModelArch, QuantType, TensorDtype};
use std::path::{Path, PathBuf};
use crate::convert::{encode_f16, encode_q4_tiled, encode_q8_row};
#[derive(Clone, Copy, PartialEq)]
enum Level {
Q8,
Q4t,
}
fn f32_bytes(vals: &[f32]) -> Vec<u8> {
let mut out = Vec::with_capacity(vals.len() * 4);
for v in vals {
out.extend_from_slice(&v.to_le_bytes());
}
out
}
fn encode_2d(name: String, vals: &[f32], rows: usize, cols: usize, level: Level) -> TensorSpec {
let (dtype, data) = if level == Level::Q4t && cols % 32 == 0 {
(TensorDtype::Q4Tiled, encode_q4_tiled(vals, rows, cols))
} else {
(TensorDtype::Q8Row, encode_q8_row(vals, rows, cols))
};
TensorSpec {
name,
dtype,
shape: vec![rows, cols],
data,
}
}
fn spec_f32(name: String, shape: Vec<usize>, vals: &[f32]) -> TensorSpec {
TensorSpec {
name,
dtype: TensorDtype::F32,
shape,
data: f32_bytes(vals),
}
}
fn config_spec(prefix: &str, dir: &Path) -> anyhow::Result<TensorSpec> {
let raw = std::fs::read(dir.join("config.json"))
.with_context(|| format!("{}/config.json", dir.display()))?;
Ok(TensorSpec {
name: format!("{prefix}.config_json"),
dtype: TensorDtype::U8,
shape: vec![raw.len()],
data: raw,
})
}
fn shard_paths(dir: &Path, index_name: &str, single_name: &str) -> anyhow::Result<Vec<PathBuf>> {
let idx_path = dir.join(index_name);
if idx_path.exists() {
let idx: serde_json::Value = serde_json::from_slice(&std::fs::read(&idx_path)?)?;
let mut shards: Vec<String> = idx["weight_map"]
.as_object()
.ok_or_else(|| anyhow!("weight_map"))?
.values()
.filter_map(|v| v.as_str().map(String::from))
.collect();
shards.sort();
shards.dedup();
Ok(shards.into_iter().map(|s| dir.join(s)).collect())
} else {
Ok(vec![dir.join(single_name)])
}
}
type Pick<'a> = &'a dyn Fn(&str, &[usize]) -> Option<(String, Option<Level>)>;
fn pack_component(
dir: &Path,
index_name: &str,
single_name: &str,
specs: &mut Vec<TensorSpec>,
pick: Pick<'_>,
) -> anyhow::Result<()> {
for shard in shard_paths(dir, index_name, single_name)? {
cortiq_engine::vae::read_safetensors_each(&shard, &mut |name, shape, data| {
if let Some((out_name, level)) = pick(name, &shape) {
specs.push(match level {
Some(l) if shape.len() == 2 => {
encode_2d(out_name, &data, shape[0], shape[1], l)
}
_ => spec_f32(out_name, shape, &data),
});
}
Ok(())
})
.map_err(|e| anyhow!("{}: {e}", shard.display()))?;
}
Ok(())
}
pub fn cmd_imagine_pack(root: &str, quant: &str, out: &str) -> anyhow::Result<()> {
let root = Path::new(root);
let level = match quant {
"q8" | "q8_row" => Level::Q8,
"q4t" | "q4_tiled" | "q4" => Level::Q4t,
other => return Err(anyhow!("--quant {other}: expected q4t or q8")),
};
let t0 = std::time::Instant::now();
let mut specs: Vec<TensorSpec> = Vec::new();
let te_dir = root.join("text_encoder");
specs.push(config_spec("te", &te_dir)?);
pack_component(
&te_dir,
"model.safetensors.index.json",
"model.safetensors",
&mut specs,
&|name, _shape| {
let n = name.strip_prefix("model.").unwrap_or(name);
if n == "lm_head.weight" {
return None; }
let big = n.ends_with("_proj.weight");
let lvl = if n == "embed_tokens.weight" {
Some(Level::Q8)
} else if big {
Some(level)
} else {
None
};
Some((format!("te.{n}"), lvl))
},
)?;
eprintln!("te packed ({:.1}s)", t0.elapsed().as_secs_f64());
let dit_dir = root.join("transformer");
specs.push(config_spec("dit", &dit_dir)?);
pack_component(
&dit_dir,
"diffusion_pytorch_model.safetensors.index.json",
"diffusion_pytorch_model.safetensors",
&mut specs,
&|name, shape| {
let lvl = if name.ends_with("norm1.linear.weight") {
Some(Level::Q8) } else if name.ends_with("caption_embedder.1.weight")
|| (shape.len() == 2
&& shape[1] >= 1024
&& (name.contains(".attn.to_") || name.contains(".feed_forward.")))
{
Some(level)
} else {
None
};
Some((format!("dit.{name}"), lvl))
},
)?;
eprintln!("dit packed ({:.1}s)", t0.elapsed().as_secs_f64());
let vae_dir = root.join("vae");
specs.push(config_spec("vae", &vae_dir)?);
pack_component(
&vae_dir,
"diffusion_pytorch_model.safetensors.index.json",
"diffusion_pytorch_model.safetensors",
&mut specs,
&|name, _shape| {
if !name.starts_with("decoder.") {
return None; }
Some((format!("vae.{name}"), None))
},
)?;
for s in specs.iter_mut() {
if s.name.starts_with("vae.") && s.shape.len() == 4 && s.dtype == TensorDtype::F32 {
let vals: Vec<f32> = s
.data
.chunks_exact(4)
.map(|c| f32::from_le_bytes(c.try_into().unwrap()))
.collect();
s.data = encode_f16(&vals);
s.dtype = TensorDtype::F16;
}
}
eprintln!("vae packed ({:.1}s)", t0.elapsed().as_secs_f64());
let te_cfg: serde_json::Value =
serde_json::from_slice(&std::fs::read(te_dir.join("config.json"))?)?;
let nl = te_cfg["num_hidden_layers"].as_u64().unwrap_or(0) as usize;
let arch: ModelArch = serde_json::from_value(serde_json::json!({
"arch_name": "lumina2-image",
"hidden_size": te_cfg["hidden_size"],
"intermediate_size": te_cfg["intermediate_size"],
"num_layers": nl,
"num_attention_heads": te_cfg["num_attention_heads"],
"num_kv_heads": te_cfg["num_key_value_heads"],
"head_dim": te_cfg["head_dim"],
"vocab_size": te_cfg["vocab_size"],
"layer_types": vec!["FullAttention"; nl],
"rms_norm_eps": te_cfg["rms_norm_eps"],
"max_position_embeddings": te_cfg["max_position_embeddings"],
"linear_conv_kernel_dim": 0,
"linear_num_key_heads": 0,
"linear_num_value_heads": 0,
}))?;
let header = CmfHeader {
format: "cmf".into(),
version: cortiq_core::CMF_VERSION,
arch,
quant_type: match level {
Level::Q8 => QuantType::Q8Row,
Level::Q4t => QuantType::Q4Block, },
provenance: Some(serde_json::json!({
"pipeline": "lumina-image-2.0",
"components": {"te": "gemma-2 encoder", "dit": "next-dit", "vae": "flux-vae decoder"},
"packed_from": root.display().to_string(),
"quant": quant,
})),
tokenizer_config: None,
section_hashes: None,
skills: Vec::new(),
shard: None,
calibration: None,
};
let vocab = std::fs::read(root.join("tokenizer").join("tokenizer.json"))
.with_context(|| "tokenizer/tokenizer.json")?;
CmfModel::write(out, &header, &specs, None, Some(&vocab))
.map_err(|e| anyhow!("write {out}: {e}"))?;
let size = std::fs::metadata(out)?.len();
println!(
"{out}: {} tensors, {:.2} GB in {:.1}s",
specs.len(),
size as f64 / (1 << 30) as f64,
t0.elapsed().as_secs_f64()
);
Ok(())
}