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::{FcdModel, LnFfn};
22use crate::fcd_ops as ops;
23use crate::sampler::SplitMix64;
24use cortiq_core::CmfModel;
25use std::sync::Arc;
26
27/// Hyper-parameters — defaults are the certified recipe.
28#[derive(Clone, Debug)]
29pub struct BakeHyper {
30    pub steps_a: usize,
31    pub steps_b: usize,
32    pub l1_init: f64,
33    pub l1_step: f64,
34    pub eval_every: usize,
35    pub lr_a: f64,
36    pub lr_b: f64,
37    pub tau: f32,
38    pub fcd_layers: usize,
39    pub seed: u64,
40    /// Target sparsity (0..1). When >0, the best checkpoint must have
41    /// at least this fraction of neurons pruned; if none qualifies the
42    /// highest-sparsity checkpoint is used.
43    pub target_sparsity: f64,
44    /// L1 aggression multiplier: scales both l1_init and l1_step.
45    /// >1.0 = harder pruning push, <1.0 = softer.
46    pub l1_mult: f64,
47    /// Round each layer's kept-neuron count UP to a multiple of this
48    /// (0/1 = off). 32 keeps the defragged FFN on grouped codecs
49    /// (in % 32 == 0) and SIMD kernels off their scalar tails.
50    pub align: usize,
51    /// Force one FFN width across all layers (the max aligned count) —
52    /// the whole-token GPU graphs require a uniform intermediate size.
53    pub uniform_inter: bool,
54    /// When non-empty, LM loss is accumulated only where the next token
55    /// is one of these ids. The whole chunk is still forwarded as context.
56    /// This is useful for supervised corpora with a long input and a
57    /// one-token answer, where ordinary all-token LM loss would drown the
58    /// task signal in prompt reconstruction.
59    pub focus_tokens: Vec<u32>,
60    /// Optional token(s) that must immediately follow a focused target.
61    /// Supervised ChatML uses the one-token label followed by `<|im_end|>`;
62    /// this prevents label names mentioned inside the user instruction from
63    /// being mistaken for answer positions.
64    pub focus_follow_tokens: Vec<u32>,
65}
66
67impl Default for BakeHyper {
68    fn default() -> Self {
69        Self {
70            steps_a: 240,
71            steps_b: 120,
72            l1_init: 0.01,
73            l1_step: 0.005,
74            eval_every: 30,
75            lr_a: 0.1,
76            lr_b: 1e-5,
77            tau: 0.5,
78            fcd_layers: 4,
79            seed: 0,
80            target_sparsity: 0.0,
81            l1_mult: 1.0,
82            align: 32,
83            uniform_inter: false,
84            focus_tokens: Vec::new(),
85            focus_follow_tokens: Vec::new(),
86        }
87    }
88}
89
90/// What the bake measured and produced.
91pub struct BakeReport {
92    /// Held-out PPL of the untouched backbone.
93    pub backbone: f64,
94    /// Held-out PPL with the best hard mask (the denoising bottom).
95    pub masked: f64,
96    /// Held-out PPL after FCD (the final specialist).
97    pub overlaid: f64,
98    pub pruned_ratio: f64,
99    pub kept_per_layer: Vec<usize>,
100    pub sec: f64,
101}
102
103/// The trained artifacts: everything the defrag writer needs, f32.
104pub struct BakeArtifacts {
105    /// Per-PHYSICAL-layer live flags: the union over visits — a weight
106    /// row is removable from disk only when no visit keeps it.
107    pub keep: Vec<Vec<bool>>,
108    /// Per-VIRTUAL-layer live flags (physical × loops, pass-major): the
109    /// mask the file ships and the runtime applies per visit.
110    pub keep_visits: Vec<Vec<bool>>,
111    /// Per-layer down_proj `[hidden, inter]` with dead columns zeroed
112    /// (FCD layers: the trained weights; others: the backbone's).
113    pub down: Vec<Vec<f32>>,
114    /// Trained gate/up for the FCD layers (`None` elsewhere).
115    pub gate_up: Vec<Option<(Vec<f32>, Vec<f32>)>>,
116    /// Which layers went through Phase B.
117    pub fcd_layers: Vec<usize>,
118    /// The trained mask logits, per virtual layer — a CONTINUOUS
119    /// per-neuron importance the hard keep flags throw away. The tube
120    /// planner ranks and orders neurons by these, not by raw
121    /// activation mass.
122    pub logits: Vec<Vec<f32>>,
123}
124
125const CLIP: f64 = 1.0;
126const B1: f64 = 0.9;
127const B2: f64 = 0.999;
128const EPS: f64 = 1e-8;
129
130/// Plain Adam over a set of f32 tensors (masks are tiny, FFN mid-size).
131struct Adam {
132    m: Vec<Vec<f64>>,
133    v: Vec<Vec<f64>>,
134    t: i32,
135    lr: f64,
136}
137
138impl Adam {
139    fn new(sizes: &[usize], lr: f64) -> Self {
140        Self {
141            m: sizes.iter().map(|&n| vec![0.0; n]).collect(),
142            v: sizes.iter().map(|&n| vec![0.0; n]).collect(),
143            t: 0,
144            lr,
145        }
146    }
147
148    /// Global-norm clip + Adam step. `params[i].len() == grads[i].len()`.
149    fn step(&mut self, params: &mut [&mut [f32]], grads: &[Vec<f64>], lr_scale: f64) {
150        let gn: f64 = grads
151            .iter()
152            .flat_map(|g| g.iter().map(|x| x * x))
153            .sum::<f64>()
154            .sqrt();
155        let clip = if gn > CLIP { CLIP / gn } else { 1.0 };
156        self.t += 1;
157        let (bc1, bc2) = (1.0 - B1.powi(self.t), 1.0 - B2.powi(self.t));
158        for (pi, p) in params.iter_mut().enumerate() {
159            for j in 0..p.len() {
160                let g = grads[pi][j] * clip;
161                let m = &mut self.m[pi][j];
162                let v = &mut self.v[pi][j];
163                *m = B1 * *m + (1.0 - B1) * g;
164                *v = B2 * *v + (1.0 - B2) * g * g;
165                let upd = (*m / bc1) / ((*v / bc2).sqrt() + EPS);
166                p[j] -= (self.lr * lr_scale * upd) as f32;
167            }
168        }
169    }
170}
171
172/// Mask logit at step zero, solved for the loop depth.
173///
174/// The gate multiplies the FFN once per VISIT, so a Looped Transformer
175/// applies it `loops` times per token and the factor compounds. What
176/// must be held constant across depths is the EFFECTIVE start — the
177/// product the stack actually sees — at the value the recipe was
178/// validated with on ordinary models, σ(2.0) = 0.881:
179///
180/// ```text
181/// σ(m0)^loops = σ(2.0)   →   m0 = logit( σ(2.0)^(1/loops) )
182/// ```
183///
184/// `loops = 1` returns 2.0 exactly, so nothing regresses. Two known
185/// wrong answers this replaces: the old hardcoded 2.0, which at two
186/// visits compounds to 0.776 and took Nanbeige 4.2 from a baseline of
187/// 4.187 to 278.4 at step 30; and a start pushed to identity, which
188/// cannot learn because the update carries σ'(m) = σ(1−σ), worth 5e-4
189/// at σ = 0.9995 against 0.105 at 2.0.
190pub fn mask_init_logit(loops: usize) -> f32 {
191    let base = 1.0f32 / (1.0 + (-2.0f32).exp());
192    let per_visit = base.powf(1.0 / loops.max(1) as f32);
193    (per_visit / (1.0 - per_visit)).ln()
194}
195
196/// Learning-rate scale for the mask step, given the loop depth.
197///
198/// The backward accumulates every visit of a physical layer into the
199/// same mask gradient, so an unscaled step is `loops` times the tuned
200/// one. One step should mean one token's worth of movement at any depth.
201pub fn mask_step_scale(loops: usize) -> f64 {
202    1.0 / loops.max(1) as f64
203}
204
205fn sigmoid(x: f32) -> f32 {
206    1.0 / (1.0 + (-x).exp())
207}
208
209fn is_scored_target(
210    ids: &[u32],
211    target_index: usize,
212    sequence_end: usize,
213    focus: &[u32],
214    follow: &[u32],
215) -> bool {
216    if focus.is_empty() {
217        return true;
218    }
219    focus.contains(&ids[target_index])
220        && (follow.is_empty()
221            || (target_index + 1 < sequence_end && follow.contains(&ids[target_index + 1])))
222}
223
224/// One forward + CE(+optionally backward through the FFN chain).
225/// Returns (nll_sum, tokens). `dmask`/`dffn` accumulate when given.
226struct Pass<'a> {
227    fm: &'a FcdModel,
228    tau: f32,
229    /// σ(m) per layer when soft; binarized when `hard`.
230    logits: &'a [Vec<f32>],
231    hard: bool,
232    /// Phase-B replacement FFN weights per layer (trained copies).
233    ffn: &'a [Option<(Vec<f32>, Vec<f32>, Vec<f32>)>],
234    /// Empty means ordinary all-token LM loss.
235    focus_tokens: &'a [u32],
236    /// Empty means no right-context constraint on focused targets.
237    focus_follow_tokens: &'a [u32],
238}
239
240impl Pass<'_> {
241    fn gates(&self, li: usize) -> Vec<f32> {
242        self.logits[li]
243            .iter()
244            .map(|&l| {
245                let s = sigmoid(l);
246                if self.hard {
247                    if s > self.tau { 1.0 } else { 0.0 }
248                } else {
249                    s
250                }
251            })
252            .collect()
253    }
254
255    fn wts<'b>(&'b self, li: usize, mats: &'b crate::fcd::LayerMats) -> LnFfn<'b> {
256        let l = &self.fm.layers[li];
257        match &self.ffn[li] {
258            Some((g, u, d)) => LnFfn {
259                iln: &l.iln,
260                pln: &l.pln,
261                gate: g,
262                up: u,
263                down: d,
264                // Trained copies move every Adam step — no prebuilt concat.
265                gu: None,
266            },
267            None => LnFfn {
268                iln: &l.iln,
269                pln: &l.pln,
270                gate: &[],
271                up: &[],
272                down: &mats.down,
273                gu: Some(&mats.gu),
274            },
275        }
276    }
277
278    /// Teacher-forced NLL over one chunk; when `grad` is set, backprop
279    /// through the FFN chain into the mask grads (and FFN grads for
280    /// Phase-B layers).
281    #[allow(clippy::too_many_arguments)]
282    fn chunk(
283        &self,
284        ids: &[u32],
285        grad: Option<(
286            &mut [Vec<f64>],
287            &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>],
288        )>,
289    ) -> (f64, usize) {
290        self.chunk_batch(ids, 1, grad)
291    }
292
293    /// `chunk` over `b` equal-length sequences flattened into `ids`.
294    ///
295    /// Everything under this level was batch-aware all along
296    /// (`layer_forward_scaled` and every attention fwd/bwd take `b`);
297    /// only this wrapper hardcoded 1. Evaluation is where it pays: the
298    /// held set is 12 chunks scored one at a time, which on a 4 B model
299    /// meant 12× the GEMM submits for the same arithmetic.
300    fn chunk_batch(
301        &self,
302        ids: &[u32],
303        b: usize,
304        grad: Option<(
305            &mut [Vec<f64>],
306            &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>],
307        )>,
308    ) -> (f64, usize) {
309        let fm = self.fm;
310        let hsz = fm.hidden;
311        debug_assert!(ids.len() % b.max(1) == 0, "ragged batch");
312        // Training differentiates one chunk at a time — the recipe's
313        // gradient accumulation is per-chunk by design.
314        debug_assert!(grad.is_none() || b == 1, "grads are per-chunk");
315        let t = ids.len() / b.max(1);
316        let n = b * t;
317        let nl = fm.layers.len();
318        // Embed.
319        let mut h = vec![0f32; n * hsz];
320        for (r, &id) in ids.iter().enumerate() {
321            h[r * hsz..(r + 1) * hsz]
322                .copy_from_slice(&fm.embed[id as usize * hsz..(id as usize + 1) * hsz]);
323        }
324        // Forward over VIRTUAL layers: a Looped Transformer runs the
325        // stack `fm.loops` times, with a final_norm at each loop
326        // boundary when the file says so. Everything below indexes
327        // activations by the virtual step and weights/grads by the
328        // physical layer `vl % nl` — so a physical layer visited twice
329        // accumulates both visits' gradients, which is what the loop
330        // means mathematically.
331        let loops = fm.loops.max(1);
332        let vn = nl * loops;
333        let mut h_ins = Vec::with_capacity(vn);
334        let mut acts = Vec::with_capacity(vn);
335        let mut masks = Vec::with_capacity(vn);
336        // Loop-boundary norms, saved for the backward: (input, inv).
337        let mut lnorms: Vec<Option<(Vec<f32>, Vec<f32>)>> = vec![None; vn];
338        for vl in 0..vn {
339            let li = vl % nl;
340            // The gate is PER VISIT: the two passes of a loop are
341            // different computations sharing one set of weights, so the
342            // mask must be allowed to differ between them. Weights stay
343            // indexed by the physical layer.
344            let g = self.gates(vl);
345            let mats_hold = fm.mats(li).expect("layer mats");
346            let wts = self.wts(li, &mats_hold);
347            let want = grad.is_some();
348            let (h2, a) = fm.layer_forward_scaled(li, &h, b, t, &wts, false, want, Some(&g));
349            h_ins.push(if want { h } else { Vec::new() });
350            acts.push(a);
351            masks.push(g);
352            h = h2;
353            // Mid-stack norm at every loop boundary except the last —
354            // the final one folds into the head below.
355            if fm.loop_norm && li + 1 == nl && vl + 1 < vn {
356                let mut hn = vec![0f32; n * hsz];
357                let mut inv = vec![0f32; n];
358                ops::rmsnorm_fwd(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv);
359                if want {
360                    lnorms[vl] = Some((h, inv));
361                }
362                h = hn;
363            }
364        }
365        // Final norm + tied LM head, CE summed over positions 1..t.
366        let mut hn = vec![0f32; n * hsz];
367        let mut inv = vec![0f32; n];
368        ops::rmsnorm_fwd(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv);
369        let lm: &[f32] = fm.lm_head.as_deref().unwrap_or(&fm.embed);
370        let vocab = lm.len() / hsz;
371        let pool = fm.pool.as_deref();
372        let mut nll = 0f64;
373        let mut dh_n = vec![0f32; n * hsz]; // dL/d hn
374        // Chunk the vocab matmul over positions to bound the logits buf.
375        // Positions are walked PER SEQUENCE: the last position of chunk
376        // i must not be scored against the first token of chunk i+1.
377        const POS_CHUNK: usize = 64;
378        let scored = (0..b)
379            .map(|bi| {
380                let base = bi * t;
381                (base + 1..base + t)
382                    .filter(|&target_index| {
383                        is_scored_target(
384                            ids,
385                            target_index,
386                            base + t,
387                            self.focus_tokens,
388                            self.focus_follow_tokens,
389                        )
390                    })
391                    .count()
392            })
393            .sum::<usize>();
394        if scored == 0 {
395            return (0.0, 0);
396        }
397        for bi in 0..b {
398            let base = bi * t;
399            let mut p0 = 0usize;
400            while p0 < t - 1 {
401                let pc = POS_CHUNK.min(t - 1 - p0);
402                let mut logits = vec![0f32; pc * vocab];
403                ops::gemm_nt(
404                    &hn[(base + p0) * hsz..(base + p0 + pc) * hsz],
405                    lm,
406                    &mut logits,
407                    pc,
408                    hsz,
409                    vocab,
410                    pool,
411                );
412                for r in 0..pc {
413                    let target_index = base + p0 + r + 1;
414                    let target_id = ids[target_index];
415                    let row = &mut logits[r * vocab..(r + 1) * vocab];
416                    if !is_scored_target(
417                        ids,
418                        target_index,
419                        base + t,
420                        self.focus_tokens,
421                        self.focus_follow_tokens,
422                    ) {
423                        if grad.is_some() {
424                            row.fill(0.0);
425                        }
426                        continue;
427                    }
428                    let target = target_id as usize;
429                    if self.focus_tokens.is_empty() {
430                        let mx = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64;
431                        let mut sum = 0f64;
432                        for v in row.iter() {
433                            sum += ((*v as f64) - mx).exp();
434                        }
435                        nll += mx + sum.ln() - row[target] as f64;
436                        if grad.is_some() {
437                            // dCE/dlogit = softmax − onehot, scaled by 1/scored.
438                            let inv_n = 1.0 / scored as f64;
439                            for v in row.iter_mut() {
440                                *v = ((((*v as f64) - mx).exp() / sum) * inv_n) as f32;
441                            }
442                            row[target] -= inv_n as f32;
443                        }
444                    } else {
445                        // A supervised classifier needs competition BETWEEN
446                        // its labels. Full-vocabulary CE merely teaches both
447                        // UP and DOWN to outrank unrelated words and can lower
448                        // PPL while greedy decoding stays one constant class.
449                        // Restrict the normalizer and gradient to the declared
450                        // one-token labels: this is exact binary/multiclass CE.
451                        let mx = self
452                            .focus_tokens
453                            .iter()
454                            .map(|&id| row[id as usize])
455                            .fold(f32::NEG_INFINITY, f32::max)
456                            as f64;
457                        let probs: Vec<(usize, f64)> = self
458                            .focus_tokens
459                            .iter()
460                            .map(|&id| {
461                                let index = id as usize;
462                                (index, ((row[index] as f64) - mx).exp())
463                            })
464                            .collect();
465                        let sum: f64 = probs.iter().map(|(_, value)| value).sum();
466                        nll += mx + sum.ln() - row[target] as f64;
467                        if grad.is_some() {
468                            let inv_n = 1.0 / scored as f64;
469                            row.fill(0.0);
470                            for (index, value) in probs {
471                                row[index] = (value / sum * inv_n) as f32;
472                            }
473                            row[target] -= inv_n as f32;
474                        }
475                    }
476                }
477                if grad.is_some() {
478                    ops::gemm_dx(
479                        &logits,
480                        lm,
481                        &mut dh_n[(base + p0) * hsz..(base + p0 + pc) * hsz],
482                        pc,
483                        hsz,
484                        vocab,
485                        pool,
486                    );
487                }
488                p0 += pc;
489            }
490        }
491        let Some((dmask, dffn)) = grad else {
492            return (nll, scored);
493        };
494        // Backward: final norm, then the FFN chain layer by layer.
495        let t_bwd = std::time::Instant::now();
496        let mut dh = vec![0f32; n * hsz];
497        ops::rmsnorm_bwd(&h, &fm.final_norm, &inv, &dh_n, fm.gemma, &mut dh, None);
498        for vl in (0..vn).rev() {
499            let li = vl % nl;
500            // Undo the loop-boundary norm this step fed into.
501            if let Some((hb, inv)) = lnorms[vl].as_ref() {
502                let mut dprev = vec![0f32; n * hsz];
503                ops::rmsnorm_bwd(hb, &fm.final_norm, inv, &dh, fm.gemma, &mut dprev, None);
504                dh = dprev;
505            }
506            let a = acts[vl].as_ref().expect("acts saved in grad mode");
507            let g = &masks[vl];
508            let inter = fm.layers[li].inter;
509            let mats_hold = fm.mats(li).expect("layer mats");
510            let wts = self.wts(li, &mats_hold);
511            // h2 = h1 + act2 @ downᵀ  →  dact2 = dh @ down.
512            let mut dact2 = vec![0f32; t * inter];
513            ops::gemm_dx(&dh, wts.down, &mut dact2, t, inter, hsz, fm.pool.as_deref());
514            if let Some((_, _, dd)) = dffn[li].as_mut() {
515                // dW_down += dhᵀ · act2 (act2 = act·g).
516                let mut act2 = a.act.clone();
517                for r in 0..t {
518                    for (x, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
519                        *x *= gv;
520                    }
521                }
522                let mut dw = vec![0f32; hsz * inter];
523                ops::gemm_dw(&dh, &act2, &mut dw, t, inter, hsz, fm.pool.as_deref());
524                for (o, &x) in dd.iter_mut().zip(&dw) {
525                    *o += x as f64;
526                }
527            }
528            // Mask grad: dm = Σ_t dact2·act · σ'(m)  (soft; STE-equal).
529            // Indexed by the VIRTUAL layer: each visit's mask row gets
530            // exactly its own visit's gradient, no cross-visit sum.
531            {
532                let dm = &mut dmask[vl];
533                for r in 0..t {
534                    let da = &dact2[r * inter..(r + 1) * inter];
535                    let aa = &a.act[r * inter..(r + 1) * inter];
536                    for j in 0..inter {
537                        dm[j] += da[j] as f64 * aa[j] as f64;
538                    }
539                }
540                // σ'(m) folded in once per chunk (constant per neuron).
541                for (j, d) in dm.iter_mut().enumerate() {
542                    let _ = j;
543                    let _ = d;
544                }
545            }
546            // dact = dact2 · g;  silu·mul backward.
547            let mut dg_pre = vec![0f32; t * inter];
548            let mut du_pre = vec![0f32; t * inter];
549            for r in 0..t {
550                for j in 0..inter {
551                    let i = r * inter + j;
552                    let da = dact2[i] * g[j];
553                    let sg = ops::silu(a.gpre[i]);
554                    dg_pre[i] = da * a.upre[i] * ops::silu_bwd(a.gpre[i]);
555                    du_pre[i] = da * sg;
556                }
557            }
558            // dn2 = dg_pre @ gate + du_pre @ up — one fused submit when
559            // the frozen concat exists; the trained-copy path keeps two.
560            let mut dn2 = vec![0f32; t * hsz];
561            if let Some(gu) = wts.gu {
562                let mut dgu = vec![0f32; t * 2 * inter];
563                for r in 0..t {
564                    let row = &mut dgu[r * 2 * inter..(r + 1) * 2 * inter];
565                    row[..inter].copy_from_slice(&dg_pre[r * inter..(r + 1) * inter]);
566                    row[inter..].copy_from_slice(&du_pre[r * inter..(r + 1) * inter]);
567                }
568                ops::gemm_dx(&dgu, gu, &mut dn2, t, hsz, 2 * inter, fm.pool.as_deref());
569            } else {
570                ops::gemm_dx(
571                    &dg_pre,
572                    wts.gate,
573                    &mut dn2,
574                    t,
575                    hsz,
576                    inter,
577                    fm.pool.as_deref(),
578                );
579                let mut dn2b = vec![0f32; t * hsz];
580                ops::gemm_dx(
581                    &du_pre,
582                    wts.up,
583                    &mut dn2b,
584                    t,
585                    hsz,
586                    inter,
587                    fm.pool.as_deref(),
588                );
589                for (x, &y) in dn2.iter_mut().zip(&dn2b) {
590                    *x += y;
591                }
592            }
593            if let Some((dgw, duw, _)) = dffn[li].as_mut() {
594                let mut dw = vec![0f32; inter * hsz];
595                ops::gemm_dw(&dg_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
596                for (o, &x) in dgw.iter_mut().zip(&dw) {
597                    *o += x as f64;
598                }
599                dw.fill(0.0);
600                ops::gemm_dw(&du_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
601                for (o, &x) in duw.iter_mut().zip(&dw) {
602                    *o += x as f64;
603                }
604            }
605            // Post-norm backward into h1; the attention branch carries
606            // no gradient (frozen), so dh1 flows straight to dh_in.
607            let mut dh1 = dh.clone(); // residual h2 = h1 + ffn
608            ops::rmsnorm_bwd(&a.h1, wts.pln, &a.inv2, &dn2, fm.gemma, &mut dh1, None);
609            dh = dh1;
610            let _ = &h_ins[vl];
611        }
612        crate::fcd::prof::add(&crate::fcd::prof::BWD, t_bwd);
613        (nll, scored)
614    }
615}
616
617/// Held-out PPL with the hard mask (and Phase-B weights when present).
618fn held_ppl(pass: &Pass, held: &[Vec<u32>]) -> f64 {
619    // One batched pass over the whole held set: same arithmetic, one
620    // GEMM per weight instead of one per chunk. On the 4 B looped model
621    // the sequential version spent 12× the submits for identical math.
622    if held.is_empty() {
623        return f64::NAN;
624    }
625    let t = held[0].len();
626    if held.iter().all(|c| c.len() == t) {
627        let flat: Vec<u32> = held.iter().flatten().copied().collect();
628        let (l, k) = pass.chunk_batch(&flat, held.len(), None);
629        return (l / k.max(1) as f64).exp();
630    }
631    let mut nll = 0f64;
632    let mut n = 0usize;
633    for c in held {
634        let (l, k) = pass.chunk(c, None);
635        nll += l;
636        n += k;
637    }
638    (nll / n.max(1) as f64).exp()
639}
640
641/// Score a WRITTEN specialist through the replica's own math (f32
642/// dequant of whatever the file carries) with the file's binary mask
643/// held hard — the decomposition probe that tells "the requant at write
644/// cost the quality" from "the runtime applies the mask differently".
645/// Returns (bare, masked) held-PPL over the chunks.
646pub fn replica_score_file_mask(
647    model: &Arc<CmfModel>,
648    chunks: &[Vec<u32>],
649) -> Result<(f64, f64), String> {
650    let o1_off = crate::nystrom::O1Cfg {
651        layers: crate::nystrom::O1Layers::List(Vec::new()),
652        m: 4,
653        w: 8,
654        sink: 1,
655        rect: crate::nystrom::O1_DEFAULT_RECT,
656    };
657    let fm = FcdModel::from_cmf(model, &o1_off, false)?;
658    let nl = fm.layers.len();
659    let loops = fm.loops.max(1);
660    let vn = nl * loops;
661    let inter = fm.layers[0].inter;
662    let ffn: Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>> = vec![None; nl];
663    // Binary mask → logits at ±50: σ crosses any τ exactly as the bit says.
664    let task = &model.masks.default_task;
665    let mask = model
666        .masks
667        .masks
668        .iter()
669        .find(|m| &m.name == task)
670        .or_else(|| model.masks.masks.first());
671    let open: Vec<Vec<f32>> = vec![vec![50.0; inter]; vn];
672    let masked_logits: Vec<Vec<f32>> = match mask {
673        Some(m) => (0..vn)
674            .map(|vl| {
675                let row = m.ffn_masks.get(vl).map(|v| v.as_slice()).unwrap_or(&[]);
676                (0..inter)
677                    .map(|j| {
678                        if (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0 {
679                            50.0
680                        } else {
681                            -50.0
682                        }
683                    })
684                    .collect()
685            })
686            .collect(),
687        None => open.clone(),
688    };
689    let score = |logits: &[Vec<f32>]| -> f64 {
690        let pass = Pass {
691            fm: &fm,
692            tau: 0.5,
693            logits,
694            hard: true,
695            ffn: &ffn,
696            focus_tokens: &[],
697            focus_follow_tokens: &[],
698        };
699        held_ppl(&pass, chunks)
700    };
701    Ok((score(&open), score(&masked_logits)))
702}
703
704/// The whole recipe. `log` receives progress lines.
705pub fn skill_bake(
706    model: &Arc<CmfModel>,
707    chunks: &[Vec<u32>],
708    held_n: usize,
709    hy: &BakeHyper,
710    mut log: impl FnMut(&str),
711) -> Result<(BakeReport, BakeArtifacts), String> {
712    let t0 = std::time::Instant::now();
713    let o1_off = crate::nystrom::O1Cfg {
714        layers: crate::nystrom::O1Layers::List(Vec::new()),
715        m: 4,
716        w: 8,
717        sink: 1,
718        rect: crate::nystrom::O1_DEFAULT_RECT,
719    };
720    let fm = FcdModel::from_cmf(model, &o1_off, false)?;
721    let nl = fm.layers.len();
722    let inter = fm.layers.iter().map(|l| l.inter).max().unwrap_or(0);
723    if fm.layers.iter().any(|l| l.inter != inter) {
724        return Err("skill bake: non-uniform FFN widths".into());
725    }
726    let held: Vec<Vec<u32>> = chunks[..held_n.min(chunks.len())].to_vec();
727    let calib: Vec<Vec<u32>> = chunks[held_n.min(chunks.len())..].to_vec();
728    if calib.len() < 12 {
729        return Err(format!(
730            "skill bake: corpus too small ({} calib chunks)",
731            calib.len()
732        ));
733    }
734    let fcd: Vec<usize> = (nl.saturating_sub(hy.fcd_layers)..nl).collect();
735    let _rng = SplitMix64::new(hy.seed);
736
737    // Trainables. The gate starts as close to OPEN as the arithmetic
738    // allows, because step zero must be the backbone and nothing else.
739    //
740    // It used to start at 2.0, and σ(2.0) = 0.881 — every FFN neuron
741    // scaled to seven eighths before a single gradient. On an ordinary
742    // stack that costs a few percent of perplexity and hides. On a
743    // LOOPED Transformer it does not: Nanbeige 4.2 runs its 22 layers
744    // twice, so each physical FFN is visited twice and the factor
745    // compounds to 0.881² = 0.776 per layer over 44 visits. Measured:
746    // baseline 4.187 → 278.4 at step 30 with 0% pruned. Nothing had been
747    // pruned; the mask had simply turned the model down.
748    //
749    // So solve for the init instead of hardcoding it — but solve for the
750    // right target. Pushing σ(m0)^loops to 0.999 starts at the backbone
751    // and cannot move: the gradient carries σ'(m) = σ(1−σ), which at
752    // σ = 0.9995 is 5e-4 against 0.105 at the old 2.0, and BOTH the data
753    // term and the L1 term are scaled by it (see the update below). That
754    // was tried: 60 steps, 0% pruned, hard-PPL equal to the baseline to
755    // three digits. Identity that cannot learn is not an improvement.
756    //
757    // The quantity to preserve is the EFFECTIVE start — what the stack
758    // actually multiplies by, once per visit compounded over the loop —
759    // at the value the recipe was validated with on ordinary models:
760    // σ(m0)^loops = σ(2.0) = 0.881. One loop reproduces the old constant
761    // exactly, so nothing regresses; two loops open the per-visit gate to
762    // 0.9385 so the compounded factor is again 0.881, with σ' = 0.058
763    // rather than 0.0005.
764    let loops = fm.loops.max(1);
765    let m0 = mask_init_logit(loops);
766    // One mask row per VIRTUAL layer: nl × loops. Unlooped: vn == nl.
767    let vn = nl * loops;
768    let mut logits: Vec<Vec<f32>> = vec![vec![m0; inter]; vn];
769    let mut ffn: Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>> = vec![None; nl];
770
771    // Baseline (no mask): even σ(m0) is not exactly 1, so measure with
772    // gates forced open via hard mask over +∞… simplest: logits +50.
773    let open: Vec<Vec<f32>> = vec![vec![50.0; inter]; vn];
774    let base_pass = Pass {
775        fm: &fm,
776        tau: hy.tau,
777        logits: &open,
778        hard: true,
779        ffn: &ffn,
780        focus_tokens: &hy.focus_tokens,
781        focus_follow_tokens: &hy.focus_follow_tokens,
782    };
783    let backbone = held_ppl(&base_pass, &held);
784    log(&format!("baseline (full): {backbone:.3}"));
785
786    // ── Phase A: mask training ──
787    let mut adam_a = Adam::new(&vec![inter; vn], hy.lr_a);
788    let mut l1 = hy.l1_init * hy.l1_mult;
789    let l1_step_eff = hy.l1_step * hy.l1_mult;
790    // best = (ppl, logits_snapshot, sparsity)
791    let mut best: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
792    // Track the highest-sparsity checkpoint as fallback.
793    let mut max_sp: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
794    let mut prev_alive: Option<Vec<Vec<bool>>> = None;
795    // In-process phase timers: this loop was estimated three different
796    // ways and every estimate came out under a fifth of the measured
797    // step time. Measure, then optimize the top line, not the guess.
798    let mut acc_chunk = 0f64;
799    let mut acc_adam = 0f64;
800    // Phase A trains in strict f32: the mask SELECTS neurons by its
801    // gradient, and f16 operand rounding on that signal — fine for every
802    // forward and eval in this file — compounds over ~90 steps into
803    // closing the wrong neurons (measured: hard-PPL 5.207 vs 4.293 at the
804    // same 2.56% sparsity; the f32 run retraces the reference trajectory
805    // to the third decimal). Evals inside the loop lift the restriction —
806    // their tensor-core numbers match f32 at print precision.
807    crate::gpu::bake_precision_strict(true);
808    for step in 0..hy.steps_a {
809        let t_step = std::time::Instant::now();
810        let chunk = &calib[step % calib.len()];
811        let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; vn];
812        let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = vec![None; nl];
813        let pass = Pass {
814            fm: &fm,
815            tau: hy.tau,
816            logits: &logits,
817            hard: false,
818            ffn: &ffn,
819            focus_tokens: &hy.focus_tokens,
820            focus_follow_tokens: &hy.focus_follow_tokens,
821        };
822        let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
823        // Fold σ'(m) into the mask grads + add the L1 term.
824        let l1_per = l1 / (inter as f64 * nl as f64);
825        for li in 0..vn {
826            for j in 0..inter {
827                let s = sigmoid(logits[li][j]) as f64;
828                dmask[li][j] = dmask[li][j] * s * (1.0 - s) + l1_per * s * (1.0 - s);
829            }
830        }
831        // One gradient per VISIT, one step per token. A Looped
832        // Transformer visits each physical layer `loops` times and the
833        // backward accumulates every visit into the same mask, so an
834        // unnormalised step is `loops` times the one the recipe was
835        // tuned with — the mask overshoots, neurons cross tau within the
836        // first evaluation window, and each of them is then missing from
837        // both passes. Dividing by the visit count makes a step mean the
838        // same thing at any loop depth.
839        // Per-visit rows: each mask logit receives exactly one visit's
840        // gradient, so the step needs no visit normalisation here — that
841        // scale now belongs to Phase B alone, where the FFN weights ARE
842        // shared across visits.
843        let t_chunk = t_step.elapsed().as_secs_f64();
844        let mut params: Vec<&mut [f32]> = logits.iter_mut().map(|v| v.as_mut_slice()).collect();
845        adam_a.step(&mut params, &dmask, 1.0);
846        acc_chunk += t_chunk;
847        acc_adam += t_step.elapsed().as_secs_f64() - t_chunk;
848        if (step + 1) % hy.eval_every == 0 {
849            l1 += l1_step_eff;
850            let pass = Pass {
851                fm: &fm,
852                tau: hy.tau,
853                logits: &logits,
854                hard: true,
855                ffn: &ffn,
856                focus_tokens: &hy.focus_tokens,
857                focus_follow_tokens: &hy.focus_follow_tokens,
858            };
859            crate::gpu::bake_precision_strict(false);
860            let hp = held_ppl(&pass, &held);
861            crate::gpu::bake_precision_strict(true);
862            // Name the neurons that crossed τ since the last eval. At
863            // 0.01% pruned = ~24 neurons for a 135 held-PPL, WHICH 24 is
864            // the whole diagnosis: it decides between "this model has no
865            // noise neurons" and "a shared mask cannot spare a neuron
866            // that only one visit of the loop needs".
867            let cur: Vec<Vec<bool>> = logits
868                .iter()
869                .map(|l| l.iter().map(|&x| sigmoid(x) > hy.tau).collect())
870                .collect();
871            if let Some(prev) = &prev_alive {
872                let died: Vec<String> = cur
873                    .iter()
874                    .zip(prev)
875                    .enumerate()
876                    .flat_map(|(li, (c, p))| {
877                        c.iter()
878                            .zip(p.iter())
879                            .enumerate()
880                            .filter(|&(_, (&cj, &pj))| pj && !cj)
881                            .map(move |(j, _)| format!("L{li}:{j}"))
882                    })
883                    .collect();
884                if !died.is_empty() {
885                    log(&format!(
886                        "    closed since last eval: {}: {}{}",
887                        died.len(),
888                        died.iter().take(32).cloned().collect::<Vec<_>>().join(" "),
889                        if died.len() > 32 { " …" } else { "" }
890                    ));
891                }
892            }
893            let alive: usize = cur.iter().map(|l| l.iter().filter(|&&b| b).count()).sum();
894            prev_alive = Some(cur);
895            let sp = 1.0 - alive as f64 / (vn * inter) as f64;
896            // Track highest-sparsity checkpoint.
897            if sp > max_sp.2 {
898                max_sp = (hp, Some(logits.clone()), sp);
899            }
900            // Best checkpoint selection: respect target_sparsity.
901            if hy.target_sparsity > 0.0 {
902                if sp >= hy.target_sparsity && hp < best.0 {
903                    best = (hp, Some(logits.clone()), sp);
904                }
905            } else if hp < best.0 {
906                best = (hp, Some(logits.clone()), sp);
907            }
908            log(&format!(
909                "  [A] step {}: L1={l1:.3} pruned={:.2}% hard-PPL={hp:.3} (bottom {}@{:.2}%) [fwd+bwd {:.1}s, adam {:.2}s per step]",
910                step + 1,
911                sp * 100.0,
912                if best.0 == f64::MAX {
913                    "—".to_string()
914                } else {
915                    format!("{:.3}", best.0)
916                },
917                best.2 * 100.0,
918                acc_chunk / (step + 1) as f64,
919                acc_adam / (step + 1) as f64
920            ));
921        }
922    }
923    // If target_sparsity was set but no checkpoint qualified, fall back
924    // to the highest-sparsity checkpoint.
925    // Phase A is over — phase B and every eval after run on the fast arms.
926    crate::gpu::bake_precision_strict(false);
927    if hy.target_sparsity > 0.0 && best.1.is_none() {
928        log(&format!(
929            "[A] target sparsity {:.0}% not reached; using max-sparsity checkpoint ({:.0}%)",
930            hy.target_sparsity * 100.0,
931            max_sp.2 * 100.0
932        ));
933        best = max_sp;
934    }
935    // Phase totals, printed unconditionally — a 5-step measurement run
936    // must report even though no eval fired.
937    {
938        use crate::fcd::prof;
939        let (a, f, bw, g, gc) = (
940            prof::take(&prof::ATTN_FWD),
941            prof::take(&prof::FFN_FWD),
942            prof::take(&prof::BWD),
943            prof::take(&prof::GEMM),
944            prof::GEMM_CALLS.swap(0, std::sync::atomic::Ordering::Relaxed),
945        );
946        log(&format!(
947            "[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)",
948            hy.steps_a,
949            if gc > 0 { g * 1000.0 / gc as f64 } else { 0.0 }
950        ));
951        log(&format!("[prof] gemm shapes:\n{}", prof::shape_report(6)));
952    }
953    if let Some(b) = best.1.take() {
954        logits = b;
955    }
956    let pass = Pass {
957        fm: &fm,
958        tau: hy.tau,
959        logits: &logits,
960        hard: true,
961        ffn: &ffn,
962        focus_tokens: &hy.focus_tokens,
963        focus_follow_tokens: &hy.focus_follow_tokens,
964    };
965    let masked = held_ppl(&pass, &held);
966    log(&format!(
967        "[A] {:.0}s: masked-PPL {masked:.3}",
968        t0.elapsed().as_secs_f64()
969    ));
970
971    // ── Phase B: FCD of the last N layers' FFN (hard mask active) ──
972    for &li in &fcd {
973        let p = format!("model.layers.{li}.");
974        ffn[li] = Some((
975            crate::fcd::deq_pub(&fm.src, &format!("{p}mlp.gate_proj.weight"))
976                .map_err(|e| format!("phase-B gate: {e}"))?,
977            crate::fcd::deq_pub(&fm.src, &format!("{p}mlp.up_proj.weight"))
978                .map_err(|e| format!("phase-B up: {e}"))?,
979            crate::fcd::deq_pub(&fm.src, &format!("{p}mlp.down_proj.weight"))
980                .map_err(|e| format!("phase-B down: {e}"))?,
981        ));
982    }
983    let sizes: Vec<usize> = fcd
984        .iter()
985        .flat_map(|&li| {
986            let (g, u, d) = ffn[li].as_ref().expect("phase-B masters");
987            [g.len(), u.len(), d.len()]
988        })
989        .collect();
990    let mut adam_b = Adam::new(&sizes, hy.lr_b);
991    // The mask-only model is a real checkpoint too. If every FCD eval is
992    // worse, restore `None` overlays rather than accidentally writing the
993    // final (rejected) training step while reporting the mask-only PPL.
994    let mut best_b: (f64, Option<Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>>>) =
995        (masked, Some(vec![None; nl]));
996    for step in 0..hy.steps_b {
997        let chunk = &calib[step % calib.len()];
998        let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; vn];
999        let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = (0..nl)
1000            .map(|li| {
1001                ffn[li]
1002                    .as_ref()
1003                    .map(|(g, u, d)| (vec![0.0; g.len()], vec![0.0; u.len()], vec![0.0; d.len()]))
1004            })
1005            .collect();
1006        let pass = Pass {
1007            fm: &fm,
1008            tau: hy.tau,
1009            logits: &logits,
1010            hard: true,
1011            ffn: &ffn,
1012            focus_tokens: &hy.focus_tokens,
1013            focus_follow_tokens: &hy.focus_follow_tokens,
1014        };
1015        let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
1016        // Cosine LR.
1017        let lr_scale = 0.5 * (1.0 + (std::f64::consts::PI * step as f64 / hy.steps_b as f64).cos());
1018        let first_fcd = fcd[0];
1019        let mut params: Vec<&mut [f32]> = Vec::new();
1020        let mut grads: Vec<Vec<f64>> = Vec::new();
1021        for (off, slot) in ffn[first_fcd..].iter_mut().enumerate() {
1022            let li = first_fcd + off;
1023            let Some((g, u, d)) = slot.as_mut() else {
1024                continue;
1025            };
1026            let (dg, du, dd) = dffn[li].take().unwrap();
1027            params.push(g.as_mut_slice());
1028            grads.push(dg);
1029            params.push(u.as_mut_slice());
1030            grads.push(du);
1031            params.push(d.as_mut_slice());
1032            grads.push(dd);
1033        }
1034        // Same visit normalisation as Phase A: dffn accumulates every
1035        // visit of a physical layer, and an FFN update perturbs BOTH
1036        // passes of the loop, so per-step damage is `loops` times what
1037        // lr_b was tuned for on ordinary stacks.
1038        adam_b.step(&mut params, &grads, lr_scale * mask_step_scale(loops));
1039        if (step + 1) % hy.eval_every == 0 {
1040            let pass = Pass {
1041                fm: &fm,
1042                tau: hy.tau,
1043                logits: &logits,
1044                hard: true,
1045                ffn: &ffn,
1046                focus_tokens: &hy.focus_tokens,
1047                focus_follow_tokens: &hy.focus_follow_tokens,
1048            };
1049            let cur = held_ppl(&pass, &held);
1050            if cur < best_b.0 {
1051                best_b = (cur, Some(ffn.clone()));
1052            }
1053            log(&format!(
1054                "  [B] step {}: held-PPL {cur:.3} (best {:.3})",
1055                step + 1,
1056                best_b.0
1057            ));
1058        }
1059    }
1060    ffn = best_b.1.take().expect("phase-B always has a checkpoint");
1061    let overlaid = best_b.0;
1062
1063    // ── Export artifacts ──
1064    // Per-visit keep flags are the mask that ships; the PHYSICAL keep is
1065    // their union, because a weight row can only be removed from disk if
1066    // no visit needs it.
1067    let keep_visits = keep_masks(&logits, hy.tau, hy.align, hy.uniform_inter);
1068    let keep: Vec<Vec<bool>> = (0..nl)
1069        .map(|li| {
1070            (0..inter)
1071                .map(|j| (0..loops).any(|v| keep_visits[v * nl + li][j]))
1072                .collect()
1073        })
1074        .collect();
1075    if hy.align > 1 || hy.uniform_inter {
1076        let raw: usize = logits
1077            .iter()
1078            .map(|l| l.iter().filter(|&&x| sigmoid(x) > hy.tau).count())
1079            .sum();
1080        // Compare like with like: raw σ-counts are over the VIRTUAL
1081        // rows, so the padded count must be too — the union rows are
1082        // fewer and the subtraction would underflow.
1083        let padded: usize = keep_visits
1084            .iter()
1085            .map(|a| a.iter().filter(|&&x| x).count())
1086            .sum::<usize>()
1087            .saturating_sub(raw);
1088        log(&format!(
1089            "align: +{padded} neurons resurrected (align {}, uniform {})",
1090            hy.align, hy.uniform_inter
1091        ));
1092    }
1093    let mut down_out = Vec::with_capacity(nl);
1094    let mut gate_up = Vec::with_capacity(nl);
1095    let mut kept_per_layer = Vec::with_capacity(nl);
1096    for li in 0..nl {
1097        let alive = &keep[li];
1098        kept_per_layer.push(alive.iter().filter(|&&a| a).count());
1099        let mut down = match &ffn[li] {
1100            Some((_, _, d)) => d.clone(),
1101            None => fm.mats(li).expect("layer mats").down.clone(),
1102        };
1103        let hsz = fm.hidden;
1104        for r in 0..hsz {
1105            for (c, &a) in alive.iter().enumerate() {
1106                if !a {
1107                    down[r * inter + c] = 0.0;
1108                }
1109            }
1110        }
1111        gate_up.push(ffn[li].as_ref().map(|(g, u, _)| (g.clone(), u.clone())));
1112        down_out.push(down);
1113    }
1114    let total: usize = keep_visits
1115        .iter()
1116        .map(|a| a.iter().filter(|&&x| x).count())
1117        .sum();
1118    let report = BakeReport {
1119        backbone,
1120        masked,
1121        overlaid,
1122        pruned_ratio: 1.0 - total as f64 / (vn * inter) as f64,
1123        kept_per_layer,
1124        sec: t0.elapsed().as_secs_f64(),
1125    };
1126    let arts = BakeArtifacts {
1127        keep,
1128        keep_visits,
1129        down: down_out,
1130        gate_up,
1131        fcd_layers: fcd,
1132        logits: logits.clone(),
1133    };
1134    Ok((report, arts))
1135}
1136
1137/// Hard-threshold keep masks from the trained logits, then resurrect
1138/// the highest-logit pruned neurons until each layer's kept count is a
1139/// multiple of `align` (rounding UP — the resurrected neurons are the
1140/// ones the mask ranked closest to the threshold, so this only moves
1141/// toward the full backbone). `uniform` additionally raises every layer
1142/// to the max layer's aligned count. A layer with 0 live neurons gets
1143/// `align.max(1)` — the defrag writer rejects empty layers.
1144fn keep_masks(logits: &[Vec<f32>], tau: f32, align: usize, uniform: bool) -> Vec<Vec<bool>> {
1145    let inter = logits[0].len();
1146    let round = |n: usize| -> usize {
1147        let n = n.max(1);
1148        if align <= 1 {
1149            n.min(inter)
1150        } else {
1151            (n.div_ceil(align) * align).min(inter)
1152        }
1153    };
1154    let mut want: Vec<usize> = logits
1155        .iter()
1156        .map(|l| round(l.iter().filter(|&&x| sigmoid(x) > tau).count()))
1157        .collect();
1158    if uniform {
1159        let k = want.iter().copied().max().unwrap_or(inter);
1160        want = vec![k; logits.len()];
1161    }
1162    logits
1163        .iter()
1164        .zip(&want)
1165        .map(|(l, &k)| {
1166            let mut idx: Vec<usize> = (0..inter).collect();
1167            idx.sort_unstable_by(|&a, &b| l[b].total_cmp(&l[a]));
1168            let mut alive = vec![false; inter];
1169            for &i in idx.iter().take(k) {
1170                alive[i] = true;
1171            }
1172            alive
1173        })
1174        .collect()
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179    use super::*;
1180
1181    fn kept(masks: &[Vec<bool>]) -> Vec<usize> {
1182        masks
1183            .iter()
1184            .map(|m| m.iter().filter(|&&a| a).count())
1185            .collect()
1186    }
1187
1188    #[test]
1189    fn terminal_focus_ignores_label_names_inside_the_prompt() {
1190        // DOWN and UP occur in the instruction, but only the final UP is an
1191        // assistant answer because it is immediately followed by im_end.
1192        let down = 10;
1193        let up = 11;
1194        let im_end = 99;
1195        let ids = [1, down, 2, up, 3, up, im_end, 4];
1196        let focus = [down, up];
1197        let follow = [im_end];
1198        assert!(!is_scored_target(&ids, 1, ids.len(), &focus, &follow));
1199        assert!(!is_scored_target(&ids, 3, ids.len(), &focus, &follow));
1200        assert!(is_scored_target(&ids, 5, ids.len(), &focus, &follow));
1201    }
1202
1203    /// align=32 rounds each layer UP by resurrecting the largest
1204    /// pruned logits; the originally-alive set stays alive.
1205    #[test]
1206    fn keep_masks_aligns_up_and_preserves_alive() {
1207        let inter = 96;
1208        // Layer 0: 40 alive (logits > 0 → σ > 0.5), the rest ramp
1209        // below threshold so resurrection order is deterministic.
1210        let l0: Vec<f32> = (0..inter)
1211            .map(|i| if i < 40 { 1.0 } else { -1.0 - i as f32 * 0.01 })
1212            .collect();
1213        // Layer 1: 64 alive — already aligned, must stay exactly 64.
1214        let l1: Vec<f32> = (0..inter)
1215            .map(|i| if i < 64 { 2.0 } else { -3.0 })
1216            .collect();
1217        let masks = keep_masks(&[l0.clone(), l1], 0.5, 32, false);
1218        assert_eq!(kept(&masks), vec![64, 64]);
1219        // The 40 originally-alive stay; resurrected are the top pruned
1220        // logits (indices 40..64 — the least-negative of the ramp).
1221        for i in 0..64 {
1222            assert!(masks[0][i], "neuron {i} should be kept");
1223        }
1224        for i in 64..inter {
1225            assert!(!masks[0][i], "neuron {i} should stay pruned");
1226        }
1227    }
1228
1229    /// uniform=true raises every layer to the max aligned count.
1230    #[test]
1231    fn keep_masks_uniform_takes_max() {
1232        let inter = 96;
1233        let l0: Vec<f32> = (0..inter)
1234            .map(|i| if i < 10 { 1.0 } else { -2.0 })
1235            .collect();
1236        let l1: Vec<f32> = (0..inter)
1237            .map(|i| if i < 70 { 1.0 } else { -2.0 })
1238            .collect();
1239        let masks = keep_masks(&[l0, l1], 0.5, 32, true);
1240        assert_eq!(kept(&masks), vec![96, 96]);
1241    }
1242
1243    /// align capped at inter; align=1 (off) keeps the raw threshold
1244    /// count; an all-pruned layer still keeps at least one neuron.
1245    #[test]
1246    fn keep_masks_edges() {
1247        let inter = 48;
1248        let l: Vec<f32> = (0..inter)
1249            .map(|i| if i < 47 { 1.0 } else { -2.0 })
1250            .collect();
1251        let masks = keep_masks(&[l.clone()], 0.5, 32, false);
1252        assert_eq!(kept(&masks), vec![48]); // 47 → 64 capped to 48
1253        let masks = keep_masks(&[l], 0.5, 1, false);
1254        assert_eq!(kept(&masks), vec![47]);
1255        let dead: Vec<f32> = vec![-5.0; inter];
1256        let masks = keep_masks(&[dead], 0.5, 32, false);
1257        assert_eq!(kept(&masks), vec![32]); // max(1) → rounded to 32
1258    }
1259}