1use crate::fcd::{FcdModel, LnFfn};
22use crate::fcd_ops as ops;
23use crate::sampler::SplitMix64;
24use cortiq_core::CmfModel;
25use std::sync::Arc;
26
27#[derive(Clone, Debug)]
29pub struct BakeHyper {
30 pub steps_a: usize,
31 pub steps_b: usize,
32 pub l1_init: f64,
33 pub l1_step: f64,
34 pub eval_every: usize,
35 pub lr_a: f64,
36 pub lr_b: f64,
37 pub tau: f32,
38 pub fcd_layers: usize,
39 pub seed: u64,
40}
41
42impl Default for BakeHyper {
43 fn default() -> Self {
44 Self {
45 steps_a: 240,
46 steps_b: 120,
47 l1_init: 0.01,
48 l1_step: 0.005,
49 eval_every: 30,
50 lr_a: 0.1,
51 lr_b: 1e-5,
52 tau: 0.5,
53 fcd_layers: 4,
54 seed: 0,
55 }
56 }
57}
58
59pub struct BakeReport {
61 pub backbone: f64,
63 pub masked: f64,
65 pub overlaid: f64,
67 pub pruned_ratio: f64,
68 pub kept_per_layer: Vec<usize>,
69 pub sec: f64,
70}
71
72pub struct BakeArtifacts {
74 pub keep: Vec<Vec<bool>>,
76 pub down: Vec<Vec<f32>>,
79 pub gate_up: Vec<Option<(Vec<f32>, Vec<f32>)>>,
81 pub fcd_layers: Vec<usize>,
83}
84
85const CLIP: f64 = 1.0;
86const B1: f64 = 0.9;
87const B2: f64 = 0.999;
88const EPS: f64 = 1e-8;
89
90struct Adam {
92 m: Vec<Vec<f64>>,
93 v: Vec<Vec<f64>>,
94 t: i32,
95 lr: f64,
96}
97
98impl Adam {
99 fn new(sizes: &[usize], lr: f64) -> Self {
100 Self {
101 m: sizes.iter().map(|&n| vec![0.0; n]).collect(),
102 v: sizes.iter().map(|&n| vec![0.0; n]).collect(),
103 t: 0,
104 lr,
105 }
106 }
107
108 fn step(&mut self, params: &mut [&mut [f32]], grads: &[Vec<f64>], lr_scale: f64) {
110 let gn: f64 = grads.iter().flat_map(|g| g.iter().map(|x| x * x)).sum::<f64>().sqrt();
111 let clip = if gn > CLIP { CLIP / gn } else { 1.0 };
112 self.t += 1;
113 let (bc1, bc2) = (1.0 - B1.powi(self.t), 1.0 - B2.powi(self.t));
114 for (pi, p) in params.iter_mut().enumerate() {
115 for j in 0..p.len() {
116 let g = grads[pi][j] * clip;
117 let m = &mut self.m[pi][j];
118 let v = &mut self.v[pi][j];
119 *m = B1 * *m + (1.0 - B1) * g;
120 *v = B2 * *v + (1.0 - B2) * g * g;
121 let upd = (*m / bc1) / ((*v / bc2).sqrt() + EPS);
122 p[j] -= (self.lr * lr_scale * upd) as f32;
123 }
124 }
125 }
126}
127
128fn sigmoid(x: f32) -> f32 {
129 1.0 / (1.0 + (-x).exp())
130}
131
132struct Pass<'a> {
135 fm: &'a FcdModel,
136 tau: f32,
137 logits: &'a [Vec<f32>],
139 hard: bool,
140 ffn: &'a [Option<(Vec<f32>, Vec<f32>, Vec<f32>)>],
142}
143
144impl Pass<'_> {
145 fn gates(&self, li: usize) -> Vec<f32> {
146 self.logits[li]
147 .iter()
148 .map(|&l| {
149 let s = sigmoid(l);
150 if self.hard {
151 if s > self.tau {
152 1.0
153 } else {
154 0.0
155 }
156 } else {
157 s
158 }
159 })
160 .collect()
161 }
162
163 fn wts<'b>(&'b self, li: usize) -> LnFfn<'b> {
164 let l = &self.fm.layers[li];
165 match &self.ffn[li] {
166 Some((g, u, d)) => LnFfn { iln: &l.iln, pln: &l.pln, gate: g, up: u, down: d },
167 None => LnFfn { iln: &l.iln, pln: &l.pln, gate: &l.gate, up: &l.up, down: &l.down },
168 }
169 }
170
171 #[allow(clippy::too_many_arguments)]
175 fn chunk(
176 &self,
177 ids: &[u32],
178 grad: Option<(&mut [Vec<f64>], &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>])>,
179 ) -> (f64, usize) {
180 let fm = self.fm;
181 let (t, hsz) = (ids.len(), fm.hidden);
182 let nl = fm.layers.len();
183 let mut h = vec![0f32; t * hsz];
185 for (r, &id) in ids.iter().enumerate() {
186 h[r * hsz..(r + 1) * hsz]
187 .copy_from_slice(&fm.embed[id as usize * hsz..(id as usize + 1) * hsz]);
188 }
189 let mut h_ins = Vec::with_capacity(nl);
191 let mut acts = Vec::with_capacity(nl);
192 let mut masks = Vec::with_capacity(nl);
193 for li in 0..nl {
194 let g = self.gates(li);
195 let wts = self.wts(li);
196 let want = grad.is_some();
197 let (h2, a) = fm.layer_forward_scaled(li, &h, 1, t, &wts, false, want, Some(&g));
198 h_ins.push(if want { h } else { Vec::new() });
199 acts.push(a);
200 masks.push(g);
201 h = h2;
202 }
203 let mut hn = vec![0f32; t * hsz];
205 let mut inv = vec![0f32; t];
206 ops::rmsnorm_fwd(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv);
207 let lm: &[f32] = fm.lm_head.as_deref().unwrap_or(&fm.embed);
208 let vocab = lm.len() / hsz;
209 let pool = fm.pool.as_deref();
210 let mut nll = 0f64;
211 let mut dh_n = vec![0f32; t * hsz]; const POS_CHUNK: usize = 32;
214 let scored = t - 1;
215 let mut p0 = 0usize;
216 while p0 < scored {
217 let pc = POS_CHUNK.min(scored - p0);
218 let mut logits = vec![0f32; pc * vocab];
219 ops::gemm_nt(&hn[p0 * hsz..(p0 + pc) * hsz], lm, &mut logits, pc, hsz, vocab, pool);
220 for r in 0..pc {
221 let target = ids[p0 + r + 1] as usize;
222 let row = &mut logits[r * vocab..(r + 1) * vocab];
223 let mx = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64;
224 let mut sum = 0f64;
225 for v in row.iter() {
226 sum += ((*v as f64) - mx).exp();
227 }
228 nll += mx + sum.ln() - row[target] as f64;
229 if grad.is_some() {
230 let inv_n = 1.0 / scored as f64;
232 for v in row.iter_mut() {
233 *v = ((((*v as f64) - mx).exp() / sum) * inv_n) as f32;
234 }
235 row[target] -= inv_n as f32;
236 }
237 }
238 if grad.is_some() {
239 ops::gemm_dx(&logits, lm, &mut dh_n[p0 * hsz..(p0 + pc) * hsz], pc, hsz, vocab, pool);
240 }
241 p0 += pc;
242 }
243 let Some((dmask, dffn)) = grad else {
244 return (nll, scored);
245 };
246 let mut dh = vec![0f32; t * hsz];
248 ops::rmsnorm_bwd(&h, &fm.final_norm, &inv, &dh_n, fm.gemma, &mut dh, None);
249 for li in (0..nl).rev() {
250 let a = acts[li].as_ref().expect("acts saved in grad mode");
251 let g = &masks[li];
252 let inter = fm.layers[li].inter;
253 let wts = self.wts(li);
254 let mut dact2 = vec![0f32; t * inter];
256 ops::gemm_dx(&dh, wts.down, &mut dact2, t, inter, hsz, fm.pool.as_deref());
257 if let Some((_, _, dd)) = dffn[li].as_mut() {
258 let mut act2 = a.act.clone();
260 for r in 0..t {
261 for (x, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
262 *x *= gv;
263 }
264 }
265 let mut dw = vec![0f32; hsz * inter];
266 ops::gemm_dw(&dh, &act2, &mut dw, t, inter, hsz, fm.pool.as_deref());
267 for (o, &x) in dd.iter_mut().zip(&dw) {
268 *o += x as f64;
269 }
270 }
271 {
273 let dm = &mut dmask[li];
274 for r in 0..t {
275 let da = &dact2[r * inter..(r + 1) * inter];
276 let aa = &a.act[r * inter..(r + 1) * inter];
277 for j in 0..inter {
278 dm[j] += da[j] as f64 * aa[j] as f64;
279 }
280 }
281 for (j, d) in dm.iter_mut().enumerate() {
283 let _ = j;
284 let _ = d;
285 }
286 }
287 let mut dg_pre = vec![0f32; t * inter];
289 let mut du_pre = vec![0f32; t * inter];
290 for r in 0..t {
291 for j in 0..inter {
292 let i = r * inter + j;
293 let da = dact2[i] * g[j];
294 let sg = ops::silu(a.gpre[i]);
295 dg_pre[i] = da * a.upre[i] * ops::silu_bwd(a.gpre[i]);
296 du_pre[i] = da * sg;
297 }
298 }
299 let mut dn2 = vec![0f32; t * hsz];
301 ops::gemm_dx(&dg_pre, wts.gate, &mut dn2, t, hsz, inter, fm.pool.as_deref());
302 let mut dn2b = vec![0f32; t * hsz];
303 ops::gemm_dx(&du_pre, wts.up, &mut dn2b, t, hsz, inter, fm.pool.as_deref());
304 for (x, &y) in dn2.iter_mut().zip(&dn2b) {
305 *x += y;
306 }
307 if let Some((dgw, duw, _)) = dffn[li].as_mut() {
308 let mut dw = vec![0f32; inter * hsz];
309 ops::gemm_dw(&dg_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
310 for (o, &x) in dgw.iter_mut().zip(&dw) {
311 *o += x as f64;
312 }
313 dw.fill(0.0);
314 ops::gemm_dw(&du_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
315 for (o, &x) in duw.iter_mut().zip(&dw) {
316 *o += x as f64;
317 }
318 }
319 let mut dh1 = dh.clone(); ops::rmsnorm_bwd(&a.h1, wts.pln, &a.inv2, &dn2, fm.gemma, &mut dh1, None);
323 dh = dh1;
324 let _ = &h_ins[li];
325 }
326 (nll, scored)
327 }
328}
329
330fn held_ppl(pass: &Pass, held: &[Vec<u32>]) -> f64 {
332 let mut nll = 0f64;
333 let mut n = 0usize;
334 for c in held {
335 let (l, k) = pass.chunk(c, None);
336 nll += l;
337 n += k;
338 }
339 (nll / n.max(1) as f64).exp()
340}
341
342pub fn skill_bake(
344 model: &Arc<CmfModel>,
345 chunks: &[Vec<u32>],
346 held_n: usize,
347 hy: &BakeHyper,
348 mut log: impl FnMut(&str),
349) -> Result<(BakeReport, BakeArtifacts), String> {
350 let t0 = std::time::Instant::now();
351 let o1_off = crate::nystrom::O1Cfg {
352 layers: crate::nystrom::O1Layers::List(Vec::new()),
353 m: 4,
354 w: 8,
355 sink: 1,
356 rect: crate::nystrom::O1_DEFAULT_RECT,
357 };
358 let fm = FcdModel::from_cmf(model, &o1_off)?;
359 let nl = fm.layers.len();
360 let inter = fm.layers.iter().map(|l| l.inter).max().unwrap_or(0);
361 if fm.layers.iter().any(|l| l.inter != inter) {
362 return Err("skill bake: non-uniform FFN widths".into());
363 }
364 let held: Vec<Vec<u32>> = chunks[..held_n.min(chunks.len())].to_vec();
365 let calib: Vec<Vec<u32>> = chunks[held_n.min(chunks.len())..].to_vec();
366 if calib.len() < 12 {
367 return Err(format!("skill bake: corpus too small ({} calib chunks)", calib.len()));
368 }
369 let fcd: Vec<usize> = (nl.saturating_sub(hy.fcd_layers)..nl).collect();
370 let _rng = SplitMix64::new(hy.seed);
371
372 let mut logits: Vec<Vec<f32>> = vec![vec![2.0; inter]; nl];
374 let mut ffn: Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>> = vec![None; nl];
375
376 let open: Vec<Vec<f32>> = vec![vec![50.0; inter]; nl];
379 let base_pass = Pass { fm: &fm, tau: hy.tau, logits: &open, hard: true, ffn: &ffn };
380 let backbone = held_ppl(&base_pass, &held);
381 log(&format!("baseline (full): {backbone:.3}"));
382
383 let mut adam_a = Adam::new(&vec![inter; nl], hy.lr_a);
385 let mut l1 = hy.l1_init;
386 let mut best: (f64, Option<Vec<Vec<f32>>>, f64) = (backbone, None, 0.0);
387 for step in 0..hy.steps_a {
388 let chunk = &calib[step % calib.len()];
389 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; nl];
390 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = vec![None; nl];
391 let pass = Pass { fm: &fm, tau: hy.tau, logits: &logits, hard: false, ffn: &ffn };
392 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
393 let l1_per = l1 / (inter as f64 * nl as f64);
395 for li in 0..nl {
396 for j in 0..inter {
397 let s = sigmoid(logits[li][j]) as f64;
398 dmask[li][j] = dmask[li][j] * s * (1.0 - s) + l1_per * s * (1.0 - s);
399 }
400 }
401 let mut params: Vec<&mut [f32]> = logits.iter_mut().map(|v| v.as_mut_slice()).collect();
402 adam_a.step(&mut params, &dmask, 1.0);
403 if (step + 1) % hy.eval_every == 0 {
404 l1 += hy.l1_step;
405 let pass = Pass { fm: &fm, tau: hy.tau, logits: &logits, hard: true, ffn: &ffn };
406 let hp = held_ppl(&pass, &held);
407 let alive: usize = logits
408 .iter()
409 .map(|l| l.iter().filter(|&&x| sigmoid(x) > hy.tau).count())
410 .sum();
411 let sp = 1.0 - alive as f64 / (nl * inter) as f64;
412 if hp < best.0 {
413 best = (hp, Some(logits.clone()), sp);
414 }
415 log(&format!(
416 " [A] step {}: L1={l1:.3} pruned={:.0}% hard-PPL={hp:.3} (bottom {:.3}@{:.0}%)",
417 step + 1,
418 sp * 100.0,
419 best.0,
420 best.2 * 100.0
421 ));
422 }
423 }
424 if let Some(b) = best.1.take() {
425 logits = b;
426 }
427 let pass = Pass { fm: &fm, tau: hy.tau, logits: &logits, hard: true, ffn: &ffn };
428 let masked = held_ppl(&pass, &held);
429 log(&format!("[A] {:.0}s: masked-PPL {masked:.3}", t0.elapsed().as_secs_f64()));
430
431 for &li in &fcd {
433 let l = &fm.layers[li];
434 ffn[li] = Some((l.gate.clone(), l.up.clone(), l.down.clone()));
435 }
436 let sizes: Vec<usize> = fcd
437 .iter()
438 .flat_map(|&li| {
439 let l = &fm.layers[li];
440 [l.gate.len(), l.up.len(), l.down.len()]
441 })
442 .collect();
443 let mut adam_b = Adam::new(&sizes, hy.lr_b);
444 let mut best_b: (f64, Option<Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>>>) = (masked, None);
445 for step in 0..hy.steps_b {
446 let chunk = &calib[step % calib.len()];
447 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; nl];
448 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = (0..nl)
449 .map(|li| {
450 ffn[li].as_ref().map(|(g, u, d)| {
451 (vec![0.0; g.len()], vec![0.0; u.len()], vec![0.0; d.len()])
452 })
453 })
454 .collect();
455 let pass = Pass { fm: &fm, tau: hy.tau, logits: &logits, hard: true, ffn: &ffn };
456 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
457 let lr_scale = 0.5 * (1.0 + (std::f64::consts::PI * step as f64 / hy.steps_b as f64).cos());
459 let first_fcd = fcd[0];
460 let mut params: Vec<&mut [f32]> = Vec::new();
461 let mut grads: Vec<Vec<f64>> = Vec::new();
462 for (off, slot) in ffn[first_fcd..].iter_mut().enumerate() {
463 let li = first_fcd + off;
464 let Some((g, u, d)) = slot.as_mut() else { continue };
465 let (dg, du, dd) = dffn[li].take().unwrap();
466 params.push(g.as_mut_slice());
467 grads.push(dg);
468 params.push(u.as_mut_slice());
469 grads.push(du);
470 params.push(d.as_mut_slice());
471 grads.push(dd);
472 }
473 adam_b.step(&mut params, &grads, lr_scale);
474 if (step + 1) % hy.eval_every == 0 {
475 let pass = Pass { fm: &fm, tau: hy.tau, logits: &logits, hard: true, ffn: &ffn };
476 let cur = held_ppl(&pass, &held);
477 if cur < best_b.0 {
478 best_b = (cur, Some(ffn.clone()));
479 }
480 log(&format!(" [B] step {}: held-PPL {cur:.3} (best {:.3})", step + 1, best_b.0));
481 }
482 }
483 if let Some(b) = best_b.1.take() {
484 ffn = b;
485 }
486 let overlaid = best_b.0;
487
488 let mut keep = Vec::with_capacity(nl);
490 let mut down_out = Vec::with_capacity(nl);
491 let mut gate_up = Vec::with_capacity(nl);
492 let mut kept_per_layer = Vec::with_capacity(nl);
493 for li in 0..nl {
494 let alive: Vec<bool> = logits[li].iter().map(|&x| sigmoid(x) > hy.tau).collect();
495 kept_per_layer.push(alive.iter().filter(|&&a| a).count());
496 let l = &fm.layers[li];
497 let mut down = match &ffn[li] {
498 Some((_, _, d)) => d.clone(),
499 None => l.down.clone(),
500 };
501 let hsz = fm.hidden;
502 for r in 0..hsz {
503 for (c, &a) in alive.iter().enumerate() {
504 if !a {
505 down[r * inter + c] = 0.0;
506 }
507 }
508 }
509 gate_up.push(ffn[li].as_ref().map(|(g, u, _)| (g.clone(), u.clone())));
510 down_out.push(down);
511 keep.push(alive);
512 }
513 let total: usize = kept_per_layer.iter().sum();
514 let report = BakeReport {
515 backbone,
516 masked,
517 overlaid,
518 pruned_ratio: 1.0 - total as f64 / (nl * inter) as f64,
519 kept_per_layer,
520 sec: t0.elapsed().as_secs_f64(),
521 };
522 let arts = BakeArtifacts { keep, down: down_out, gate_up, fcd_layers: fcd };
523 Ok((report, arts))
524}