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 pub target_sparsity: f64,
44 pub l1_mult: f64,
47 pub align: usize,
51 pub uniform_inter: bool,
54}
55
56impl Default for BakeHyper {
57 fn default() -> Self {
58 Self {
59 steps_a: 240,
60 steps_b: 120,
61 l1_init: 0.01,
62 l1_step: 0.005,
63 eval_every: 30,
64 lr_a: 0.1,
65 lr_b: 1e-5,
66 tau: 0.5,
67 fcd_layers: 4,
68 seed: 0,
69 target_sparsity: 0.0,
70 l1_mult: 1.0,
71 align: 32,
72 uniform_inter: false,
73 }
74 }
75}
76
77pub struct BakeReport {
79 pub backbone: f64,
81 pub masked: f64,
83 pub overlaid: f64,
85 pub pruned_ratio: f64,
86 pub kept_per_layer: Vec<usize>,
87 pub sec: f64,
88}
89
90pub struct BakeArtifacts {
92 pub keep: Vec<Vec<bool>>,
95 pub keep_visits: Vec<Vec<bool>>,
98 pub down: Vec<Vec<f32>>,
101 pub gate_up: Vec<Option<(Vec<f32>, Vec<f32>)>>,
103 pub fcd_layers: Vec<usize>,
105}
106
107const CLIP: f64 = 1.0;
108const B1: f64 = 0.9;
109const B2: f64 = 0.999;
110const EPS: f64 = 1e-8;
111
112struct Adam {
114 m: Vec<Vec<f64>>,
115 v: Vec<Vec<f64>>,
116 t: i32,
117 lr: f64,
118}
119
120impl Adam {
121 fn new(sizes: &[usize], lr: f64) -> Self {
122 Self {
123 m: sizes.iter().map(|&n| vec![0.0; n]).collect(),
124 v: sizes.iter().map(|&n| vec![0.0; n]).collect(),
125 t: 0,
126 lr,
127 }
128 }
129
130 fn step(&mut self, params: &mut [&mut [f32]], grads: &[Vec<f64>], lr_scale: f64) {
132 let gn: f64 = grads
133 .iter()
134 .flat_map(|g| g.iter().map(|x| x * x))
135 .sum::<f64>()
136 .sqrt();
137 let clip = if gn > CLIP { CLIP / gn } else { 1.0 };
138 self.t += 1;
139 let (bc1, bc2) = (1.0 - B1.powi(self.t), 1.0 - B2.powi(self.t));
140 for (pi, p) in params.iter_mut().enumerate() {
141 for j in 0..p.len() {
142 let g = grads[pi][j] * clip;
143 let m = &mut self.m[pi][j];
144 let v = &mut self.v[pi][j];
145 *m = B1 * *m + (1.0 - B1) * g;
146 *v = B2 * *v + (1.0 - B2) * g * g;
147 let upd = (*m / bc1) / ((*v / bc2).sqrt() + EPS);
148 p[j] -= (self.lr * lr_scale * upd) as f32;
149 }
150 }
151 }
152}
153
154pub fn mask_init_logit(loops: usize) -> f32 {
173 let base = 1.0f32 / (1.0 + (-2.0f32).exp());
174 let per_visit = base.powf(1.0 / loops.max(1) as f32);
175 (per_visit / (1.0 - per_visit)).ln()
176}
177
178pub fn mask_step_scale(loops: usize) -> f64 {
184 1.0 / loops.max(1) as f64
185}
186
187fn sigmoid(x: f32) -> f32 {
188 1.0 / (1.0 + (-x).exp())
189}
190
191struct Pass<'a> {
194 fm: &'a FcdModel,
195 tau: f32,
196 logits: &'a [Vec<f32>],
198 hard: bool,
199 ffn: &'a [Option<(Vec<f32>, Vec<f32>, Vec<f32>)>],
201}
202
203impl Pass<'_> {
204 fn gates(&self, li: usize) -> Vec<f32> {
205 self.logits[li]
206 .iter()
207 .map(|&l| {
208 let s = sigmoid(l);
209 if self.hard {
210 if s > self.tau { 1.0 } else { 0.0 }
211 } else {
212 s
213 }
214 })
215 .collect()
216 }
217
218 fn wts<'b>(&'b self, li: usize, mats: &'b crate::fcd::LayerMats) -> LnFfn<'b> {
219 let l = &self.fm.layers[li];
220 match &self.ffn[li] {
221 Some((g, u, d)) => LnFfn {
222 iln: &l.iln,
223 pln: &l.pln,
224 gate: g,
225 up: u,
226 down: d,
227 gu: None,
229 },
230 None => LnFfn {
231 iln: &l.iln,
232 pln: &l.pln,
233 gate: &[],
234 up: &[],
235 down: &mats.down,
236 gu: Some(&mats.gu),
237 },
238 }
239 }
240
241 #[allow(clippy::too_many_arguments)]
245 fn chunk(
246 &self,
247 ids: &[u32],
248 grad: Option<(
249 &mut [Vec<f64>],
250 &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>],
251 )>,
252 ) -> (f64, usize) {
253 self.chunk_batch(ids, 1, grad)
254 }
255
256 fn chunk_batch(
264 &self,
265 ids: &[u32],
266 b: usize,
267 grad: Option<(
268 &mut [Vec<f64>],
269 &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>],
270 )>,
271 ) -> (f64, usize) {
272 let fm = self.fm;
273 let hsz = fm.hidden;
274 debug_assert!(ids.len() % b.max(1) == 0, "ragged batch");
275 debug_assert!(grad.is_none() || b == 1, "grads are per-chunk");
278 let t = ids.len() / b.max(1);
279 let n = b * t;
280 let nl = fm.layers.len();
281 let mut h = vec![0f32; n * hsz];
283 for (r, &id) in ids.iter().enumerate() {
284 h[r * hsz..(r + 1) * hsz]
285 .copy_from_slice(&fm.embed[id as usize * hsz..(id as usize + 1) * hsz]);
286 }
287 let loops = fm.loops.max(1);
295 let vn = nl * loops;
296 let mut h_ins = Vec::with_capacity(vn);
297 let mut acts = Vec::with_capacity(vn);
298 let mut masks = Vec::with_capacity(vn);
299 let mut lnorms: Vec<Option<(Vec<f32>, Vec<f32>)>> = vec![None; vn];
301 for vl in 0..vn {
302 let li = vl % nl;
303 let g = self.gates(vl);
308 let mats_hold = fm.mats(li).expect("layer mats");
309 let wts = self.wts(li, &mats_hold);
310 let want = grad.is_some();
311 let (h2, a) = fm.layer_forward_scaled(li, &h, b, t, &wts, false, want, Some(&g));
312 h_ins.push(if want { h } else { Vec::new() });
313 acts.push(a);
314 masks.push(g);
315 h = h2;
316 if fm.loop_norm && li + 1 == nl && vl + 1 < vn {
319 let mut hn = vec![0f32; n * hsz];
320 let mut inv = vec![0f32; n];
321 ops::rmsnorm_fwd(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv);
322 if want {
323 lnorms[vl] = Some((h, inv));
324 }
325 h = hn;
326 }
327 }
328 let mut hn = vec![0f32; n * hsz];
330 let mut inv = vec![0f32; n];
331 ops::rmsnorm_fwd(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv);
332 let lm: &[f32] = fm.lm_head.as_deref().unwrap_or(&fm.embed);
333 let vocab = lm.len() / hsz;
334 let pool = fm.pool.as_deref();
335 let mut nll = 0f64;
336 let mut dh_n = vec![0f32; n * hsz]; const POS_CHUNK: usize = 64;
341 let scored = b * (t - 1);
342 for bi in 0..b {
343 let base = bi * t;
344 let mut p0 = 0usize;
345 while p0 < t - 1 {
346 let pc = POS_CHUNK.min(t - 1 - p0);
347 let mut logits = vec![0f32; pc * vocab];
348 ops::gemm_nt(
349 &hn[(base + p0) * hsz..(base + p0 + pc) * hsz],
350 lm,
351 &mut logits,
352 pc,
353 hsz,
354 vocab,
355 pool,
356 );
357 for r in 0..pc {
358 let target = ids[base + p0 + r + 1] as usize;
359 let row = &mut logits[r * vocab..(r + 1) * vocab];
360 let mx = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64;
361 let mut sum = 0f64;
362 for v in row.iter() {
363 sum += ((*v as f64) - mx).exp();
364 }
365 nll += mx + sum.ln() - row[target] as f64;
366 if grad.is_some() {
367 let inv_n = 1.0 / scored as f64;
369 for v in row.iter_mut() {
370 *v = ((((*v as f64) - mx).exp() / sum) * inv_n) as f32;
371 }
372 row[target] -= inv_n as f32;
373 }
374 }
375 if grad.is_some() {
376 ops::gemm_dx(
377 &logits,
378 lm,
379 &mut dh_n[(base + p0) * hsz..(base + p0 + pc) * hsz],
380 pc,
381 hsz,
382 vocab,
383 pool,
384 );
385 }
386 p0 += pc;
387 }
388 }
389 let Some((dmask, dffn)) = grad else {
390 return (nll, scored);
391 };
392 let t_bwd = std::time::Instant::now();
394 let mut dh = vec![0f32; n * hsz];
395 ops::rmsnorm_bwd(&h, &fm.final_norm, &inv, &dh_n, fm.gemma, &mut dh, None);
396 for vl in (0..vn).rev() {
397 let li = vl % nl;
398 if let Some((hb, inv)) = lnorms[vl].as_ref() {
400 let mut dprev = vec![0f32; n * hsz];
401 ops::rmsnorm_bwd(hb, &fm.final_norm, inv, &dh, fm.gemma, &mut dprev, None);
402 dh = dprev;
403 }
404 let a = acts[vl].as_ref().expect("acts saved in grad mode");
405 let g = &masks[vl];
406 let inter = fm.layers[li].inter;
407 let mats_hold = fm.mats(li).expect("layer mats");
408 let wts = self.wts(li, &mats_hold);
409 let mut dact2 = vec![0f32; t * inter];
411 ops::gemm_dx(&dh, wts.down, &mut dact2, t, inter, hsz, fm.pool.as_deref());
412 if let Some((_, _, dd)) = dffn[li].as_mut() {
413 let mut act2 = a.act.clone();
415 for r in 0..t {
416 for (x, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
417 *x *= gv;
418 }
419 }
420 let mut dw = vec![0f32; hsz * inter];
421 ops::gemm_dw(&dh, &act2, &mut dw, t, inter, hsz, fm.pool.as_deref());
422 for (o, &x) in dd.iter_mut().zip(&dw) {
423 *o += x as f64;
424 }
425 }
426 {
430 let dm = &mut dmask[vl];
431 for r in 0..t {
432 let da = &dact2[r * inter..(r + 1) * inter];
433 let aa = &a.act[r * inter..(r + 1) * inter];
434 for j in 0..inter {
435 dm[j] += da[j] as f64 * aa[j] as f64;
436 }
437 }
438 for (j, d) in dm.iter_mut().enumerate() {
440 let _ = j;
441 let _ = d;
442 }
443 }
444 let mut dg_pre = vec![0f32; t * inter];
446 let mut du_pre = vec![0f32; t * inter];
447 for r in 0..t {
448 for j in 0..inter {
449 let i = r * inter + j;
450 let da = dact2[i] * g[j];
451 let sg = ops::silu(a.gpre[i]);
452 dg_pre[i] = da * a.upre[i] * ops::silu_bwd(a.gpre[i]);
453 du_pre[i] = da * sg;
454 }
455 }
456 let mut dn2 = vec![0f32; t * hsz];
459 if let Some(gu) = wts.gu {
460 let mut dgu = vec![0f32; t * 2 * inter];
461 for r in 0..t {
462 let row = &mut dgu[r * 2 * inter..(r + 1) * 2 * inter];
463 row[..inter].copy_from_slice(&dg_pre[r * inter..(r + 1) * inter]);
464 row[inter..].copy_from_slice(&du_pre[r * inter..(r + 1) * inter]);
465 }
466 ops::gemm_dx(&dgu, gu, &mut dn2, t, hsz, 2 * inter, fm.pool.as_deref());
467 } else {
468 ops::gemm_dx(
469 &dg_pre,
470 wts.gate,
471 &mut dn2,
472 t,
473 hsz,
474 inter,
475 fm.pool.as_deref(),
476 );
477 let mut dn2b = vec![0f32; t * hsz];
478 ops::gemm_dx(
479 &du_pre,
480 wts.up,
481 &mut dn2b,
482 t,
483 hsz,
484 inter,
485 fm.pool.as_deref(),
486 );
487 for (x, &y) in dn2.iter_mut().zip(&dn2b) {
488 *x += y;
489 }
490 }
491 if let Some((dgw, duw, _)) = dffn[li].as_mut() {
492 let mut dw = vec![0f32; inter * hsz];
493 ops::gemm_dw(&dg_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
494 for (o, &x) in dgw.iter_mut().zip(&dw) {
495 *o += x as f64;
496 }
497 dw.fill(0.0);
498 ops::gemm_dw(&du_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
499 for (o, &x) in duw.iter_mut().zip(&dw) {
500 *o += x as f64;
501 }
502 }
503 let mut dh1 = dh.clone(); ops::rmsnorm_bwd(&a.h1, wts.pln, &a.inv2, &dn2, fm.gemma, &mut dh1, None);
507 dh = dh1;
508 let _ = &h_ins[vl];
509 }
510 crate::fcd::prof::add(&crate::fcd::prof::BWD, t_bwd);
511 (nll, scored)
512 }
513}
514
515fn held_ppl(pass: &Pass, held: &[Vec<u32>]) -> f64 {
517 if held.is_empty() {
521 return f64::NAN;
522 }
523 let t = held[0].len();
524 if held.iter().all(|c| c.len() == t) {
525 let flat: Vec<u32> = held.iter().flatten().copied().collect();
526 let (l, k) = pass.chunk_batch(&flat, held.len(), None);
527 return (l / k.max(1) as f64).exp();
528 }
529 let mut nll = 0f64;
530 let mut n = 0usize;
531 for c in held {
532 let (l, k) = pass.chunk(c, None);
533 nll += l;
534 n += k;
535 }
536 (nll / n.max(1) as f64).exp()
537}
538
539pub fn skill_bake(
541 model: &Arc<CmfModel>,
542 chunks: &[Vec<u32>],
543 held_n: usize,
544 hy: &BakeHyper,
545 mut log: impl FnMut(&str),
546) -> Result<(BakeReport, BakeArtifacts), String> {
547 let t0 = std::time::Instant::now();
548 let o1_off = crate::nystrom::O1Cfg {
549 layers: crate::nystrom::O1Layers::List(Vec::new()),
550 m: 4,
551 w: 8,
552 sink: 1,
553 rect: crate::nystrom::O1_DEFAULT_RECT,
554 };
555 let fm = FcdModel::from_cmf(model, &o1_off)?;
556 let nl = fm.layers.len();
557 let inter = fm.layers.iter().map(|l| l.inter).max().unwrap_or(0);
558 if fm.layers.iter().any(|l| l.inter != inter) {
559 return Err("skill bake: non-uniform FFN widths".into());
560 }
561 let held: Vec<Vec<u32>> = chunks[..held_n.min(chunks.len())].to_vec();
562 let calib: Vec<Vec<u32>> = chunks[held_n.min(chunks.len())..].to_vec();
563 if calib.len() < 12 {
564 return Err(format!(
565 "skill bake: corpus too small ({} calib chunks)",
566 calib.len()
567 ));
568 }
569 let fcd: Vec<usize> = (nl.saturating_sub(hy.fcd_layers)..nl).collect();
570 let _rng = SplitMix64::new(hy.seed);
571
572 let loops = fm.loops.max(1);
600 let m0 = mask_init_logit(loops);
601 let vn = nl * loops;
603 let mut logits: Vec<Vec<f32>> = vec![vec![m0; inter]; vn];
604 let mut ffn: Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>> = vec![None; nl];
605
606 let open: Vec<Vec<f32>> = vec![vec![50.0; inter]; vn];
609 let base_pass = Pass {
610 fm: &fm,
611 tau: hy.tau,
612 logits: &open,
613 hard: true,
614 ffn: &ffn,
615 };
616 let backbone = held_ppl(&base_pass, &held);
617 log(&format!("baseline (full): {backbone:.3}"));
618
619 let mut adam_a = Adam::new(&vec![inter; vn], hy.lr_a);
621 let mut l1 = hy.l1_init * hy.l1_mult;
622 let l1_step_eff = hy.l1_step * hy.l1_mult;
623 let mut best: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
625 let mut max_sp: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
627 let mut prev_alive: Option<Vec<Vec<bool>>> = None;
628 let mut acc_chunk = 0f64;
632 let mut acc_adam = 0f64;
633 crate::gpu::bake_precision_strict(true);
641 for step in 0..hy.steps_a {
642 let t_step = std::time::Instant::now();
643 let chunk = &calib[step % calib.len()];
644 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; vn];
645 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = vec![None; nl];
646 let pass = Pass {
647 fm: &fm,
648 tau: hy.tau,
649 logits: &logits,
650 hard: false,
651 ffn: &ffn,
652 };
653 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
654 let l1_per = l1 / (inter as f64 * nl as f64);
656 for li in 0..vn {
657 for j in 0..inter {
658 let s = sigmoid(logits[li][j]) as f64;
659 dmask[li][j] = dmask[li][j] * s * (1.0 - s) + l1_per * s * (1.0 - s);
660 }
661 }
662 let t_chunk = t_step.elapsed().as_secs_f64();
675 let mut params: Vec<&mut [f32]> = logits.iter_mut().map(|v| v.as_mut_slice()).collect();
676 adam_a.step(&mut params, &dmask, 1.0);
677 acc_chunk += t_chunk;
678 acc_adam += t_step.elapsed().as_secs_f64() - t_chunk;
679 if (step + 1) % hy.eval_every == 0 {
680 l1 += l1_step_eff;
681 let pass = Pass {
682 fm: &fm,
683 tau: hy.tau,
684 logits: &logits,
685 hard: true,
686 ffn: &ffn,
687 };
688 crate::gpu::bake_precision_strict(false);
689 let hp = held_ppl(&pass, &held);
690 crate::gpu::bake_precision_strict(true);
691 let cur: Vec<Vec<bool>> = logits
697 .iter()
698 .map(|l| l.iter().map(|&x| sigmoid(x) > hy.tau).collect())
699 .collect();
700 if let Some(prev) = &prev_alive {
701 let died: Vec<String> = cur
702 .iter()
703 .zip(prev)
704 .enumerate()
705 .flat_map(|(li, (c, p))| {
706 c.iter()
707 .zip(p.iter())
708 .enumerate()
709 .filter(|&(_, (&cj, &pj))| pj && !cj)
710 .map(move |(j, _)| format!("L{li}:{j}"))
711 })
712 .collect();
713 if !died.is_empty() {
714 log(&format!(
715 " closed since last eval: {}: {}{}",
716 died.len(),
717 died.iter().take(32).cloned().collect::<Vec<_>>().join(" "),
718 if died.len() > 32 { " …" } else { "" }
719 ));
720 }
721 }
722 let alive: usize = cur.iter().map(|l| l.iter().filter(|&&b| b).count()).sum();
723 prev_alive = Some(cur);
724 let sp = 1.0 - alive as f64 / (vn * inter) as f64;
725 if sp > max_sp.2 {
727 max_sp = (hp, Some(logits.clone()), sp);
728 }
729 if hy.target_sparsity > 0.0 {
731 if sp >= hy.target_sparsity && hp < best.0 {
732 best = (hp, Some(logits.clone()), sp);
733 }
734 } else if hp < best.0 {
735 best = (hp, Some(logits.clone()), sp);
736 }
737 log(&format!(
738 " [A] step {}: L1={l1:.3} pruned={:.2}% hard-PPL={hp:.3} (bottom {}@{:.2}%) [fwd+bwd {:.1}s, adam {:.2}s per step]",
739 step + 1,
740 sp * 100.0,
741 if best.0 == f64::MAX {
742 "—".to_string()
743 } else {
744 format!("{:.3}", best.0)
745 },
746 best.2 * 100.0,
747 acc_chunk / (step + 1) as f64,
748 acc_adam / (step + 1) as f64
749 ));
750 }
751 }
752 crate::gpu::bake_precision_strict(false);
756 if hy.target_sparsity > 0.0 && best.1.is_none() {
757 log(&format!(
758 "[A] target sparsity {:.0}% not reached; using max-sparsity checkpoint ({:.0}%)",
759 hy.target_sparsity * 100.0,
760 max_sp.2 * 100.0
761 ));
762 best = max_sp;
763 }
764 {
767 use crate::fcd::prof;
768 let (a, f, bw, g, gc) = (
769 prof::take(&prof::ATTN_FWD),
770 prof::take(&prof::FFN_FWD),
771 prof::take(&prof::BWD),
772 prof::take(&prof::GEMM),
773 prof::GEMM_CALLS.swap(0, std::sync::atomic::Ordering::Relaxed),
774 );
775 log(&format!(
776 "[prof] phase A over {} step(s): attn-fwd {a:.1}s | ffn-fwd {f:.1}s | bwd {bw:.1}s | gemm total {g:.1}s in {gc} calls ({:.1} ms/call)",
777 hy.steps_a,
778 if gc > 0 { g * 1000.0 / gc as f64 } else { 0.0 }
779 ));
780 log(&format!("[prof] gemm shapes:\n{}", prof::shape_report(6)));
781 }
782 if let Some(b) = best.1.take() {
783 logits = b;
784 }
785 let pass = Pass {
786 fm: &fm,
787 tau: hy.tau,
788 logits: &logits,
789 hard: true,
790 ffn: &ffn,
791 };
792 let masked = held_ppl(&pass, &held);
793 log(&format!(
794 "[A] {:.0}s: masked-PPL {masked:.3}",
795 t0.elapsed().as_secs_f64()
796 ));
797
798 for &li in &fcd {
800 let p = format!("model.layers.{li}.");
801 ffn[li] = Some((
802 crate::fcd::deq_pub(&fm.src, &format!("{p}mlp.gate_proj.weight"))
803 .map_err(|e| format!("phase-B gate: {e}"))?,
804 crate::fcd::deq_pub(&fm.src, &format!("{p}mlp.up_proj.weight"))
805 .map_err(|e| format!("phase-B up: {e}"))?,
806 crate::fcd::deq_pub(&fm.src, &format!("{p}mlp.down_proj.weight"))
807 .map_err(|e| format!("phase-B down: {e}"))?,
808 ));
809 }
810 let sizes: Vec<usize> = fcd
811 .iter()
812 .flat_map(|&li| {
813 let (g, u, d) = ffn[li].as_ref().expect("phase-B masters");
814 [g.len(), u.len(), d.len()]
815 })
816 .collect();
817 let mut adam_b = Adam::new(&sizes, hy.lr_b);
818 let mut best_b: (f64, Option<Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>>>) = (masked, None);
819 for step in 0..hy.steps_b {
820 let chunk = &calib[step % calib.len()];
821 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; vn];
822 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = (0..nl)
823 .map(|li| {
824 ffn[li]
825 .as_ref()
826 .map(|(g, u, d)| (vec![0.0; g.len()], vec![0.0; u.len()], vec![0.0; d.len()]))
827 })
828 .collect();
829 let pass = Pass {
830 fm: &fm,
831 tau: hy.tau,
832 logits: &logits,
833 hard: true,
834 ffn: &ffn,
835 };
836 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
837 let lr_scale = 0.5 * (1.0 + (std::f64::consts::PI * step as f64 / hy.steps_b as f64).cos());
839 let first_fcd = fcd[0];
840 let mut params: Vec<&mut [f32]> = Vec::new();
841 let mut grads: Vec<Vec<f64>> = Vec::new();
842 for (off, slot) in ffn[first_fcd..].iter_mut().enumerate() {
843 let li = first_fcd + off;
844 let Some((g, u, d)) = slot.as_mut() else {
845 continue;
846 };
847 let (dg, du, dd) = dffn[li].take().unwrap();
848 params.push(g.as_mut_slice());
849 grads.push(dg);
850 params.push(u.as_mut_slice());
851 grads.push(du);
852 params.push(d.as_mut_slice());
853 grads.push(dd);
854 }
855 adam_b.step(&mut params, &grads, lr_scale * mask_step_scale(loops));
860 if (step + 1) % hy.eval_every == 0 {
861 let pass = Pass {
862 fm: &fm,
863 tau: hy.tau,
864 logits: &logits,
865 hard: true,
866 ffn: &ffn,
867 };
868 let cur = held_ppl(&pass, &held);
869 if cur < best_b.0 {
870 best_b = (cur, Some(ffn.clone()));
871 }
872 log(&format!(
873 " [B] step {}: held-PPL {cur:.3} (best {:.3})",
874 step + 1,
875 best_b.0
876 ));
877 }
878 }
879 if let Some(b) = best_b.1.take() {
880 ffn = b;
881 }
882 let overlaid = best_b.0;
883
884 let keep_visits = keep_masks(&logits, hy.tau, hy.align, hy.uniform_inter);
889 let keep: Vec<Vec<bool>> = (0..nl)
890 .map(|li| {
891 (0..inter)
892 .map(|j| (0..loops).any(|v| keep_visits[v * nl + li][j]))
893 .collect()
894 })
895 .collect();
896 if hy.align > 1 || hy.uniform_inter {
897 let raw: usize = logits
898 .iter()
899 .map(|l| l.iter().filter(|&&x| sigmoid(x) > hy.tau).count())
900 .sum();
901 let padded: usize = keep_visits
905 .iter()
906 .map(|a| a.iter().filter(|&&x| x).count())
907 .sum::<usize>()
908 .saturating_sub(raw);
909 log(&format!(
910 "align: +{padded} neurons resurrected (align {}, uniform {})",
911 hy.align, hy.uniform_inter
912 ));
913 }
914 let mut down_out = Vec::with_capacity(nl);
915 let mut gate_up = Vec::with_capacity(nl);
916 let mut kept_per_layer = Vec::with_capacity(nl);
917 for li in 0..nl {
918 let alive = &keep[li];
919 kept_per_layer.push(alive.iter().filter(|&&a| a).count());
920 let mut down = match &ffn[li] {
921 Some((_, _, d)) => d.clone(),
922 None => fm.mats(li).expect("layer mats").down.clone(),
923 };
924 let hsz = fm.hidden;
925 for r in 0..hsz {
926 for (c, &a) in alive.iter().enumerate() {
927 if !a {
928 down[r * inter + c] = 0.0;
929 }
930 }
931 }
932 gate_up.push(ffn[li].as_ref().map(|(g, u, _)| (g.clone(), u.clone())));
933 down_out.push(down);
934 }
935 let total: usize = keep_visits
936 .iter()
937 .map(|a| a.iter().filter(|&&x| x).count())
938 .sum();
939 let report = BakeReport {
940 backbone,
941 masked,
942 overlaid,
943 pruned_ratio: 1.0 - total as f64 / (vn * inter) as f64,
944 kept_per_layer,
945 sec: t0.elapsed().as_secs_f64(),
946 };
947 let arts = BakeArtifacts {
948 keep,
949 keep_visits,
950 down: down_out,
951 gate_up,
952 fcd_layers: fcd,
953 };
954 Ok((report, arts))
955}
956
957fn keep_masks(logits: &[Vec<f32>], tau: f32, align: usize, uniform: bool) -> Vec<Vec<bool>> {
965 let inter = logits[0].len();
966 let round = |n: usize| -> usize {
967 let n = n.max(1);
968 if align <= 1 {
969 n.min(inter)
970 } else {
971 (n.div_ceil(align) * align).min(inter)
972 }
973 };
974 let mut want: Vec<usize> = logits
975 .iter()
976 .map(|l| round(l.iter().filter(|&&x| sigmoid(x) > tau).count()))
977 .collect();
978 if uniform {
979 let k = want.iter().copied().max().unwrap_or(inter);
980 want = vec![k; logits.len()];
981 }
982 logits
983 .iter()
984 .zip(&want)
985 .map(|(l, &k)| {
986 let mut idx: Vec<usize> = (0..inter).collect();
987 idx.sort_unstable_by(|&a, &b| l[b].total_cmp(&l[a]));
988 let mut alive = vec![false; inter];
989 for &i in idx.iter().take(k) {
990 alive[i] = true;
991 }
992 alive
993 })
994 .collect()
995}
996
997#[cfg(test)]
998mod tests {
999 use super::*;
1000
1001 fn kept(masks: &[Vec<bool>]) -> Vec<usize> {
1002 masks
1003 .iter()
1004 .map(|m| m.iter().filter(|&&a| a).count())
1005 .collect()
1006 }
1007
1008 #[test]
1011 fn keep_masks_aligns_up_and_preserves_alive() {
1012 let inter = 96;
1013 let l0: Vec<f32> = (0..inter)
1016 .map(|i| if i < 40 { 1.0 } else { -1.0 - i as f32 * 0.01 })
1017 .collect();
1018 let l1: Vec<f32> = (0..inter)
1020 .map(|i| if i < 64 { 2.0 } else { -3.0 })
1021 .collect();
1022 let masks = keep_masks(&[l0.clone(), l1], 0.5, 32, false);
1023 assert_eq!(kept(&masks), vec![64, 64]);
1024 for i in 0..64 {
1027 assert!(masks[0][i], "neuron {i} should be kept");
1028 }
1029 for i in 64..inter {
1030 assert!(!masks[0][i], "neuron {i} should stay pruned");
1031 }
1032 }
1033
1034 #[test]
1036 fn keep_masks_uniform_takes_max() {
1037 let inter = 96;
1038 let l0: Vec<f32> = (0..inter)
1039 .map(|i| if i < 10 { 1.0 } else { -2.0 })
1040 .collect();
1041 let l1: Vec<f32> = (0..inter)
1042 .map(|i| if i < 70 { 1.0 } else { -2.0 })
1043 .collect();
1044 let masks = keep_masks(&[l0, l1], 0.5, 32, true);
1045 assert_eq!(kept(&masks), vec![96, 96]);
1046 }
1047
1048 #[test]
1051 fn keep_masks_edges() {
1052 let inter = 48;
1053 let l: Vec<f32> = (0..inter)
1054 .map(|i| if i < 47 { 1.0 } else { -2.0 })
1055 .collect();
1056 let masks = keep_masks(&[l.clone()], 0.5, 32, false);
1057 assert_eq!(kept(&masks), vec![48]); let masks = keep_masks(&[l], 0.5, 1, false);
1059 assert_eq!(kept(&masks), vec![47]);
1060 let dead: Vec<f32> = vec![-5.0; inter];
1061 let masks = keep_masks(&[dead], 0.5, 32, false);
1062 assert_eq!(kept(&masks), vec![32]); }
1064}