1use crate::fcd::{FcdModel, LnFfn};
22use crate::fcd_ops as ops;
23use crate::sampler::SplitMix64;
24use cortiq_core::CmfModel;
25use std::sync::Arc;
26
27#[derive(Clone, Debug)]
29pub struct BakeHyper {
30 pub steps_a: usize,
31 pub steps_b: usize,
32 pub l1_init: f64,
33 pub l1_step: f64,
34 pub eval_every: usize,
35 pub lr_a: f64,
36 pub lr_b: f64,
37 pub tau: f32,
38 pub fcd_layers: usize,
39 pub seed: u64,
40 pub target_sparsity: f64,
44 pub l1_mult: f64,
47 pub align: usize,
51 pub uniform_inter: bool,
54}
55
56impl Default for BakeHyper {
57 fn default() -> Self {
58 Self {
59 steps_a: 240,
60 steps_b: 120,
61 l1_init: 0.01,
62 l1_step: 0.005,
63 eval_every: 30,
64 lr_a: 0.1,
65 lr_b: 1e-5,
66 tau: 0.5,
67 fcd_layers: 4,
68 seed: 0,
69 target_sparsity: 0.0,
70 l1_mult: 1.0,
71 align: 32,
72 uniform_inter: false,
73 }
74 }
75}
76
77pub struct BakeReport {
79 pub backbone: f64,
81 pub masked: f64,
83 pub overlaid: f64,
85 pub pruned_ratio: f64,
86 pub kept_per_layer: Vec<usize>,
87 pub sec: f64,
88}
89
90pub struct BakeArtifacts {
92 pub keep: Vec<Vec<bool>>,
94 pub down: Vec<Vec<f32>>,
97 pub gate_up: Vec<Option<(Vec<f32>, Vec<f32>)>>,
99 pub fcd_layers: Vec<usize>,
101}
102
103const CLIP: f64 = 1.0;
104const B1: f64 = 0.9;
105const B2: f64 = 0.999;
106const EPS: f64 = 1e-8;
107
108struct Adam {
110 m: Vec<Vec<f64>>,
111 v: Vec<Vec<f64>>,
112 t: i32,
113 lr: f64,
114}
115
116impl Adam {
117 fn new(sizes: &[usize], lr: f64) -> Self {
118 Self {
119 m: sizes.iter().map(|&n| vec![0.0; n]).collect(),
120 v: sizes.iter().map(|&n| vec![0.0; n]).collect(),
121 t: 0,
122 lr,
123 }
124 }
125
126 fn step(&mut self, params: &mut [&mut [f32]], grads: &[Vec<f64>], lr_scale: f64) {
128 let gn: f64 = grads
129 .iter()
130 .flat_map(|g| g.iter().map(|x| x * x))
131 .sum::<f64>()
132 .sqrt();
133 let clip = if gn > CLIP { CLIP / gn } else { 1.0 };
134 self.t += 1;
135 let (bc1, bc2) = (1.0 - B1.powi(self.t), 1.0 - B2.powi(self.t));
136 for (pi, p) in params.iter_mut().enumerate() {
137 for j in 0..p.len() {
138 let g = grads[pi][j] * clip;
139 let m = &mut self.m[pi][j];
140 let v = &mut self.v[pi][j];
141 *m = B1 * *m + (1.0 - B1) * g;
142 *v = B2 * *v + (1.0 - B2) * g * g;
143 let upd = (*m / bc1) / ((*v / bc2).sqrt() + EPS);
144 p[j] -= (self.lr * lr_scale * upd) as f32;
145 }
146 }
147 }
148}
149
150fn sigmoid(x: f32) -> f32 {
151 1.0 / (1.0 + (-x).exp())
152}
153
154struct Pass<'a> {
157 fm: &'a FcdModel,
158 tau: f32,
159 logits: &'a [Vec<f32>],
161 hard: bool,
162 ffn: &'a [Option<(Vec<f32>, Vec<f32>, Vec<f32>)>],
164}
165
166impl Pass<'_> {
167 fn gates(&self, li: usize) -> Vec<f32> {
168 self.logits[li]
169 .iter()
170 .map(|&l| {
171 let s = sigmoid(l);
172 if self.hard {
173 if s > self.tau { 1.0 } else { 0.0 }
174 } else {
175 s
176 }
177 })
178 .collect()
179 }
180
181 fn wts<'b>(&'b self, li: usize) -> LnFfn<'b> {
182 let l = &self.fm.layers[li];
183 match &self.ffn[li] {
184 Some((g, u, d)) => LnFfn {
185 iln: &l.iln,
186 pln: &l.pln,
187 gate: g,
188 up: u,
189 down: d,
190 },
191 None => LnFfn {
192 iln: &l.iln,
193 pln: &l.pln,
194 gate: &l.gate,
195 up: &l.up,
196 down: &l.down,
197 },
198 }
199 }
200
201 #[allow(clippy::too_many_arguments)]
205 fn chunk(
206 &self,
207 ids: &[u32],
208 grad: Option<(
209 &mut [Vec<f64>],
210 &mut [Option<(Vec<f64>, Vec<f64>, Vec<f64>)>],
211 )>,
212 ) -> (f64, usize) {
213 let fm = self.fm;
214 let (t, hsz) = (ids.len(), fm.hidden);
215 let nl = fm.layers.len();
216 let mut h = vec![0f32; t * hsz];
218 for (r, &id) in ids.iter().enumerate() {
219 h[r * hsz..(r + 1) * hsz]
220 .copy_from_slice(&fm.embed[id as usize * hsz..(id as usize + 1) * hsz]);
221 }
222 let loops = fm.loops.max(1);
230 let vn = nl * loops;
231 let mut h_ins = Vec::with_capacity(vn);
232 let mut acts = Vec::with_capacity(vn);
233 let mut masks = Vec::with_capacity(vn);
234 let mut lnorms: Vec<Option<(Vec<f32>, Vec<f32>)>> = vec![None; vn];
236 for vl in 0..vn {
237 let li = vl % nl;
238 let g = self.gates(li);
239 let wts = self.wts(li);
240 let want = grad.is_some();
241 let (h2, a) = fm.layer_forward_scaled(li, &h, 1, t, &wts, false, want, Some(&g));
242 h_ins.push(if want { h } else { Vec::new() });
243 acts.push(a);
244 masks.push(g);
245 h = h2;
246 if fm.loop_norm && li + 1 == nl && vl + 1 < vn {
249 let mut hn = vec![0f32; t * hsz];
250 let mut inv = vec![0f32; t];
251 ops::rmsnorm_fwd(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv);
252 if want {
253 lnorms[vl] = Some((h, inv));
254 }
255 h = hn;
256 }
257 }
258 let mut hn = vec![0f32; t * hsz];
260 let mut inv = vec![0f32; t];
261 ops::rmsnorm_fwd(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv);
262 let lm: &[f32] = fm.lm_head.as_deref().unwrap_or(&fm.embed);
263 let vocab = lm.len() / hsz;
264 let pool = fm.pool.as_deref();
265 let mut nll = 0f64;
266 let mut dh_n = vec![0f32; t * hsz]; const POS_CHUNK: usize = 32;
269 let scored = t - 1;
270 let mut p0 = 0usize;
271 while p0 < scored {
272 let pc = POS_CHUNK.min(scored - p0);
273 let mut logits = vec![0f32; pc * vocab];
274 ops::gemm_nt(
275 &hn[p0 * hsz..(p0 + pc) * hsz],
276 lm,
277 &mut logits,
278 pc,
279 hsz,
280 vocab,
281 pool,
282 );
283 for r in 0..pc {
284 let target = ids[p0 + r + 1] as usize;
285 let row = &mut logits[r * vocab..(r + 1) * vocab];
286 let mx = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64;
287 let mut sum = 0f64;
288 for v in row.iter() {
289 sum += ((*v as f64) - mx).exp();
290 }
291 nll += mx + sum.ln() - row[target] as f64;
292 if grad.is_some() {
293 let inv_n = 1.0 / scored as f64;
295 for v in row.iter_mut() {
296 *v = ((((*v as f64) - mx).exp() / sum) * inv_n) as f32;
297 }
298 row[target] -= inv_n as f32;
299 }
300 }
301 if grad.is_some() {
302 ops::gemm_dx(
303 &logits,
304 lm,
305 &mut dh_n[p0 * hsz..(p0 + pc) * hsz],
306 pc,
307 hsz,
308 vocab,
309 pool,
310 );
311 }
312 p0 += pc;
313 }
314 let Some((dmask, dffn)) = grad else {
315 return (nll, scored);
316 };
317 let mut dh = vec![0f32; t * hsz];
319 ops::rmsnorm_bwd(&h, &fm.final_norm, &inv, &dh_n, fm.gemma, &mut dh, None);
320 for vl in (0..vn).rev() {
321 let li = vl % nl;
322 if let Some((hb, inv)) = lnorms[vl].as_ref() {
324 let mut dprev = vec![0f32; t * hsz];
325 ops::rmsnorm_bwd(hb, &fm.final_norm, inv, &dh, fm.gemma, &mut dprev, None);
326 dh = dprev;
327 }
328 let a = acts[vl].as_ref().expect("acts saved in grad mode");
329 let g = &masks[vl];
330 let inter = fm.layers[li].inter;
331 let wts = self.wts(li);
332 let mut dact2 = vec![0f32; t * inter];
334 ops::gemm_dx(&dh, wts.down, &mut dact2, t, inter, hsz, fm.pool.as_deref());
335 if let Some((_, _, dd)) = dffn[li].as_mut() {
336 let mut act2 = a.act.clone();
338 for r in 0..t {
339 for (x, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
340 *x *= gv;
341 }
342 }
343 let mut dw = vec![0f32; hsz * inter];
344 ops::gemm_dw(&dh, &act2, &mut dw, t, inter, hsz, fm.pool.as_deref());
345 for (o, &x) in dd.iter_mut().zip(&dw) {
346 *o += x as f64;
347 }
348 }
349 {
351 let dm = &mut dmask[li];
352 for r in 0..t {
353 let da = &dact2[r * inter..(r + 1) * inter];
354 let aa = &a.act[r * inter..(r + 1) * inter];
355 for j in 0..inter {
356 dm[j] += da[j] as f64 * aa[j] as f64;
357 }
358 }
359 for (j, d) in dm.iter_mut().enumerate() {
361 let _ = j;
362 let _ = d;
363 }
364 }
365 let mut dg_pre = vec![0f32; t * inter];
367 let mut du_pre = vec![0f32; t * inter];
368 for r in 0..t {
369 for j in 0..inter {
370 let i = r * inter + j;
371 let da = dact2[i] * g[j];
372 let sg = ops::silu(a.gpre[i]);
373 dg_pre[i] = da * a.upre[i] * ops::silu_bwd(a.gpre[i]);
374 du_pre[i] = da * sg;
375 }
376 }
377 let mut dn2 = vec![0f32; t * hsz];
379 ops::gemm_dx(
380 &dg_pre,
381 wts.gate,
382 &mut dn2,
383 t,
384 hsz,
385 inter,
386 fm.pool.as_deref(),
387 );
388 let mut dn2b = vec![0f32; t * hsz];
389 ops::gemm_dx(
390 &du_pre,
391 wts.up,
392 &mut dn2b,
393 t,
394 hsz,
395 inter,
396 fm.pool.as_deref(),
397 );
398 for (x, &y) in dn2.iter_mut().zip(&dn2b) {
399 *x += y;
400 }
401 if let Some((dgw, duw, _)) = dffn[li].as_mut() {
402 let mut dw = vec![0f32; inter * hsz];
403 ops::gemm_dw(&dg_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
404 for (o, &x) in dgw.iter_mut().zip(&dw) {
405 *o += x as f64;
406 }
407 dw.fill(0.0);
408 ops::gemm_dw(&du_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
409 for (o, &x) in duw.iter_mut().zip(&dw) {
410 *o += x as f64;
411 }
412 }
413 let mut dh1 = dh.clone(); ops::rmsnorm_bwd(&a.h1, wts.pln, &a.inv2, &dn2, fm.gemma, &mut dh1, None);
417 dh = dh1;
418 let _ = &h_ins[vl];
419 }
420 (nll, scored)
421 }
422}
423
424fn held_ppl(pass: &Pass, held: &[Vec<u32>]) -> f64 {
426 let mut nll = 0f64;
427 let mut n = 0usize;
428 for c in held {
429 let (l, k) = pass.chunk(c, None);
430 nll += l;
431 n += k;
432 }
433 (nll / n.max(1) as f64).exp()
434}
435
436pub fn skill_bake(
438 model: &Arc<CmfModel>,
439 chunks: &[Vec<u32>],
440 held_n: usize,
441 hy: &BakeHyper,
442 mut log: impl FnMut(&str),
443) -> Result<(BakeReport, BakeArtifacts), String> {
444 let t0 = std::time::Instant::now();
445 let o1_off = crate::nystrom::O1Cfg {
446 layers: crate::nystrom::O1Layers::List(Vec::new()),
447 m: 4,
448 w: 8,
449 sink: 1,
450 rect: crate::nystrom::O1_DEFAULT_RECT,
451 };
452 let fm = FcdModel::from_cmf(model, &o1_off)?;
453 let nl = fm.layers.len();
454 let inter = fm.layers.iter().map(|l| l.inter).max().unwrap_or(0);
455 if fm.layers.iter().any(|l| l.inter != inter) {
456 return Err("skill bake: non-uniform FFN widths".into());
457 }
458 let held: Vec<Vec<u32>> = chunks[..held_n.min(chunks.len())].to_vec();
459 let calib: Vec<Vec<u32>> = chunks[held_n.min(chunks.len())..].to_vec();
460 if calib.len() < 12 {
461 return Err(format!(
462 "skill bake: corpus too small ({} calib chunks)",
463 calib.len()
464 ));
465 }
466 let fcd: Vec<usize> = (nl.saturating_sub(hy.fcd_layers)..nl).collect();
467 let _rng = SplitMix64::new(hy.seed);
468
469 let mut logits: Vec<Vec<f32>> = vec![vec![2.0; inter]; nl];
471 let mut ffn: Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>> = vec![None; nl];
472
473 let open: Vec<Vec<f32>> = vec![vec![50.0; inter]; nl];
476 let base_pass = Pass {
477 fm: &fm,
478 tau: hy.tau,
479 logits: &open,
480 hard: true,
481 ffn: &ffn,
482 };
483 let backbone = held_ppl(&base_pass, &held);
484 log(&format!("baseline (full): {backbone:.3}"));
485
486 let mut adam_a = Adam::new(&vec![inter; nl], hy.lr_a);
488 let mut l1 = hy.l1_init * hy.l1_mult;
489 let l1_step_eff = hy.l1_step * hy.l1_mult;
490 let mut best: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
492 let mut max_sp: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
494 for step in 0..hy.steps_a {
495 let chunk = &calib[step % calib.len()];
496 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; nl];
497 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = vec![None; nl];
498 let pass = Pass {
499 fm: &fm,
500 tau: hy.tau,
501 logits: &logits,
502 hard: false,
503 ffn: &ffn,
504 };
505 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
506 let l1_per = l1 / (inter as f64 * nl as f64);
508 for li in 0..nl {
509 for j in 0..inter {
510 let s = sigmoid(logits[li][j]) as f64;
511 dmask[li][j] = dmask[li][j] * s * (1.0 - s) + l1_per * s * (1.0 - s);
512 }
513 }
514 let mut params: Vec<&mut [f32]> = logits.iter_mut().map(|v| v.as_mut_slice()).collect();
515 adam_a.step(&mut params, &dmask, 1.0);
516 if (step + 1) % hy.eval_every == 0 {
517 l1 += l1_step_eff;
518 let pass = Pass {
519 fm: &fm,
520 tau: hy.tau,
521 logits: &logits,
522 hard: true,
523 ffn: &ffn,
524 };
525 let hp = held_ppl(&pass, &held);
526 let alive: usize = logits
527 .iter()
528 .map(|l| l.iter().filter(|&&x| sigmoid(x) > hy.tau).count())
529 .sum();
530 let sp = 1.0 - alive as f64 / (nl * inter) as f64;
531 if sp > max_sp.2 {
533 max_sp = (hp, Some(logits.clone()), sp);
534 }
535 if hy.target_sparsity > 0.0 {
537 if sp >= hy.target_sparsity && hp < best.0 {
538 best = (hp, Some(logits.clone()), sp);
539 }
540 } else if hp < best.0 {
541 best = (hp, Some(logits.clone()), sp);
542 }
543 log(&format!(
544 " [A] step {}: L1={l1:.3} pruned={:.0}% hard-PPL={hp:.3} (bottom {}@{:.0}%)",
545 step + 1,
546 sp * 100.0,
547 if best.0 == f64::MAX {
548 "—".to_string()
549 } else {
550 format!("{:.3}", best.0)
551 },
552 best.2 * 100.0
553 ));
554 }
555 }
556 if hy.target_sparsity > 0.0 && best.1.is_none() {
559 log(&format!(
560 "[A] target sparsity {:.0}% not reached; using max-sparsity checkpoint ({:.0}%)",
561 hy.target_sparsity * 100.0,
562 max_sp.2 * 100.0
563 ));
564 best = max_sp;
565 }
566 if let Some(b) = best.1.take() {
567 logits = b;
568 }
569 let pass = Pass {
570 fm: &fm,
571 tau: hy.tau,
572 logits: &logits,
573 hard: true,
574 ffn: &ffn,
575 };
576 let masked = held_ppl(&pass, &held);
577 log(&format!(
578 "[A] {:.0}s: masked-PPL {masked:.3}",
579 t0.elapsed().as_secs_f64()
580 ));
581
582 for &li in &fcd {
584 let l = &fm.layers[li];
585 ffn[li] = Some((l.gate.clone(), l.up.clone(), l.down.clone()));
586 }
587 let sizes: Vec<usize> = fcd
588 .iter()
589 .flat_map(|&li| {
590 let l = &fm.layers[li];
591 [l.gate.len(), l.up.len(), l.down.len()]
592 })
593 .collect();
594 let mut adam_b = Adam::new(&sizes, hy.lr_b);
595 let mut best_b: (f64, Option<Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>>>) = (masked, None);
596 for step in 0..hy.steps_b {
597 let chunk = &calib[step % calib.len()];
598 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; nl];
599 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = (0..nl)
600 .map(|li| {
601 ffn[li]
602 .as_ref()
603 .map(|(g, u, d)| (vec![0.0; g.len()], vec![0.0; u.len()], vec![0.0; d.len()]))
604 })
605 .collect();
606 let pass = Pass {
607 fm: &fm,
608 tau: hy.tau,
609 logits: &logits,
610 hard: true,
611 ffn: &ffn,
612 };
613 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
614 let lr_scale = 0.5 * (1.0 + (std::f64::consts::PI * step as f64 / hy.steps_b as f64).cos());
616 let first_fcd = fcd[0];
617 let mut params: Vec<&mut [f32]> = Vec::new();
618 let mut grads: Vec<Vec<f64>> = Vec::new();
619 for (off, slot) in ffn[first_fcd..].iter_mut().enumerate() {
620 let li = first_fcd + off;
621 let Some((g, u, d)) = slot.as_mut() else {
622 continue;
623 };
624 let (dg, du, dd) = dffn[li].take().unwrap();
625 params.push(g.as_mut_slice());
626 grads.push(dg);
627 params.push(u.as_mut_slice());
628 grads.push(du);
629 params.push(d.as_mut_slice());
630 grads.push(dd);
631 }
632 adam_b.step(&mut params, &grads, lr_scale);
633 if (step + 1) % hy.eval_every == 0 {
634 let pass = Pass {
635 fm: &fm,
636 tau: hy.tau,
637 logits: &logits,
638 hard: true,
639 ffn: &ffn,
640 };
641 let cur = held_ppl(&pass, &held);
642 if cur < best_b.0 {
643 best_b = (cur, Some(ffn.clone()));
644 }
645 log(&format!(
646 " [B] step {}: held-PPL {cur:.3} (best {:.3})",
647 step + 1,
648 best_b.0
649 ));
650 }
651 }
652 if let Some(b) = best_b.1.take() {
653 ffn = b;
654 }
655 let overlaid = best_b.0;
656
657 let keep = keep_masks(&logits, hy.tau, hy.align, hy.uniform_inter);
659 if hy.align > 1 || hy.uniform_inter {
660 let raw: usize = logits
661 .iter()
662 .map(|l| l.iter().filter(|&&x| sigmoid(x) > hy.tau).count())
663 .sum();
664 let padded: usize = keep
665 .iter()
666 .map(|a| a.iter().filter(|&&x| x).count())
667 .sum::<usize>()
668 - raw;
669 log(&format!(
670 "align: +{padded} neurons resurrected (align {}, uniform {})",
671 hy.align, hy.uniform_inter
672 ));
673 }
674 let mut down_out = Vec::with_capacity(nl);
675 let mut gate_up = Vec::with_capacity(nl);
676 let mut kept_per_layer = Vec::with_capacity(nl);
677 for li in 0..nl {
678 let alive = &keep[li];
679 kept_per_layer.push(alive.iter().filter(|&&a| a).count());
680 let l = &fm.layers[li];
681 let mut down = match &ffn[li] {
682 Some((_, _, d)) => d.clone(),
683 None => l.down.clone(),
684 };
685 let hsz = fm.hidden;
686 for r in 0..hsz {
687 for (c, &a) in alive.iter().enumerate() {
688 if !a {
689 down[r * inter + c] = 0.0;
690 }
691 }
692 }
693 gate_up.push(ffn[li].as_ref().map(|(g, u, _)| (g.clone(), u.clone())));
694 down_out.push(down);
695 }
696 let total: usize = kept_per_layer.iter().sum();
697 let report = BakeReport {
698 backbone,
699 masked,
700 overlaid,
701 pruned_ratio: 1.0 - total as f64 / (nl * inter) as f64,
702 kept_per_layer,
703 sec: t0.elapsed().as_secs_f64(),
704 };
705 let arts = BakeArtifacts {
706 keep,
707 down: down_out,
708 gate_up,
709 fcd_layers: fcd,
710 };
711 Ok((report, arts))
712}
713
714fn keep_masks(logits: &[Vec<f32>], tau: f32, align: usize, uniform: bool) -> Vec<Vec<bool>> {
722 let inter = logits[0].len();
723 let round = |n: usize| -> usize {
724 let n = n.max(1);
725 if align <= 1 {
726 n.min(inter)
727 } else {
728 (n.div_ceil(align) * align).min(inter)
729 }
730 };
731 let mut want: Vec<usize> = logits
732 .iter()
733 .map(|l| round(l.iter().filter(|&&x| sigmoid(x) > tau).count()))
734 .collect();
735 if uniform {
736 let k = want.iter().copied().max().unwrap_or(inter);
737 want = vec![k; logits.len()];
738 }
739 logits
740 .iter()
741 .zip(&want)
742 .map(|(l, &k)| {
743 let mut idx: Vec<usize> = (0..inter).collect();
744 idx.sort_unstable_by(|&a, &b| l[b].total_cmp(&l[a]));
745 let mut alive = vec![false; inter];
746 for &i in idx.iter().take(k) {
747 alive[i] = true;
748 }
749 alive
750 })
751 .collect()
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 fn kept(masks: &[Vec<bool>]) -> Vec<usize> {
759 masks
760 .iter()
761 .map(|m| m.iter().filter(|&&a| a).count())
762 .collect()
763 }
764
765 #[test]
768 fn keep_masks_aligns_up_and_preserves_alive() {
769 let inter = 96;
770 let l0: Vec<f32> = (0..inter)
773 .map(|i| if i < 40 { 1.0 } else { -1.0 - i as f32 * 0.01 })
774 .collect();
775 let l1: Vec<f32> = (0..inter)
777 .map(|i| if i < 64 { 2.0 } else { -3.0 })
778 .collect();
779 let masks = keep_masks(&[l0.clone(), l1], 0.5, 32, false);
780 assert_eq!(kept(&masks), vec![64, 64]);
781 for i in 0..64 {
784 assert!(masks[0][i], "neuron {i} should be kept");
785 }
786 for i in 64..inter {
787 assert!(!masks[0][i], "neuron {i} should stay pruned");
788 }
789 }
790
791 #[test]
793 fn keep_masks_uniform_takes_max() {
794 let inter = 96;
795 let l0: Vec<f32> = (0..inter)
796 .map(|i| if i < 10 { 1.0 } else { -2.0 })
797 .collect();
798 let l1: Vec<f32> = (0..inter)
799 .map(|i| if i < 70 { 1.0 } else { -2.0 })
800 .collect();
801 let masks = keep_masks(&[l0, l1], 0.5, 32, true);
802 assert_eq!(kept(&masks), vec![96, 96]);
803 }
804
805 #[test]
808 fn keep_masks_edges() {
809 let inter = 48;
810 let l: Vec<f32> = (0..inter)
811 .map(|i| if i < 47 { 1.0 } else { -2.0 })
812 .collect();
813 let masks = keep_masks(&[l.clone()], 0.5, 32, false);
814 assert_eq!(kept(&masks), vec![48]); let masks = keep_masks(&[l], 0.5, 1, false);
816 assert_eq!(kept(&masks), vec![47]);
817 let dead: Vec<f32> = vec![-5.0; inter];
818 let masks = keep_masks(&[dead], 0.5, 32, false);
819 assert_eq!(kept(&masks), vec![32]); }
821}