use crate::guard::dense_matmul;
use crate::kernels::{
monarch_blocks, monarch_project, rel_err, tt_params, tt_reconstruct, tt_svd, tucker_hosvd,
tucker_params, tucker_reconstruct,
};
use crate::probe::{
best_bitwidth, nm_sparsity_error, outlier_channels, per_channel_quant_error, quant_error,
ternary_error,
};
use crate::svd::truncated_svd;
#[derive(Clone, Debug, PartialEq)]
pub enum Decomp {
Dense,
Sparse24,
LowRank(usize),
Monarch,
Tucker([usize; 3], [usize; 3]),
Tt(Vec<usize>, usize),
Int8,
Int4,
Ternary,
}
impl Decomp {
pub fn label(&self) -> String {
match self {
Decomp::Dense => "dense".into(),
Decomp::Sparse24 => "sparse2:4".into(),
Decomp::LowRank(r) => format!("lowrank(r={r})"),
Decomp::Monarch => "monarch".into(),
Decomp::Tucker(d, r) => format!(
"tucker({}×{}×{}→{},{},{})",
d[0], d[1], d[2], r[0], r[1], r[2]
),
Decomp::Tt(d, cap) => format!("tt({}modes,χ≤{cap})", d.len()),
Decomp::Int8 => "int8/chan".into(),
Decomp::Int4 => "int4/chan".into(),
Decomp::Ternary => "ternary{-1,0,1}".into(),
}
}
}
#[derive(Clone, Debug)]
pub struct DecompOption {
pub decomp: Decomp,
pub rel_err: f32,
pub bytes: usize,
pub flops: u64,
}
#[derive(Clone, Debug)]
pub struct LayerProfile {
pub name: String,
pub rows: usize,
pub cols: usize,
pub dense_bytes: usize,
pub options: Vec<DecompOption>,
}
impl LayerProfile {
pub fn dense(&self) -> &DecompOption {
&self.options[0]
}
pub fn best_within(&self, budget: f32) -> &DecompOption {
self.options
.iter()
.filter(|o| o.rel_err <= budget)
.min_by_key(|o| o.bytes)
.unwrap_or(&self.options[0])
}
}
fn balanced_factor(n: usize) -> (usize, usize) {
let mut a = (n as f64).sqrt() as usize;
while a > 1 && !n.is_multiple_of(a) {
a -= 1;
}
(a, n / a)
}
fn monarch_dense(w1: &[f32], w2: &[f32], m: usize) -> Vec<f32> {
let n = m * m;
let mut w = vec![0f32; n * n];
for l in 0..m {
for s in 0..m {
for ri in 0..m {
for j in 0..m {
w[(ri * m + j) * n + (l * m + s)] =
w1[ri * m * m + j * m + l] * w2[l * m * m + s * m + ri];
}
}
}
}
w
}
pub fn profile_layer(name: &str, w: &[f32], rows: usize, cols: usize) -> LayerProfile {
let numel = rows * cols;
let dense_bytes = numel * 4;
let dense_flops = (rows * cols) as u64; let mut options = vec![DecompOption {
decomp: Decomp::Dense,
rel_err: 0.0,
bytes: dense_bytes,
flops: dense_flops,
}];
options.push(DecompOption {
decomp: Decomp::Int8,
rel_err: per_channel_quant_error(w, rows, cols, 8),
bytes: numel, flops: dense_flops,
});
options.push(DecompOption {
decomp: Decomp::Int4,
rel_err: per_channel_quant_error(w, rows, cols, 4),
bytes: numel / 2, flops: dense_flops,
});
options.push(DecompOption {
decomp: Decomp::Ternary,
rel_err: ternary_error(w),
bytes: numel / 4 + 4, flops: dense_flops / 2, });
options.push(DecompOption {
decomp: Decomp::Sparse24,
rel_err: nm_sparsity_error(w, rows, cols, 2, 4),
bytes: numel * 2 + numel / 4, flops: dense_flops / 2,
});
let rmax = rows.min(cols);
let rcap = rmax.min(48);
let (us, sv, vs) = truncated_svd(w, rows, cols, rcap);
let energy: f32 = sv.iter().map(|s| s * s).sum::<f32>().max(1e-30);
let rank_for = |frac: f32| -> usize {
let mut cum = 0f32;
for (i, &s) in sv.iter().enumerate() {
cum += s * s;
if cum >= frac * energy {
return i + 1;
}
}
rcap
};
let mut seen_r: Vec<usize> = Vec::new();
for &frac in &[0.90f32, 0.99, 0.999] {
let rr = rank_for(frac).clamp(1, rcap);
if seen_r.contains(&rr) || (rows + cols) * rr >= numel {
continue; }
seen_r.push(rr);
let mut a = vec![0f32; rows * rr];
let mut b = vec![0f32; rr * cols];
for i in 0..rows {
for t in 0..rr {
a[i * rr + t] = us[i * rcap + t];
}
}
for t in 0..rr {
let s = sv[t];
for j in 0..cols {
b[t * cols + j] = s * vs[j * rcap + t];
}
}
let recon = dense_matmul(&a, &b, rows, rr, cols);
options.push(DecompOption {
decomp: Decomp::LowRank(rr),
rel_err: rel_err(w, &recon),
bytes: (rows + cols) * rr * 4,
flops: ((rows + cols) * rr) as u64,
});
}
if rows == cols {
if let Some(m) = monarch_blocks(rows) {
let (w1, w2) = monarch_project(w, m);
let recon = monarch_dense(&w1, &w2, m);
options.push(DecompOption {
decomp: Decomp::Monarch,
rel_err: rel_err(w, &recon),
bytes: 2 * m * m * m * 4,
flops: (2 * m * m * m) as u64,
});
}
}
let (c1, c2) = balanced_factor(cols);
if c1 > 1 && c2 > 1 {
let dims = [rows, c1, c2];
const TCAP: usize = 32;
let ranks = [
(rows / 2).clamp(1, TCAP.min(rows)),
(c1 / 2).max(1).min(c1),
(c2 / 2).max(1).min(c2),
];
let (core, factors) = tucker_hosvd(w, dims, ranks);
let recon = tucker_reconstruct(&core, dims, ranks, &factors);
let p = tucker_params(dims, ranks);
if p < numel {
options.push(DecompOption {
decomp: Decomp::Tucker(dims, ranks),
rel_err: rel_err(w, &recon),
bytes: p * 4,
flops: p as u64,
});
}
}
let (rf1, rf2) = balanced_factor(rows);
let (cf1, cf2) = balanced_factor(cols);
if rf1 > 1 && rf2 > 1 && cf1 > 1 && cf2 > 1 {
let modes = vec![rf1, rf2, cf1, cf2];
let cap = 16usize;
let cores = tt_svd(w, &modes, cap);
let recon = tt_reconstruct(&cores);
let p = tt_params(&cores);
if p < numel {
options.push(DecompOption {
decomp: Decomp::Tt(modes, cap),
rel_err: rel_err(w, &recon),
bytes: p * 4,
flops: p as u64,
});
}
}
LayerProfile {
name: name.into(),
rows,
cols,
dense_bytes,
options,
}
}
#[derive(Clone, Debug)]
pub struct QuantStat {
pub name: String,
pub layer: Option<usize>,
pub kind: String,
pub numel: usize,
pub int8_pt: f32,
pub int8_pc: f32,
pub int4_pc: f32,
pub int3_pc: f32,
pub outlier: f32,
pub best_bits: Option<u32>,
}
pub fn parse_layer_kind(name: &str) -> (Option<usize>, String) {
let layer = name
.strip_prefix("model.layers.")
.and_then(|r| r.split('.').next())
.and_then(|s| s.parse::<usize>().ok());
let kind = name
.trim_end_matches(".weight")
.rsplit('.')
.next()
.unwrap_or(name)
.to_string();
(layer, kind)
}
pub fn quant_stat(name: &str, w: &[f32], rows: usize, cols: usize, budget: f32) -> QuantStat {
let (layer, kind) = parse_layer_kind(name);
QuantStat {
name: name.into(),
layer,
kind,
numel: rows * cols,
int8_pt: quant_error(w, 8),
int8_pc: per_channel_quant_error(w, rows, cols, 8),
int4_pc: per_channel_quant_error(w, rows, cols, 4),
int3_pc: per_channel_quant_error(w, rows, cols, 3),
outlier: outlier_channels(w, rows, cols, 5.0).1,
best_bits: best_bitwidth(w, rows, cols, budget),
}
}
#[derive(Clone, Debug)]
pub struct Allocation {
pub choice: Vec<usize>,
pub total_err: f32,
pub total_bytes: usize,
pub dense_bytes: usize,
}
impl Allocation {
pub fn compression(&self) -> f32 {
self.dense_bytes as f32 / self.total_bytes.max(1) as f32
}
}
fn eval(layers: &[LayerProfile], choice: &[usize]) -> (f32, usize) {
let mut err = 0f32;
let mut bytes = 0usize;
for (l, &c) in layers.iter().zip(choice) {
err += l.options[c].rel_err;
bytes += l.options[c].bytes;
}
(err, bytes)
}
pub fn greedy_allocate(layers: &[LayerProfile], budget_frac: f32) -> Allocation {
let dense_bytes: usize = layers.iter().map(|l| l.dense_bytes).sum();
let budget = (dense_bytes as f32 * budget_frac) as usize;
let mut choice: Vec<usize> = vec![0; layers.len()]; loop {
let (_, bytes) = eval(layers, &choice);
if bytes <= budget {
break;
}
let mut best: Option<(f32, usize, usize)> = None; for (li, l) in layers.iter().enumerate() {
let cur = &l.options[choice[li]];
for (oi, o) in l.options.iter().enumerate() {
if o.bytes >= cur.bytes {
continue; }
let d_err = (o.rel_err - cur.rel_err).max(0.0);
let d_bytes = (cur.bytes - o.bytes) as f32;
let score = d_err / d_bytes; if best.map(|(b, _, _)| score < b).unwrap_or(true) {
best = Some((score, li, oi));
}
}
}
match best {
Some((_, li, oi)) => choice[li] = oi,
None => break, }
}
let (total_err, total_bytes) = eval(layers, &choice);
Allocation {
choice,
total_err,
total_bytes,
dense_bytes,
}
}
#[derive(Clone, Copy, Debug)]
pub struct DadoConfig {
pub iters: usize,
pub pop: usize,
pub elite_frac: f32,
pub penalty: f32,
pub seed: u64,
}
impl Default for DadoConfig {
fn default() -> Self {
Self {
iters: 60,
pop: 200,
elite_frac: 0.2,
penalty: 50.0,
seed: 0xDAD0_0501,
}
}
}
pub fn dado_allocate(layers: &[LayerProfile], budget_frac: f32, cfg: DadoConfig) -> Allocation {
let dense_bytes: usize = layers.iter().map(|l| l.dense_bytes).sum();
let budget = (dense_bytes as f32 * budget_frac).max(1.0);
let nl = layers.len();
let seed_alloc = greedy_allocate(layers, budget_frac);
let mut p: Vec<Vec<f32>> = layers
.iter()
.enumerate()
.map(|(i, l)| {
let k = l.options.len();
let mut d = vec![0.4 / k as f32; k];
d[seed_alloc.choice[i]] += 0.6;
d
})
.collect();
let mut s = cfg.seed | 1;
let mut rnd = || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
(s >> 11) as f64 / (1u64 << 53) as f64
};
let sample_cat = |dist: &[f32], u: f64| -> usize {
let mut acc = 0.0;
for (i, &pi) in dist.iter().enumerate() {
acc += pi as f64;
if u <= acc {
return i;
}
}
dist.len() - 1
};
let loss = |choice: &[usize]| -> f32 {
let (err, bytes) = eval(layers, choice);
let over = ((bytes as f32 - budget) / budget).max(0.0);
err + cfg.penalty * over
};
let mut best_choice: Option<Vec<usize>> = Some(seed_alloc.choice.clone());
let mut best_loss = seed_alloc.total_err;
let n_elite = ((cfg.pop as f32) * cfg.elite_frac).max(1.0) as usize;
for _ in 0..cfg.iters {
let mut pop: Vec<(f32, Vec<f32>, Vec<usize>)> = Vec::with_capacity(cfg.pop);
for _ in 0..cfg.pop {
let choice: Vec<usize> = (0..nl).map(|i| sample_cat(&p[i], rnd())).collect();
let (err, bytes) = eval(layers, &choice);
let lo = loss(&choice);
if bytes as f32 <= budget && err < best_loss {
best_loss = err;
best_choice = Some(choice.clone());
}
pop.push((lo, Vec::new(), choice));
}
pop.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let elite = &pop[..n_elite];
for i in 0..nl {
let k = layers[i].options.len();
let mut counts = vec![1.0f32; k]; for (_, _, ch) in elite {
counts[ch[i]] += 1.0;
}
let tot: f32 = counts.iter().sum();
for j in 0..k {
p[i][j] = counts[j] / tot;
}
}
}
let choice = best_choice.expect("seeded with greedy");
let (total_err, total_bytes) = eval(layers, &choice);
Allocation {
choice,
total_err,
total_bytes,
dense_bytes,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Dist, sample};
#[test]
fn profile_flags_the_right_decomp_per_dist() {
let (k, n) = (64usize, 64usize);
let lr = sample(Dist::LowRank, k, n, 1);
let prof = profile_layer("lr", &lr, k, n);
let low = prof
.options
.iter()
.find(|o| matches!(o.decomp, Decomp::LowRank(_)))
.unwrap();
assert!(
low.rel_err < 0.05,
"low-rank weight should factor cleanly, err {}",
low.rel_err
);
assert!(low.bytes < prof.dense_bytes);
}
#[test]
fn dado_matches_greedy_on_additive_objective() {
let layers = vec![
profile_layer("a", &sample(Dist::LowRank, 64, 64, 1), 64, 64),
profile_layer("b", &sample(Dist::Quantized, 48, 32, 2), 48, 32),
profile_layer("c", &sample(Dist::Gaussian, 32, 48, 3), 32, 48),
profile_layer("d", &sample(Dist::LowRank, 36, 36, 4), 36, 36),
];
let g = greedy_allocate(&layers, 0.4);
let d = dado_allocate(&layers, 0.4, DadoConfig::default());
assert!(
g.total_bytes as f32
<= layers.iter().map(|l| l.dense_bytes).sum::<usize>() as f32 * 0.4 + 1.0
);
assert!(
d.total_bytes as f32
<= layers.iter().map(|l| l.dense_bytes).sum::<usize>() as f32 * 0.4 + 1.0
);
assert!(
d.total_err <= g.total_err + 1e-6,
"dado {} vs greedy {}",
d.total_err,
g.total_err
);
}
}