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 mut h_ins = Vec::with_capacity(nl);
224 let mut acts = Vec::with_capacity(nl);
225 let mut masks = Vec::with_capacity(nl);
226 for li in 0..nl {
227 let g = self.gates(li);
228 let wts = self.wts(li);
229 let want = grad.is_some();
230 let (h2, a) = fm.layer_forward_scaled(li, &h, 1, t, &wts, false, want, Some(&g));
231 h_ins.push(if want { h } else { Vec::new() });
232 acts.push(a);
233 masks.push(g);
234 h = h2;
235 }
236 let mut hn = vec![0f32; t * hsz];
238 let mut inv = vec![0f32; t];
239 ops::rmsnorm_fwd(&h, &fm.final_norm, fm.eps, fm.gemma, &mut hn, &mut inv);
240 let lm: &[f32] = fm.lm_head.as_deref().unwrap_or(&fm.embed);
241 let vocab = lm.len() / hsz;
242 let pool = fm.pool.as_deref();
243 let mut nll = 0f64;
244 let mut dh_n = vec![0f32; t * hsz]; const POS_CHUNK: usize = 32;
247 let scored = t - 1;
248 let mut p0 = 0usize;
249 while p0 < scored {
250 let pc = POS_CHUNK.min(scored - p0);
251 let mut logits = vec![0f32; pc * vocab];
252 ops::gemm_nt(
253 &hn[p0 * hsz..(p0 + pc) * hsz],
254 lm,
255 &mut logits,
256 pc,
257 hsz,
258 vocab,
259 pool,
260 );
261 for r in 0..pc {
262 let target = ids[p0 + r + 1] as usize;
263 let row = &mut logits[r * vocab..(r + 1) * vocab];
264 let mx = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64;
265 let mut sum = 0f64;
266 for v in row.iter() {
267 sum += ((*v as f64) - mx).exp();
268 }
269 nll += mx + sum.ln() - row[target] as f64;
270 if grad.is_some() {
271 let inv_n = 1.0 / scored as f64;
273 for v in row.iter_mut() {
274 *v = ((((*v as f64) - mx).exp() / sum) * inv_n) as f32;
275 }
276 row[target] -= inv_n as f32;
277 }
278 }
279 if grad.is_some() {
280 ops::gemm_dx(
281 &logits,
282 lm,
283 &mut dh_n[p0 * hsz..(p0 + pc) * hsz],
284 pc,
285 hsz,
286 vocab,
287 pool,
288 );
289 }
290 p0 += pc;
291 }
292 let Some((dmask, dffn)) = grad else {
293 return (nll, scored);
294 };
295 let mut dh = vec![0f32; t * hsz];
297 ops::rmsnorm_bwd(&h, &fm.final_norm, &inv, &dh_n, fm.gemma, &mut dh, None);
298 for li in (0..nl).rev() {
299 let a = acts[li].as_ref().expect("acts saved in grad mode");
300 let g = &masks[li];
301 let inter = fm.layers[li].inter;
302 let wts = self.wts(li);
303 let mut dact2 = vec![0f32; t * inter];
305 ops::gemm_dx(&dh, wts.down, &mut dact2, t, inter, hsz, fm.pool.as_deref());
306 if let Some((_, _, dd)) = dffn[li].as_mut() {
307 let mut act2 = a.act.clone();
309 for r in 0..t {
310 for (x, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
311 *x *= gv;
312 }
313 }
314 let mut dw = vec![0f32; hsz * inter];
315 ops::gemm_dw(&dh, &act2, &mut dw, t, inter, hsz, fm.pool.as_deref());
316 for (o, &x) in dd.iter_mut().zip(&dw) {
317 *o += x as f64;
318 }
319 }
320 {
322 let dm = &mut dmask[li];
323 for r in 0..t {
324 let da = &dact2[r * inter..(r + 1) * inter];
325 let aa = &a.act[r * inter..(r + 1) * inter];
326 for j in 0..inter {
327 dm[j] += da[j] as f64 * aa[j] as f64;
328 }
329 }
330 for (j, d) in dm.iter_mut().enumerate() {
332 let _ = j;
333 let _ = d;
334 }
335 }
336 let mut dg_pre = vec![0f32; t * inter];
338 let mut du_pre = vec![0f32; t * inter];
339 for r in 0..t {
340 for j in 0..inter {
341 let i = r * inter + j;
342 let da = dact2[i] * g[j];
343 let sg = ops::silu(a.gpre[i]);
344 dg_pre[i] = da * a.upre[i] * ops::silu_bwd(a.gpre[i]);
345 du_pre[i] = da * sg;
346 }
347 }
348 let mut dn2 = vec![0f32; t * hsz];
350 ops::gemm_dx(
351 &dg_pre,
352 wts.gate,
353 &mut dn2,
354 t,
355 hsz,
356 inter,
357 fm.pool.as_deref(),
358 );
359 let mut dn2b = vec![0f32; t * hsz];
360 ops::gemm_dx(
361 &du_pre,
362 wts.up,
363 &mut dn2b,
364 t,
365 hsz,
366 inter,
367 fm.pool.as_deref(),
368 );
369 for (x, &y) in dn2.iter_mut().zip(&dn2b) {
370 *x += y;
371 }
372 if let Some((dgw, duw, _)) = dffn[li].as_mut() {
373 let mut dw = vec![0f32; inter * hsz];
374 ops::gemm_dw(&dg_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
375 for (o, &x) in dgw.iter_mut().zip(&dw) {
376 *o += x as f64;
377 }
378 dw.fill(0.0);
379 ops::gemm_dw(&du_pre, &a.n2, &mut dw, t, hsz, inter, fm.pool.as_deref());
380 for (o, &x) in duw.iter_mut().zip(&dw) {
381 *o += x as f64;
382 }
383 }
384 let mut dh1 = dh.clone(); ops::rmsnorm_bwd(&a.h1, wts.pln, &a.inv2, &dn2, fm.gemma, &mut dh1, None);
388 dh = dh1;
389 let _ = &h_ins[li];
390 }
391 (nll, scored)
392 }
393}
394
395fn held_ppl(pass: &Pass, held: &[Vec<u32>]) -> f64 {
397 let mut nll = 0f64;
398 let mut n = 0usize;
399 for c in held {
400 let (l, k) = pass.chunk(c, None);
401 nll += l;
402 n += k;
403 }
404 (nll / n.max(1) as f64).exp()
405}
406
407pub fn skill_bake(
409 model: &Arc<CmfModel>,
410 chunks: &[Vec<u32>],
411 held_n: usize,
412 hy: &BakeHyper,
413 mut log: impl FnMut(&str),
414) -> Result<(BakeReport, BakeArtifacts), String> {
415 let t0 = std::time::Instant::now();
416 let o1_off = crate::nystrom::O1Cfg {
417 layers: crate::nystrom::O1Layers::List(Vec::new()),
418 m: 4,
419 w: 8,
420 sink: 1,
421 rect: crate::nystrom::O1_DEFAULT_RECT,
422 };
423 let fm = FcdModel::from_cmf(model, &o1_off)?;
424 let nl = fm.layers.len();
425 let inter = fm.layers.iter().map(|l| l.inter).max().unwrap_or(0);
426 if fm.layers.iter().any(|l| l.inter != inter) {
427 return Err("skill bake: non-uniform FFN widths".into());
428 }
429 let held: Vec<Vec<u32>> = chunks[..held_n.min(chunks.len())].to_vec();
430 let calib: Vec<Vec<u32>> = chunks[held_n.min(chunks.len())..].to_vec();
431 if calib.len() < 12 {
432 return Err(format!(
433 "skill bake: corpus too small ({} calib chunks)",
434 calib.len()
435 ));
436 }
437 let fcd: Vec<usize> = (nl.saturating_sub(hy.fcd_layers)..nl).collect();
438 let _rng = SplitMix64::new(hy.seed);
439
440 let mut logits: Vec<Vec<f32>> = vec![vec![2.0; inter]; nl];
442 let mut ffn: Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>> = vec![None; nl];
443
444 let open: Vec<Vec<f32>> = vec![vec![50.0; inter]; nl];
447 let base_pass = Pass {
448 fm: &fm,
449 tau: hy.tau,
450 logits: &open,
451 hard: true,
452 ffn: &ffn,
453 };
454 let backbone = held_ppl(&base_pass, &held);
455 log(&format!("baseline (full): {backbone:.3}"));
456
457 let mut adam_a = Adam::new(&vec![inter; nl], hy.lr_a);
459 let mut l1 = hy.l1_init * hy.l1_mult;
460 let l1_step_eff = hy.l1_step * hy.l1_mult;
461 let mut best: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
463 let mut max_sp: (f64, Option<Vec<Vec<f32>>>, f64) = (f64::MAX, None, 0.0);
465 for step in 0..hy.steps_a {
466 let chunk = &calib[step % calib.len()];
467 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; nl];
468 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = vec![None; nl];
469 let pass = Pass {
470 fm: &fm,
471 tau: hy.tau,
472 logits: &logits,
473 hard: false,
474 ffn: &ffn,
475 };
476 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
477 let l1_per = l1 / (inter as f64 * nl as f64);
479 for li in 0..nl {
480 for j in 0..inter {
481 let s = sigmoid(logits[li][j]) as f64;
482 dmask[li][j] = dmask[li][j] * s * (1.0 - s) + l1_per * s * (1.0 - s);
483 }
484 }
485 let mut params: Vec<&mut [f32]> = logits.iter_mut().map(|v| v.as_mut_slice()).collect();
486 adam_a.step(&mut params, &dmask, 1.0);
487 if (step + 1) % hy.eval_every == 0 {
488 l1 += l1_step_eff;
489 let pass = Pass {
490 fm: &fm,
491 tau: hy.tau,
492 logits: &logits,
493 hard: true,
494 ffn: &ffn,
495 };
496 let hp = held_ppl(&pass, &held);
497 let alive: usize = logits
498 .iter()
499 .map(|l| l.iter().filter(|&&x| sigmoid(x) > hy.tau).count())
500 .sum();
501 let sp = 1.0 - alive as f64 / (nl * inter) as f64;
502 if sp > max_sp.2 {
504 max_sp = (hp, Some(logits.clone()), sp);
505 }
506 if hy.target_sparsity > 0.0 {
508 if sp >= hy.target_sparsity && hp < best.0 {
509 best = (hp, Some(logits.clone()), sp);
510 }
511 } else if hp < best.0 {
512 best = (hp, Some(logits.clone()), sp);
513 }
514 log(&format!(
515 " [A] step {}: L1={l1:.3} pruned={:.0}% hard-PPL={hp:.3} (bottom {}@{:.0}%)",
516 step + 1,
517 sp * 100.0,
518 if best.0 == f64::MAX {
519 "—".to_string()
520 } else {
521 format!("{:.3}", best.0)
522 },
523 best.2 * 100.0
524 ));
525 }
526 }
527 if hy.target_sparsity > 0.0 && best.1.is_none() {
530 log(&format!(
531 "[A] target sparsity {:.0}% not reached; using max-sparsity checkpoint ({:.0}%)",
532 hy.target_sparsity * 100.0,
533 max_sp.2 * 100.0
534 ));
535 best = max_sp;
536 }
537 if let Some(b) = best.1.take() {
538 logits = b;
539 }
540 let pass = Pass {
541 fm: &fm,
542 tau: hy.tau,
543 logits: &logits,
544 hard: true,
545 ffn: &ffn,
546 };
547 let masked = held_ppl(&pass, &held);
548 log(&format!(
549 "[A] {:.0}s: masked-PPL {masked:.3}",
550 t0.elapsed().as_secs_f64()
551 ));
552
553 for &li in &fcd {
555 let l = &fm.layers[li];
556 ffn[li] = Some((l.gate.clone(), l.up.clone(), l.down.clone()));
557 }
558 let sizes: Vec<usize> = fcd
559 .iter()
560 .flat_map(|&li| {
561 let l = &fm.layers[li];
562 [l.gate.len(), l.up.len(), l.down.len()]
563 })
564 .collect();
565 let mut adam_b = Adam::new(&sizes, hy.lr_b);
566 let mut best_b: (f64, Option<Vec<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>>>) = (masked, None);
567 for step in 0..hy.steps_b {
568 let chunk = &calib[step % calib.len()];
569 let mut dmask: Vec<Vec<f64>> = vec![vec![0.0; inter]; nl];
570 let mut dffn: Vec<Option<(Vec<f64>, Vec<f64>, Vec<f64>)>> = (0..nl)
571 .map(|li| {
572 ffn[li]
573 .as_ref()
574 .map(|(g, u, d)| (vec![0.0; g.len()], vec![0.0; u.len()], vec![0.0; d.len()]))
575 })
576 .collect();
577 let pass = Pass {
578 fm: &fm,
579 tau: hy.tau,
580 logits: &logits,
581 hard: true,
582 ffn: &ffn,
583 };
584 let _ = pass.chunk(chunk, Some((&mut dmask, &mut dffn)));
585 let lr_scale = 0.5 * (1.0 + (std::f64::consts::PI * step as f64 / hy.steps_b as f64).cos());
587 let first_fcd = fcd[0];
588 let mut params: Vec<&mut [f32]> = Vec::new();
589 let mut grads: Vec<Vec<f64>> = Vec::new();
590 for (off, slot) in ffn[first_fcd..].iter_mut().enumerate() {
591 let li = first_fcd + off;
592 let Some((g, u, d)) = slot.as_mut() else {
593 continue;
594 };
595 let (dg, du, dd) = dffn[li].take().unwrap();
596 params.push(g.as_mut_slice());
597 grads.push(dg);
598 params.push(u.as_mut_slice());
599 grads.push(du);
600 params.push(d.as_mut_slice());
601 grads.push(dd);
602 }
603 adam_b.step(&mut params, &grads, lr_scale);
604 if (step + 1) % hy.eval_every == 0 {
605 let pass = Pass {
606 fm: &fm,
607 tau: hy.tau,
608 logits: &logits,
609 hard: true,
610 ffn: &ffn,
611 };
612 let cur = held_ppl(&pass, &held);
613 if cur < best_b.0 {
614 best_b = (cur, Some(ffn.clone()));
615 }
616 log(&format!(
617 " [B] step {}: held-PPL {cur:.3} (best {:.3})",
618 step + 1,
619 best_b.0
620 ));
621 }
622 }
623 if let Some(b) = best_b.1.take() {
624 ffn = b;
625 }
626 let overlaid = best_b.0;
627
628 let keep = keep_masks(&logits, hy.tau, hy.align, hy.uniform_inter);
630 if hy.align > 1 || hy.uniform_inter {
631 let raw: usize = logits
632 .iter()
633 .map(|l| l.iter().filter(|&&x| sigmoid(x) > hy.tau).count())
634 .sum();
635 let padded: usize = keep
636 .iter()
637 .map(|a| a.iter().filter(|&&x| x).count())
638 .sum::<usize>()
639 - raw;
640 log(&format!(
641 "align: +{padded} neurons resurrected (align {}, uniform {})",
642 hy.align, hy.uniform_inter
643 ));
644 }
645 let mut down_out = Vec::with_capacity(nl);
646 let mut gate_up = Vec::with_capacity(nl);
647 let mut kept_per_layer = Vec::with_capacity(nl);
648 for li in 0..nl {
649 let alive = &keep[li];
650 kept_per_layer.push(alive.iter().filter(|&&a| a).count());
651 let l = &fm.layers[li];
652 let mut down = match &ffn[li] {
653 Some((_, _, d)) => d.clone(),
654 None => l.down.clone(),
655 };
656 let hsz = fm.hidden;
657 for r in 0..hsz {
658 for (c, &a) in alive.iter().enumerate() {
659 if !a {
660 down[r * inter + c] = 0.0;
661 }
662 }
663 }
664 gate_up.push(ffn[li].as_ref().map(|(g, u, _)| (g.clone(), u.clone())));
665 down_out.push(down);
666 }
667 let total: usize = kept_per_layer.iter().sum();
668 let report = BakeReport {
669 backbone,
670 masked,
671 overlaid,
672 pruned_ratio: 1.0 - total as f64 / (nl * inter) as f64,
673 kept_per_layer,
674 sec: t0.elapsed().as_secs_f64(),
675 };
676 let arts = BakeArtifacts {
677 keep,
678 down: down_out,
679 gate_up,
680 fcd_layers: fcd,
681 };
682 Ok((report, arts))
683}
684
685fn keep_masks(logits: &[Vec<f32>], tau: f32, align: usize, uniform: bool) -> Vec<Vec<bool>> {
693 let inter = logits[0].len();
694 let round = |n: usize| -> usize {
695 let n = n.max(1);
696 if align <= 1 {
697 n.min(inter)
698 } else {
699 (n.div_ceil(align) * align).min(inter)
700 }
701 };
702 let mut want: Vec<usize> = logits
703 .iter()
704 .map(|l| round(l.iter().filter(|&&x| sigmoid(x) > tau).count()))
705 .collect();
706 if uniform {
707 let k = want.iter().copied().max().unwrap_or(inter);
708 want = vec![k; logits.len()];
709 }
710 logits
711 .iter()
712 .zip(&want)
713 .map(|(l, &k)| {
714 let mut idx: Vec<usize> = (0..inter).collect();
715 idx.sort_unstable_by(|&a, &b| l[b].total_cmp(&l[a]));
716 let mut alive = vec![false; inter];
717 for &i in idx.iter().take(k) {
718 alive[i] = true;
719 }
720 alive
721 })
722 .collect()
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728
729 fn kept(masks: &[Vec<bool>]) -> Vec<usize> {
730 masks
731 .iter()
732 .map(|m| m.iter().filter(|&&a| a).count())
733 .collect()
734 }
735
736 #[test]
739 fn keep_masks_aligns_up_and_preserves_alive() {
740 let inter = 96;
741 let l0: Vec<f32> = (0..inter)
744 .map(|i| if i < 40 { 1.0 } else { -1.0 - i as f32 * 0.01 })
745 .collect();
746 let l1: Vec<f32> = (0..inter)
748 .map(|i| if i < 64 { 2.0 } else { -3.0 })
749 .collect();
750 let masks = keep_masks(&[l0.clone(), l1], 0.5, 32, false);
751 assert_eq!(kept(&masks), vec![64, 64]);
752 for i in 0..64 {
755 assert!(masks[0][i], "neuron {i} should be kept");
756 }
757 for i in 64..inter {
758 assert!(!masks[0][i], "neuron {i} should stay pruned");
759 }
760 }
761
762 #[test]
764 fn keep_masks_uniform_takes_max() {
765 let inter = 96;
766 let l0: Vec<f32> = (0..inter)
767 .map(|i| if i < 10 { 1.0 } else { -2.0 })
768 .collect();
769 let l1: Vec<f32> = (0..inter)
770 .map(|i| if i < 70 { 1.0 } else { -2.0 })
771 .collect();
772 let masks = keep_masks(&[l0, l1], 0.5, 32, true);
773 assert_eq!(kept(&masks), vec![96, 96]);
774 }
775
776 #[test]
779 fn keep_masks_edges() {
780 let inter = 48;
781 let l: Vec<f32> = (0..inter)
782 .map(|i| if i < 47 { 1.0 } else { -2.0 })
783 .collect();
784 let masks = keep_masks(&[l.clone()], 0.5, 32, false);
785 assert_eq!(kept(&masks), vec![48]); let masks = keep_masks(&[l], 0.5, 1, false);
787 assert_eq!(kept(&masks), vec![47]);
788 let dead: Vec<f32> = vec![-5.0; inter];
789 let masks = keep_masks(&[dead], 0.5, 32, false);
790 assert_eq!(kept(&masks), vec![32]); }
792}