pub mod cmfio;
pub mod data;
pub mod dtgma;
pub mod eval;
#[cfg(feature = "gpu")]
pub mod heal_gpu;
pub mod illum;
pub mod moe;
pub mod plan;
pub mod stages;
use anyhow::{Context, Result, bail, ensure};
use serde_json::{Value, json};
use std::io::Write;
use std::path::{Path, PathBuf};
#[derive(clap::Args, Debug, Clone)]
pub struct CompressArgs {
pub source: String,
pub data: String,
pub output: String,
pub quant: Option<String>,
#[arg(long)]
pub teacher: Option<String>,
#[command(flatten)]
pub api: crate::teacher_api::ApiArgs,
#[arg(long)]
pub gen_max_tokens: Option<usize>,
#[arg(long, default_value_t = 1)]
pub gen_samples: u32,
#[arg(long, default_value = "auto")]
pub gen_sampling: String,
#[arg(long)]
pub no_think: bool,
#[arg(long)]
pub ffn: Option<String>,
#[arg(long)]
pub size: Option<String>,
#[arg(long)]
pub ratio: Option<f64>,
#[arg(long, default_value = "balanced")]
pub recipe: String,
#[arg(long, default_value = "importance")]
pub select: String,
#[arg(long, default_value = "uniform")]
pub allocate: String,
#[arg(long, default_value_t = 0)]
pub protect_first: usize,
#[arg(long, default_value_t = 0)]
pub protect_last: usize,
#[arg(long, default_value_t = 0.25)]
pub min_width: f64,
#[arg(long, default_value_t = 128)]
pub align: usize,
#[arg(long, default_value = "auto")]
pub refit: String,
#[arg(long, default_value = "0.03,0.1,0.3")]
pub ridge: String,
#[arg(long, default_value_t = 2048)]
pub window: usize,
#[arg(long)]
pub illum_tokens: Option<usize>,
#[arg(long)]
pub heal: Option<String>,
#[arg(long, default_value = "auto")]
pub heal_layers: String,
#[arg(long)]
pub heal_epochs: Option<f64>,
#[arg(long)]
pub heal_lr: Option<f64>,
#[arg(long, default_value_t = 0.7)]
pub kd: f64,
#[arg(long, default_value_t = 512)]
pub seq: usize,
#[arg(long, default_value_t = 2)]
pub bs: usize,
#[arg(long, default_value_t = 25)]
pub eval_every: usize,
#[arg(long, default_value = "auto")]
pub heal_device: String,
#[arg(long, default_value_t = 2048)]
pub heal_window: usize,
#[arg(long, default_value_t = 64)]
pub heal_topk: usize,
#[arg(long, default_value = "mix=0.5")]
pub heal_loss: String,
#[arg(long, default_value = "flat")]
pub heal_schedule: String,
#[arg(long, default_value = "ffn")]
pub heal_params: String,
#[arg(long, default_value = "generated")]
pub loss_on: String,
#[arg(long)]
pub calib_tokens: Option<usize>,
#[arg(long, default_value_t = 2048)]
pub calib_window: usize,
#[arg(long, default_value = "kv")]
pub escort: String,
#[arg(long = "tensor-quant", value_name = "PATTERN=QUANT")]
pub tensor_quant: Vec<String>,
#[arg(long, default_value_t = 16)]
pub eval_prompts: usize,
#[arg(long, default_value_t = 384)]
pub eval_gen_tokens: usize,
#[arg(long)]
pub general_eval: Option<String>,
#[arg(long, value_name = "MODEL.cmf")]
pub compare: Vec<String>,
#[arg(long)]
pub work_dir: Option<String>,
#[arg(long)]
pub plan_only: bool,
#[arg(long)]
pub until: Option<String>,
#[arg(long)]
pub from: Option<String>,
#[arg(long)]
pub fresh: bool,
#[arg(long)]
pub threads: Option<usize>,
#[arg(long)]
pub allow_lossy_source: bool,
#[arg(long)]
pub write_on_fail: bool,
#[arg(long, hide = true)]
pub stage: Option<String>,
}
impl CompressArgs {
pub fn gen_max_tokens(&self) -> usize {
let api = self.teacher.as_deref().is_some_and(crate::teacher_api::is_api_spec);
self.gen_max_tokens.unwrap_or(if api { 8192 } else { 2048 })
}
}
pub const STAGES: [&str; 7] = ["S0.prepare", "S1.illuminate", "S2.plan", "S3.fold", "S4.heal", "S5.quant", "S6.eval"];
pub fn stage_index(s: &str) -> Result<usize> {
let t = s.trim().to_ascii_lowercase();
for (i, name) in STAGES.iter().enumerate() {
let (code, word) = name.split_once('.').unwrap();
if t == code.to_ascii_lowercase() || t == word || t == name.to_ascii_lowercase() || t == i.to_string() {
return Ok(i);
}
}
bail!("unknown stage {s:?}; expected one of {}", STAGES.join(", "))
}
pub struct Run {
pub a: CompressArgs,
pub prompts: Option<String>,
pub work: PathBuf,
pub quant: crate::convert::Quant,
pub structural: bool,
pub heal: bool,
pub heal_epochs: f64,
pub calib_tokens: Option<usize>,
pub threads: usize,
}
impl Run {
pub fn new(mut a: CompressArgs) -> Result<Self> {
let quant = match a.quant.as_deref() {
Some(q) => crate::convert::parse_quant(q)?,
None => source_codec(&a.source)?,
};
let recipe = a.recipe.as_str();
ensure!(
matches!(recipe, "fast" | "balanced" | "full" | "quant-only"),
"--recipe {recipe:?}: expected fast, balanced, full or quant-only"
);
let has_target = a.ffn.is_some() || a.size.is_some() || a.ratio.is_some();
let structural = recipe != "quant-only";
ensure!(
!structural || has_target,
"give a target (--ffn, --size or --ratio), or --recipe quant-only for calibration-only quantization"
);
ensure!(
[a.ffn.is_some(), a.size.is_some(), a.ratio.is_some()].iter().filter(|&&x| x).count() <= 1,
"give only one of --ffn, --size, --ratio"
);
let heal = match a.heal.as_deref() {
Some("none") => false,
Some("fcd") => true,
Some(other) => bail!("--heal {other:?}: expected none or fcd"),
None => structural && recipe != "fast",
};
let heal_epochs = a.heal_epochs.unwrap_or(if recipe == "full" { 3.0 } else { 2.0 });
let threads = a.threads.unwrap_or_else(stages::cpu_budget);
let work = PathBuf::from(a.work_dir.clone().unwrap_or_else(|| format!("{}.work", a.output)));
let calib_tokens = a.calib_tokens.or(if recipe == "fast" { Some(32768) } else { None });
let prompts = (!a.data.contains(".tdata")).then(|| a.data.clone());
if prompts.is_some() {
a.data = work.join(STAGES[0]).join("task.tdata").display().to_string();
}
Ok(Self { a, prompts, work, quant, structural, heal, heal_epochs, calib_tokens, threads })
}
pub fn stage_dir(&self, i: usize) -> PathBuf {
self.work.join(STAGES[i])
}
pub fn done_path(&self, i: usize) -> PathBuf {
self.stage_dir(i).join("done.json")
}
pub fn done(&self, i: usize) -> Option<Value> {
let v: Value = serde_json::from_slice(&std::fs::read(self.done_path(i)).ok()?).ok()?;
let key = self.key(i).ok()?;
(v.get("key").and_then(Value::as_str) == Some(key.as_str())).then_some(v)
}
pub fn mark_done(&self, i: usize, mut info: Value) -> Result<()> {
info["key"] = json!(self.key(i)?);
info["stage"] = json!(STAGES[i]);
info["finished"] = json!(now());
let p = self.done_path(i);
std::fs::create_dir_all(p.parent().unwrap())?;
std::fs::write(&p, serde_json::to_vec_pretty(&info)?)?;
Ok(())
}
pub fn require(&self, i: usize) -> Result<Value> {
self.done(i).with_context(|| format!("{} has not been run (or its inputs changed)", STAGES[i]))
}
pub fn key(&self, i: usize) -> Result<String> {
let a = &self.a;
let own = match i {
0 => json!({
"source": source_identity(&a.source), "lossy": a.allow_lossy_source,
"prompts": self.prompts.as_deref().map(|p| prompts_identity(p, a)),
}),
1 => json!({ "data": data_identity(&a.data), "window": a.window, "illum_tokens": a.illum_tokens, "curves": 2 }),
2 => {
let mut k = json!({
"ffn": a.ffn, "size": a.size, "ratio": a.ratio, "allocate": a.allocate,
"protect": [a.protect_first, a.protect_last], "min_width": a.min_width, "align": a.align,
"select": a.select,
});
if a.select == "dtgma" {
let mut knobs: Vec<String> = std::env::vars().filter(|(n, _)| n.starts_with("CMF_DTGMA_")).map(|(n, v)| format!("{n}={v}")).collect();
knobs.sort();
k["dtgma"] = json!({ "v": 2, "device": a.heal_device, "window": a.heal_window, "loss_on": a.loss_on, "knobs": knobs });
}
k
}
3 => json!({ "refit": a.refit, "ridge": a.ridge }),
4 => json!({
"gate": "relative-v2",
"heal": self.heal, "layers": a.heal_layers, "epochs": self.heal_epochs, "lr": a.heal_lr,
"kd": a.kd, "seq": a.seq, "bs": a.bs, "eval_every": a.eval_every, "loss_on": a.loss_on,
"data": data_identity(&a.data), "device": a.heal_device, "heal_window": a.heal_window,
"heal_topk": a.heal_topk, "heal_params": a.heal_params, "heal_loss": a.heal_loss, "heal_schedule": a.heal_schedule,
}),
5 => json!({
"o1": "off",
"quant": crate::convert::quant_name(self.quant), "calib_tokens": self.calib_tokens,
"calib_window": a.calib_window, "escort": a.escort, "tensor_quant": a.tensor_quant,
"data": data_identity(&a.data),
}),
6 => json!({
"o1": "off",
"eval_prompts": a.eval_prompts, "eval_gen_tokens": a.eval_gen_tokens,
"general_eval": a.general_eval, "data": data_identity(&a.data),
"compare": a.compare,
}),
_ => bail!("stage index {i}"),
};
let up = match i {
0 => None,
5 if !self.structural => Some(0),
_ => Some(i - 1),
};
let up_key = match up {
Some(u) => self.key(u)?,
None => String::new(),
};
let all = json!({
"stage": STAGES[i], "own": own, "up": up_key,
"cortiq": env!("CARGO_PKG_VERSION"), "structural": self.structural,
});
Ok(crate::knowledge::sha256_hex(serde_json::to_string(&all)?.as_bytes())[..16].to_string())
}
pub fn log(&self, line: &str) {
println!("{line}");
let _ = std::fs::create_dir_all(&self.work);
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(self.work.join("JOURNAL.md")) {
let _ = writeln!(f, "{line}");
}
}
pub fn stages(&self) -> Vec<usize> {
if self.structural { (0..7).collect() } else { vec![0, 5, 6] }
}
}
pub fn now() -> String {
let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
let (d, s) = (t / 86400, t % 86400);
let z = d as i64 + 719468;
let era = z.div_euclid(146097);
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{y:04}-{m:02}-{day:02}T{:02}:{:02}:{:02}Z", s / 3600, s % 3600 / 60, s % 60)
}
fn source_identity(src: &str) -> String {
let p = Path::new(src);
if p.is_file() {
let m = std::fs::metadata(p).ok();
let mt = m.as_ref().and_then(|m| m.modified().ok()).and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok());
return format!("{}:{}:{}", p.canonicalize().unwrap_or(p.into()).display(), m.map_or(0, |m| m.len()), mt.map_or(0, |d| d.as_secs()));
}
if p.is_dir() {
let cfg = std::fs::read(p.join("config.json")).unwrap_or_default();
return format!("{}:{}", p.canonicalize().unwrap_or(p.into()).display(), crate::knowledge::sha256_hex(&cfg));
}
src.to_string()
}
fn source_codec(source: &str) -> Result<crate::convert::Quant> {
use crate::convert::Quant;
use cortiq_core::TensorDtype;
if !source.ends_with(".cmf") {
return Ok(Quant::F16);
}
let m = cortiq_core::CmfModel::open_sharded(source).with_context(|| format!("opening {source}"))?;
let mut by: Vec<(TensorDtype, u64)> = Vec::new();
for e in m.tensors.iter().filter(|e| e.shape.len() == 2) {
match by.iter_mut().find(|(d, _)| *d == e.dtype) {
Some((_, n)) => *n += e.n_elems() as u64,
None => by.push((e.dtype, e.n_elems() as u64)),
}
}
Ok(match by.into_iter().max_by_key(|&(_, n)| n).map(|(d, _)| d) {
Some(TensorDtype::Q4TiledP) => Quant::Q4TiledP,
Some(TensorDtype::Q2TiledP) => Quant::Q2TiledP,
Some(TensorDtype::F16 | TensorDtype::Bf16 | TensorDtype::F32) | None => Quant::F16,
_ => Quant::Q8_2f,
})
}
fn prompts_identity(path: &str, a: &CompressArgs) -> String {
let p = std::fs::read(path).unwrap_or_default();
format!(
"{}:{}:{}:{}:{}:{}",
crate::knowledge::sha256_hex(&p),
a.gen_max_tokens(),
a.gen_samples,
a.gen_sampling,
a.no_think,
a.teacher.as_deref().unwrap_or("source")
) + &match (&a.api.api_base, &a.api.api_extra) {
(None, None) => String::new(),
(b, e) => format!(":{}:{}", b.as_deref().unwrap_or(""), e.as_deref().unwrap_or("")),
}
}
fn data_identity(spec: &str) -> String {
let root = spec.rfind(".tdata").map(|i| &spec[..i + 6]).unwrap_or(spec);
let m = std::fs::read(Path::new(root).join("manifest.json")).unwrap_or_default();
format!("{spec}:{}", crate::knowledge::sha256_hex(&m))
}
fn stage_purpose(i: usize) -> &'static str {
[
"load the source; let it answer the task prompts",
"light up the FFN neurons on the task (activation statistics)",
"choose each layer's width",
"keep the task's neurons, refit the rest into them",
"heal: train the narrowed model back toward the source",
"write the output codec",
"score on held-out task answers; quality gates",
][i]
}
pub fn run(a: CompressArgs) -> Result<()> {
if let Some(stage) = a.stage.clone() {
let run = Run::new(a)?;
stages::run_stage(&run, &stage)?;
use std::io::Write;
let _ = std::io::stdout().flush();
let _ = std::io::stderr().flush();
unsafe { libc::_exit(0) }
}
orchestrate(Run::new(a)?)
}
fn orchestrate(run: Run) -> Result<()> {
if run.a.fresh && run.work.exists() {
std::fs::remove_dir_all(&run.work).with_context(|| format!("removing {}", run.work.display()))?;
}
std::fs::create_dir_all(&run.work)?;
let a = run.a.clone();
run.log(&format!(
"\n## compress {} — {}\n\n`{}`\n",
now(),
a.output,
std::env::args().collect::<Vec<_>>().join(" ")
));
let order = run.stages();
let until = match (&a.until, a.plan_only) {
(_, true) => 0,
(Some(u), _) => stage_index(u)?,
(None, _) => 6,
};
let from = a.from.as_deref().map(stage_index).transpose()?;
let job_t0 = std::time::Instant::now();
let n_stages = order.iter().filter(|&&i| i <= until).count();
for (k, &i) in order.iter().enumerate() {
if i > until {
break;
}
let forced = from.is_some_and(|f| i >= f);
if !forced && let Some(v) = run.done(i) {
run.log(&format!("[{}] cached ({})", STAGES[i], v.get("finished").and_then(Value::as_str).unwrap_or("")));
continue;
}
let _ = std::fs::remove_file(run.done_path(i));
let label = STAGES[i];
run.log(&format!("[{label}] start {} — stage {}/{n_stages}: {}", now(), k + 1, stage_purpose(i)));
let t0 = std::time::Instant::now();
spawn_stage(&run, label)?;
let v = run.done(i).with_context(|| format!("{label} finished without recording its result"))?;
run.log(&format!(
"[{label}] done in {:.0}s · {} since the start",
t0.elapsed().as_secs_f64(),
crate::fmt_eta(job_t0.elapsed().as_secs_f64())
));
if i == 2 && v.get("plan").is_some() {
stages::print_plan(&run, &v)?;
}
}
if a.plan_only {
return stages::print_arithmetic_plan(&run);
}
if until < 6 {
run.log(&format!("stopped after {} (--until)", STAGES[until]));
return Ok(());
}
let s6 = run.require(6)?;
let pass = s6.get("pass").and_then(Value::as_bool).unwrap_or(false);
eval::print_report(&run, &s6)?;
let target = if pass {
PathBuf::from(&a.output)
} else if a.write_on_fail {
PathBuf::from(format!("{}.rejected.cmf", a.output.trim_end_matches(".cmf")))
} else {
run.log("quality gates FAILED — no output written (see the report; --write-on-fail writes it anyway)");
std::process::exit(5);
};
stages::publish(&run, &target)?;
run.log(&format!("wrote {}", target.display()));
if !pass {
std::process::exit(5);
}
Ok(())
}
fn spawn_stage(run: &Run, label: &str) -> Result<()> {
let mut exe = std::env::current_exe()?;
if !exe.exists()
&& let Some(s) = exe.to_str()
&& let Some(p) = s.strip_suffix(" (deleted)")
{
exe = PathBuf::from(p);
}
let mut argv: Vec<String> = std::env::args().skip(1).collect();
argv.retain(|x| x != "--fresh" && x != "--plan-only");
argv.push("--stage".into());
argv.push(label.into());
let mut cmd = std::process::Command::new(exe);
cmd.args(&argv);
let calib = matches!(label, "S1.illuminate" | "S2.plan" | "S3.fold" | "S5.quant");
if calib {
if cfg!(target_os = "macos") {
cmd.env("CMF_GPU", "0");
} else {
cmd.env("CMF_GPU_WGPU_GRAPH", "0");
}
}
cmd.env("CMF_GPU_PROBE", "0");
cmd.env("CMF_O1", "off");
if std::env::var_os("CMF_THREADS").is_none() {
cmd.env("CMF_THREADS", run.threads.to_string());
}
let st = cmd.status().with_context(|| format!("starting {label}"))?;
if !st.success() {
run.log(&format!("[{label}] FAILED ({st}) — rerun the same command to resume"));
bail!("{label} failed ({st})");
}
Ok(())
}