Skip to main content

cortiq_engine/
skillbake.rs

1//! Native DTG-MA skill bake (Patent 2) — no Python, no torch.
2//!
3//! The certified recipe of `converter/make_skill_l1fcd.py`, in Rust on
4//! the `FcdModel` f32 replica:
5//!
6//! - **Phase A** — a trainable L1 mask over FFN neurons (one logit per
7//!   neuron, applied to the input of down_proj as σ(m)): pure LM loss
8//!   on the task corpus + a progressive L1 penalty. Every 30 steps the
9//!   binarized mask (σ>τ) is scored on held-out chunks; the best
10//!   checkpoint — the *denoising bottom* — is restored at the end.
11//!   Pruning noise neurons IMPROVES the model before it starts to hurt.
12//! - **Phase B** — FCD: the FFN of the last N layers trains against the
13//!   same LM loss with the hard mask active (cosine LR), held-out
14//!   gated, best checkpoint restored.
15//!
16//! Attention (softmax and GDN alike) is FROZEN and carries no gradient
17//! — exactly like the reference recipe (`torch.no_grad()` around the
18//! attention branch): the backward walks the residual stream through
19//! the FFN chain only, which is what makes a pure-Rust backward small.
20
21use 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/// Supervised chat records scored on the assistant's answer only: every
28/// target from the first token after `start` (the rendered generation
29/// prompt, e.g. `<|Bot|></think>`) through the first `end` token (the
30/// turn terminator) carries the LM loss; the prompt is context. This is
31/// the multi-token sibling of `focus_tokens` — a JSON verdict, a number,
32/// a sentence — where a one-token label does not fit.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct AnswerSpan {
35    /// Token ids of the generation prompt; the answer starts right after
36    /// its LAST occurrence in the record.
37    pub start: Vec<u32>,
38    /// The turn terminator (one token); the last scored target.
39    pub end: u32,
40    /// Decoded text of every vocabulary id. When present, the answer's
41    /// top-level JSON values are located in it and the held score grades
42    /// whole verdicts (`answer_values`): a question counts as correct only
43    /// when every token of its value is the argmax. Token accuracy over
44    /// the span alone is dominated by the braces, quotes and question ids
45    /// the model copies from the prompt.
46    pub values: Option<TokenText>,
47}
48
49/// Decoded text per token id (see `AnswerSpan::values`).
50#[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
59/// The target positions of each top-level value of the JSON object an
60/// answer span holds (`{"q1": "option_24", "q2": 3}` → two groups), in
61/// order. A token overlapping a value's characters belongs to it, so a
62/// token that merges a quote with the label's first letters is graded with
63/// the label. Positions are indices into `ids`, within `first..=last`.
64pub 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
135/// The scored target range `(first, last)` (inclusive, indices into
136/// `ids`) of one record, or `None` when the record carries no answer.
137pub 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/// Hyper-parameters — defaults are the certified recipe.
154#[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    /// Independent fixed-length records per optimizer step.  The loss is
166    /// normalized over all focused targets in the batch.
167    pub batch: usize,
168    /// Focused records per cached final-FFN optimizer step.  This is separate
169    /// from `batch` because Phase A stores every layer's activations while a
170    /// one-layer FCD cache stores only two hidden vectors per record.
171    pub fcd_batch: usize,
172    pub seed: u64,
173    /// Target sparsity (0..1). When >0, the best checkpoint must have
174    /// at least this fraction of neurons pruned; if none qualifies the
175    /// highest-sparsity checkpoint is used.
176    pub target_sparsity: f64,
177    /// L1 aggression multiplier: scales both l1_init and l1_step.
178    /// >1.0 = harder pruning push, <1.0 = softer.
179    pub l1_mult: f64,
180    /// Effective unlooped mask logit at step zero. The per-visit value is
181    /// solved so that the product over loop visits equals sigmoid(init).
182    /// 2.0 preserves the native recipe; 4.0 reproduces the older DTG-MA
183    /// trading notebooks' near-identity start.
184    pub mask_init: f32,
185    /// Penalize softplus(logit), whose derivative is sigmoid(logit), instead
186    /// of penalizing sigmoid(logit) itself. This reproduces the older DTG-MA
187    /// recipe and avoids an extra (1-sigmoid) attenuation near an open gate.
188    pub softplus_l1: bool,
189    /// Select Phase-A checkpoints by held-out hard balanced accuracy when
190    /// focused class tokens are configured. Otherwise held-out PPL remains
191    /// the checkpoint metric.
192    pub checkpoint_accuracy: bool,
193    /// Optional strict lower bound for a focused checkpoint's raw accuracy.
194    /// A checkpoint is eligible only when its measured accuracy is greater
195    /// than this value. This lets callers impose a natural-distribution
196    /// majority guard instead of selecting from a balanced holdout.
197    pub checkpoint_min_accuracy: Option<f64>,
198    /// Optional strict lower bound for focused balanced accuracy. Combined
199    /// with `checkpoint_min_accuracy`, this prevents a majority-only mask
200    /// from becoming the shipped specialist.
201    pub checkpoint_min_balanced_accuracy: Option<f64>,
202    /// When a joint accuracy guard is configured, rank eligible checkpoints
203    /// by raw accuracy first, then balanced accuracy and PPL. The historical
204    /// balanced-first selector remains the default for compatibility.
205    pub checkpoint_raw_priority: bool,
206    /// Round each layer's kept-neuron count UP to a multiple of this
207    /// (0/1 = off). 32 keeps the defragged FFN on grouped codecs
208    /// (in % 32 == 0) and SIMD kernels off their scalar tails.
209    pub align: usize,
210    /// Force one FFN width across all layers (the max aligned count) —
211    /// the whole-token GPU graphs require a uniform intermediate size.
212    pub uniform_inter: bool,
213    /// When non-empty, LM loss is accumulated only where the next token
214    /// is one of these ids. The whole chunk is still forwarded as context.
215    /// This is useful for supervised corpora with a long input and a
216    /// one-token answer, where ordinary all-token LM loss would drown the
217    /// task signal in prompt reconstruction.
218    pub focus_tokens: Vec<u32>,
219    /// Optional token(s) that must immediately follow a focused target.
220    /// Supervised ChatML uses the one-token label followed by `<|im_end|>`;
221    /// this prevents label names mentioned inside the user instruction from
222    /// being mistaken for answer positions.
223    pub focus_follow_tokens: Vec<u32>,
224    /// Score the assistant's answer span of each chat record instead of
225    /// every token (or one-token labels). Exclusive with `focus_tokens`.
226    pub answer_span: Option<AnswerSpan>,
227    /// Start Phase A from these mask logits (one row per virtual layer,
228    /// e.g. a previous run's archaeology `selected_logits`) instead of
229    /// the uniform init — so a mask trained once can be FCD-polished
230    /// again, or Phase A resumed, in a separate run (`--steps-a 0` keeps it).
231    pub mask_logits: Option<Vec<Vec<f32>>>,
232    /// Patent 2 Soft-Fold compilation: the neurons that survive τ keep
233    /// their LEARNED gain σ(m) (folded into down_proj by the writer) instead
234    /// of being snapped to 1. Evaluation and Phase B then run that exact
235    /// function (σ(m) above τ, 0 below), so what is scored is what ships.
236    pub soft_fold: bool,
237    /// Full-layer FCD: Phase B also trains the attention projections
238    /// (q/k/v/o, the projected head gate) and both norm gains of the FCD
239    /// layers — the notebook's "unfreeze the last N layers entirely".
240    pub fcd_attn: bool,
241    /// Write every Phase-A evaluation's mask logits to
242    /// `<dir>/phase-a-step-<n>.json` as it happens (a `--mask-from` input),
243    /// so a long run survives being stopped.
244    pub checkpoint_dir: Option<String>,
245    /// Tags of the held records (in held order before the length sort):
246    /// held scores then report per-tag verdict accuracy and gate on its
247    /// macro average.
248    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
288/// What the bake measured and produced.
289pub struct BakeReport {
290    /// Held-out PPL of the untouched backbone.
291    pub backbone: f64,
292    /// Held-out PPL with the best hard mask (the denoising bottom).
293    pub masked: f64,
294    /// Held-out PPL after FCD (the final specialist).
295    pub overlaid: f64,
296    pub pruned_ratio: f64,
297    pub kept_per_layer: Vec<usize>,
298    /// Hard focused-label accuracy, present when focus tokens were supplied.
299    pub backbone_accuracy: Option<f64>,
300    pub masked_accuracy: Option<f64>,
301    pub overlaid_accuracy: Option<f64>,
302    /// Macro recall over focused labels. Unlike raw accuracy this cannot be
303    /// improved by collapsing to the majority UP/DOWN class.
304    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    /// The mask logits at this evaluation — so any iteration's mask can
319    /// be picked after the run (`--mask-from` with a chosen checkpoint).
320    pub logits: Vec<Vec<f32>>,
321}
322
323/// Trained attention + norm masters of a full-layer FCD layer (f32).
324#[derive(Clone)]
325pub struct AttnMasters {
326    /// Fused `[qrows + 2·kvdim, hidden]` (q rows first, then k, then v).
327    pub wqkv: Vec<f32>,
328    /// `[hidden, nh·hd]`.
329    pub wo: Vec<f32>,
330    /// Per-head gate projection `[nh, hidden]` (Spark-X2.5), if the layer has one.
331    pub gproj: Option<Vec<f32>>,
332    pub iln: Vec<f32>,
333    pub pln: Vec<f32>,
334}
335
336/// Their gradients (f64 accumulators), same shapes.
337pub 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
357/// The trained artifacts: everything the defrag writer needs, f32.
358pub struct BakeArtifacts {
359    /// Per-PHYSICAL-layer live flags: the union over visits — a weight
360    /// row is removable from disk only when no visit keeps it.
361    pub keep: Vec<Vec<bool>>,
362    /// Per-VIRTUAL-layer live flags (physical × loops, pass-major): the
363    /// mask the file ships and the runtime applies per visit.
364    pub keep_visits: Vec<Vec<bool>>,
365    /// Per-layer down_proj `[hidden, inter]` with dead columns zeroed
366    /// (FCD layers: the trained weights; others: the backbone's).
367    pub down: Vec<Vec<f32>>,
368    /// Trained gate/up for the FCD layers (`None` elsewhere).
369    pub gate_up: Vec<Option<(Vec<f32>, Vec<f32>)>>,
370    /// Which layers went through Phase B.
371    pub fcd_layers: Vec<usize>,
372    /// Trained attention/norm masters (full-layer FCD), per layer.
373    pub attn: Vec<Option<AttnMasters>>,
374    /// The trained mask logits, per virtual layer — a CONTINUOUS
375    /// per-neuron importance the hard keep flags throw away. The tube
376    /// planner ranks and orders neurons by these, not by raw
377    /// activation mass.
378    pub logits: Vec<Vec<f32>>,
379    /// Phase-A logits after the last requested optimization step, before
380    /// restoring the selected hard-validation checkpoint.
381    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
390/// Plain Adam over a set of f32 tensors (masks are tiny, FFN mid-size).
391struct 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    /// Global-norm clip + Adam step. `params[i].len() == grads[i].len()`.
409    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
432/// Mask logit at step zero, solved for the loop depth.
433///
434/// The gate multiplies the FFN once per VISIT, so a Looped Transformer
435/// applies it `loops` times per token and the factor compounds. What
436/// must be held constant across depths is the EFFECTIVE start — the
437/// product the stack actually sees — at the value the recipe was
438/// validated with on ordinary models, σ(2.0) = 0.881:
439///
440/// ```text
441/// σ(m0)^loops = σ(2.0)   →   m0 = logit( σ(2.0)^(1/loops) )
442/// ```
443///
444/// `loops = 1` returns 2.0 exactly, so nothing regresses. Two known
445/// wrong answers this replaces: the old hardcoded 2.0, which at two
446/// visits compounds to 0.776 and took Nanbeige 4.2 from a baseline of
447/// 4.187 to 278.4 at step 30; and a start pushed to identity, which
448/// cannot learn because the update carries σ'(m) = σ(1−σ), worth 5e-4
449/// at σ = 0.9995 against 0.105 at 2.0.
450pub 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
460/// Learning-rate scale for the mask step, given the loop depth.
461///
462/// The backward accumulates every visit of a physical layer into the
463/// same mask gradient, so an unscaled step is `loops` times the tuned
464/// one. One step should mean one token's worth of movement at any depth.
465pub 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
493/// The scored target indices (into the flat batch) of the sequence at
494/// `base..base + t`: its answer span, its focused labels, or every
495/// position.
496fn 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
515/// One forward + CE(+optionally backward through the FFN chain).
516/// Returns (nll_sum, tokens). `dmask`/`dffn` accumulate when given.
517struct Pass<'a> {
518    fm: &'a FcdModel,
519    tau: f32,
520    /// σ(m) per layer when soft; binarized when `hard`.
521    logits: &'a [Vec<f32>],
522    hard: bool,
523    /// Phase-B replacement FFN weights per layer (trained copies).
524    ffn: &'a [Option<(Vec<f32>, Vec<f32>, Vec<f32>)>],
525    /// Empty means ordinary all-token LM loss.
526    focus_tokens: &'a [u32],
527    /// Empty means no right-context constraint on focused targets.
528    focus_follow_tokens: &'a [u32],
529    /// Answer-span scoring (see `AnswerSpan`); None = focus/all-token.
530    span: Option<&'a AnswerSpan>,
531    /// `hard` gates keep σ(m) above τ instead of snapping to 1 (Soft-Fold).
532    soft_fold: bool,
533    /// Full-layer FCD masters per layer (attention + norms); None = frozen.
534    attn_tr: &'a [Option<AttnMasters>],
535}
536
537/// Raw-pointer carriers for pooled row writes (disjoint ranges per worker).
538struct 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    /// Answer-span verdicts (`answer_values`) and those predicted whole.
564    questions: usize,
565    questions_correct: usize,
566    /// (verdicts, right) per sequence of the LAST scored call — the held
567    /// scorer attributes them to the records' tags; never merged.
568    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    /// The held score's accuracy pair: whole-verdict accuracy when the span
589    /// located values (then the token accuracy rides along for the log),
590    /// otherwise the token/class accuracy alone.
591    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    /// Whole-verdict accuracy in answer-span mode with located values,
630    /// else token (span) or class (focus) accuracy.
631    accuracy: Option<f64>,
632    balanced_accuracy: Option<f64>,
633    /// The span's token accuracy when `accuracy` is the verdict accuracy.
634    token_accuracy: Option<f64>,
635    /// Verdict accuracy over all questions when `accuracy` is the macro
636    /// over held tags (families); None otherwise.
637    micro_accuracy: Option<f64>,
638    /// (tag, verdicts, right) per held tag, in tag order.
639    per_tag: Vec<(String, usize, usize)>,
640}
641
642/// Held records' tags (e.g. the benchmark family of each validation
643/// record): the names, and one name index per held record.
644#[derive(Clone, Debug, Default)]
645pub struct HeldTags {
646    pub names: Vec<String>,
647    pub of: Vec<usize>,
648}
649
650/// " | <tag> 61.0 (41)" per tag — the macro gate's ingredients.
651fn 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/// Frozen boundary immediately before one trainable final FFN.  This is not
668/// an adapter or a donor checkpoint: both vectors are extracted directly from
669/// the opened CMF, kept in RAM, and discarded when the native bake ends.
670#[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                // Trained copies move every Adam step — no prebuilt concat.
730                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    /// Extract the exact frozen boundary of the final FFN for focused answer
744    /// positions.  Only the ordinary one-pass/final-layer case is cacheable:
745    /// looped stacks revisit the same FFN after its own changed output and
746    /// correctly fall back to the full Phase-B path.
747    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    /// Teacher-forced NLL over one chunk; when `grad` is set, backprop
821    /// through the FFN chain into the mask grads (and FFN grads for
822    /// Phase-B layers).
823    #[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    /// `chunk` over `b` equal-length sequences flattened into `ids`.
837    ///
838    /// Everything under this level was batch-aware all along
839    /// (`layer_forward_scaled` and every attention fwd/bwd take `b`);
840    /// only this wrapper hardcoded 1. Evaluation is where it pays: the
841    /// held set is 12 chunks scored one at a time, which on a 4 B model
842    /// meant 12× the GEMM submits for the same arithmetic.
843    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        // Embed.
875        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        // Forward over VIRTUAL layers: a Looped Transformer runs the
881        // stack `fm.loops` times, with a final_norm at each loop
882        // boundary when the file says so. Everything below indexes
883        // activations by the virtual step and weights/grads by the
884        // physical layer `vl % nl` — so a physical layer visited twice
885        // accumulates both visits' gradients, which is what the loop
886        // means mathematically.
887        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        // Loop-boundary norms, saved for the backward: (input, inv).
893        let mut lnorms: Vec<Option<(Vec<f32>, Vec<f32>)>> = vec![None; vn];
894        for vl in 0..vn {
895            let li = vl % nl;
896            // The gate is PER VISIT: the two passes of a loop are
897            // different computations sharing one set of weights, so the
898            // mask must be allowed to differ between them. Weights stay
899            // indexed by the physical layer.
900            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            // A frozen layer's backward runs on the card from the plane the
905            // forward parks there; only a trained (Phase-B) layer needs its
906            // gate/up/act planes on the host.
907            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            // Mid-stack norm at every loop boundary except the last —
931            // the final one folds into the head below.
932            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        // Final norm + tied LM head, CE summed over positions 1..t.
943        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]; // dL/d hn
950        // Chunk the vocab matmul over positions to bound the logits buf.
951        // Positions are walked PER SEQUENCE: the last position of chunk
952        // i must not be scored against the first token of chunk i+1.
953        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            // Ordinary language-model mode: the complete vocabulary
972            // distribution at every scored target — every position, or
973            // the answer span of each record. The scored rows are gathered
974            // so a span pays for its own positions only, not the prompt's.
975            let inv_n = 1.0 / scored as f64;
976            // Argmax hit per scored target, for the whole-verdict count.
977            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            // CMF_BAKE_QDEBUG=N prints gold vs argmax of the first N verdicts
981            // of every held call (diagnosing the verdict locator).
982            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                    // Teacher-forced token accuracy of the answer span — the
1009                    // one number a verdict corpus is really graded on.
1010                    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                        // dCE/dlogit = softmax − onehot, scaled by 1/scored.
1024                        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            // Whole verdicts: a question is right when every token of its
1045            // value is the argmax (what greedy decoding emits after the
1046            // same prefix).
1047            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            // Exact classifier mode. Only the declared label rows can enter
1080            // either the normalizer or its gradient, so multiplying every
1081            // hidden by the full ~250k-row LM head is pure wasted work. It
1082            // made a two-label archaeology run spend more time in the head
1083            // than in mask Adam and limited experiments to a tiny fraction
1084            // of the updates used by the original notebooks.
1085            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        // Backward: final norm, then the FFN chain layer by layer.
1158        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            // Undo the loop-boundary norm this step fed into.
1164            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                    // Phase A on the card: dact = dh·down, the mask gradient
1180                    // from the resident plane, dgu through the act link
1181                    // with the gate folded in, dn2 = dgu·gu. Only dn2 and
1182                    // dm[inter] come back.
1183                    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                // h2 = h1 + act2 @ downᵀ  →  dact2 = dh @ down.
1200                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                    // dW_down += dhᵀ · act2 (act2 = act·g).
1204                    let mut act2 = a.act.clone();
1205                    {
1206                        let ap = SendMutF(act2.as_mut_ptr());
1207                        pooled_rows(n, pool, &|lo, hi| {
1208                            // SAFETY: disjoint row ranges per worker.
1209                            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                // Mask grad: dm = Σ_t dact2·act · σ'(m)  (soft; STE-equal).
1224                // Indexed by the VIRTUAL layer: each visit's mask row gets
1225                // exactly its own visit's gradient, no cross-visit sum.
1226                {
1227                    let dm = &mut dmask[vl];
1228                    // Parallel over neuron ranges: each worker owns a column
1229                    // span of dm and walks every row for it.
1230                    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                        // SAFETY: disjoint column ranges per worker.
1235                        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                    // σ'(m) folded in once per chunk (constant per neuron).
1245                    for (j, d) in dm.iter_mut().enumerate() {
1246                        let _ = j;
1247                        let _ = d;
1248                    }
1249                }
1250                // dact = dact2 · g;  act·mul backward (pooled).
1251                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                // dn2 = dg_pre @ gate + du_pre @ up — one fused submit when
1266                // the frozen concat exists; the trained-copy path keeps two.
1267                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                                // SAFETY: disjoint rows per worker.
1275                                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            // Post-norm backward into h1; a frozen attention branch carries
1320            // no gradient, so dh1 flows straight to dh_in.
1321            let mut dh1 = dh.clone(); // residual h2 = h1 + ffn
1322            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                // Full-layer FCD: through the trained attention — post-norm
1325                // gain, o_proj, the head gate, the attention core (device),
1326                // RoPE, q/k/v projections, the input norm — into dh_in.
1327                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
1339/// dw[j] = Σ_r dy[r,j]·x[r,j]·inv[r] — the RMSNorm gain gradient (Qwen
1340/// style), pooled over column spans.
1341fn 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        // SAFETY: disjoint column spans per worker.
1348        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    /// The attention branch's backward for a full-layer FCD layer: from
1360    /// `dattn` (the gradient at attn_out) to the gradient at h_in through
1361    /// the attention path, accumulating the trained weights' gradients.
1362    #[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        // n1: the normed input the projections read (recomputed from h_in).
1385        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        // ao_eff = ao ⊙ gain; dWo += dattnᵀ·ao_eff; dao_eff = dattn·Wo.
1389        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        // Head gate: dao = dao_eff·gain; dgain = Σ_c dao_eff·ao; dgraw = dgain·σ′.
1410        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        // Attention core: dq/dk (rotated space), dv — device chain, host fallback.
1434        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        // RoPE backward (no qk-norm on these layers), per head.
1478        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        // dqkv = [dq | dk | dv]; dWqkv += dqkvᵀ·n1; dn1 += dqkv·Wqkv.
1491        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        // Input norm: gain gradient + through-grad into h_in.
1512        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
1519/// Held-out PPL with the hard mask (and Phase-B weights when present).
1520/// The device chains' phase timers (0 weights, 1 submit→poll, 2 copies);
1521/// zero without the gpu feature.
1522fn 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
1534/// " | acc X% bal Y%" (focused classes), " | tok-acc X%" (answer span),
1535/// or nothing (all-token LM).
1536fn 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
1561/// `held_score` with per-tag verdict counts: when the records carry tags
1562/// and the span locates values, the score's accuracy is the MACRO verdict
1563/// accuracy over tags (each family weighs the same, as in a benchmark
1564/// index), the micro one rides along.
1565fn held_score_tagged(pass: &Pass, held: &[Vec<u32>], tags: Option<&HeldTags>) -> HeldScore {
1566    // Bounded batched passes over the held set: equal-length records share
1567    // one GEMM per weight, while the cap keeps activation/GPU scratch memory
1568    // predictable for a full validation sweep. Aggregation is exact because
1569    // NLL and focused-class counts are additive across groups.
1570    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    // Up to 32 records per pass, bounded by tokens: 32 x 3072-token chat
1584    // records made a 98k-row pass whose gate+up plane (n x 2*inter f32)
1585    // crossed the 2 GiB device binding limit and panicked wgpu.
1586    // 8k tokens keeps a 4 B model's gate+up plane (n × 20480 f32) under
1587    // the 2 GiB binding ceiling once the scratch rounds up; CMF_BAKE_EVAL_TOKENS
1588    // overrides.
1589    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
1667/// The calibration records as runs of equal length, each cut into blocks
1668/// of at most `batch` records: `(start, count)`. Records are expected
1669/// sorted by length (stable, so the shuffle within a length survives);
1670/// the batch at `step` is `blocks[step % blocks.len()]`.
1671fn 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    // Equal-length records only: with length buckets the records are
1694    // sorted by length and the blocks never mix widths.
1695    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
1728/// Exact final-FFN focused loss, optionally with gradients for the three full
1729/// FCD projections.  The upstream representation came from the same CMF and
1730/// mask in `build_focused_fcd_cache`; no external checkpoint format is used.
1731fn 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
1926/// Score a WRITTEN specialist through the replica's own math (f32
1927/// dequant of whatever the file carries) with the file's binary mask
1928/// held hard — the decomposition probe that tells "the requant at write
1929/// cost the quality" from "the runtime applies the mask differently".
1930/// Returns (bare, masked) held-PPL over the chunks.
1931pub 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    // Binary mask → logits at ±50: σ crosses any τ exactly as the bit says.
1950    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
1993/// The whole recipe. `log` receives progress lines.
1994pub 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    // The bake's backward never enters attention (frozen, residual
2011    // through-path only), so the attention activations are dead weight:
2012    // on the device chain they were ~700 MB of PCIe readback per layer.
2013    fm.attn_acts = false;
2014    let nl = fm.layers.len();
2015    // Full-layer FCD masters (attention + norms), filled before Phase B;
2016    // every Pass carries the slice so a trained layer's forward reads them.
2017    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    // Stable sorts by length: with --length-buckets the batches and the
2020    // held-out groups stay homogeneous while the shuffle within a length
2021    // survives; without buckets every record has the chunk width and
2022    // nothing moves.
2023    let mut held: Vec<Vec<u32>> = chunks[..held_n.min(chunks.len())].to_vec();
2024    // The tags follow their records through the stable length sort.
2025    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    // Analysis-only callers can request a native, batched focused score over
2041    // a validation corpus without training or writing a specialist.  The
2042    // scoring path already handles per-layer FFN widths; the optimizer and
2043    // defragment writer below still require one common width, so return the
2044    // identity checkpoint before that training-only constraint is applied.
2045    // (A supplied mask is not an identity run: it goes through the scored
2046    // path below so the mask is evaluated and written.)
2047    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    // Trainables. The gate starts as close to OPEN as the arithmetic
2112    // allows, because step zero must be the backbone and nothing else.
2113    //
2114    // It used to start at 2.0, and σ(2.0) = 0.881 — every FFN neuron
2115    // scaled to seven eighths before a single gradient. On an ordinary
2116    // stack that costs a few percent of perplexity and hides. On a
2117    // LOOPED Transformer it does not: Nanbeige 4.2 runs its 22 layers
2118    // twice, so each physical FFN is visited twice and the factor
2119    // compounds to 0.881² = 0.776 per layer over 44 visits. Measured:
2120    // baseline 4.187 → 278.4 at step 30 with 0% pruned. Nothing had been
2121    // pruned; the mask had simply turned the model down.
2122    //
2123    // So solve for the init instead of hardcoding it — but solve for the
2124    // right target. Pushing σ(m0)^loops to 0.999 starts at the backbone
2125    // and cannot move: the gradient carries σ'(m) = σ(1−σ), which at
2126    // σ = 0.9995 is 5e-4 against 0.105 at the old 2.0, and BOTH the data
2127    // term and the L1 term are scaled by it (see the update below). That
2128    // was tried: 60 steps, 0% pruned, hard-PPL equal to the baseline to
2129    // three digits. Identity that cannot learn is not an improvement.
2130    //
2131    // The quantity to preserve is the EFFECTIVE start — what the stack
2132    // actually multiplies by, once per visit compounded over the loop —
2133    // at the value the recipe was validated with on ordinary models:
2134    // σ(m0)^loops = σ(2.0) = 0.881. One loop reproduces the old constant
2135    // exactly, so nothing regresses; two loops open the per-visit gate to
2136    // 0.9385 so the compounded factor is again 0.881, with σ' = 0.058
2137    // rather than 0.0005.
2138    let loops = fm.loops.max(1);
2139    let m0 = mask_init_logit_for(loops, hy.mask_init);
2140    // One mask row per VIRTUAL layer: nl × loops. Unlooped: vn == nl.
2141    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    // Baseline (no mask): even σ(m0) is not exactly 1, so measure with
2159    // gates forced open via hard mask over +∞… simplest: logits +50.
2160    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    // CMF_BAKE_SKIP_BASELINE=1 with a supplied mask: a checkpoint
2174    // measurement that already knows the backbone's score skips its pass.
2175    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    // A resumed mask is scored before it trains on: the run's own start,
2198    // and with --steps-a 0 a held-out measurement of any checkpoint.
2199    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    // ── Phase A: mask training ──
2220    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    // best = (ppl, logits_snapshot, sparsity)
2224    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    // Track the highest-sparsity checkpoint as fallback.
2230    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    // In-process phase timers: this loop was estimated three different
2234    // ways and every estimate came out under a fifth of the measured
2235    // step time. Measure, then optimize the top line, not the guess.
2236    let mut acc_chunk = 0f64;
2237    let mut acc_adam = 0f64;
2238    // Phase A trains in strict f32: the mask SELECTS neurons by its
2239    // gradient, and f16 operand rounding on that signal — fine for every
2240    // forward and eval in this file — compounds over ~90 steps into
2241    // closing the wrong neurons (measured: hard-PPL 5.207 vs 4.293 at the
2242    // same 2.56% sparsity; the f32 run retraces the reference trajectory
2243    // to the third decimal). Evals inside the loop lift the restriction —
2244    // their tensor-core numbers match f32 at print precision.
2245    // CMF_BAKE_A_STRICT=0 lifts it: Phase A then rides the tensor-core
2246    // chains like Phase B (tf32-class operands) — the choice of a bake
2247    // that measures its mask on the held-out set anyway and cannot
2248    // afford a CPU-only phase on a 4 B model.
2249    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        // Fold σ'(m) into the mask grads + add the L1 term.
2273        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        // One gradient per VISIT, one step per token. A Looped
2282        // Transformer visits each physical layer `loops` times and the
2283        // backward accumulates every visit into the same mask, so an
2284        // unnormalised step is `loops` times the one the recipe was
2285        // tuned with — the mask overshoots, neurons cross tau within the
2286        // first evaluation window, and each of them is then missing from
2287        // both passes. Dividing by the visit count makes a step mean the
2288        // same thing at any loop depth.
2289        // Per-visit rows: each mask logit receives exactly one visit's
2290        // gradient, so the step needs no visit normalisation here — that
2291        // scale now belongs to Phase B alone, where the FFN weights ARE
2292        // shared across visits.
2293        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            // Name the neurons that crossed τ since the last eval. At
2317            // 0.01% pruned = ~24 neurons for a 135 held-PPL, WHICH 24 is
2318            // the whole diagnosis: it decides between "this model has no
2319            // noise neurons" and "a shared mask cannot spare a neuron
2320            // that only one visit of the loop needs".
2321            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            // Track highest-sparsity checkpoint.
2351            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            // Best checkpoint selection: respect target_sparsity and any
2387            // caller-declared natural-distribution quality guards. The
2388            // guards are strict (`>`, not `>=`) so a majority baseline cannot
2389            // sneak through on an exactly tied checkpoint.
2390            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                // Answer-span mode: the held-out accuracy of the hard mask
2424                // decides — whole verdicts when the span locates values,
2425                // else span tokens (the experiments' accuracy gate); PPL
2426                // breaks ties.
2427                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    // If target_sparsity was set but no checkpoint qualified, fall back
2457    // to the highest-sparsity checkpoint.
2458    // Phase A is over — phase B and every eval after run on the fast arms.
2459    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    // Phase totals, printed unconditionally — a 5-step measurement run
2477    // must report even though no eval fired.
2478    {
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    // With no mask-training steps the hard mask is still exactly all-open,
2523    // so rescoring the same validation batch is pure duplicate work.  This
2524    // matters for focused 512-token records where one gate can take minutes
2525    // on a laptop, and keeps short FCD sweeps practical without changing a
2526    // single number.
2527    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    // ── Phase B: FCD of the last N layers' FFN (hard mask active) ──
2540    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    // The mask-only model is a real checkpoint too. If every FCD eval is
2581    // worse, restore `None` overlays rather than accidentally writing the
2582    // final (rejected) training step while reporting the mask-only PPL.
2583    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            // Cosine LR.
2744            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            // Same visit normalisation as Phase A: dffn accumulates every
2782            // visit of a physical layer, and an FFN update perturbs BOTH
2783            // passes of the loop, so per-step damage is `loops` times what
2784            // lr_b was tuned for on ordinary stacks.
2785            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                // CMF_BAKE_B_TAKE_LAST=1: the correction the recipe prescribes
2816                // (Patent 2 §4.5: one light FCD step) ships as trained — a
2817                // held set of a few verdicts per family cannot veto it on noise;
2818                // its score is still logged.
2819                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    // ── Export artifacts ──
2843    // Per-visit keep flags are the mask that ships; the PHYSICAL keep is
2844    // their union, because a weight row can only be removed from disk if
2845    // no visit needs it.
2846    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        // Compare like with like: raw σ-counts are over the VIRTUAL
2860        // rows, so the padded count must be too — the union rows are
2861        // fewer and the subtraction would underflow.
2862        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
2926/// Hard-threshold keep masks from the trained logits, then resurrect
2927/// the highest-logit pruned neurons until each layer's kept count is a
2928/// multiple of `align` (rounding UP — the resurrected neurons are the
2929/// ones the mask ranked closest to the threshold, so this only moves
2930/// toward the full backbone). `uniform` additionally raises every layer
2931/// to the max layer's aligned count. A layer with 0 live neurons gets
2932/// `align.max(1)` — the defrag writer rejects empty layers.
2933fn 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        // DOWN and UP occur in the instruction, but only the final UP is an
2980        // assistant answer because it is immediately followed by im_end.
2981        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        // {"q1": "option_24", "q2": [ "a" ], "q3": 3}<end>, split the way a
3014        // byte-level BPE might: quotes merged with keys and labels.
3015        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        // A run of equal lengths is cut into blocks of at most `batch`;
3085        // the tail block is smaller, and the schedule wraps.
3086        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        // Mixed lengths never share a block (length buckets).
3092        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    /// align=32 rounds each layer UP by resurrecting the largest
3097    /// pruned logits; the originally-alive set stays alive.
3098    #[test]
3099    fn keep_masks_aligns_up_and_preserves_alive() {
3100        let inter = 96;
3101        // Layer 0: 40 alive (logits > 0 → σ > 0.5), the rest ramp
3102        // below threshold so resurrection order is deterministic.
3103        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        // Layer 1: 64 alive — already aligned, must stay exactly 64.
3107        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        // The 40 originally-alive stay; resurrected are the top pruned
3113        // logits (indices 40..64 — the least-negative of the ramp).
3114        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    /// uniform=true raises every layer to the max aligned count.
3123    #[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    /// align capped at inter; align=1 (off) keeps the raw threshold
3137    /// count; an all-pruned layer still keeps at least one neuron.
3138    #[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]); // 47 → 64 capped to 48
3146        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]); // max(1) → rounded to 32
3151    }
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        // prompt … [7 8] answer 20 21 [1] pad 1 1
3166        let ids = [5, 7, 8, 9, 7, 8, 20, 21, 1, 1, 1];
3167        assert_eq!(answer_targets(&ids, &sp), Some((6, 8)));
3168        // No terminator: the span runs to the end of the record.
3169        let ids = [5, 7, 8, 20, 21];
3170        assert_eq!(answer_targets(&ids, &sp), Some((3, 4)));
3171        // No prompt, or a prompt with nothing after it: unscored.
3172        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}