1use crate::fcd::{AttnW, FcdModel, LnFfn, act_bwd_rows, add_into_pooled, pooled_rows, rmsnorm_bwd_pooled, rmsnorm_fwd_pooled};
22use crate::fcd_ops as ops;
23use crate::sampler::SplitMix64;
24use cortiq_core::CmfModel;
25use std::sync::Arc;
26
27#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct AnswerSpan {
35 pub start: Vec<u32>,
38 pub end: u32,
40 pub values: Option<TokenText>,
47}
48
49#[derive(Clone, PartialEq, Eq)]
51pub struct TokenText(pub Arc<Vec<String>>);
52
53impl std::fmt::Debug for TokenText {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(f, "TokenText({} ids)", self.0.len())
56 }
57}
58
59pub fn answer_values(ids: &[u32], first: usize, last: usize, text: &TokenText) -> Vec<Vec<usize>> {
65 let mut groups = Vec::new();
66 let mut cur: Option<Vec<usize>> = None;
67 let (mut depth, mut in_str, mut esc, mut expect_value) = (0i32, false, false, false);
68 let mark = |cur: &mut Option<Vec<usize>>, p: usize| {
69 if let Some(g) = cur.as_mut() {
70 if g.last() != Some(&p) {
71 g.push(p);
72 }
73 }
74 };
75 for p in first..=last.min(ids.len().saturating_sub(1)) {
76 let piece = text.0.get(ids[p] as usize).map(String::as_str).unwrap_or("");
77 for ch in piece.chars() {
78 if in_str {
79 if esc {
80 esc = false;
81 } else if ch == '\\' {
82 esc = true;
83 } else if ch == '"' {
84 in_str = false;
85 }
86 mark(&mut cur, p);
87 continue;
88 }
89 if expect_value && !ch.is_whitespace() {
90 expect_value = false;
91 cur = Some(Vec::new());
92 }
93 match ch {
94 '"' => {
95 in_str = true;
96 mark(&mut cur, p);
97 }
98 '{' | '[' => {
99 depth += 1;
100 mark(&mut cur, p);
101 }
102 '}' | ']' => {
103 if depth == 1 {
104 if let Some(g) = cur.take() {
105 if !g.is_empty() {
106 groups.push(g);
107 }
108 }
109 } else {
110 mark(&mut cur, p);
111 }
112 depth -= 1;
113 }
114 ',' if depth == 1 => {
115 if let Some(g) = cur.take() {
116 if !g.is_empty() {
117 groups.push(g);
118 }
119 }
120 }
121 ':' if depth == 1 && cur.is_none() => expect_value = true,
122 c if c.is_whitespace() && depth == 1 => {}
123 _ => mark(&mut cur, p),
124 }
125 }
126 }
127 if let Some(g) = cur {
128 if !g.is_empty() {
129 groups.push(g);
130 }
131 }
132 groups
133}
134
135pub fn answer_targets(ids: &[u32], span: &AnswerSpan) -> Option<(usize, usize)> {
138 let m = span.start.len();
139 if m == 0 || ids.len() <= m {
140 return None;
141 }
142 let at = (0..=ids.len() - m).rev().find(|&i| ids[i..i + m] == span.start[..])?;
143 let first = at + m;
144 if first >= ids.len() {
145 return None;
146 }
147 let last = (first..ids.len())
148 .find(|&i| ids[i] == span.end)
149 .unwrap_or(ids.len() - 1);
150 Some((first, last))
151}
152
153#[derive(Clone, Debug)]
155pub struct BakeHyper {
156 pub steps_a: usize,
157 pub steps_b: usize,
158 pub l1_init: f64,
159 pub l1_step: f64,
160 pub eval_every: usize,
161 pub lr_a: f64,
162 pub lr_b: f64,
163 pub tau: f32,
164 pub fcd_layers: usize,
165 pub batch: usize,
168 pub fcd_batch: usize,
172 pub seed: u64,
173 pub target_sparsity: f64,
177 pub l1_mult: f64,
180 pub mask_init: f32,
185 pub softplus_l1: bool,
189 pub checkpoint_accuracy: bool,
193 pub checkpoint_min_accuracy: Option<f64>,
198 pub checkpoint_min_balanced_accuracy: Option<f64>,
202 pub checkpoint_raw_priority: bool,
206 pub align: usize,
210 pub uniform_inter: bool,
213 pub focus_tokens: Vec<u32>,
219 pub focus_follow_tokens: Vec<u32>,
224 pub answer_span: Option<AnswerSpan>,
227 pub mask_logits: Option<Vec<Vec<f32>>>,
232 pub soft_fold: bool,
237 pub fcd_attn: bool,
241 pub checkpoint_dir: Option<String>,
245 pub held_tags: Option<HeldTags>,
249}
250
251impl Default for BakeHyper {
252 fn default() -> Self {
253 Self {
254 steps_a: 240,
255 steps_b: 120,
256 l1_init: 0.01,
257 l1_step: 0.005,
258 eval_every: 30,
259 lr_a: 0.1,
260 lr_b: 1e-5,
261 tau: 0.5,
262 fcd_layers: 4,
263 batch: 1,
264 fcd_batch: 128,
265 seed: 0,
266 target_sparsity: 0.0,
267 l1_mult: 1.0,
268 mask_init: 2.0,
269 softplus_l1: false,
270 checkpoint_accuracy: false,
271 checkpoint_min_accuracy: None,
272 checkpoint_min_balanced_accuracy: None,
273 checkpoint_raw_priority: false,
274 align: 32,
275 uniform_inter: false,
276 focus_tokens: Vec::new(),
277 focus_follow_tokens: Vec::new(),
278 answer_span: None,
279 mask_logits: None,
280 soft_fold: false,
281 fcd_attn: false,
282 checkpoint_dir: None,
283 held_tags: None,
284 }
285 }
286}
287
288pub struct BakeReport {
290 pub backbone: f64,
292 pub masked: f64,
294 pub overlaid: f64,
296 pub pruned_ratio: f64,
297 pub kept_per_layer: Vec<usize>,
298 pub backbone_accuracy: Option<f64>,
300 pub masked_accuracy: Option<f64>,
301 pub overlaid_accuracy: Option<f64>,
302 pub backbone_balanced_accuracy: Option<f64>,
305 pub masked_balanced_accuracy: Option<f64>,
306 pub overlaid_balanced_accuracy: Option<f64>,
307 pub selected_step: usize,
308 pub sec: f64,
309}
310
311pub struct BakeCheckpoint {
312 pub step: usize,
313 pub l1: f64,
314 pub ppl: f64,
315 pub sparsity: f64,
316 pub accuracy: Option<f64>,
317 pub balanced_accuracy: Option<f64>,
318 pub logits: Vec<Vec<f32>>,
321}
322
323#[derive(Clone)]
325pub struct AttnMasters {
326 pub wqkv: Vec<f32>,
328 pub wo: Vec<f32>,
330 pub gproj: Option<Vec<f32>>,
332 pub iln: Vec<f32>,
333 pub pln: Vec<f32>,
334}
335
336pub struct AttnGrads {
338 pub wqkv: Vec<f64>,
339 pub wo: Vec<f64>,
340 pub gproj: Option<Vec<f64>>,
341 pub iln: Vec<f64>,
342 pub pln: Vec<f64>,
343}
344
345impl AttnGrads {
346 fn zeros_like(m: &AttnMasters) -> Self {
347 Self {
348 wqkv: vec![0.0; m.wqkv.len()],
349 wo: vec![0.0; m.wo.len()],
350 gproj: m.gproj.as_ref().map(|g| vec![0.0; g.len()]),
351 iln: vec![0.0; m.iln.len()],
352 pln: vec![0.0; m.pln.len()],
353 }
354 }
355}
356
357pub struct BakeArtifacts {
359 pub keep: Vec<Vec<bool>>,
362 pub keep_visits: Vec<Vec<bool>>,
365 pub down: Vec<Vec<f32>>,
368 pub gate_up: Vec<Option<(Vec<f32>, Vec<f32>)>>,
370 pub fcd_layers: Vec<usize>,
372 pub attn: Vec<Option<AttnMasters>>,
374 pub logits: Vec<Vec<f32>>,
379 pub final_logits: Vec<Vec<f32>>,
382 pub checkpoints: Vec<BakeCheckpoint>,
383}
384
385const CLIP: f64 = 1.0;
386const B1: f64 = 0.9;
387const B2: f64 = 0.999;
388const EPS: f64 = 1e-8;
389
390struct Adam {
392 m: Vec<Vec<f64>>,
393 v: Vec<Vec<f64>>,
394 t: i32,
395 lr: f64,
396}
397
398impl Adam {
399 fn new(sizes: &[usize], lr: f64) -> Self {
400 Self {
401 m: sizes.iter().map(|&n| vec![0.0; n]).collect(),
402 v: sizes.iter().map(|&n| vec![0.0; n]).collect(),
403 t: 0,
404 lr,
405 }
406 }
407
408 fn step(&mut self, params: &mut [&mut [f32]], grads: &[Vec<f64>], lr_scale: f64) {
410 let gn: f64 = grads
411 .iter()
412 .flat_map(|g| g.iter().map(|x| x * x))
413 .sum::<f64>()
414 .sqrt();
415 let clip = if gn > CLIP { CLIP / gn } else { 1.0 };
416 self.t += 1;
417 let (bc1, bc2) = (1.0 - B1.powi(self.t), 1.0 - B2.powi(self.t));
418 for (pi, p) in params.iter_mut().enumerate() {
419 for j in 0..p.len() {
420 let g = grads[pi][j] * clip;
421 let m = &mut self.m[pi][j];
422 let v = &mut self.v[pi][j];
423 *m = B1 * *m + (1.0 - B1) * g;
424 *v = B2 * *v + (1.0 - B2) * g * g;
425 let upd = (*m / bc1) / ((*v / bc2).sqrt() + EPS);
426 p[j] -= (self.lr * lr_scale * upd) as f32;
427 }
428 }
429 }
430}
431
432pub fn mask_init_logit_for(loops: usize, effective_logit: f32) -> f32 {
451 let base = 1.0f32 / (1.0 + (-effective_logit).exp());
452 let per_visit = base.powf(1.0 / loops.max(1) as f32);
453 (per_visit / (1.0 - per_visit)).ln()
454}
455
456pub fn mask_init_logit(loops: usize) -> f32 {
457 mask_init_logit_for(loops, 2.0)
458}
459
460pub fn mask_step_scale(loops: usize) -> f64 {
466 1.0 / loops.max(1) as f64
467}
468
469fn sigmoid(x: f32) -> f32 {
470 1.0 / (1.0 + (-x).exp())
471}
472
473fn sparsity_grad(logit: f32, softplus_l1: bool) -> f64 {
474 let s = sigmoid(logit) as f64;
475 if softplus_l1 { s } else { s * (1.0 - s) }
476}
477
478fn is_scored_target(
479 ids: &[u32],
480 target_index: usize,
481 sequence_end: usize,
482 focus: &[u32],
483 follow: &[u32],
484) -> bool {
485 if focus.is_empty() {
486 return true;
487 }
488 focus.contains(&ids[target_index])
489 && (follow.is_empty()
490 || (target_index + 1 < sequence_end && follow.contains(&ids[target_index + 1])))
491}
492
493fn scored_targets(
497 ids: &[u32],
498 base: usize,
499 t: usize,
500 focus: &[u32],
501 follow: &[u32],
502 span: Option<&AnswerSpan>,
503) -> Vec<usize> {
504 match span {
505 Some(sp) => match answer_targets(&ids[base..base + t], sp) {
506 Some((first, last)) => (base + first.max(1)..=base + last).collect(),
507 None => Vec::new(),
508 },
509 None => (base + 1..base + t)
510 .filter(|&target_index| is_scored_target(ids, target_index, base + t, focus, follow))
511 .collect(),
512 }
513}
514
515struct Pass<'a> {
518 fm: &'a FcdModel,
519 tau: f32,
520 logits: &'a [Vec<f32>],
522 hard: bool,
523 ffn: &'a [Option<(Vec<f32>, Vec<f32>, Vec<f32>)>],
525 focus_tokens: &'a [u32],
527 focus_follow_tokens: &'a [u32],
529 span: Option<&'a AnswerSpan>,
531 soft_fold: bool,
533 attn_tr: &'a [Option<AttnMasters>],
535}
536
537struct SendMutF(*mut f32);
539unsafe impl Send for SendMutF {}
540unsafe impl Sync for SendMutF {}
541impl SendMutF {
542 #[inline]
543 unsafe fn at(&self, i: usize) -> *mut f32 {
544 unsafe { self.0.add(i) }
545 }
546}
547struct SendMutF64(*mut f64);
548unsafe impl Send for SendMutF64 {}
549unsafe impl Sync for SendMutF64 {}
550impl SendMutF64 {
551 #[inline]
552 unsafe fn at(&self, i: usize) -> *mut f64 {
553 unsafe { self.0.add(i) }
554 }
555}
556
557#[derive(Clone, Debug, Default)]
558struct FocusStats {
559 total: usize,
560 correct: usize,
561 class_total: Vec<usize>,
562 class_correct: Vec<usize>,
563 questions: usize,
565 questions_correct: usize,
566 seq: Vec<(usize, usize)>,
569}
570
571impl FocusStats {
572 fn new(classes: usize) -> Self {
573 Self {
574 class_total: vec![0; classes],
575 class_correct: vec![0; classes],
576 ..Self::default()
577 }
578 }
579
580 fn accuracy(&self) -> Option<f64> {
581 (self.total > 0).then(|| self.correct as f64 / self.total as f64)
582 }
583
584 fn question_accuracy(&self) -> Option<f64> {
585 (self.questions > 0).then(|| self.questions_correct as f64 / self.questions as f64)
586 }
587
588 fn accuracies(&self) -> (Option<f64>, Option<f64>) {
592 match self.question_accuracy() {
593 Some(q) => (Some(q), self.accuracy()),
594 None => (self.accuracy(), None),
595 }
596 }
597
598 fn balanced_accuracy(&self) -> Option<f64> {
599 let recalls: Vec<f64> = self
600 .class_total
601 .iter()
602 .zip(&self.class_correct)
603 .filter_map(|(&n, &ok)| (n > 0).then(|| ok as f64 / n as f64))
604 .collect();
605 (!recalls.is_empty()).then(|| recalls.iter().sum::<f64>() / recalls.len() as f64)
606 }
607
608 fn merge(&mut self, other: &Self) {
609 self.total += other.total;
610 self.correct += other.correct;
611 self.questions += other.questions;
612 self.questions_correct += other.questions_correct;
613 if self.class_total.len() < other.class_total.len() {
614 self.class_total.resize(other.class_total.len(), 0);
615 self.class_correct.resize(other.class_correct.len(), 0);
616 }
617 for (dst, src) in self.class_total.iter_mut().zip(&other.class_total) {
618 *dst += src;
619 }
620 for (dst, src) in self.class_correct.iter_mut().zip(&other.class_correct) {
621 *dst += src;
622 }
623 }
624}
625
626#[derive(Clone, Debug)]
627struct HeldScore {
628 ppl: f64,
629 accuracy: Option<f64>,
632 balanced_accuracy: Option<f64>,
633 token_accuracy: Option<f64>,
635 micro_accuracy: Option<f64>,
638 per_tag: Vec<(String, usize, usize)>,
640}
641
642#[derive(Clone, Debug, Default)]
645pub struct HeldTags {
646 pub names: Vec<String>,
647 pub of: Vec<usize>,
648}
649
650fn tag_line(score: &HeldScore) -> Option<String> {
652 (!score.per_tag.is_empty()).then(|| {
653 score
654 .per_tag
655 .iter()
656 .map(|(name, n, ok)| {
657 format!(
658 "{name} {:.1} ({n})",
659 if *n > 0 { *ok as f64 * 100.0 / *n as f64 } else { f64::NAN }
660 )
661 })
662 .collect::<Vec<_>>()
663 .join(" | ")
664 })
665}
666
667#[derive(Default)]
671struct FocusedFcdCache {
672 h1: Vec<f32>,
673 n2: Vec<f32>,
674 targets: Vec<usize>,
675}
676
677impl FocusedFcdCache {
678 fn len(&self) -> usize {
679 self.targets.len()
680 }
681
682 fn append(&mut self, mut other: Self) {
683 self.h1.append(&mut other.h1);
684 self.n2.append(&mut other.n2);
685 self.targets.append(&mut other.targets);
686 }
687}
688
689fn gather_rows(values: &[f32], rows: &[usize], width: usize) -> Vec<f32> {
690 let mut out = Vec::with_capacity(rows.len() * width);
691 for &row in rows {
692 out.extend_from_slice(&values[row * width..(row + 1) * width]);
693 }
694 out
695}
696
697impl Pass<'_> {
698 fn gates(&self, li: usize) -> Vec<f32> {
699 self.logits[li]
700 .iter()
701 .map(|&l| {
702 let s = sigmoid(l);
703 if self.hard {
704 if s > self.tau {
705 if self.soft_fold { s } else { 1.0 }
706 } else {
707 0.0
708 }
709 } else {
710 s
711 }
712 })
713 .collect()
714 }
715
716 fn wts<'b>(&'b self, li: usize, mats: &'b crate::fcd::LayerMats) -> LnFfn<'b> {
717 let l = &self.fm.layers[li];
718 let (iln, pln): (&[f32], &[f32]) = match &self.attn_tr[li] {
719 Some(m) => (&m.iln, &m.pln),
720 None => (&l.iln, &l.pln),
721 };
722 match &self.ffn[li] {
723 Some((g, u, d)) => LnFfn {
724 iln,
725 pln,
726 gate: g,
727 up: u,
728 down: d,
729 gu: None,
731 },
732 None => LnFfn {
733 iln,
734 pln,
735 gate: &[],
736 up: &[],
737 down: &mats.down,
738 gu: Some(&mats.gu),
739 },
740 }
741 }
742
743 fn cache_final_ffn_batch(&self, ids: &[u32], batch: usize) -> Result<FocusedFcdCache, String> {
748 let fm = self.fm;
749 if fm.loops.max(1) != 1 || fm.layers.is_empty() || self.focus_tokens.is_empty() {
750 return Err("focused final-FFN cache needs a one-pass stack and focus tokens".into());
751 }
752 if ids.len() % batch.max(1) != 0 {
753 return Err("focused final-FFN cache received a ragged batch".into());
754 }
755 let t = ids.len() / batch.max(1);
756 let hsz = fm.hidden;
757 let last = fm.layers.len() - 1;
758 let mut sources = Vec::new();
759 let mut targets = Vec::new();
760 for bi in 0..batch {
761 let base = bi * t;
762 for target_index in base + 1..base + t {
763 if !is_scored_target(
764 ids,
765 target_index,
766 base + t,
767 self.focus_tokens,
768 self.focus_follow_tokens,
769 ) {
770 continue;
771 }
772 sources.push(target_index - 1);
773 targets.push(
774 self.focus_tokens
775 .iter()
776 .position(|&id| id == ids[target_index])
777 .expect("focused target belongs to focus_tokens"),
778 );
779 }
780 }
781 if sources.is_empty() {
782 return Ok(FocusedFcdCache::default());
783 }
784
785 let mut hidden = vec![0f32; ids.len() * hsz];
786 for (row, &id) in ids.iter().enumerate() {
787 hidden[row * hsz..(row + 1) * hsz]
788 .copy_from_slice(&fm.embed[id as usize * hsz..(id as usize + 1) * hsz]);
789 }
790 for layer in 0..=last {
791 let gate = self.gates(layer);
792 let mats = fm.mats(layer)?;
793 let weights = self.wts(layer, &mats);
794 let (next, acts) = fm.layer_forward_scaled(
795 layer,
796 &hidden,
797 batch,
798 t,
799 &weights,
800 false,
801 layer == last,
802 Some(&gate),
803 true,
804 None,
805 false,
806 );
807 if layer == last {
808 let acts = acts.expect("last FFN boundary requested");
809 return Ok(FocusedFcdCache {
810 h1: gather_rows(&acts.h1, &sources, hsz),
811 n2: gather_rows(&acts.n2, &sources, hsz),
812 targets,
813 });
814 }
815 hidden = next;
816 }
817 unreachable!("non-empty stack has a final layer")
818 }
819
820 #[allow(clippy::too_many_arguments)]
824 fn chunk(
825 &self,
826 ids: &[u32],
827 grad: Option<(
828 &mut [Vec<f64>],
829 &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>],
830 &mut [Option<AttnGrads>],
831 )>,
832 ) -> (f64, usize) {
833 self.chunk_batch(ids, 1, grad)
834 }
835
836 fn chunk_batch(
844 &self,
845 ids: &[u32],
846 b: usize,
847 grad: Option<(
848 &mut [Vec<f64>],
849 &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>],
850 &mut [Option<AttnGrads>],
851 )>,
852 ) -> (f64, usize) {
853 self.chunk_batch_scored(ids, b, grad, None)
854 }
855
856 fn chunk_batch_scored(
857 &self,
858 ids: &[u32],
859 b: usize,
860 grad: Option<(
861 &mut [Vec<f64>],
862 &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>],
863 &mut [Option<AttnGrads>],
864 )>,
865 mut focus_stats: Option<&mut FocusStats>,
866 ) -> (f64, usize) {
867 let fm = self.fm;
868 let hsz = fm.hidden;
869 let pool = fm.pool.as_deref();
870 debug_assert!(ids.len() % b.max(1) == 0, "ragged batch");
871 let t = ids.len() / b.max(1);
872 let n = b * t;
873 let nl = fm.layers.len();
874 let mut h = vec![0f32; n * hsz];
876 for (r, &id) in ids.iter().enumerate() {
877 h[r * hsz..(r + 1) * hsz]
878 .copy_from_slice(&fm.embed[id as usize * hsz..(id as usize + 1) * hsz]);
879 }
880 let loops = fm.loops.max(1);
888 let vn = nl * loops;
889 let mut h_ins = Vec::with_capacity(vn);
890 let mut acts = Vec::with_capacity(vn);
891 let mut masks = Vec::with_capacity(vn);
892 let mut lnorms: Vec<Option<(Vec<f32>, Vec<f32>)>> = vec![None; vn];
894 for vl in 0..vn {
895 let li = vl % nl;
896 let g = self.gates(vl);
901 let mats_hold = fm.mats(li).expect("layer mats");
902 let wts = self.wts(li, &mats_hold);
903 let want = grad.is_some();
904 let planes_host = !(want && self.ffn[li].is_none());
908 let attn_w = self.attn_tr[li].as_ref().map(|m| AttnW {
909 wqkv: &m.wqkv,
910 wo: &m.wo,
911 head_gate: m.gproj.as_deref(),
912 });
913 let (h2, a) = fm.layer_forward_scaled(
914 li,
915 &h,
916 b,
917 t,
918 &wts,
919 false,
920 want,
921 Some(&g),
922 planes_host,
923 attn_w,
924 want && attn_w.is_some(),
925 );
926 h_ins.push(if want { h } else { Vec::new() });
927 acts.push(a);
928 masks.push(g);
929 h = h2;
930 if fm.loop_norm && li + 1 == nl && vl + 1 < vn {
933 let mut hn = vec![0f32; n * hsz];
934 let mut inv = vec![0f32; n];
935 rmsnorm_fwd_pooled(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv, pool);
936 if want {
937 lnorms[vl] = Some((h, inv));
938 }
939 h = hn;
940 }
941 }
942 let mut hn = vec![0f32; n * hsz];
944 let mut inv = vec![0f32; n];
945 rmsnorm_fwd_pooled(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv, pool);
946 let lm: &[f32] = fm.lm_head.as_deref().unwrap_or(&fm.embed);
947 let vocab = lm.len() / hsz;
948 let mut nll = 0f64;
949 let mut dh_n = vec![0f32; n * hsz]; const POS_CHUNK: usize = 64;
954 let targets: Vec<usize> = (0..b)
955 .flat_map(|bi| {
956 scored_targets(
957 ids,
958 bi * t,
959 t,
960 self.focus_tokens,
961 self.focus_follow_tokens,
962 self.span,
963 )
964 })
965 .collect();
966 let scored = targets.len();
967 if scored == 0 {
968 return (0.0, 0);
969 }
970 if self.focus_tokens.is_empty() {
971 let inv_n = 1.0 / scored as f64;
976 let mut hits: Option<Vec<bool>> = (focus_stats.is_some()
978 && self.span.is_some_and(|sp| sp.values.is_some()))
979 .then(|| vec![false; scored]);
980 let qdebug: usize = std::env::var("CMF_BAKE_QDEBUG").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
983 let mut preds: Vec<u32> = if qdebug > 0 && hits.is_some() { vec![0; scored] } else { Vec::new() };
984 let mut p0 = 0usize;
985 while p0 < scored {
986 let pc = POS_CHUNK.min(scored - p0);
987 let rows = &targets[p0..p0 + pc];
988 let src: Vec<usize> = rows.iter().map(|&target_index| target_index - 1).collect();
989 let hsrc = gather_rows(&hn, &src, hsz);
990 let mut logits = vec![0f32; pc * vocab];
991 ops::gemm_nt(&hsrc, lm, &mut logits, pc, hsz, vocab, pool);
992 for (r, &target_index) in rows.iter().enumerate() {
993 let target = ids[target_index] as usize;
994 let row = &mut logits[r * vocab..(r + 1) * vocab];
995 let (mut mx, mut arg) = (f32::NEG_INFINITY, 0usize);
996 for (j, &v) in row.iter().enumerate() {
997 if v > mx {
998 mx = v;
999 arg = j;
1000 }
1001 }
1002 let mx = mx as f64;
1003 let mut sum = 0f64;
1004 for v in row.iter() {
1005 sum += ((*v as f64) - mx).exp();
1006 }
1007 nll += mx + sum.ln() - row[target] as f64;
1008 if let (Some(stats), Some(_)) = (focus_stats.as_deref_mut(), self.span) {
1011 stats.total += 1;
1012 if arg == target {
1013 stats.correct += 1;
1014 }
1015 }
1016 if let Some(h) = hits.as_mut() {
1017 h[p0 + r] = arg == target;
1018 }
1019 if !preds.is_empty() {
1020 preds[p0 + r] = arg as u32;
1021 }
1022 if grad.is_some() {
1023 for v in row.iter_mut() {
1025 *v = ((((*v as f64) - mx).exp() / sum) * inv_n) as f32;
1026 }
1027 row[target] -= inv_n as f32;
1028 }
1029 }
1030 if grad.is_some() {
1031 let mut dsrc = vec![0f32; pc * hsz];
1032 ops::gemm_dx(&logits, lm, &mut dsrc, pc, hsz, vocab, pool);
1033 for (r, &s) in src.iter().enumerate() {
1034 for (d, &x) in dh_n[s * hsz..(s + 1) * hsz]
1035 .iter_mut()
1036 .zip(&dsrc[r * hsz..(r + 1) * hsz])
1037 {
1038 *d += x;
1039 }
1040 }
1041 }
1042 p0 += pc;
1043 }
1044 if let (Some(h), Some(stats), Some(sp)) = (&hits, focus_stats.as_deref_mut(), self.span) {
1048 if let Some(text) = &sp.values {
1049 let at: std::collections::HashMap<usize, bool> =
1050 targets.iter().copied().zip(h.iter().copied()).collect();
1051 let pred_at: std::collections::HashMap<usize, u32> =
1052 targets.iter().copied().zip(preds.iter().copied()).collect();
1053 let mut shown = 0usize;
1054 stats.seq = vec![(0, 0); b];
1055 for bi in 0..b {
1056 let seq = &ids[bi * t..(bi + 1) * t];
1057 let Some((first, last)) = answer_targets(seq, sp) else {
1058 continue;
1059 };
1060 if shown < qdebug {
1061 let gold: Vec<String> = (first.max(1)..=last).map(|q| format!("{:?}", text.0.get(seq[q] as usize).map(String::as_str).unwrap_or("?"))).collect();
1062 let got: Vec<String> = (first.max(1)..=last).map(|q| format!("{:?}", pred_at.get(&(bi * t + q)).and_then(|&id| text.0.get(id as usize)).map(String::as_str).unwrap_or("?"))).collect();
1063 let groups = answer_values(seq, first.max(1), last, text);
1064 eprintln!("[qdebug] gold {} | argmax {} | value groups {:?} (span starts at {})", gold.join(" "), got.join(" "), groups, first.max(1));
1065 shown += 1;
1066 }
1067 for group in answer_values(seq, first.max(1), last, text) {
1068 stats.questions += 1;
1069 stats.seq[bi].0 += 1;
1070 if group.iter().all(|&q| at.get(&(bi * t + q)).copied().unwrap_or(false)) {
1071 stats.questions_correct += 1;
1072 stats.seq[bi].1 += 1;
1073 }
1074 }
1075 }
1076 }
1077 }
1078 } else {
1079 let inv_n = 1.0 / scored as f64;
1086 for bi in 0..b {
1087 let base = bi * t;
1088 for target_index in base + 1..base + t {
1089 if !is_scored_target(
1090 ids,
1091 target_index,
1092 base + t,
1093 self.focus_tokens,
1094 self.focus_follow_tokens,
1095 ) {
1096 continue;
1097 }
1098 let target_class = self
1099 .focus_tokens
1100 .iter()
1101 .position(|&id| id == ids[target_index])
1102 .expect("focused target belongs to focus_tokens");
1103 let source = target_index - 1;
1104 let hidden = &hn[source * hsz..(source + 1) * hsz];
1105 let class_logits: Vec<f32> = self
1106 .focus_tokens
1107 .iter()
1108 .map(|&id| {
1109 let row = &lm[id as usize * hsz..(id as usize + 1) * hsz];
1110 hidden.iter().zip(row).map(|(&x, &w)| x * w).sum()
1111 })
1112 .collect();
1113 let mx = class_logits
1114 .iter()
1115 .copied()
1116 .fold(f32::NEG_INFINITY, f32::max) as f64;
1117 let probs: Vec<f64> = class_logits
1118 .iter()
1119 .map(|&value| ((value as f64) - mx).exp())
1120 .collect();
1121 let sum: f64 = probs.iter().sum();
1122 nll += mx + sum.ln() - class_logits[target_class] as f64;
1123 if let Some(stats) = focus_stats.as_deref_mut() {
1124 let predicted_class = class_logits
1125 .iter()
1126 .enumerate()
1127 .max_by(|(_, left), (_, right)| left.total_cmp(right))
1128 .map(|(index, _)| index)
1129 .expect("focus_tokens is non-empty");
1130 stats.total += 1;
1131 stats.class_total[target_class] += 1;
1132 if predicted_class == target_class {
1133 stats.correct += 1;
1134 stats.class_correct[target_class] += 1;
1135 }
1136 }
1137 if grad.is_some() {
1138 let dh = &mut dh_n[source * hsz..(source + 1) * hsz];
1139 for (class, (&id, probability)) in
1140 self.focus_tokens.iter().zip(probs).enumerate()
1141 {
1142 let coefficient = (probability / sum
1143 - usize::from(class == target_class) as f64)
1144 * inv_n;
1145 let row = &lm[id as usize * hsz..(id as usize + 1) * hsz];
1146 for (value, &weight) in dh.iter_mut().zip(row) {
1147 *value += (coefficient * weight as f64) as f32;
1148 }
1149 }
1150 }
1151 }
1152 }
1153 }
1154 let Some((dmask, dffn, dattn)) = grad else {
1155 return (nll, scored);
1156 };
1157 let t_bwd = std::time::Instant::now();
1159 let mut dh = vec![0f32; n * hsz];
1160 rmsnorm_bwd_pooled(&h, &fm.final_norm, &inv, &dh_n, fm.gemma, &mut dh, pool);
1161 for vl in (0..vn).rev() {
1162 let li = vl % nl;
1163 if let Some((hb, inv)) = lnorms[vl].as_ref() {
1165 let mut dprev = vec![0f32; n * hsz];
1166 rmsnorm_bwd_pooled(hb, &fm.final_norm, inv, &dh, fm.gemma, &mut dprev, pool);
1167 dh = dprev;
1168 }
1169 let a = acts[vl].as_ref().expect("acts saved in grad mode");
1170 let g = &masks[vl];
1171 let inter = fm.layers[li].inter;
1172 let mats_hold = fm.mats(li).expect("layer mats");
1173 let wts = self.wts(li, &mats_hold);
1174 let mut dn2 = vec![0f32; n * hsz];
1175 let mut device_done = false;
1176 #[cfg(feature = "gpu")]
1177 if a.act.is_empty() && dffn[li].is_none() {
1178 if let Some(gu) = wts.gu {
1179 let mut dm_dev = vec![0f32; inter];
1184 if crate::gpu_wgpu::ffn_bwd_chain_f32(
1185 &dh, wts.down, gu, li, &mut dn2, n, hsz, inter, fm.act, Some(g), Some(&mut dm_dev),
1186 ) {
1187 for (d, &x) in dmask[vl].iter_mut().zip(&dm_dev) {
1188 *d += x as f64;
1189 }
1190 device_done = true;
1191 }
1192 }
1193 }
1194 if !device_done {
1195 assert!(
1196 !a.act.is_empty(),
1197 "layer {li}: the forward left its FFN plane on the device but the device backward declined"
1198 );
1199 let mut dact2 = vec![0f32; n * inter];
1201 ops::gemm_dx(&dh, wts.down, &mut dact2, n, inter, hsz, fm.pool.as_deref());
1202 if let Some((_, _, dd)) = dffn[li].as_mut() {
1203 let mut act2 = a.act.clone();
1205 {
1206 let ap = SendMutF(act2.as_mut_ptr());
1207 pooled_rows(n, pool, &|lo, hi| {
1208 let rows = unsafe { std::slice::from_raw_parts_mut(ap.at(lo * inter), (hi - lo) * inter) };
1210 for row in rows.chunks_exact_mut(inter) {
1211 for (x, &gv) in row.iter_mut().zip(g) {
1212 *x *= gv;
1213 }
1214 }
1215 });
1216 }
1217 let mut dw = vec![0f32; hsz * inter];
1218 ops::gemm_dw(&dh, &act2, &mut dw, n, inter, hsz, fm.pool.as_deref());
1219 for (o, &x) in dd.iter_mut().zip(&dw) {
1220 *o += x as f64;
1221 }
1222 }
1223 {
1227 let dm = &mut dmask[vl];
1228 let dmp = SendMutF64(dm.as_mut_ptr());
1231 let (dact2_r, act_r) = (&dact2, &a.act);
1232 pooled_rows(inter.div_ceil(256), pool, &|lo, hi| {
1233 let (js, je) = (lo * 256, (hi * 256).min(inter));
1234 let dmj = unsafe { std::slice::from_raw_parts_mut(dmp.at(js), je - js) };
1236 for r in 0..n {
1237 let da = &dact2_r[r * inter + js..r * inter + je];
1238 let aa = &act_r[r * inter + js..r * inter + je];
1239 for ((d, &x), &y) in dmj.iter_mut().zip(da).zip(aa) {
1240 *d += x as f64 * y as f64;
1241 }
1242 }
1243 });
1244 for (j, d) in dm.iter_mut().enumerate() {
1246 let _ = j;
1247 let _ = d;
1248 }
1249 }
1250 let mut dg_pre = vec![0f32; n * inter];
1252 let mut du_pre = vec![0f32; n * inter];
1253 act_bwd_rows(
1254 fm.act,
1255 &a.gpre,
1256 &a.upre,
1257 &dact2,
1258 Some(g),
1259 &mut dg_pre,
1260 &mut du_pre,
1261 n,
1262 inter,
1263 fm.pool.as_deref(),
1264 );
1265 if let Some(gu) = wts.gu {
1268 let mut dgu = vec![0f32; n * 2 * inter];
1269 {
1270 let gp = SendMutF(dgu.as_mut_ptr());
1271 let (dgr, dur) = (&dg_pre, &du_pre);
1272 pooled_rows(n, pool, &|lo, hi| {
1273 for r in lo..hi {
1274 let row = unsafe { std::slice::from_raw_parts_mut(gp.at(r * 2 * inter), 2 * inter) };
1276 row[..inter].copy_from_slice(&dgr[r * inter..(r + 1) * inter]);
1277 row[inter..].copy_from_slice(&dur[r * inter..(r + 1) * inter]);
1278 }
1279 });
1280 }
1281 ops::gemm_dx(&dgu, gu, &mut dn2, n, hsz, 2 * inter, fm.pool.as_deref());
1282 } else {
1283 ops::gemm_dx(
1284 &dg_pre,
1285 wts.gate,
1286 &mut dn2,
1287 n,
1288 hsz,
1289 inter,
1290 fm.pool.as_deref(),
1291 );
1292 let mut dn2b = vec![0f32; n * hsz];
1293 ops::gemm_dx(
1294 &du_pre,
1295 wts.up,
1296 &mut dn2b,
1297 n,
1298 hsz,
1299 inter,
1300 fm.pool.as_deref(),
1301 );
1302 for (x, &y) in dn2.iter_mut().zip(&dn2b) {
1303 *x += y;
1304 }
1305 }
1306 if let Some((dgw, duw, _)) = dffn[li].as_mut() {
1307 let mut dw = vec![0f32; inter * hsz];
1308 ops::gemm_dw(&dg_pre, &a.n2, &mut dw, n, hsz, inter, fm.pool.as_deref());
1309 for (o, &x) in dgw.iter_mut().zip(&dw) {
1310 *o += x as f64;
1311 }
1312 dw.fill(0.0);
1313 ops::gemm_dw(&du_pre, &a.n2, &mut dw, n, hsz, inter, fm.pool.as_deref());
1314 for (o, &x) in duw.iter_mut().zip(&dw) {
1315 *o += x as f64;
1316 }
1317 }
1318 }
1319 let mut dh1 = dh.clone(); rmsnorm_bwd_pooled(&a.h1, wts.pln, &a.inv2, &dn2, fm.gemma, &mut dh1, pool);
1323 if let (Some(m), Some(dg)) = (&self.attn_tr[li], dattn[li].as_mut()) {
1324 norm_gain_grad(&a.h1, &a.inv2, &dn2, &mut dg.pln, pool);
1328 let dh_attn = self.attn_backward(li, vl, &h_ins[vl], a, &dh1, m, dg, b, t);
1329 add_into_pooled(&mut dh1, &dh_attn, pool);
1330 }
1331 dh = dh1;
1332 }
1333 crate::fcd::prof::add(&crate::fcd::prof::BWD, t_bwd);
1334 (nll, scored)
1335 }
1336}
1337
1338
1339fn norm_gain_grad(x: &[f32], inv: &[f32], dy: &[f32], dw: &mut [f64], pool: Option<&crate::pool::Pool>) {
1342 let d = dw.len();
1343 let n = x.len() / d;
1344 let dp = SendMutF64(dw.as_mut_ptr());
1345 pooled_rows(d.div_ceil(64), pool, &|lo, hi| {
1346 let (js, je) = (lo * 64, (hi * 64).min(d));
1347 let out = unsafe { std::slice::from_raw_parts_mut(dp.at(js), je - js) };
1349 for r in 0..n {
1350 let inv_r = inv[r] as f64;
1351 for (o, j) in out.iter_mut().zip(js..je) {
1352 *o += dy[r * d + j] as f64 * x[r * d + j] as f64 * inv_r;
1353 }
1354 }
1355 });
1356}
1357
1358impl Pass<'_> {
1359 #[allow(clippy::too_many_arguments)]
1363 fn attn_backward(
1364 &self,
1365 li: usize,
1366 _vl: usize,
1367 h_in: &[f32],
1368 a: &crate::fcd::LayerActs,
1369 dattn: &[f32],
1370 m: &AttnMasters,
1371 dg: &mut AttnGrads,
1372 b: usize,
1373 t: usize,
1374 ) -> Vec<f32> {
1375 let fm = self.fm;
1376 let pool = fm.pool.as_deref();
1377 let (hsz, nh, nkv, hd) = (fm.hidden, fm.nh, fm.nkv, fm.hd);
1378 let n = b * t;
1379 let (qdim, kvdim) = (nh * hd, nkv * hd);
1380 let (window, local) = fm.attn_geometry(li).expect("full-layer FCD on a non-attention layer");
1381 let (qrot, krot, vproj, ao, head_gain) = a
1382 .attn_full()
1383 .expect("full-layer FCD needs the attention activations");
1384 let mut n1 = vec![0f32; n * hsz];
1386 let mut inv1 = vec![0f32; n];
1387 rmsnorm_fwd_pooled(h_in, &m.iln, fm.eps, fm.gemma, &mut n1, &mut inv1, pool);
1388 let mut ao_eff = ao.to_vec();
1390 if !head_gain.is_empty() {
1391 for r in 0..n {
1392 for h in 0..nh {
1393 let gain = head_gain[r * nh + h];
1394 for x in &mut ao_eff[r * qdim + h * hd..r * qdim + (h + 1) * hd] {
1395 *x *= gain;
1396 }
1397 }
1398 }
1399 }
1400 {
1401 let mut dw = vec![0f32; hsz * qdim];
1402 ops::gemm_dw(dattn, &ao_eff, &mut dw, n, qdim, hsz, pool);
1403 for (o, &x) in dg.wo.iter_mut().zip(&dw) {
1404 *o += x as f64;
1405 }
1406 }
1407 let mut dao_eff = vec![0f32; n * qdim];
1408 ops::gemm_dx(dattn, &m.wo, &mut dao_eff, n, qdim, hsz, pool);
1409 let mut dn1 = vec![0f32; n * hsz];
1411 let mut dao = dao_eff;
1412 if let (Some(gw), Some(dgw)) = (&m.gproj, dg.gproj.as_mut()) {
1413 let mut dgraw = vec![0f32; n * nh];
1414 for r in 0..n {
1415 for h in 0..nh {
1416 let gain = head_gain[r * nh + h];
1417 let s = r * qdim + h * hd;
1418 let mut dgain = 0f32;
1419 for c in 0..hd {
1420 dgain += dao[s + c] * ao[s + c];
1421 dao[s + c] *= gain;
1422 }
1423 dgraw[r * nh + h] = dgain * gain * (1.0 - gain);
1424 }
1425 }
1426 let mut dw = vec![0f32; nh * hsz];
1427 ops::gemm_dw(&dgraw, &n1, &mut dw, n, hsz, nh, pool);
1428 for (o, &x) in dgw.iter_mut().zip(&dw) {
1429 *o += x as f64;
1430 }
1431 ops::gemm_dx(&dgraw, gw, &mut dn1, n, hsz, nh, pool);
1432 }
1433 let mut dqrot = vec![0f32; n * qdim];
1435 let mut dkrot = vec![0f32; n * kvdim];
1436 let mut dvproj = vec![0f32; n * kvdim];
1437 let mut done = false;
1438 #[cfg(feature = "gpu")]
1439 {
1440 done = crate::gpu_wgpu::attn_bwd_chain_f32(
1441 qrot, krot, vproj, &dao, b, t, nh, nkv, hd, window, &mut dqrot, &mut dkrot, &mut dvproj,
1442 );
1443 }
1444 if !done {
1445 let rep = nh / nkv;
1446 for bi in 0..b {
1447 for h in 0..nh {
1448 let g = h / rep;
1449 let mut q64 = vec![0f64; t * hd];
1450 let mut k64 = vec![0f64; t * hd];
1451 let mut v64 = vec![0f64; t * hd];
1452 let mut do64 = vec![0f64; t * hd];
1453 for p in 0..t {
1454 let r = bi * t + p;
1455 for c in 0..hd {
1456 q64[p * hd + c] = qrot[r * qdim + h * hd + c] as f64;
1457 k64[p * hd + c] = krot[r * kvdim + g * hd + c] as f64;
1458 v64[p * hd + c] = vproj[r * kvdim + g * hd + c] as f64;
1459 do64[p * hd + c] = dao[r * qdim + h * hd + c] as f64;
1460 }
1461 }
1462 let mut dq = vec![0f64; t * hd];
1463 let mut dk = vec![0f64; t * hd];
1464 let mut dv = vec![0f64; t * hd];
1465 ops::attn_head_bwd_window(&q64, &k64, &v64, &do64, t, hd, hd, window, &mut dq, &mut dk, &mut dv);
1466 for p in 0..t {
1467 let r = bi * t + p;
1468 for c in 0..hd {
1469 dqrot[r * qdim + h * hd + c] += dq[p * hd + c] as f32;
1470 dkrot[r * kvdim + g * hd + c] += dk[p * hd + c] as f32;
1471 dvproj[r * kvdim + g * hd + c] += dv[p * hd + c] as f32;
1472 }
1473 }
1474 }
1475 }
1476 }
1477 let (rotary_dim, inv_freq) = fm.layer_rope(local);
1479 for r in 0..n {
1480 let pos = r % t;
1481 for h in 0..nh {
1482 let s = r * qdim + h * hd;
1483 ops::rope_bwd(&mut dqrot[s..s + rotary_dim], pos, inv_freq);
1484 }
1485 for g in 0..nkv {
1486 let s = r * kvdim + g * hd;
1487 ops::rope_bwd(&mut dkrot[s..s + rotary_dim], pos, inv_freq);
1488 }
1489 }
1490 let fused = qdim + 2 * kvdim;
1492 let mut dqkv = vec![0f32; n * fused];
1493 for r in 0..n {
1494 let row = &mut dqkv[r * fused..(r + 1) * fused];
1495 row[..qdim].copy_from_slice(&dqrot[r * qdim..(r + 1) * qdim]);
1496 row[qdim..qdim + kvdim].copy_from_slice(&dkrot[r * kvdim..(r + 1) * kvdim]);
1497 row[qdim + kvdim..].copy_from_slice(&dvproj[r * kvdim..(r + 1) * kvdim]);
1498 }
1499 {
1500 let mut dw = vec![0f32; fused * hsz];
1501 ops::gemm_dw(&dqkv, &n1, &mut dw, n, hsz, fused, pool);
1502 for (o, &x) in dg.wqkv.iter_mut().zip(&dw) {
1503 *o += x as f64;
1504 }
1505 }
1506 {
1507 let mut dx = vec![0f32; n * hsz];
1508 ops::gemm_dx(&dqkv, &m.wqkv, &mut dx, n, hsz, fused, pool);
1509 add_into_pooled(&mut dn1, &dx, pool);
1510 }
1511 norm_gain_grad(h_in, &inv1, &dn1, &mut dg.iln, pool);
1513 let mut dh_in = vec![0f32; n * hsz];
1514 rmsnorm_bwd_pooled(h_in, &m.iln, &inv1, &dn1, fm.gemma, &mut dh_in, pool);
1515 dh_in
1516 }
1517}
1518
1519fn chain_prof(i: usize) -> f64 {
1523 #[cfg(feature = "gpu")]
1524 {
1525 crate::gpu_wgpu::chain_prof_take(i)
1526 }
1527 #[cfg(not(feature = "gpu"))]
1528 {
1529 let _ = i;
1530 0.0
1531 }
1532}
1533
1534fn score_suffix(score: &HeldScore) -> String {
1537 match (score.accuracy, score.balanced_accuracy) {
1538 (Some(a), Some(b)) => format!(" | acc {:.2}% bal {:.2}%", a * 100.0, b * 100.0),
1539 (Some(a), None) => match (score.micro_accuracy, score.token_accuracy) {
1540 (Some(m), Some(t)) => format!(
1541 " | q-acc macro {:.2}% micro {:.2}% tok {:.2}%",
1542 a * 100.0,
1543 m * 100.0,
1544 t * 100.0
1545 ),
1546 (None, Some(t)) => format!(" | q-acc {:.2}% tok-acc {:.2}%", a * 100.0, t * 100.0),
1547 _ => format!(" | tok-acc {:.2}%", a * 100.0),
1548 },
1549 _ => String::new(),
1550 }
1551}
1552
1553fn held_ppl(pass: &Pass, held: &[Vec<u32>]) -> f64 {
1554 held_score(pass, held).ppl
1555}
1556
1557fn held_score(pass: &Pass, held: &[Vec<u32>]) -> HeldScore {
1558 held_score_tagged(pass, held, None)
1559}
1560
1561fn held_score_tagged(pass: &Pass, held: &[Vec<u32>], tags: Option<&HeldTags>) -> HeldScore {
1566 if held.is_empty() {
1571 return HeldScore {
1572 ppl: f64::NAN,
1573 accuracy: None,
1574 balanced_accuracy: None,
1575 token_accuracy: None,
1576 micro_accuracy: None,
1577 per_tag: Vec::new(),
1578 };
1579 }
1580 let mut nll = 0f64;
1581 let mut n = 0usize;
1582 let mut stats = FocusStats::new(pass.focus_tokens.len());
1583 const GROUP_MAX: usize = 32;
1590 let group_tokens: usize = std::env::var("CMF_BAKE_EVAL_TOKENS")
1591 .ok()
1592 .and_then(|v| v.parse().ok())
1593 .unwrap_or(8_192);
1594 let groups: Vec<&[Vec<u32>]> = {
1595 let mut v = Vec::new();
1596 let mut i = 0;
1597 while i < held.len() {
1598 let len = held[i].len();
1599 let cap = (group_tokens / len.max(1)).clamp(1, GROUP_MAX);
1600 let mut j = i;
1601 while j < held.len() && held[j].len() == len && j - i < cap {
1602 j += 1;
1603 }
1604 v.push(&held[i..j]);
1605 i = j;
1606 }
1607 v
1608 };
1609 let mut per_tag: Vec<(usize, usize)> = vec![(0, 0); tags.map_or(0, |t| t.names.len())];
1610 let mut credit = |at: usize, part: &FocusStats| {
1611 if let Some(tg) = tags {
1612 for (j, &(q, ok)) in part.seq.iter().enumerate() {
1613 if let Some(&ti) = tg.of.get(at + j) {
1614 per_tag[ti].0 += q;
1615 per_tag[ti].1 += ok;
1616 }
1617 }
1618 }
1619 };
1620 let mut at = 0usize;
1621 for group in groups {
1622 let t = group[0].len();
1623 if group.iter().all(|c| c.len() == t) {
1624 let flat: Vec<u32> = group.iter().flatten().copied().collect();
1625 let mut part = FocusStats::new(pass.focus_tokens.len());
1626 let (l, k) = pass.chunk_batch_scored(&flat, group.len(), None, Some(&mut part));
1627 nll += l;
1628 n += k;
1629 credit(at, &part);
1630 stats.merge(&part);
1631 } else {
1632 for (j, c) in group.iter().enumerate() {
1633 let mut part = FocusStats::new(pass.focus_tokens.len());
1634 let (l, k) = pass.chunk_batch_scored(c, 1, None, Some(&mut part));
1635 nll += l;
1636 n += k;
1637 credit(at + j, &part);
1638 stats.merge(&part);
1639 }
1640 }
1641 at += group.len();
1642 }
1643 let (mut accuracy, token_accuracy) = stats.accuracies();
1644 let mut micro_accuracy = None;
1645 let mut tagged = Vec::new();
1646 if let Some(tg) = tags
1647 && token_accuracy.is_some()
1648 {
1649 let rates: Vec<f64> =
1650 per_tag.iter().filter(|(q, _)| *q > 0).map(|&(q, ok)| ok as f64 / q as f64).collect();
1651 if !rates.is_empty() {
1652 micro_accuracy = accuracy;
1653 accuracy = Some(rates.iter().sum::<f64>() / rates.len() as f64);
1654 tagged = tg.names.iter().cloned().zip(per_tag).map(|(name, (q, ok))| (name, q, ok)).collect();
1655 }
1656 }
1657 HeldScore {
1658 ppl: (nll / n.max(1) as f64).exp(),
1659 accuracy,
1660 balanced_accuracy: stats.balanced_accuracy(),
1661 token_accuracy,
1662 micro_accuracy,
1663 per_tag: tagged,
1664 }
1665}
1666
1667fn length_blocks(calib: &[Vec<u32>], batch: usize) -> Vec<(usize, usize)> {
1672 let batch = batch.max(1);
1673 let mut blocks = Vec::new();
1674 let mut i = 0;
1675 while i < calib.len() {
1676 let len = calib[i].len();
1677 let mut j = i;
1678 while j < calib.len() && calib[j].len() == len && j - i < batch {
1679 j += 1;
1680 }
1681 blocks.push((i, j - i));
1682 i = j;
1683 }
1684 blocks
1685}
1686
1687fn calibration_batch(
1688 calib: &[Vec<u32>],
1689 step: usize,
1690 requested: usize,
1691) -> Result<(Vec<u32>, usize), String> {
1692 let batch = requested.max(1).min(calib.len());
1693 let blocks = length_blocks(calib, batch);
1696 let (start, count) = blocks[step % blocks.len()];
1697 let width = calib[start].len();
1698 let mut flat = Vec::with_capacity(count * width);
1699 for record in &calib[start..start + count] {
1700 if record.len() != width {
1701 return Err(format!(
1702 "skill bake: --batch needs equal-length records ({} != {width})",
1703 record.len()
1704 ));
1705 }
1706 flat.extend_from_slice(record);
1707 }
1708 Ok((flat, count))
1709}
1710
1711fn build_focused_fcd_cache(
1712 pass: &Pass<'_>,
1713 records: &[Vec<u32>],
1714 extraction_batch: usize,
1715) -> Result<FocusedFcdCache, String> {
1716 let mut cache = FocusedFcdCache::default();
1717 for group in records.chunks(extraction_batch.max(1)) {
1718 let width = group[0].len();
1719 if group.iter().any(|record| record.len() != width) {
1720 return Err("focused final-FFN cache needs equal-length records".into());
1721 }
1722 let flat: Vec<u32> = group.iter().flatten().copied().collect();
1723 cache.append(pass.cache_final_ffn_batch(&flat, group.len())?);
1724 }
1725 Ok(cache)
1726}
1727
1728fn cached_fcd_run(
1732 fm: &FcdModel,
1733 cache: &FocusedFcdCache,
1734 indices: &[usize],
1735 weights: (&[f32], &[f32], &[f32]),
1736 gate: &[f32],
1737 focus_tokens: &[u32],
1738 want_grad: bool,
1739) -> (HeldScore, Option<(Vec<f64>, Vec<f64>, Vec<f64>)>) {
1740 let (gate_w, up_w, down_w) = weights;
1741 let rows = indices.len();
1742 let hidden = fm.hidden;
1743 let inter = gate.len();
1744 if rows == 0 {
1745 return (
1746 HeldScore {
1747 ppl: f64::NAN,
1748 accuracy: None,
1749 balanced_accuracy: None,
1750 token_accuracy: None,
1751 micro_accuracy: None,
1752 per_tag: Vec::new(),
1753 },
1754 None,
1755 );
1756 }
1757 let h1 = gather_rows(&cache.h1, indices, hidden);
1758 let n2 = gather_rows(&cache.n2, indices, hidden);
1759 let mut gate_pre = vec![0f32; rows * inter];
1760 let mut up_pre = vec![0f32; rows * inter];
1761 ops::gemm_nt(
1762 &n2,
1763 gate_w,
1764 &mut gate_pre,
1765 rows,
1766 hidden,
1767 inter,
1768 fm.pool.as_deref(),
1769 );
1770 ops::gemm_nt(
1771 &n2,
1772 up_w,
1773 &mut up_pre,
1774 rows,
1775 hidden,
1776 inter,
1777 fm.pool.as_deref(),
1778 );
1779 let mut act = vec![0f32; rows * inter];
1780 for row in 0..rows {
1781 for column in 0..inter {
1782 let at = row * inter + column;
1783 act[at] = fm.act.fwd(gate_pre[at]) * up_pre[at] * gate[column];
1784 }
1785 }
1786 let mut ffn = vec![0f32; rows * hidden];
1787 ops::gemm_nt(
1788 &act,
1789 down_w,
1790 &mut ffn,
1791 rows,
1792 inter,
1793 hidden,
1794 fm.pool.as_deref(),
1795 );
1796 let mut h2 = h1;
1797 for (value, &delta) in h2.iter_mut().zip(&ffn) {
1798 *value += delta;
1799 }
1800 let mut normed = vec![0f32; rows * hidden];
1801 let mut inv = vec![0f32; rows];
1802 ops::rmsnorm_fwd(&h2, &fm.final_norm, fm.eps, fm.gemma, &mut normed, &mut inv);
1803
1804 let lm: &[f32] = fm.lm_head.as_deref().unwrap_or(&fm.embed);
1805 let mut stats = FocusStats::new(focus_tokens.len());
1806 let mut nll = 0f64;
1807 let mut dh_normed = want_grad.then(|| vec![0f32; rows * hidden]);
1808 for (local_row, &cache_row) in indices.iter().enumerate() {
1809 let target = cache.targets[cache_row];
1810 let state = &normed[local_row * hidden..(local_row + 1) * hidden];
1811 let logits: Vec<f32> = focus_tokens
1812 .iter()
1813 .map(|&id| {
1814 state
1815 .iter()
1816 .zip(&lm[id as usize * hidden..(id as usize + 1) * hidden])
1817 .map(|(&left, &right)| left * right)
1818 .sum()
1819 })
1820 .collect();
1821 let mx = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max) as f64;
1822 let exps: Vec<f64> = logits
1823 .iter()
1824 .map(|&value| ((value as f64) - mx).exp())
1825 .collect();
1826 let sum: f64 = exps.iter().sum();
1827 nll += mx + sum.ln() - logits[target] as f64;
1828 let predicted = logits
1829 .iter()
1830 .enumerate()
1831 .max_by(|(_, left), (_, right)| left.total_cmp(right))
1832 .map(|(class, _)| class)
1833 .expect("focused classes are non-empty");
1834 stats.total += 1;
1835 stats.class_total[target] += 1;
1836 if predicted == target {
1837 stats.correct += 1;
1838 stats.class_correct[target] += 1;
1839 }
1840 if let Some(gradient) = dh_normed.as_mut() {
1841 let row = &mut gradient[local_row * hidden..(local_row + 1) * hidden];
1842 for (class, (&id, probability)) in focus_tokens.iter().zip(exps).enumerate() {
1843 let coefficient =
1844 (probability / sum - usize::from(class == target) as f64) / rows as f64;
1845 let head = &lm[id as usize * hidden..(id as usize + 1) * hidden];
1846 for (value, &weight) in row.iter_mut().zip(head) {
1847 *value += (coefficient * weight as f64) as f32;
1848 }
1849 }
1850 }
1851 }
1852 let score = HeldScore {
1853 ppl: (nll / rows as f64).exp(),
1854 accuracy: stats.accuracy(),
1855 balanced_accuracy: stats.balanced_accuracy(),
1856 token_accuracy: None,
1857 micro_accuracy: None,
1858 per_tag: Vec::new(),
1859 };
1860 let Some(dh_normed) = dh_normed else {
1861 return (score, None);
1862 };
1863
1864 let mut dh = vec![0f32; rows * hidden];
1865 ops::rmsnorm_bwd(
1866 &h2,
1867 &fm.final_norm,
1868 &inv,
1869 &dh_normed,
1870 fm.gemma,
1871 &mut dh,
1872 None,
1873 );
1874 let mut dact = vec![0f32; rows * inter];
1875 ops::gemm_dx(
1876 &dh,
1877 down_w,
1878 &mut dact,
1879 rows,
1880 inter,
1881 hidden,
1882 fm.pool.as_deref(),
1883 );
1884 let mut dg_pre = vec![0f32; rows * inter];
1885 let mut du_pre = vec![0f32; rows * inter];
1886 for row in 0..rows {
1887 for column in 0..inter {
1888 let at = row * inter + column;
1889 let da = dact[at] * gate[column];
1890 dg_pre[at] = da * up_pre[at] * fm.act.bwd(gate_pre[at]);
1891 du_pre[at] = da * fm.act.fwd(gate_pre[at]);
1892 }
1893 }
1894 let mut dg = vec![0f32; inter * hidden];
1895 let mut du = vec![0f32; inter * hidden];
1896 let mut dd = vec![0f32; hidden * inter];
1897 ops::gemm_dw(
1898 &dg_pre,
1899 &n2,
1900 &mut dg,
1901 rows,
1902 hidden,
1903 inter,
1904 fm.pool.as_deref(),
1905 );
1906 ops::gemm_dw(
1907 &du_pre,
1908 &n2,
1909 &mut du,
1910 rows,
1911 hidden,
1912 inter,
1913 fm.pool.as_deref(),
1914 );
1915 ops::gemm_dw(&dh, &act, &mut dd, rows, inter, hidden, fm.pool.as_deref());
1916 (
1917 score,
1918 Some((
1919 dg.into_iter().map(f64::from).collect(),
1920 du.into_iter().map(f64::from).collect(),
1921 dd.into_iter().map(f64::from).collect(),
1922 )),
1923 )
1924}
1925
1926pub fn replica_score_file_mask(
1932 model: &Arc<CmfModel>,
1933 chunks: &[Vec<u32>],
1934) -> Result<(f64, f64), String> {
1935 let o1_off = crate::nystrom::O1Cfg {
1936 layers: crate::nystrom::O1Layers::List(Vec::new()),
1937 m: 4,
1938 w: 8,
1939 sink: 1,
1940 rect: crate::nystrom::O1_DEFAULT_RECT,
1941 };
1942 let fm = FcdModel::from_cmf(model, &o1_off, false)?;
1943 let nl = fm.layers.len();
1944 let loops = fm.loops.max(1);
1945 let vn = nl * loops;
1946 let inter = fm.layers[0].inter;
1947 let ffn: Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>> = vec![None; nl];
1948 let no_attn: Vec<Option<AttnMasters>> = vec![None; ffn.len()];
1949 let task = &model.masks.default_task;
1951 let mask = model
1952 .masks
1953 .masks
1954 .iter()
1955 .find(|m| &m.name == task)
1956 .or_else(|| model.masks.masks.first());
1957 let open: Vec<Vec<f32>> = vec![vec![50.0; inter]; vn];
1958 let masked_logits: Vec<Vec<f32>> = match mask {
1959 Some(m) => (0..vn)
1960 .map(|vl| {
1961 let row = m.ffn_masks.get(vl).map(|v| v.as_slice()).unwrap_or(&[]);
1962 (0..inter)
1963 .map(|j| {
1964 if (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0 {
1965 50.0
1966 } else {
1967 -50.0
1968 }
1969 })
1970 .collect()
1971 })
1972 .collect(),
1973 None => open.clone(),
1974 };
1975 let score = |logits: &[Vec<f32>]| -> f64 {
1976 let pass = Pass {
1977 fm: &fm,
1978 tau: 0.5,
1979 logits,
1980 hard: true,
1981 ffn: &ffn,
1982 focus_tokens: &[],
1983 focus_follow_tokens: &[],
1984 span: None,
1985 soft_fold: false,
1986 attn_tr: &no_attn,
1987 };
1988 held_ppl(&pass, chunks)
1989 };
1990 Ok((score(&open), score(&masked_logits)))
1991}
1992
1993pub fn skill_bake(
1995 model: &Arc<CmfModel>,
1996 chunks: &[Vec<u32>],
1997 held_n: usize,
1998 hy: &BakeHyper,
1999 mut log: impl FnMut(&str),
2000) -> Result<(BakeReport, BakeArtifacts), String> {
2001 let t0 = std::time::Instant::now();
2002 let o1_off = crate::nystrom::O1Cfg {
2003 layers: crate::nystrom::O1Layers::List(Vec::new()),
2004 m: 4,
2005 w: 8,
2006 sink: 1,
2007 rect: crate::nystrom::O1_DEFAULT_RECT,
2008 };
2009 let mut fm = FcdModel::from_cmf(model, &o1_off, false)?;
2010 fm.attn_acts = false;
2014 let nl = fm.layers.len();
2015 let mut attn_tr: Vec<Option<AttnMasters>> = vec![None; nl];
2018 let inter = fm.layers.iter().map(|l| l.inter).max().unwrap_or(0);
2019 let mut held: Vec<Vec<u32>> = chunks[..held_n.min(chunks.len())].to_vec();
2024 let mut order: Vec<usize> = (0..held.len()).collect();
2026 order.sort_by_key(|&i| held[i].len());
2027 let held_tags: Option<HeldTags> = hy.held_tags.as_ref().map(|t| HeldTags {
2028 names: t.names.clone(),
2029 of: order.iter().map(|&i| t.of.get(i).copied().unwrap_or(0)).collect(),
2030 });
2031 held = order.iter().map(|&i| std::mem::take(&mut held[i])).collect();
2032 let mut calib: Vec<Vec<u32>> = chunks[held_n.min(chunks.len())..].to_vec();
2033 calib.sort_by_key(Vec::len);
2034 if calib.len() < 12 {
2035 return Err(format!(
2036 "skill bake: corpus too small ({} calib chunks)",
2037 calib.len()
2038 ));
2039 }
2040 if hy.steps_a == 0 && hy.steps_b == 0 && hy.fcd_layers == 0 && hy.mask_logits.is_none() {
2048 let logits: Vec<Vec<f32>> = fm
2049 .layers
2050 .iter()
2051 .map(|layer| vec![100.0; layer.inter])
2052 .collect();
2053 let ffn = vec![None; nl];
2054 let pass = Pass {
2055 fm: &fm,
2056 tau: hy.tau,
2057 logits: &logits,
2058 hard: true,
2059 ffn: &ffn,
2060 focus_tokens: &hy.focus_tokens,
2061 focus_follow_tokens: &hy.focus_follow_tokens,
2062 span: hy.answer_span.as_ref(),
2063 soft_fold: hy.soft_fold,
2064 attn_tr: &attn_tr,
2065 };
2066 let score = held_score(&pass, &held);
2067 let keep: Vec<Vec<bool>> = fm
2068 .layers
2069 .iter()
2070 .map(|layer| vec![true; layer.inter])
2071 .collect();
2072 let loops = fm.loops.max(1);
2073 let keep_visits = (0..loops).flat_map(|_| keep.iter().cloned()).collect();
2074 let report = BakeReport {
2075 backbone: score.ppl,
2076 masked: score.ppl,
2077 overlaid: score.ppl,
2078 pruned_ratio: 0.0,
2079 kept_per_layer: keep.iter().map(Vec::len).collect(),
2080 backbone_accuracy: score.accuracy,
2081 masked_accuracy: score.accuracy,
2082 overlaid_accuracy: score.accuracy,
2083 backbone_balanced_accuracy: score.balanced_accuracy,
2084 masked_balanced_accuracy: score.balanced_accuracy,
2085 overlaid_balanced_accuracy: score.balanced_accuracy,
2086 selected_step: 0,
2087 sec: t0.elapsed().as_secs_f64(),
2088 };
2089 let arts = BakeArtifacts {
2090 keep,
2091 keep_visits,
2092 down: vec![Vec::new(); nl],
2093 gate_up: vec![None; nl],
2094 fcd_layers: Vec::new(),
2095 attn: (0..nl).map(|_| None).collect(),
2096 logits: logits.clone(),
2097 final_logits: logits,
2098 checkpoints: Vec::new(),
2099 };
2100 return Ok((report, arts));
2101 }
2102 if fm.layers.iter().any(|l| l.inter != inter) {
2103 return Err("skill bake: non-uniform FFN widths".into());
2104 }
2105 if hy.batch == 0 {
2106 return Err("skill bake: batch must be positive".into());
2107 }
2108 let fcd: Vec<usize> = (nl.saturating_sub(hy.fcd_layers)..nl).collect();
2109 let _rng = SplitMix64::new(hy.seed);
2110
2111 let loops = fm.loops.max(1);
2139 let m0 = mask_init_logit_for(loops, hy.mask_init);
2140 let vn = nl * loops;
2142 let mut logits: Vec<Vec<f32>> = match &hy.mask_logits {
2143 Some(m) => {
2144 if m.len() != vn || m.iter().any(|row| row.len() != inter) {
2145 return Err(format!(
2146 "skill bake: --mask-from has {} rows of {} logits; this model needs {vn} × {inter}",
2147 m.len(),
2148 m.first().map(Vec::len).unwrap_or(0)
2149 ));
2150 }
2151 log(&format!("mask: resuming from {} supplied logit rows", m.len()));
2152 m.clone()
2153 }
2154 None => vec![vec![m0; inter]; vn],
2155 };
2156 let mut ffn: Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>> = vec![None; nl];
2157
2158 let open: Vec<Vec<f32>> = vec![vec![50.0; inter]; vn];
2161 let base_pass = Pass {
2162 fm: &fm,
2163 tau: hy.tau,
2164 logits: &open,
2165 hard: true,
2166 ffn: &ffn,
2167 focus_tokens: &hy.focus_tokens,
2168 focus_follow_tokens: &hy.focus_follow_tokens,
2169 span: hy.answer_span.as_ref(),
2170 soft_fold: hy.soft_fold,
2171 attn_tr: &attn_tr,
2172 };
2173 let skip_baseline = hy.mask_logits.is_some()
2176 && std::env::var("CMF_BAKE_SKIP_BASELINE").is_ok_and(|v| v == "1");
2177 let backbone_score = if skip_baseline {
2178 HeldScore {
2179 ppl: f64::NAN,
2180 accuracy: None,
2181 balanced_accuracy: None,
2182 token_accuracy: None,
2183 micro_accuracy: None,
2184 per_tag: Vec::new(),
2185 }
2186 } else {
2187 held_score_tagged(&base_pass, &held, held_tags.as_ref())
2188 };
2189 let backbone = backbone_score.ppl;
2190 log(&format!(
2191 "baseline (full): {backbone:.3}{}",
2192 score_suffix(&backbone_score)
2193 ));
2194 if let Some(line) = tag_line(&backbone_score) {
2195 log(&format!(" per tag: {line}"));
2196 }
2197 if hy.mask_logits.is_some() {
2200 let start_pass = Pass {
2201 fm: &fm,
2202 tau: hy.tau,
2203 logits: &logits,
2204 hard: true,
2205 ffn: &ffn,
2206 focus_tokens: &hy.focus_tokens,
2207 focus_follow_tokens: &hy.focus_follow_tokens,
2208 span: hy.answer_span.as_ref(),
2209 soft_fold: hy.soft_fold,
2210 attn_tr: &attn_tr,
2211 };
2212 let s0 = held_score_tagged(&start_pass, &held, held_tags.as_ref());
2213 log(&format!("supplied mask: {:.3}{}", s0.ppl, score_suffix(&s0)));
2214 if let Some(line) = tag_line(&s0) {
2215 log(&format!(" per tag: {line}"));
2216 }
2217 }
2218
2219 let mut adam_a = Adam::new(&vec![inter; vn], hy.lr_a);
2221 let mut l1 = hy.l1_init * hy.l1_mult;
2222 let l1_step_eff = hy.l1_step * hy.l1_mult;
2223 let mut best: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
2225 let mut best_accuracy = f64::NEG_INFINITY;
2226 let mut best_balanced = f64::NEG_INFINITY;
2227 let mut best_step = 0usize;
2228 let mut checkpoints = Vec::new();
2229 let mut max_sp: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
2231 let mut max_sp_step = 0usize;
2232 let mut prev_alive: Option<Vec<Vec<bool>>> = None;
2233 let mut acc_chunk = 0f64;
2237 let mut acc_adam = 0f64;
2238 let strict_a = std::env::var("CMF_BAKE_A_STRICT")
2250 .map(|v| v != "0")
2251 .unwrap_or(true);
2252 crate::gpu::bake_precision_strict(strict_a);
2253 for step in 0..hy.steps_a {
2254 let t_step = std::time::Instant::now();
2255 let (batch_ids, batch) = calibration_batch(&calib, step, hy.batch)?;
2256 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; vn];
2257 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = vec![None; nl];
2258 let mut dattn: Vec<Option<AttnGrads>> = (0..nl).map(|_| None).collect();
2259 let pass = Pass {
2260 fm: &fm,
2261 tau: hy.tau,
2262 logits: &logits,
2263 hard: false,
2264 ffn: &ffn,
2265 focus_tokens: &hy.focus_tokens,
2266 focus_follow_tokens: &hy.focus_follow_tokens,
2267 span: hy.answer_span.as_ref(),
2268 soft_fold: hy.soft_fold,
2269 attn_tr: &attn_tr,
2270 };
2271 let _ = pass.chunk_batch(&batch_ids, batch, Some((&mut dmask, &mut dffn, &mut dattn)));
2272 let l1_per = l1 / (inter as f64 * nl as f64);
2274 for li in 0..vn {
2275 for j in 0..inter {
2276 let s = sigmoid(logits[li][j]) as f64;
2277 let sparse_grad = sparsity_grad(logits[li][j], hy.softplus_l1);
2278 dmask[li][j] = dmask[li][j] * s * (1.0 - s) + l1_per * sparse_grad;
2279 }
2280 }
2281 let t_chunk = t_step.elapsed().as_secs_f64();
2294 let mut params: Vec<&mut [f32]> = logits.iter_mut().map(|v| v.as_mut_slice()).collect();
2295 adam_a.step(&mut params, &dmask, 1.0);
2296 acc_chunk += t_chunk;
2297 acc_adam += t_step.elapsed().as_secs_f64() - t_chunk;
2298 if (step + 1) % hy.eval_every == 0 {
2299 l1 += l1_step_eff;
2300 let pass = Pass {
2301 fm: &fm,
2302 tau: hy.tau,
2303 logits: &logits,
2304 hard: true,
2305 ffn: &ffn,
2306 focus_tokens: &hy.focus_tokens,
2307 focus_follow_tokens: &hy.focus_follow_tokens,
2308 span: hy.answer_span.as_ref(),
2309 soft_fold: hy.soft_fold,
2310 attn_tr: &attn_tr,
2311 };
2312 crate::gpu::bake_precision_strict(false);
2313 let hs = held_score_tagged(&pass, &held, held_tags.as_ref());
2314 let hp = hs.ppl;
2315 crate::gpu::bake_precision_strict(strict_a);
2316 let cur: Vec<Vec<bool>> = logits
2322 .iter()
2323 .map(|l| l.iter().map(|&x| sigmoid(x) > hy.tau).collect())
2324 .collect();
2325 if let Some(prev) = &prev_alive {
2326 let died: Vec<String> = cur
2327 .iter()
2328 .zip(prev)
2329 .enumerate()
2330 .flat_map(|(li, (c, p))| {
2331 c.iter()
2332 .zip(p.iter())
2333 .enumerate()
2334 .filter(|&(_, (&cj, &pj))| pj && !cj)
2335 .map(move |(j, _)| format!("L{li}:{j}"))
2336 })
2337 .collect();
2338 if !died.is_empty() {
2339 log(&format!(
2340 " closed since last eval: {}: {}{}",
2341 died.len(),
2342 died.iter().take(32).cloned().collect::<Vec<_>>().join(" "),
2343 if died.len() > 32 { " …" } else { "" }
2344 ));
2345 }
2346 }
2347 let alive: usize = cur.iter().map(|l| l.iter().filter(|&&b| b).count()).sum();
2348 prev_alive = Some(cur);
2349 let sp = 1.0 - alive as f64 / (vn * inter) as f64;
2350 if sp > max_sp.2 {
2352 max_sp = (hp, Some(logits.clone()), sp);
2353 max_sp_step = step + 1;
2354 }
2355 checkpoints.push(BakeCheckpoint {
2356 step: step + 1,
2357 l1,
2358 ppl: hp,
2359 sparsity: sp,
2360 accuracy: hs.accuracy,
2361 balanced_accuracy: hs.balanced_accuracy,
2362 logits: logits.clone(),
2363 });
2364 if let Some(dir) = &hy.checkpoint_dir {
2365 let doc = serde_json::json!({
2366 "schema": "cmf-bake-checkpoint-v1",
2367 "step": step + 1,
2368 "l1": l1,
2369 "ppl": hp,
2370 "sparsity": sp,
2371 "accuracy": hs.accuracy,
2372 "micro_accuracy": hs.micro_accuracy,
2373 "token_accuracy": hs.token_accuracy,
2374 "per_tag": hs.per_tag,
2375 "selected_logits": &logits,
2376 });
2377 let path = format!("{dir}/phase-a-step-{}.json", step + 1);
2378 match serde_json::to_vec(&doc) {
2379 Ok(bytes) => match std::fs::write(&path, bytes) {
2380 Ok(()) => log(&format!(" [A] checkpoint written: {path}")),
2381 Err(e) => log(&format!(" [A] checkpoint NOT written ({path}): {e}")),
2382 },
2383 Err(e) => log(&format!(" [A] checkpoint NOT serialised: {e}")),
2384 }
2385 }
2386 let eligible_sparsity = hy.target_sparsity <= 0.0 || sp >= hy.target_sparsity;
2391 let eligible_accuracy = hy
2392 .checkpoint_min_accuracy
2393 .map_or(true, |min| hs.accuracy.is_some_and(|value| value > min));
2394 let eligible_balanced = hy.checkpoint_min_balanced_accuracy.map_or(true, |min| {
2395 hs.balanced_accuracy.is_some_and(|value| value > min)
2396 });
2397 let eligible = eligible_sparsity && eligible_accuracy && eligible_balanced;
2398 if eligible && hy.checkpoint_raw_priority && !hy.focus_tokens.is_empty() {
2399 let acc = hs.accuracy.unwrap_or(f64::NEG_INFINITY);
2400 let bal = hs.balanced_accuracy.unwrap_or(f64::NEG_INFINITY);
2401 if acc > best_accuracy
2402 || (acc == best_accuracy && bal > best_balanced)
2403 || (acc == best_accuracy && bal == best_balanced && hp < best.0)
2404 {
2405 best_balanced = bal;
2406 best_accuracy = acc;
2407 best = (hp, Some(logits.clone()), sp);
2408 best_step = step + 1;
2409 }
2410 } else if eligible && hy.checkpoint_accuracy && !hy.focus_tokens.is_empty() {
2411 let acc = hs.accuracy.unwrap_or(f64::NEG_INFINITY);
2412 let bal = hs.balanced_accuracy.unwrap_or(f64::NEG_INFINITY);
2413 if bal > best_balanced
2414 || (bal == best_balanced && acc > best_accuracy)
2415 || (bal == best_balanced && acc == best_accuracy && hp < best.0)
2416 {
2417 best_balanced = bal;
2418 best_accuracy = acc;
2419 best = (hp, Some(logits.clone()), sp);
2420 best_step = step + 1;
2421 }
2422 } else if eligible && hy.checkpoint_accuracy && hy.answer_span.is_some() {
2423 let acc = hs.accuracy.unwrap_or(f64::NEG_INFINITY);
2428 if acc > best_accuracy || (acc == best_accuracy && hp < best.0) {
2429 best_accuracy = acc;
2430 best = (hp, Some(logits.clone()), sp);
2431 best_step = step + 1;
2432 }
2433 } else if eligible && hp < best.0 {
2434 best = (hp, Some(logits.clone()), sp);
2435 best_step = step + 1;
2436 }
2437 log(&format!(
2438 " [A] step {}: L1={l1:.3} pruned={:.2}% hard-PPL={hp:.3}{} (bottom {}@{:.2}%) [fwd+bwd {:.1}s, adam {:.2}s per step]",
2439 step + 1,
2440 sp * 100.0,
2441 score_suffix(&hs),
2442 if best.0 == f64::MAX {
2443 "—".to_string()
2444 } else {
2445 format!("{:.3}", best.0)
2446 },
2447 best.2 * 100.0,
2448 acc_chunk / (step + 1) as f64,
2449 acc_adam / (step + 1) as f64
2450 ));
2451 if let Some(line) = tag_line(&hs) {
2452 log(&format!(" per tag: {line}"));
2453 }
2454 }
2455 }
2456 crate::gpu::bake_precision_strict(false);
2460 if (hy.checkpoint_min_accuracy.is_some() || hy.checkpoint_min_balanced_accuracy.is_some())
2461 && best.1.is_none()
2462 {
2463 return Err(
2464 "skill bake: no Phase-A checkpoint met the configured focused accuracy guards".into(),
2465 );
2466 }
2467 if hy.target_sparsity > 0.0 && best.1.is_none() {
2468 log(&format!(
2469 "[A] target sparsity {:.0}% not reached; using max-sparsity checkpoint ({:.0}%)",
2470 hy.target_sparsity * 100.0,
2471 max_sp.2 * 100.0
2472 ));
2473 best = max_sp;
2474 best_step = max_sp_step;
2475 }
2476 {
2479 use crate::fcd::prof;
2480 let (a, f, bw, g, gc) = (
2481 prof::take(&prof::ATTN_FWD),
2482 prof::take(&prof::FFN_FWD),
2483 prof::take(&prof::BWD),
2484 prof::take(&prof::GEMM),
2485 prof::GEMM_CALLS.swap(0, std::sync::atomic::Ordering::Relaxed),
2486 );
2487 log(&format!(
2488 "[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)",
2489 hy.steps_a,
2490 if gc > 0 { g * 1000.0 / gc as f64 } else { 0.0 }
2491 ));
2492 log(&format!(
2493 "[prof] inside: attn-chain {:.1}s | ffn-pre {:.1}s | ffn-chain {:.1}s | ffn-act(split+act) {:.1}s | ffn-host {:.1}s | ffn-post {:.1}s | chain-w {:.1}s gpu {:.1}s copy {:.1}s",
2494 prof::take(&prof::ATTN_CHAIN),
2495 prof::take(&prof::FFN_PRE),
2496 prof::take(&prof::FFN_CHAIN),
2497 prof::take(&prof::FFN_ACT),
2498 prof::take(&prof::FFN_HOST),
2499 prof::take(&prof::FFN_POST),
2500 chain_prof(0),
2501 chain_prof(1),
2502 chain_prof(2),
2503 ));
2504 log(&format!("[prof] gemm shapes:\n{}", prof::shape_report(6)));
2505 }
2506 let final_logits = logits.clone();
2507 if let Some(b) = best.1.take() {
2508 logits = b;
2509 }
2510 let pass = Pass {
2511 fm: &fm,
2512 tau: hy.tau,
2513 logits: &logits,
2514 hard: true,
2515 ffn: &ffn,
2516 focus_tokens: &hy.focus_tokens,
2517 focus_follow_tokens: &hy.focus_follow_tokens,
2518 span: hy.answer_span.as_ref(),
2519 soft_fold: hy.soft_fold,
2520 attn_tr: &attn_tr,
2521 };
2522 let masked_score = if hy.steps_a == 0 && hy.mask_logits.is_none() {
2528 backbone_score.clone()
2529 } else {
2530 held_score_tagged(&pass, &held, held_tags.as_ref())
2531 };
2532 let masked = masked_score.ppl;
2533 log(&format!(
2534 "[A] {:.0}s: masked-PPL {masked:.3}{}",
2535 t0.elapsed().as_secs_f64(),
2536 score_suffix(&masked_score)
2537 ));
2538
2539 for &li in &fcd {
2541 let p = format!("model.layers.{li}.");
2542 ffn[li] = Some((
2543 crate::fcd::deq_pub(&fm.src, &format!("{p}mlp.gate_proj.weight"))
2544 .map_err(|e| format!("phase-B gate: {e}"))?,
2545 crate::fcd::deq_pub(&fm.src, &format!("{p}mlp.up_proj.weight"))
2546 .map_err(|e| format!("phase-B up: {e}"))?,
2547 crate::fcd::deq_pub(&fm.src, &format!("{p}mlp.down_proj.weight"))
2548 .map_err(|e| format!("phase-B down: {e}"))?,
2549 ));
2550 }
2551 if hy.fcd_attn {
2552 for &li in &fcd {
2553 if fm.attn_geometry(li).is_none() {
2554 return Err(format!("--fcd-attn: layer {li} is not a softmax-attention layer"));
2555 }
2556 let mats = fm.mats(li)?;
2557 attn_tr[li] = Some(AttnMasters {
2558 wqkv: mats.wqkv.clone(),
2559 wo: mats.wo.clone(),
2560 gproj: fm.head_gate_weights(li).map(<[f32]>::to_vec),
2561 iln: fm.layers[li].iln.clone(),
2562 pln: fm.layers[li].pln.clone(),
2563 });
2564 }
2565 log(&format!("[B] full-layer FCD: attention + norms of {} layer(s) train too", fcd.len()));
2566 }
2567 let sizes: Vec<usize> = fcd
2568 .iter()
2569 .flat_map(|&li| {
2570 let (g, u, d) = ffn[li].as_ref().expect("phase-B masters");
2571 let mut v = vec![g.len(), u.len(), d.len()];
2572 if let Some(m) = &attn_tr[li] {
2573 v.extend([m.wqkv.len(), m.wo.len(), m.gproj.as_ref().map_or(0, Vec::len), m.iln.len(), m.pln.len()]);
2574 }
2575 v
2576 })
2577 .filter(|&n| n > 0)
2578 .collect();
2579 let mut adam_b = Adam::new(&sizes, hy.lr_b);
2580 let mut best_b: (
2584 HeldScore,
2585 Option<Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>>>,
2586 ) = (masked_score.clone(), Some(vec![None; nl]));
2587 let mut best_attn: Vec<Option<AttnMasters>> = attn_tr.clone();
2588 let cached_phase_b =
2589 hy.steps_b > 0 && fcd.len() == 1 && loops == 1 && !hy.focus_tokens.is_empty();
2590 if cached_phase_b {
2591 let last = fcd[0];
2592 let pass = Pass {
2593 fm: &fm,
2594 tau: hy.tau,
2595 logits: &logits,
2596 hard: true,
2597 ffn: &ffn,
2598 focus_tokens: &hy.focus_tokens,
2599 focus_follow_tokens: &hy.focus_follow_tokens,
2600 span: hy.answer_span.as_ref(),
2601 soft_fold: hy.soft_fold,
2602 attn_tr: &attn_tr,
2603 };
2604 log(&format!(
2605 "[B-cache] extracting native CMF boundaries: {} train + {} held records",
2606 calib.len(),
2607 held.len()
2608 ));
2609 let train_cache = build_focused_fcd_cache(&pass, &calib, 32)?;
2610 let held_cache = build_focused_fcd_cache(&pass, &held, 32)?;
2611 if train_cache.len() < 12 || held_cache.len() == 0 {
2612 return Err(format!(
2613 "focused final-FFN cache is too small: {} train, {} held answers",
2614 train_cache.len(),
2615 held_cache.len()
2616 ));
2617 }
2618 let gate = pass.gates(last);
2619 let held_indices: Vec<usize> = (0..held_cache.len()).collect();
2620 let (initial_cached, _) = {
2621 let (g, u, d) = ffn[last].as_ref().expect("cached FCD master");
2622 cached_fcd_run(
2623 &fm,
2624 &held_cache,
2625 &held_indices,
2626 (g, u, d),
2627 &gate,
2628 &hy.focus_tokens,
2629 false,
2630 )
2631 };
2632 log(&format!(
2633 "[B-cache] ready: {} train / {} held | parity PPL {:.3} vs full {:.3}{}",
2634 train_cache.len(),
2635 held_cache.len(),
2636 initial_cached.ppl,
2637 masked_score.ppl,
2638 initial_cached
2639 .accuracy
2640 .map(|value| format!(" | acc {:.2}%", value * 100.0))
2641 .unwrap_or_default()
2642 ));
2643 if (initial_cached.ppl - masked_score.ppl).abs() > 5e-3
2644 || initial_cached.accuracy != masked_score.accuracy
2645 {
2646 return Err(format!(
2647 "focused final-FFN cache parity failed: PPL {:.6} vs {:.6}, accuracy {:?} vs {:?}",
2648 initial_cached.ppl,
2649 masked_score.ppl,
2650 initial_cached.accuracy,
2651 masked_score.accuracy
2652 ));
2653 }
2654 for step in 0..hy.steps_b {
2655 let count = hy.fcd_batch.min(train_cache.len());
2656 let indices: Vec<usize> = (0..count)
2657 .map(|offset| (step * count + offset) % train_cache.len())
2658 .collect();
2659 let (_, gradients) = {
2660 let (g, u, d) = ffn[last].as_ref().expect("cached FCD master");
2661 cached_fcd_run(
2662 &fm,
2663 &train_cache,
2664 &indices,
2665 (g, u, d),
2666 &gate,
2667 &hy.focus_tokens,
2668 true,
2669 )
2670 };
2671 let (dg, du, dd) = gradients.expect("cached FCD requested gradients");
2672 let lr_scale =
2673 0.5 * (1.0 + (std::f64::consts::PI * step as f64 / hy.steps_b as f64).cos());
2674 let (g, u, d) = ffn[last].as_mut().expect("cached FCD master");
2675 let mut params = vec![g.as_mut_slice(), u.as_mut_slice(), d.as_mut_slice()];
2676 let grads = vec![dg, du, dd];
2677 adam_b.step(&mut params, &grads, lr_scale);
2678 if (step + 1) % hy.eval_every == 0 {
2679 let cur = {
2680 let (g, u, d) = ffn[last].as_ref().expect("cached FCD master");
2681 cached_fcd_run(
2682 &fm,
2683 &held_cache,
2684 &held_indices,
2685 (g, u, d),
2686 &gate,
2687 &hy.focus_tokens,
2688 false,
2689 )
2690 .0
2691 };
2692 let cur_bal = cur.balanced_accuracy.unwrap_or(f64::NEG_INFINITY);
2693 let best_bal = best_b.0.balanced_accuracy.unwrap_or(f64::NEG_INFINITY);
2694 let cur_acc = cur.accuracy.unwrap_or(f64::NEG_INFINITY);
2695 let best_acc = best_b.0.accuracy.unwrap_or(f64::NEG_INFINITY);
2696 let better = if hy.checkpoint_accuracy {
2697 cur_bal > best_bal
2698 || (cur_bal == best_bal && cur_acc > best_acc)
2699 || (cur_bal == best_bal && cur_acc == best_acc && cur.ppl < best_b.0.ppl)
2700 } else {
2701 cur.ppl < best_b.0.ppl
2702 };
2703 if better {
2704 best_b = (cur.clone(), Some(ffn.clone()));
2705 best_attn = attn_tr.clone();
2706 }
2707 log(&format!(
2708 " [B-cache] step {}: held-PPL {:.3} acc={:.2}% bal={:.2}% (best {:.3})",
2709 step + 1,
2710 cur.ppl,
2711 cur_acc * 100.0,
2712 cur_bal * 100.0,
2713 best_b.0.ppl
2714 ));
2715 }
2716 }
2717 } else {
2718 for step in 0..hy.steps_b {
2719 let (batch_ids, batch) = calibration_batch(&calib, step, hy.batch)?;
2720 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; vn];
2721 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = (0..nl)
2722 .map(|li| {
2723 ffn[li].as_ref().map(|(g, u, d)| {
2724 (vec![0.0; g.len()], vec![0.0; u.len()], vec![0.0; d.len()])
2725 })
2726 })
2727 .collect();
2728 let mut dattn: Vec<Option<AttnGrads>> =
2729 attn_tr.iter().map(|m| m.as_ref().map(AttnGrads::zeros_like)).collect();
2730 let pass = Pass {
2731 fm: &fm,
2732 tau: hy.tau,
2733 logits: &logits,
2734 hard: true,
2735 ffn: &ffn,
2736 focus_tokens: &hy.focus_tokens,
2737 focus_follow_tokens: &hy.focus_follow_tokens,
2738 span: hy.answer_span.as_ref(),
2739 soft_fold: hy.soft_fold,
2740 attn_tr: &attn_tr,
2741 };
2742 let _ = pass.chunk_batch(&batch_ids, batch, Some((&mut dmask, &mut dffn, &mut dattn)));
2743 let lr_scale =
2745 0.5 * (1.0 + (std::f64::consts::PI * step as f64 / hy.steps_b as f64).cos());
2746 let first_fcd = fcd[0];
2747 let mut params: Vec<&mut [f32]> = Vec::new();
2748 let mut grads: Vec<Vec<f64>> = Vec::new();
2749 let attn_tr_slots = &mut attn_tr[first_fcd..];
2750 for (off, (slot, aslot)) in ffn[first_fcd..]
2751 .iter_mut()
2752 .zip(attn_tr_slots.iter_mut())
2753 .enumerate()
2754 {
2755 let li = first_fcd + off;
2756 let Some((g, u, d)) = slot.as_mut() else {
2757 continue;
2758 };
2759 let (dg, du, dd) = dffn[li].take().unwrap();
2760 params.push(g.as_mut_slice());
2761 grads.push(dg);
2762 params.push(u.as_mut_slice());
2763 grads.push(du);
2764 params.push(d.as_mut_slice());
2765 grads.push(dd);
2766 if let (Some(m), Some(ag)) = (aslot.as_mut(), dattn[li].take()) {
2767 params.push(m.wqkv.as_mut_slice());
2768 grads.push(ag.wqkv);
2769 params.push(m.wo.as_mut_slice());
2770 grads.push(ag.wo);
2771 if let (Some(gp), Some(dgp)) = (m.gproj.as_mut(), ag.gproj) {
2772 params.push(gp.as_mut_slice());
2773 grads.push(dgp);
2774 }
2775 params.push(m.iln.as_mut_slice());
2776 grads.push(ag.iln);
2777 params.push(m.pln.as_mut_slice());
2778 grads.push(ag.pln);
2779 }
2780 }
2781 adam_b.step(&mut params, &grads, lr_scale * mask_step_scale(loops));
2786 if (step + 1) % hy.eval_every == 0 {
2787 let pass = Pass {
2788 fm: &fm,
2789 tau: hy.tau,
2790 logits: &logits,
2791 hard: true,
2792 ffn: &ffn,
2793 focus_tokens: &hy.focus_tokens,
2794 focus_follow_tokens: &hy.focus_follow_tokens,
2795 span: hy.answer_span.as_ref(),
2796 soft_fold: hy.soft_fold,
2797 attn_tr: &attn_tr,
2798 };
2799 let cur = held_score_tagged(&pass, &held, held_tags.as_ref());
2800 let better = if hy.checkpoint_accuracy && hy.answer_span.is_some() {
2801 let cur_acc = cur.accuracy.unwrap_or(f64::NEG_INFINITY);
2802 let best_acc = best_b.0.accuracy.unwrap_or(f64::NEG_INFINITY);
2803 cur_acc > best_acc || (cur_acc == best_acc && cur.ppl < best_b.0.ppl)
2804 } else if hy.checkpoint_accuracy && !hy.focus_tokens.is_empty() {
2805 let cur_bal = cur.balanced_accuracy.unwrap_or(f64::NEG_INFINITY);
2806 let best_bal = best_b.0.balanced_accuracy.unwrap_or(f64::NEG_INFINITY);
2807 let cur_acc = cur.accuracy.unwrap_or(f64::NEG_INFINITY);
2808 let best_acc = best_b.0.accuracy.unwrap_or(f64::NEG_INFINITY);
2809 cur_bal > best_bal
2810 || (cur_bal == best_bal && cur_acc > best_acc)
2811 || (cur_bal == best_bal && cur_acc == best_acc && cur.ppl < best_b.0.ppl)
2812 } else {
2813 cur.ppl < best_b.0.ppl
2814 };
2815 let take_last = std::env::var("CMF_BAKE_B_TAKE_LAST").is_ok_and(|v| v == "1");
2820 if better || (take_last && step + 1 == hy.steps_b) {
2821 best_b = (cur.clone(), Some(ffn.clone()));
2822 best_attn = attn_tr.clone();
2823 }
2824 log(&format!(
2825 " [B] step {}: held-PPL {:.3}{} (best {:.3})",
2826 step + 1,
2827 cur.ppl,
2828 score_suffix(&cur),
2829 best_b.0.ppl
2830 ));
2831 if let Some(line) = tag_line(&cur) {
2832 log(&format!(" per tag: {line}"));
2833 }
2834 }
2835 }
2836 }
2837 ffn = best_b.1.take().expect("phase-B always has a checkpoint");
2838 attn_tr = best_attn;
2839 let overlaid_score = best_b.0;
2840 let overlaid = overlaid_score.ppl;
2841
2842 let keep_visits = keep_masks(&logits, hy.tau, hy.align, hy.uniform_inter);
2847 let keep: Vec<Vec<bool>> = (0..nl)
2848 .map(|li| {
2849 (0..inter)
2850 .map(|j| (0..loops).any(|v| keep_visits[v * nl + li][j]))
2851 .collect()
2852 })
2853 .collect();
2854 if hy.align > 1 || hy.uniform_inter {
2855 let raw: usize = logits
2856 .iter()
2857 .map(|l| l.iter().filter(|&&x| sigmoid(x) > hy.tau).count())
2858 .sum();
2859 let padded: usize = keep_visits
2863 .iter()
2864 .map(|a| a.iter().filter(|&&x| x).count())
2865 .sum::<usize>()
2866 .saturating_sub(raw);
2867 log(&format!(
2868 "align: +{padded} neurons resurrected (align {}, uniform {})",
2869 hy.align, hy.uniform_inter
2870 ));
2871 }
2872 let mut down_out = Vec::with_capacity(nl);
2873 let mut gate_up = Vec::with_capacity(nl);
2874 let mut kept_per_layer = Vec::with_capacity(nl);
2875 for li in 0..nl {
2876 let alive = &keep[li];
2877 kept_per_layer.push(alive.iter().filter(|&&a| a).count());
2878 let mut down = match &ffn[li] {
2879 Some((_, _, d)) => d.clone(),
2880 None => fm.mats(li).expect("layer mats").down.clone(),
2881 };
2882 let hsz = fm.hidden;
2883 for r in 0..hsz {
2884 for (c, &a) in alive.iter().enumerate() {
2885 if !a {
2886 down[r * inter + c] = 0.0;
2887 }
2888 }
2889 }
2890 gate_up.push(ffn[li].as_ref().map(|(g, u, _)| (g.clone(), u.clone())));
2891 down_out.push(down);
2892 }
2893 let total: usize = keep_visits
2894 .iter()
2895 .map(|a| a.iter().filter(|&&x| x).count())
2896 .sum();
2897 let report = BakeReport {
2898 backbone,
2899 masked,
2900 overlaid,
2901 pruned_ratio: 1.0 - total as f64 / (vn * inter) as f64,
2902 kept_per_layer,
2903 backbone_accuracy: backbone_score.accuracy,
2904 masked_accuracy: masked_score.accuracy,
2905 overlaid_accuracy: overlaid_score.accuracy,
2906 backbone_balanced_accuracy: backbone_score.balanced_accuracy,
2907 masked_balanced_accuracy: masked_score.balanced_accuracy,
2908 overlaid_balanced_accuracy: overlaid_score.balanced_accuracy,
2909 selected_step: best_step,
2910 sec: t0.elapsed().as_secs_f64(),
2911 };
2912 let arts = BakeArtifacts {
2913 keep,
2914 keep_visits,
2915 down: down_out,
2916 gate_up,
2917 attn: attn_tr,
2918 fcd_layers: fcd,
2919 logits: logits.clone(),
2920 final_logits,
2921 checkpoints,
2922 };
2923 Ok((report, arts))
2924}
2925
2926fn keep_masks(logits: &[Vec<f32>], tau: f32, align: usize, uniform: bool) -> Vec<Vec<bool>> {
2934 let inter = logits[0].len();
2935 let round = |n: usize| -> usize {
2936 let n = n.max(1);
2937 if align <= 1 {
2938 n.min(inter)
2939 } else {
2940 (n.div_ceil(align) * align).min(inter)
2941 }
2942 };
2943 let mut want: Vec<usize> = logits
2944 .iter()
2945 .map(|l| round(l.iter().filter(|&&x| sigmoid(x) > tau).count()))
2946 .collect();
2947 if uniform {
2948 let k = want.iter().copied().max().unwrap_or(inter);
2949 want = vec![k; logits.len()];
2950 }
2951 logits
2952 .iter()
2953 .zip(&want)
2954 .map(|(l, &k)| {
2955 let mut idx: Vec<usize> = (0..inter).collect();
2956 idx.sort_unstable_by(|&a, &b| l[b].total_cmp(&l[a]));
2957 let mut alive = vec![false; inter];
2958 for &i in idx.iter().take(k) {
2959 alive[i] = true;
2960 }
2961 alive
2962 })
2963 .collect()
2964}
2965
2966#[cfg(test)]
2967mod tests {
2968 use super::*;
2969
2970 fn kept(masks: &[Vec<bool>]) -> Vec<usize> {
2971 masks
2972 .iter()
2973 .map(|m| m.iter().filter(|&&a| a).count())
2974 .collect()
2975 }
2976
2977 #[test]
2978 fn terminal_focus_ignores_label_names_inside_the_prompt() {
2979 let down = 10;
2982 let up = 11;
2983 let im_end = 99;
2984 let ids = [1, down, 2, up, 3, up, im_end, 4];
2985 let focus = [down, up];
2986 let follow = [im_end];
2987 assert!(!is_scored_target(&ids, 1, ids.len(), &focus, &follow));
2988 assert!(!is_scored_target(&ids, 3, ids.len(), &focus, &follow));
2989 assert!(is_scored_target(&ids, 5, ids.len(), &focus, &follow));
2990 }
2991
2992 #[test]
2993 fn configurable_mask_init_preserves_effective_gate_across_loops() {
2994 for effective_logit in [2.0, 4.0] {
2995 let target = sigmoid(effective_logit);
2996 for loops in [1usize, 2, 4] {
2997 let per_visit = sigmoid(mask_init_logit_for(loops, effective_logit));
2998 assert!((per_visit.powi(loops as i32) - target).abs() < 2e-6);
2999 }
3000 }
3001 }
3002
3003 #[test]
3004 fn softplus_penalty_keeps_a_gradient_near_an_open_gate() {
3005 let gate_penalty = sparsity_grad(4.0, false);
3006 let softplus_penalty = sparsity_grad(4.0, true);
3007 assert!(softplus_penalty > gate_penalty * 50.0);
3008 assert!((softplus_penalty - sigmoid(4.0) as f64).abs() < 1e-7);
3009 }
3010
3011 #[test]
3012 fn answer_values_group_each_top_level_value() {
3013 let pieces = [
3016 "{\"", "q1", "\":", " \"", "option", "_", "24", "\",", " \"q2", "\":", " [", " \"a\"", " ]", ",", " \"q3\":",
3017 " 3", "}", "<end>",
3018 ];
3019 let text = TokenText(Arc::new(pieces.iter().map(|p| p.to_string()).collect()));
3020 let ids: Vec<u32> = (0..pieces.len() as u32).collect();
3021 let groups = answer_values(&ids, 0, ids.len() - 1, &text);
3022 assert_eq!(groups, vec![vec![3, 4, 5, 6, 7], vec![10, 11, 12], vec![15]]);
3023 }
3024
3025 #[test]
3026 fn answer_values_keep_escaped_quotes_inside_the_value() {
3027 let pieces = ["{\"q\": \"a", "\\\"", "b\"}", "<end>"];
3028 let text = TokenText(Arc::new(pieces.iter().map(|p| p.to_string()).collect()));
3029 let ids: Vec<u32> = (0..pieces.len() as u32).collect();
3030 assert_eq!(answer_values(&ids, 0, 3, &text), vec![vec![0, 1, 2]]);
3031 }
3032
3033 #[test]
3034 fn balanced_accuracy_exposes_majority_class_collapse() {
3035 let stats = FocusStats {
3036 total: 100,
3037 correct: 90,
3038 class_total: vec![90, 10],
3039 class_correct: vec![90, 0],
3040 ..FocusStats::default()
3041 };
3042 assert_eq!(stats.accuracy(), Some(0.9));
3043 assert_eq!(stats.balanced_accuracy(), Some(0.5));
3044 }
3045
3046 #[test]
3047 fn grouped_focus_stats_merge_is_additive() {
3048 let mut all = FocusStats {
3049 total: 3,
3050 correct: 2,
3051 class_total: vec![2, 1],
3052 class_correct: vec![1, 1],
3053 questions: 2,
3054 questions_correct: 1,
3055 seq: Vec::new(),
3056 };
3057 let second = FocusStats {
3058 total: 4,
3059 correct: 3,
3060 class_total: vec![1, 3],
3061 class_correct: vec![1, 2],
3062 questions: 3,
3063 questions_correct: 3,
3064 seq: Vec::new(),
3065 };
3066 all.merge(&second);
3067 assert_eq!(all.question_accuracy(), Some(4.0 / 5.0));
3068 assert_eq!(all.accuracies(), (Some(4.0 / 5.0), Some(5.0 / 7.0)));
3069 assert_eq!(all.total, 7);
3070 assert_eq!(all.correct, 5);
3071 assert_eq!(all.class_total, vec![3, 4]);
3072 assert_eq!(all.class_correct, vec![2, 3]);
3073 assert_eq!(all.accuracy(), Some(5.0 / 7.0));
3074 assert_eq!(all.balanced_accuracy(), Some((2.0 / 3.0 + 3.0 / 4.0) / 2.0));
3075 }
3076
3077 #[test]
3078 fn calibration_batches_keep_records_independent_and_wrap_deterministically() {
3079 let records = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
3080 assert_eq!(
3081 calibration_batch(&records, 0, 2).unwrap(),
3082 (vec![1, 2, 3, 4], 2)
3083 );
3084 assert_eq!(calibration_batch(&records, 1, 2).unwrap(), (vec![5, 6], 1));
3087 assert_eq!(
3088 calibration_batch(&records, 2, 2).unwrap(),
3089 (vec![1, 2, 3, 4], 2)
3090 );
3091 assert_eq!(calibration_batch(&[vec![1, 2], vec![3]], 0, 2).unwrap(), (vec![1, 2], 1));
3093 assert_eq!(calibration_batch(&[vec![1, 2], vec![3]], 1, 2).unwrap(), (vec![3], 1));
3094 }
3095
3096 #[test]
3099 fn keep_masks_aligns_up_and_preserves_alive() {
3100 let inter = 96;
3101 let l0: Vec<f32> = (0..inter)
3104 .map(|i| if i < 40 { 1.0 } else { -1.0 - i as f32 * 0.01 })
3105 .collect();
3106 let l1: Vec<f32> = (0..inter)
3108 .map(|i| if i < 64 { 2.0 } else { -3.0 })
3109 .collect();
3110 let masks = keep_masks(&[l0.clone(), l1], 0.5, 32, false);
3111 assert_eq!(kept(&masks), vec![64, 64]);
3112 for i in 0..64 {
3115 assert!(masks[0][i], "neuron {i} should be kept");
3116 }
3117 for i in 64..inter {
3118 assert!(!masks[0][i], "neuron {i} should stay pruned");
3119 }
3120 }
3121
3122 #[test]
3124 fn keep_masks_uniform_takes_max() {
3125 let inter = 96;
3126 let l0: Vec<f32> = (0..inter)
3127 .map(|i| if i < 10 { 1.0 } else { -2.0 })
3128 .collect();
3129 let l1: Vec<f32> = (0..inter)
3130 .map(|i| if i < 70 { 1.0 } else { -2.0 })
3131 .collect();
3132 let masks = keep_masks(&[l0, l1], 0.5, 32, true);
3133 assert_eq!(kept(&masks), vec![96, 96]);
3134 }
3135
3136 #[test]
3139 fn keep_masks_edges() {
3140 let inter = 48;
3141 let l: Vec<f32> = (0..inter)
3142 .map(|i| if i < 47 { 1.0 } else { -2.0 })
3143 .collect();
3144 let masks = keep_masks(&[l.clone()], 0.5, 32, false);
3145 assert_eq!(kept(&masks), vec![48]); let masks = keep_masks(&[l], 0.5, 1, false);
3147 assert_eq!(kept(&masks), vec![47]);
3148 let dead: Vec<f32> = vec![-5.0; inter];
3149 let masks = keep_masks(&[dead], 0.5, 32, false);
3150 assert_eq!(kept(&masks), vec![32]); }
3152}
3153
3154#[cfg(test)]
3155mod answer_span_tests {
3156 use super::*;
3157
3158 #[test]
3159 fn answer_targets_follow_the_last_prompt_to_the_terminator() {
3160 let sp = AnswerSpan {
3161 start: vec![7, 8],
3162 end: 1,
3163 values: None,
3164 };
3165 let ids = [5, 7, 8, 9, 7, 8, 20, 21, 1, 1, 1];
3167 assert_eq!(answer_targets(&ids, &sp), Some((6, 8)));
3168 let ids = [5, 7, 8, 20, 21];
3170 assert_eq!(answer_targets(&ids, &sp), Some((3, 4)));
3171 assert_eq!(answer_targets(&[5, 6, 1], &sp), None);
3173 assert_eq!(answer_targets(&[5, 7, 8], &sp), None);
3174 }
3175
3176 #[test]
3177 fn scored_targets_cover_every_position_without_focus_or_span() {
3178 let ids = [1u32, 2, 3, 4, 5, 6];
3179 assert_eq!(scored_targets(&ids, 0, 3, &[], &[], None), vec![1, 2]);
3180 assert_eq!(scored_targets(&ids, 3, 3, &[], &[], None), vec![4, 5]);
3181 let sp = AnswerSpan {
3182 start: vec![2],
3183 end: 6,
3184 values: None,
3185 };
3186 assert_eq!(scored_targets(&ids, 0, 6, &[], &[], Some(&sp)), vec![2, 3, 4, 5]);
3187 }
3188}