use anyhow::{Context, bail};
use cortiq_core::format::{CmfModel, TensorSpec};
use cortiq_core::quant::dequant_tensor;
use cortiq_core::types::TensorDtype;
use cortiq_engine::qtensor::sgemm_public;
use std::collections::HashMap;
use std::sync::Arc;
fn cholesky(a: &mut [f64], n: usize) -> bool {
for i in 0..n {
for j in 0..=i {
let mut s = a[i * n + j];
for k in 0..j {
s -= a[i * n + k] * a[j * n + k];
}
if i == j {
if s <= 0.0 {
return false;
}
a[i * n + i] = s.sqrt();
} else {
a[i * n + j] = s / a[j * n + j];
}
}
for j in i + 1..n {
a[i * n + j] = 0.0;
}
}
true
}
fn chol_solve(l: &[f64], n: usize, b: &mut [f64], nrhs: usize) {
for r in 0..nrhs {
let x = &mut b[r * n..(r + 1) * n];
for i in 0..n {
let mut s = x[i];
for k in 0..i {
s -= l[i * n + k] * x[k];
}
x[i] = s / l[i * n + i];
}
for i in (0..n).rev() {
let mut s = x[i];
for k in i + 1..n {
s -= l[k * n + i] * x[k];
}
x[i] = s / l[i * n + i];
}
}
}
fn read_acts(path: &str, hidden: usize) -> anyhow::Result<Vec<f32>> {
let raw = std::fs::read(path).with_context(|| format!("reading {path}"))?;
if raw.len() % (hidden * 4) != 0 {
bail!("{path}: {} bytes is not a whole number of {hidden}-wide rows", raw.len());
}
Ok(raw
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect())
}
pub fn cmd_awnp(
model_path: &str,
acts_prefix: &str,
output: &str,
drop: f64,
ridge: f64,
rescale: bool,
) -> anyhow::Result<()> {
if !(0.0..0.9).contains(&drop) {
bail!("--drop must be in [0, 0.9)");
}
let model = Arc::new(CmfModel::open_sharded(model_path)?);
let mut by_layer: HashMap<usize, Vec<usize>> = HashMap::new();
for (i, e) in model.tensors.iter().enumerate() {
let n = &e.name;
if !(n.contains(".mlp.") && (n.ends_with("gate_proj.weight") || n.ends_with("up_proj.weight")))
{
continue;
}
let Some(rest) = n.strip_prefix("model.layers.") else {
continue;
};
let Some((li, _)) = rest.split_once('.') else {
continue;
};
if let Ok(li) = li.parse::<usize>() {
by_layer.entry(li).or_default().push(i);
}
}
let mut projected: HashMap<usize, Vec<u8>> = HashMap::new();
let mut layers_done = 0usize;
let mut kept_report = Vec::new();
let mut lids: Vec<usize> = by_layer.keys().copied().collect();
lids.sort_unstable();
for li in lids {
let path = format!("{acts_prefix}.{li}.f32");
if !std::path::Path::new(&path).exists() {
continue;
}
let idxs = &by_layer[&li];
let hidden = *model.tensors[idxs[0]].shape.get(1).context("2-D expert weight")? as usize;
let x = read_acts(&path, hidden)?;
let n_rows = x.len() / hidden;
if n_rows < 2 * hidden {
bail!(
"layer {li}: {n_rows} calibration rows for {hidden} channels. The covariance \
needs n well above the channel count — below it the matrix is rank-deficient, \
any large enough subset reconstructs exactly, and the projection error reads a \
flat 0% that measures the sample, not the model."
);
}
let mut xt = vec![0f32; hidden * n_rows];
for t in 0..n_rows {
for j in 0..hidden {
xt[j * n_rows + t] = x[t * hidden + j];
}
}
let mut c32 = vec![0f32; hidden * hidden];
sgemm_public(
hidden, hidden, n_rows, 1.0 / n_rows as f32,
&xt, n_rows, &x, hidden, false, &mut c32, hidden,
);
let c: Vec<f64> = c32.iter().map(|&v| v as f64).collect();
let mut energy = vec![0f64; hidden];
let mut buf = Vec::new();
for &ti in idxs {
let e = &model.tensors[ti];
let (rows, cols) = (e.shape[0] as usize, e.shape[1] as usize);
buf.clear();
buf.resize(rows * cols, 0.0);
dequant_tensor(e, model.entry_bytes(e), &mut buf).map_err(anyhow::Error::msg)?;
for r in 0..rows {
for j in 0..cols {
let v = buf[r * cols + j] as f64;
energy[j] += v * v;
}
}
}
let mut order: Vec<usize> = (0..hidden).collect();
order.sort_by(|&a, &b| energy[b].partial_cmp(&energy[a]).unwrap());
let keep_n = hidden - (hidden as f64 * drop).round() as usize;
let mut keep: Vec<usize> = order[..keep_n].to_vec();
keep.sort_unstable();
let mut css = vec![0f64; keep_n * keep_n];
for (a, &ia) in keep.iter().enumerate() {
for (b, &ib) in keep.iter().enumerate() {
css[a * keep_n + b] = c[ia * hidden + ib];
}
}
let scale = (0..keep_n).map(|i| css[i * keep_n + i]).sum::<f64>() / keep_n as f64;
for i in 0..keep_n {
css[i * keep_n + i] += scale * ridge;
}
if !cholesky(&mut css, keep_n) {
bail!("layer {li}: C[S,S] not positive definite even with a ridge");
}
let mut qt = vec![0f64; hidden * keep_n];
for i in 0..hidden {
for (a, &ia) in keep.iter().enumerate() {
qt[i * keep_n + a] = c[i * hidden + ia];
}
}
chol_solve(&css, keep_n, &mut qt, hidden);
let q32: Vec<f32> = qt.iter().map(|&v| v as f32).collect();
for &ti in idxs {
let e = &model.tensors[ti];
let (rows, cols) = (e.shape[0] as usize, e.shape[1] as usize);
buf.clear();
buf.resize(rows * cols, 0.0);
dequant_tensor(e, model.entry_bytes(e), &mut buf).map_err(anyhow::Error::msg)?;
let mut small = vec![0f32; rows * keep_n];
sgemm_public(
rows, keep_n, cols, 1.0, &buf, cols, &q32, keep_n, false, &mut small, keep_n,
);
let mut wide = vec![0f32; rows * cols];
for r in 0..rows {
for (a, &ia) in keep.iter().enumerate() {
wide[r * cols + ia] = small[r * keep_n + a];
}
}
if rescale {
let mean_sq = |v: &[f32]| -> f64 {
v.iter().map(|&x| (x as f64) * (x as f64)).sum::<f64>() / v.len() as f64
};
let (before, after) = (mean_sq(&buf), mean_sq(&wide));
if after > 0.0 {
let g = (before / after).sqrt() as f32;
for v in wide.iter_mut() {
*v *= g;
}
}
}
projected.insert(ti, crate::convert::encode_for(e.dtype, &wide, rows, cols));
}
layers_done += 1;
kept_report.push((li, keep_n, hidden, n_rows));
}
if layers_done == 0 {
bail!("no layer had a calibration dump at '{acts_prefix}.<layer>.f32'");
}
let n_rewritten = projected.len();
let specs: Vec<TensorSpec> = model
.tensors
.iter()
.enumerate()
.map(|(i, e)| TensorSpec {
name: e.name.clone(),
dtype: e.dtype,
shape: e.shape.clone(),
data: projected
.remove(&i)
.unwrap_or_else(|| model.entry_bytes(e).to_vec()),
})
.collect();
CmfModel::write(output, &model.header, &specs, Some(&model.masks), model.vocab.as_deref())?;
let (li, k, h, n) = kept_report[0];
println!(
"AWNP: {layers_done} layers projected, {rewritten} tensors rewritten\n\
dropped {:.1}% of input channels (layer {li}: {k}/{h} kept from {n} rows)\n\
→ {output}",
drop * 100.0,
rewritten = n_rewritten,
);
Ok(())
}