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
111 .iter()
112 .flat_map(|g| g.iter().map(|x| x * x))
113 .sum::<f64>()
114 .sqrt();
115 let clip = if gn > CLIP { CLIP / gn } else { 1.0 };
116 self.t += 1;
117 let (bc1, bc2) = (1.0 - B1.powi(self.t), 1.0 - B2.powi(self.t));
118 for (pi, p) in params.iter_mut().enumerate() {
119 for j in 0..p.len() {
120 let g = grads[pi][j] * clip;
121 let m = &mut self.m[pi][j];
122 let v = &mut self.v[pi][j];
123 *m = B1 * *m + (1.0 - B1) * g;
124 *v = B2 * *v + (1.0 - B2) * g * g;
125 let upd = (*m / bc1) / ((*v / bc2).sqrt() + EPS);
126 p[j] -= (self.lr * lr_scale * upd) as f32;
127 }
128 }
129 }
130}
131
132fn sigmoid(x: f32) -> f32 {
133 1.0 / (1.0 + (-x).exp())
134}
135
136struct Pass<'a> {
139 fm: &'a FcdModel,
140 tau: f32,
141 logits: &'a [Vec<f32>],
143 hard: bool,
144 ffn: &'a [Option<(Vec<f32>, Vec<f32>, Vec<f32>)>],
146}
147
148impl Pass<'_> {
149 fn gates(&self, li: usize) -> Vec<f32> {
150 self.logits[li]
151 .iter()
152 .map(|&l| {
153 let s = sigmoid(l);
154 if self.hard {
155 if s > self.tau { 1.0 } else { 0.0 }
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 {
167 iln: &l.iln,
168 pln: &l.pln,
169 gate: g,
170 up: u,
171 down: d,
172 },
173 None => LnFfn {
174 iln: &l.iln,
175 pln: &l.pln,
176 gate: &l.gate,
177 up: &l.up,
178 down: &l.down,
179 },
180 }
181 }
182
183 #[allow(clippy::too_many_arguments)]
187 fn chunk(
188 &self,
189 ids: &[u32],
190 grad: Option<(
191 &mut [Vec<f64>],
192 &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>],
193 )>,
194 ) -> (f64, usize) {
195 let fm = self.fm;
196 let (t, hsz) = (ids.len(), fm.hidden);
197 let nl = fm.layers.len();
198 let mut h = vec![0f32; t * hsz];
200 for (r, &id) in ids.iter().enumerate() {
201 h[r * hsz..(r + 1) * hsz]
202 .copy_from_slice(&fm.embed[id as usize * hsz..(id as usize + 1) * hsz]);
203 }
204 let mut h_ins = Vec::with_capacity(nl);
206 let mut acts = Vec::with_capacity(nl);
207 let mut masks = Vec::with_capacity(nl);
208 for li in 0..nl {
209 let g = self.gates(li);
210 let wts = self.wts(li);
211 let want = grad.is_some();
212 let (h2, a) = fm.layer_forward_scaled(li, &h, 1, t, &wts, false, want, Some(&g));
213 h_ins.push(if want { h } else { Vec::new() });
214 acts.push(a);
215 masks.push(g);
216 h = h2;
217 }
218 let mut hn = vec![0f32; t * hsz];
220 let mut inv = vec![0f32; t];
221 ops::rmsnorm_fwd(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv);
222 let lm: &[f32] = fm.lm_head.as_deref().unwrap_or(&fm.embed);
223 let vocab = lm.len() / hsz;
224 let pool = fm.pool.as_deref();
225 let mut nll = 0f64;
226 let mut dh_n = vec![0f32; t * hsz]; const POS_CHUNK: usize = 32;
229 let scored = t - 1;
230 let mut p0 = 0usize;
231 while p0 < scored {
232 let pc = POS_CHUNK.min(scored - p0);
233 let mut logits = vec![0f32; pc * vocab];
234 ops::gemm_nt(
235 &hn[p0 * hsz..(p0 + pc) * hsz],
236 lm,
237 &mut logits,
238 pc,
239 hsz,
240 vocab,
241 pool,
242 );
243 for r in 0..pc {
244 let target = ids[p0 + r + 1] as usize;
245 let row = &mut logits[r * vocab..(r + 1) * vocab];
246 let mx = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64;
247 let mut sum = 0f64;
248 for v in row.iter() {
249 sum += ((*v as f64) - mx).exp();
250 }
251 nll += mx + sum.ln() - row[target] as f64;
252 if grad.is_some() {
253 let inv_n = 1.0 / scored as f64;
255 for v in row.iter_mut() {
256 *v = ((((*v as f64) - mx).exp() / sum) * inv_n) as f32;
257 }
258 row[target] -= inv_n as f32;
259 }
260 }
261 if grad.is_some() {
262 ops::gemm_dx(
263 &logits,
264 lm,
265 &mut dh_n[p0 * hsz..(p0 + pc) * hsz],
266 pc,
267 hsz,
268 vocab,
269 pool,
270 );
271 }
272 p0 += pc;
273 }
274 let Some((dmask, dffn)) = grad else {
275 return (nll, scored);
276 };
277 let mut dh = vec![0f32; t * hsz];
279 ops::rmsnorm_bwd(&h, &fm.final_norm, &inv, &dh_n, fm.gemma, &mut dh, None);
280 for li in (0..nl).rev() {
281 let a = acts[li].as_ref().expect("acts saved in grad mode");
282 let g = &masks[li];
283 let inter = fm.layers[li].inter;
284 let wts = self.wts(li);
285 let mut dact2 = vec![0f32; t * inter];
287 ops::gemm_dx(&dh, wts.down, &mut dact2, t, inter, hsz, fm.pool.as_deref());
288 if let Some((_, _, dd)) = dffn[li].as_mut() {
289 let mut act2 = a.act.clone();
291 for r in 0..t {
292 for (x, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
293 *x *= gv;
294 }
295 }
296 let mut dw = vec![0f32; hsz * inter];
297 ops::gemm_dw(&dh, &act2, &mut dw, t, inter, hsz, fm.pool.as_deref());
298 for (o, &x) in dd.iter_mut().zip(&dw) {
299 *o += x as f64;
300 }
301 }
302 {
304 let dm = &mut dmask[li];
305 for r in 0..t {
306 let da = &dact2[r * inter..(r + 1) * inter];
307 let aa = &a.act[r * inter..(r + 1) * inter];
308 for j in 0..inter {
309 dm[j] += da[j] as f64 * aa[j] as f64;
310 }
311 }
312 for (j, d) in dm.iter_mut().enumerate() {
314 let _ = j;
315 let _ = d;
316 }
317 }
318 let mut dg_pre = vec![0f32; t * inter];
320 let mut du_pre = vec![0f32; t * inter];
321 for r in 0..t {
322 for j in 0..inter {
323 let i = r * inter + j;
324 let da = dact2[i] * g[j];
325 let sg = ops::silu(a.gpre[i]);
326 dg_pre[i] = da * a.upre[i] * ops::silu_bwd(a.gpre[i]);
327 du_pre[i] = da * sg;
328 }
329 }
330 let mut dn2 = vec![0f32; t * hsz];
332 ops::gemm_dx(
333 &dg_pre,
334 wts.gate,
335 &mut dn2,
336 t,
337 hsz,
338 inter,
339 fm.pool.as_deref(),
340 );
341 let mut dn2b = vec![0f32; t * hsz];
342 ops::gemm_dx(
343 &du_pre,
344 wts.up,
345 &mut dn2b,
346 t,
347 hsz,
348 inter,
349 fm.pool.as_deref(),
350 );
351 for (x, &y) in dn2.iter_mut().zip(&dn2b) {
352 *x += y;
353 }
354 if let Some((dgw, duw, _)) = dffn[li].as_mut() {
355 let mut dw = vec![0f32; inter * hsz];
356 ops::gemm_dw(&dg_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
357 for (o, &x) in dgw.iter_mut().zip(&dw) {
358 *o += x as f64;
359 }
360 dw.fill(0.0);
361 ops::gemm_dw(&du_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
362 for (o, &x) in duw.iter_mut().zip(&dw) {
363 *o += x as f64;
364 }
365 }
366 let mut dh1 = dh.clone(); ops::rmsnorm_bwd(&a.h1, wts.pln, &a.inv2, &dn2, fm.gemma, &mut dh1, None);
370 dh = dh1;
371 let _ = &h_ins[li];
372 }
373 (nll, scored)
374 }
375}
376
377fn held_ppl(pass: &Pass, held: &[Vec<u32>]) -> f64 {
379 let mut nll = 0f64;
380 let mut n = 0usize;
381 for c in held {
382 let (l, k) = pass.chunk(c, None);
383 nll += l;
384 n += k;
385 }
386 (nll / n.max(1) as f64).exp()
387}
388
389pub fn skill_bake(
391 model: &Arc<CmfModel>,
392 chunks: &[Vec<u32>],
393 held_n: usize,
394 hy: &BakeHyper,
395 mut log: impl FnMut(&str),
396) -> Result<(BakeReport, BakeArtifacts), String> {
397 let t0 = std::time::Instant::now();
398 let o1_off = crate::nystrom::O1Cfg {
399 layers: crate::nystrom::O1Layers::List(Vec::new()),
400 m: 4,
401 w: 8,
402 sink: 1,
403 rect: crate::nystrom::O1_DEFAULT_RECT,
404 };
405 let fm = FcdModel::from_cmf(model, &o1_off)?;
406 let nl = fm.layers.len();
407 let inter = fm.layers.iter().map(|l| l.inter).max().unwrap_or(0);
408 if fm.layers.iter().any(|l| l.inter != inter) {
409 return Err("skill bake: non-uniform FFN widths".into());
410 }
411 let held: Vec<Vec<u32>> = chunks[..held_n.min(chunks.len())].to_vec();
412 let calib: Vec<Vec<u32>> = chunks[held_n.min(chunks.len())..].to_vec();
413 if calib.len() < 12 {
414 return Err(format!(
415 "skill bake: corpus too small ({} calib chunks)",
416 calib.len()
417 ));
418 }
419 let fcd: Vec<usize> = (nl.saturating_sub(hy.fcd_layers)..nl).collect();
420 let _rng = SplitMix64::new(hy.seed);
421
422 let mut logits: Vec<Vec<f32>> = vec![vec![2.0; inter]; nl];
424 let mut ffn: Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>> = vec![None; nl];
425
426 let open: Vec<Vec<f32>> = vec![vec![50.0; inter]; nl];
429 let base_pass = Pass {
430 fm: &fm,
431 tau: hy.tau,
432 logits: &open,
433 hard: true,
434 ffn: &ffn,
435 };
436 let backbone = held_ppl(&base_pass, &held);
437 log(&format!("baseline (full): {backbone:.3}"));
438
439 let mut adam_a = Adam::new(&vec![inter; nl], hy.lr_a);
441 let mut l1 = hy.l1_init;
442 let mut best: (f64, Option<Vec<Vec<f32>>>, f64) = (backbone, None, 0.0);
443 for step in 0..hy.steps_a {
444 let chunk = &calib[step % calib.len()];
445 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; nl];
446 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = vec![None; nl];
447 let pass = Pass {
448 fm: &fm,
449 tau: hy.tau,
450 logits: &logits,
451 hard: false,
452 ffn: &ffn,
453 };
454 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
455 let l1_per = l1 / (inter as f64 * nl as f64);
457 for li in 0..nl {
458 for j in 0..inter {
459 let s = sigmoid(logits[li][j]) as f64;
460 dmask[li][j] = dmask[li][j] * s * (1.0 - s) + l1_per * s * (1.0 - s);
461 }
462 }
463 let mut params: Vec<&mut [f32]> = logits.iter_mut().map(|v| v.as_mut_slice()).collect();
464 adam_a.step(&mut params, &dmask, 1.0);
465 if (step + 1) % hy.eval_every == 0 {
466 l1 += hy.l1_step;
467 let pass = Pass {
468 fm: &fm,
469 tau: hy.tau,
470 logits: &logits,
471 hard: true,
472 ffn: &ffn,
473 };
474 let hp = held_ppl(&pass, &held);
475 let alive: usize = logits
476 .iter()
477 .map(|l| l.iter().filter(|&&x| sigmoid(x) > hy.tau).count())
478 .sum();
479 let sp = 1.0 - alive as f64 / (nl * inter) as f64;
480 if hp < best.0 {
481 best = (hp, Some(logits.clone()), sp);
482 }
483 log(&format!(
484 " [A] step {}: L1={l1:.3} pruned={:.0}% hard-PPL={hp:.3} (bottom {:.3}@{:.0}%)",
485 step + 1,
486 sp * 100.0,
487 best.0,
488 best.2 * 100.0
489 ));
490 }
491 }
492 if let Some(b) = best.1.take() {
493 logits = b;
494 }
495 let pass = Pass {
496 fm: &fm,
497 tau: hy.tau,
498 logits: &logits,
499 hard: true,
500 ffn: &ffn,
501 };
502 let masked = held_ppl(&pass, &held);
503 log(&format!(
504 "[A] {:.0}s: masked-PPL {masked:.3}",
505 t0.elapsed().as_secs_f64()
506 ));
507
508 for &li in &fcd {
510 let l = &fm.layers[li];
511 ffn[li] = Some((l.gate.clone(), l.up.clone(), l.down.clone()));
512 }
513 let sizes: Vec<usize> = fcd
514 .iter()
515 .flat_map(|&li| {
516 let l = &fm.layers[li];
517 [l.gate.len(), l.up.len(), l.down.len()]
518 })
519 .collect();
520 let mut adam_b = Adam::new(&sizes, hy.lr_b);
521 let mut best_b: (f64, Option<Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>>>) = (masked, None);
522 for step in 0..hy.steps_b {
523 let chunk = &calib[step % calib.len()];
524 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; nl];
525 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = (0..nl)
526 .map(|li| {
527 ffn[li]
528 .as_ref()
529 .map(|(g, u, d)| (vec![0.0; g.len()], vec![0.0; u.len()], vec![0.0; d.len()]))
530 })
531 .collect();
532 let pass = Pass {
533 fm: &fm,
534 tau: hy.tau,
535 logits: &logits,
536 hard: true,
537 ffn: &ffn,
538 };
539 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
540 let lr_scale = 0.5 * (1.0 + (std::f64::consts::PI * step as f64 / hy.steps_b as f64).cos());
542 let first_fcd = fcd[0];
543 let mut params: Vec<&mut [f32]> = Vec::new();
544 let mut grads: Vec<Vec<f64>> = Vec::new();
545 for (off, slot) in ffn[first_fcd..].iter_mut().enumerate() {
546 let li = first_fcd + off;
547 let Some((g, u, d)) = slot.as_mut() else {
548 continue;
549 };
550 let (dg, du, dd) = dffn[li].take().unwrap();
551 params.push(g.as_mut_slice());
552 grads.push(dg);
553 params.push(u.as_mut_slice());
554 grads.push(du);
555 params.push(d.as_mut_slice());
556 grads.push(dd);
557 }
558 adam_b.step(&mut params, &grads, lr_scale);
559 if (step + 1) % hy.eval_every == 0 {
560 let pass = Pass {
561 fm: &fm,
562 tau: hy.tau,
563 logits: &logits,
564 hard: true,
565 ffn: &ffn,
566 };
567 let cur = held_ppl(&pass, &held);
568 if cur < best_b.0 {
569 best_b = (cur, Some(ffn.clone()));
570 }
571 log(&format!(
572 " [B] step {}: held-PPL {cur:.3} (best {:.3})",
573 step + 1,
574 best_b.0
575 ));
576 }
577 }
578 if let Some(b) = best_b.1.take() {
579 ffn = b;
580 }
581 let overlaid = best_b.0;
582
583 let mut keep = Vec::with_capacity(nl);
585 let mut down_out = Vec::with_capacity(nl);
586 let mut gate_up = Vec::with_capacity(nl);
587 let mut kept_per_layer = Vec::with_capacity(nl);
588 for li in 0..nl {
589 let alive: Vec<bool> = logits[li].iter().map(|&x| sigmoid(x) > hy.tau).collect();
590 kept_per_layer.push(alive.iter().filter(|&&a| a).count());
591 let l = &fm.layers[li];
592 let mut down = match &ffn[li] {
593 Some((_, _, d)) => d.clone(),
594 None => l.down.clone(),
595 };
596 let hsz = fm.hidden;
597 for r in 0..hsz {
598 for (c, &a) in alive.iter().enumerate() {
599 if !a {
600 down[r * inter + c] = 0.0;
601 }
602 }
603 }
604 gate_up.push(ffn[li].as_ref().map(|(g, u, _)| (g.clone(), u.clone())));
605 down_out.push(down);
606 keep.push(alive);
607 }
608 let total: usize = kept_per_layer.iter().sum();
609 let report = BakeReport {
610 backbone,
611 masked,
612 overlaid,
613 pruned_ratio: 1.0 - total as f64 / (nl * inter) as f64,
614 kept_per_layer,
615 sec: t0.elapsed().as_secs_f64(),
616 };
617 let arts = BakeArtifacts {
618 keep,
619 down: down_out,
620 gate_up,
621 fcd_layers: fcd,
622 };
623 Ok((report, arts))
624}