1use crate::fcd_ops::{self as ops, NysCfg};
25use crate::nystrom::{O1Cfg, O1Layers};
26use crate::pipeline::{DenseFfn, FfnKind, Pipeline};
27use crate::pool::Pool;
28use crate::qtensor::QTensor;
29use crate::sampler::{SamplerConfig, SplitMix64};
30use cortiq_core::{CmfModel, LayerType, NormStyle, TensorDtype};
31use std::sync::Arc;
32
33const LM_CHUNK: usize = 32;
36
37const ADAM_B1: f64 = 0.9;
39const ADAM_B2: f64 = 0.999;
40const ADAM_EPS: f64 = 1e-8;
41const ADAM_WD: f64 = 0.01;
42
43#[derive(Clone, Debug)]
45pub struct FcdHyper {
46 pub steps: usize,
47 pub lr: f64,
48 pub kl_w: f64,
49 pub eval_every: usize,
50 pub bs: usize,
51 pub seq: usize,
52 pub seed: u64,
53}
54
55impl Default for FcdHyper {
56 fn default() -> Self {
57 Self {
58 steps: 300,
59 lr: 5e-5,
60 kl_w: 0.7,
61 eval_every: 25,
62 bs: 2,
63 seq: 512,
64 seed: 0,
65 }
66 }
67}
68
69#[derive(Clone, Debug)]
72pub struct FcdReport {
73 pub converted: Vec<usize>,
74 pub teacher_ppl: f64,
76 pub ppl_start: f64,
78 pub ppl_best: f64,
80 pub best_step: usize,
81 pub ppl_final: f64,
83 pub steps_run: usize,
84 pub sec_per_step: f64,
85 pub losses: Vec<(f64, f64)>,
87 pub gate: Option<GateReport>,
89}
90
91#[derive(Clone, Debug)]
93pub struct GateReport {
94 pub baseline: Vec<f64>,
96 pub evals: Vec<(usize, f64, Vec<f64>, bool)>,
98 pub chosen: Option<usize>,
101}
102
103pub fn loop_score(ids: &[u32]) -> f64 {
108 if ids.len() < 5 {
109 return 0.0;
110 }
111 let grams: std::collections::HashSet<&[u32]> = ids.windows(4).collect();
112 1.0 - grams.len() as f64 / ids.windows(4).count() as f64
113}
114
115#[derive(Clone, Debug)]
119pub struct GenGateCfg {
120 pub prompts: Vec<Vec<u32>>,
123 pub gen_tokens: usize,
124 pub threshold: f64,
126 pub baseline_slack: f64,
128}
129
130impl GenGateCfg {
131 pub fn standard(va: &[u32]) -> Option<Self> {
134 let l = va.len().saturating_sub(500);
135 if l < 400 {
136 return None;
137 }
138 let prompts = [l / 10, l / 2, 8 * l / 10]
139 .iter()
140 .map(|&off| va[off..off + 400].to_vec())
141 .collect();
142 Some(Self {
143 prompts,
144 gen_tokens: 60,
145 threshold: 0.35,
146 baseline_slack: 0.10,
147 })
148 }
149}
150
151pub fn gate_pass(scores: &[f64], baseline: &[f64], threshold: f64, slack: f64) -> bool {
155 scores
156 .iter()
157 .zip(baseline)
158 .all(|(&s, &b)| s <= threshold && s <= b + slack)
159}
160
161pub fn select_checkpoint(
166 evals: &[(usize, f64, Vec<f64>)],
167 baseline: &[f64],
168 threshold: f64,
169 slack: f64,
170) -> Option<usize> {
171 let mut best: Option<usize> = None;
172 for (i, (_, ppl, scores)) in evals.iter().enumerate() {
173 if !gate_pass(scores, baseline, threshold, slack) {
174 continue;
175 }
176 if best.map(|b| *ppl < evals[b].1).unwrap_or(true) {
177 best = Some(i);
178 }
179 }
180 best
181}
182
183pub mod prof {
189 use std::sync::atomic::{AtomicU64, Ordering};
190 pub static ATTN_FWD: AtomicU64 = AtomicU64::new(0);
191 pub static FFN_FWD: AtomicU64 = AtomicU64::new(0);
192 pub static BWD: AtomicU64 = AtomicU64::new(0);
193 pub static GEMM: AtomicU64 = AtomicU64::new(0);
194 pub static GEMM_CALLS: AtomicU64 = AtomicU64::new(0);
195 #[inline]
196 pub fn add(c: &AtomicU64, t: std::time::Instant) {
197 c.fetch_add(t.elapsed().as_nanos() as u64, Ordering::Relaxed);
198 }
199 pub fn take(c: &AtomicU64) -> f64 {
201 c.swap(0, Ordering::Relaxed) as f64 / 1e9
202 }
203
204 use std::sync::Mutex;
205 pub static SHAPES: Mutex<Vec<((usize, usize, usize), (u64, u64))>> = Mutex::new(Vec::new());
209 pub fn gemm_shape(n: usize, k: usize, m: usize, t0: std::time::Instant) {
210 let ns = t0.elapsed().as_nanos() as u64;
211 let mut g = SHAPES.lock().unwrap();
212 match g.iter_mut().find(|(s, _)| *s == (n, k, m)) {
213 Some((_, (c, tt))) => {
214 *c += 1;
215 *tt += ns;
216 }
217 None => g.push(((n, k, m), (1, ns))),
218 }
219 }
220 pub fn shape_report(top: usize) -> String {
221 let mut g = SHAPES.lock().unwrap();
222 g.sort_by_key(|(_, (_, ns))| std::cmp::Reverse(*ns));
223 let out = g
224 .iter()
225 .take(top)
226 .map(|((n, k, m), (c, ns))| {
227 format!(
228 " [{n}x{k}x{m}] {c} calls, {:.1}s total, {:.1} ms/call",
229 *ns as f64 / 1e9,
230 *ns as f64 / 1e6 / *c as f64
231 )
232 })
233 .collect::<Vec<_>>()
234 .join("\n");
235 g.clear();
236 out
237 }
238}
239
240enum FcdAttn {
243 Full {
244 qrows: usize,
248 q_norm: Option<Vec<f32>>,
249 k_norm: Option<Vec<f32>>,
250 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
251 output_gate: bool,
254 },
255 Gdn {
258 wqkv: Vec<f32>,
259 wz: Vec<f32>,
260 wa: Vec<f32>,
261 wb: Vec<f32>,
262 conv: Vec<f32>,
263 a_log: Vec<f32>,
264 dt_bias: Vec<f32>,
265 norm: Vec<f32>,
266 wout: Vec<f32>,
267 },
268}
269
270pub(crate) struct FcdLayer {
271 attn: FcdAttn,
272 pub(crate) inter: usize,
273 pub(crate) iln: Vec<f32>,
276 pub(crate) pln: Vec<f32>,
277}
278
279#[derive(Clone, Copy)]
281struct GdnDims {
282 nv: usize,
283 nk: usize,
284 dk: usize,
285 dv: usize,
286 kk: usize,
287}
288
289impl GdnDims {
290 fn c_dim(&self) -> usize {
291 2 * self.nk * self.dk + self.nv * self.dv
292 }
293 fn vd(&self) -> usize {
294 self.nv * self.dv
295 }
296}
297
298pub struct FcdModel {
300 pub hidden: usize,
301 pub nh: usize,
302 pub nkv: usize,
303 pub hd: usize,
304 pub nl: usize,
305 pub vocab: usize,
306 pub(crate) eps: f64,
307 pub(crate) gemma: bool,
308 rotary_dim: usize,
309 inv_freq: Vec<f64>,
310 pub(crate) embed: Vec<f32>,
312 pub(crate) lm_head: Option<Vec<f32>>,
313 pub(crate) final_norm: Vec<f32>,
314 pub(crate) layers: Vec<FcdLayer>,
315 pub(crate) src: std::sync::Arc<CmfModel>,
318 mats_cache: Vec<std::sync::Mutex<Option<std::sync::Arc<LayerMats>>>>,
323 mats_window: usize,
325 o1_flags: Vec<bool>,
327 nys: NysCfg,
328 gdn: Option<GdnDims>,
330 pub(crate) loops: usize,
337 pub(crate) loop_norm: bool,
338 pub(crate) pool: Option<Arc<Pool>>,
339}
340
341fn deq(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
342 let e = model
343 .tensor(name)
344 .ok_or_else(|| format!("tensor '{name}' not found"))?;
345 let mut out = vec![0f32; e.n_elems()];
346 cortiq_core::quant::dequant_tensor(e, model.entry_bytes(e), &mut out)?;
347 Ok(out)
348}
349
350
351
352fn physical_total_bytes() -> Option<u64> {
354 #[cfg(target_os = "macos")]
355 {
356 let out = std::process::Command::new("sysctl")
357 .args(["-n", "hw.memsize"])
358 .output()
359 .ok()?;
360 return String::from_utf8_lossy(&out.stdout).trim().parse().ok();
361 }
362 #[cfg(target_os = "linux")]
363 {
364 let mem = std::fs::read_to_string("/proc/meminfo").ok()?;
365 let kb: u64 = mem
366 .lines()
367 .find(|l| l.starts_with("MemTotal:"))?
368 .split_whitespace()
369 .nth(1)?
370 .parse()
371 .ok()?;
372 return Some(kb * 1024);
373 }
374 #[allow(unreachable_code)]
375 None
376}
377
378fn available_ram_bytes() -> Option<u64> {
381 #[cfg(target_os = "macos")]
382 {
383 let total: u64 = {
387 let out = std::process::Command::new("sysctl")
388 .args(["-n", "hw.memsize"])
389 .output()
390 .ok()?;
391 String::from_utf8_lossy(&out.stdout).trim().parse().ok()?
392 };
393 let out = std::process::Command::new("memory_pressure")
394 .arg("-Q")
395 .output()
396 .ok()?;
397 let text = String::from_utf8_lossy(&out.stdout);
398 let pct: u64 = text
399 .lines()
400 .find(|l| l.contains("free percentage"))?
401 .split(':')
402 .nth(1)?
403 .trim()
404 .trim_end_matches('%')
405 .parse()
406 .ok()?;
407 return Some(total / 100 * pct);
408 }
409 #[cfg(target_os = "linux")]
410 {
411 let mem = std::fs::read_to_string("/proc/meminfo").ok()?;
412 let kb: u64 = mem
413 .lines()
414 .find(|l| l.starts_with("MemAvailable:"))?
415 .split_whitespace()
416 .nth(1)?
417 .parse()
418 .ok()?;
419 return Some(kb * 1024);
420 }
421 #[allow(unreachable_code)]
422 None
423}
424
425
426pub(crate) fn deq_pub(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
429 deq(model, name)
430}
431
432pub(crate) struct LayerMats {
437 pub(crate) wqkv: Vec<f32>,
438 pub(crate) wo: Vec<f32>,
439 pub(crate) gu: Vec<f32>,
440 pub(crate) down: Vec<f32>,
441}
442
443impl FcdModel {
444 pub(crate) fn mats(&self, li: usize) -> Result<std::sync::Arc<LayerMats>, String> {
449 {
450 let slot = self.mats_cache[li].lock().unwrap();
451 if let Some(m) = slot.as_ref() {
452 return Ok(m.clone());
453 }
454 }
455 let p = format!("model.layers.{li}.");
456 let d = |name: String| -> Result<Vec<f32>, String> { deq(&self.src, &name) };
457 let built = if matches!(self.layers[li].attn, FcdAttn::Full { .. }) {
458 let wq = d(format!("{p}self_attn.q_proj.weight"))?;
459 let wk = d(format!("{p}self_attn.k_proj.weight"))?;
460 let wv = d(format!("{p}self_attn.v_proj.weight"))?;
461 let mut wqkv = Vec::with_capacity(wq.len() + wk.len() + wv.len());
462 wqkv.extend_from_slice(&wq);
463 wqkv.extend_from_slice(&wk);
464 wqkv.extend_from_slice(&wv);
465 let gate = d(format!("{p}mlp.gate_proj.weight"))?;
466 let up = d(format!("{p}mlp.up_proj.weight"))?;
467 let mut gu = Vec::with_capacity(gate.len() + up.len());
468 gu.extend_from_slice(&gate);
469 gu.extend_from_slice(&up);
470 LayerMats {
471 wqkv,
472 wo: d(format!("{p}self_attn.o_proj.weight"))?,
473 gu,
474 down: d(format!("{p}mlp.down_proj.weight"))?,
475 }
476 } else {
477 let gate = d(format!("{p}mlp.gate_proj.weight"))?;
480 let up = d(format!("{p}mlp.up_proj.weight"))?;
481 let mut gu = Vec::with_capacity(gate.len() + up.len());
482 gu.extend_from_slice(&gate);
483 gu.extend_from_slice(&up);
484 LayerMats {
485 wqkv: Vec::new(),
486 wo: Vec::new(),
487 gu,
488 down: d(format!("{p}mlp.down_proj.weight"))?,
489 }
490 };
491 let arc = std::sync::Arc::new(built);
492 *self.mats_cache[li].lock().unwrap() = Some(arc.clone());
493 if self.mats_window > 0 {
496 let w = self.mats_window;
497 for (j, slot) in self.mats_cache.iter().enumerate() {
498 let dist = li.abs_diff(j).min(self.layers.len() - li.abs_diff(j));
499 if dist > w {
500 *slot.lock().unwrap() = None;
501 }
502 }
503 }
504 Ok(arc)
505 }
506}
507
508impl FcdModel {
509 pub fn from_cmf(model: &std::sync::Arc<CmfModel>, o1: &O1Cfg) -> Result<Self, String> {
512 let arch = model.arch().clone();
513 if arch.hidden_act != "silu" {
514 return Err(format!(
515 "fcd/skill-bake: hidden_act '{}' not supported yet (SiLU only)",
516 arch.hidden_act
517 ));
518 }
519 let has_linear = arch
520 .layer_types
521 .iter()
522 .any(|t| matches!(t, LayerType::LinearAttention));
523 let gdn = if has_linear {
524 let lc = arch
525 .linear_core
526 .as_ref()
527 .ok_or_else(|| "model has linear layers but no arch.linear_core".to_string())?;
528 if lc.kind != "gated_delta_net" {
529 return Err(format!(
530 "linear core '{}' has no FCD backward (only gated_delta_net)",
531 lc.kind
532 ));
533 }
534 Some(GdnDims {
535 nv: lc.num_heads,
536 nk: arch
537 .linear_num_key_heads
538 .ok_or("linear core needs arch.linear_num_key_heads")?,
539 dk: arch
540 .linear_key_head_dim
541 .ok_or("linear core needs arch.linear_key_head_dim")?,
542 dv: lc.value_head_dim,
543 kk: arch
544 .linear_conv_kernel_dim
545 .ok_or("linear core needs arch.linear_conv_kernel_dim")?,
546 })
547 } else {
548 None
549 };
550 let (nh, nkv, hd, h) = (
551 arch.num_attention_heads,
552 arch.num_kv_heads,
553 arch.head_dim,
554 arch.hidden_size,
555 );
556 let embed = deq(model, "model.embed_tokens.weight")?;
557 let lm_head = if model.tensor("lm_head.weight").is_some() {
558 Some(deq(model, "lm_head.weight")?)
559 } else if arch.tie_word_embeddings {
560 None
561 } else {
562 return Err("no lm_head.weight and tie_word_embeddings is false".into());
563 };
564 let final_norm = deq(model, "model.norm.weight")?;
565
566 let mut layers = Vec::with_capacity(arch.num_layers);
567 for li in 0..arch.num_layers {
568 let p = format!("model.layers.{li}.");
569 if model.tensor(&format!("{p}mlp.gate.weight")).is_some() {
570 return Err(format!(
571 "layer {li} is MoE — FCD polish supports dense FFN only"
572 ));
573 }
574 let attn = match arch.layer_types.get(li) {
575 Some(LayerType::LinearAttention) => {
576 let la = |n: &str| deq(model, &format!("{p}linear_attn.{n}"));
577 FcdAttn::Gdn {
578 wqkv: la("in_proj_qkv.weight")?,
579 wz: la("in_proj_z.weight")?,
580 wa: la("in_proj_a.weight")?,
581 wb: la("in_proj_b.weight")?,
582 conv: la("conv1d.weight")?,
583 a_log: la("A_log")?,
584 dt_bias: la("dt_bias")?,
585 norm: la("norm.weight")?,
586 wout: la("out_proj.weight")?,
587 }
588 }
589 _ => {
590 let wq = deq(model, &format!("{p}self_attn.q_proj.weight"))?;
591 let output_gate = wq.len() == 2 * nh * hd * h;
592 let opt = |n: &str| -> Option<Vec<f32>> {
593 model
594 .tensor(&format!("{p}self_attn.{n}"))
595 .and_then(|_| deq(model, &format!("{p}self_attn.{n}")).ok())
596 };
597 let bias = match (opt("q_proj.bias"), opt("k_proj.bias"), opt("v_proj.bias")) {
598 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
599 _ => None,
600 };
601 let qrows = wq.len() / h;
602 drop(wq);
603 FcdAttn::Full {
604 qrows,
605 q_norm: opt("q_norm.weight"),
606 k_norm: opt("k_norm.weight"),
607 bias,
608 output_gate,
609 }
610 }
611 };
612 let gate = deq(model, &format!("{p}mlp.gate_proj.weight"))?;
613 let inter = gate.len() / h;
614 drop(gate);
615 layers.push(FcdLayer {
616 attn,
617 inter,
618 iln: deq(model, &format!("{p}input_layernorm.weight"))?,
619 pln: deq(model, &format!("{p}post_attention_layernorm.weight"))?,
620 });
621 }
622
623 let rotary_dim = ((hd as f32 * arch.partial_rotary_factor) as usize)
624 .max(2)
625 .min(hd);
626 let base = arch.rope_theta;
627 let inv_freq: Vec<f64> = (0..rotary_dim / 2)
628 .map(|i| 1.0 / base.powf(2.0 * i as f64 / rotary_dim as f64))
629 .collect();
630 let loops = arch.num_loops.max(1);
631 let loop_norm = arch.loop_final_norm;
632 let mut flags = o1.layer_flags(arch.num_layers);
633 flags.resize(arch.num_layers, false);
634 for (li, f) in flags.iter_mut().enumerate() {
637 if *f && !matches!(layers[li].attn, FcdAttn::Full { .. }) {
638 *f = false;
639 }
640 }
641 let per_layer: u64 = layers
645 .first()
646 .map(|_| {
647 let qrows = match layers[0].attn {
648 FcdAttn::Full { qrows, .. } => qrows,
649 _ => 0,
650 };
651 ((qrows + 2 * (nkv * hd)) as u64 * h as u64 + (h as u64 * nh as u64 * hd as u64) + 3 * (layers[0].inter as u64 * h as u64)) * 4
655 })
656 .unwrap_or(0);
657 let mats_window = if let Ok(v) = std::env::var("CMF_BAKE_MATS_LAYERS") {
658 v.parse().unwrap_or(0)
659 } else {
660 let avail = available_ram_bytes().unwrap_or(u64::MAX);
661 let cap = physical_total_bytes()
662 .map(|t| t / 5 * 3)
663 .unwrap_or(u64::MAX)
664 .min(avail.saturating_sub(avail / 10));
665 let all = per_layer.saturating_mul(arch.num_layers as u64);
666 if all + 4 * 1024 * 1024 * 1024 <= cap {
667 0 } else {
669 let w = (cap.saturating_sub(3 * 1024 * 1024 * 1024) / per_layer.max(1))
670 .clamp(2, arch.num_layers as u64) as usize;
671 tracing::info!(
672 "fcd replica: streaming {w} of {} layers (~{:.1} GB resident of ~{:.1} GB total)",
673 arch.num_layers,
674 (w as u64 * per_layer) as f64 / 1e9,
675 all as f64 / 1e9,
676 );
677 w
678 }
679 };
680 let mats_cache = (0..arch.num_layers)
681 .map(|_| std::sync::Mutex::new(None))
682 .collect();
683 Ok(Self {
684 src: model.clone(),
685 mats_cache,
686 mats_window,
687 hidden: h,
688 nh,
689 nkv,
690 hd,
691 nl: arch.num_layers,
692 vocab: arch.vocab_size.min(embed.len() / h),
693 eps: arch.rms_norm_eps,
694 gemma: matches!(arch.norm_style, NormStyle::Gemma),
695 rotary_dim,
696 inv_freq,
697 embed,
698 lm_head,
699 final_norm,
700 layers,
701 o1_flags: flags,
702 nys: NysCfg {
705 m: o1.m,
706 w: o1.w,
707 sink: o1.sink,
708 prefill: None,
709 },
710 gdn,
711 loops,
712 loop_norm,
713 pool: Pool::from_env(),
714 })
715 }
716
717 pub fn converted(&self) -> Vec<usize> {
719 (0..self.nl).filter(|&i| self.o1_flags[i]).collect()
720 }
721
722 fn head_weight(&self) -> &[f32] {
723 self.lm_head.as_deref().unwrap_or(&self.embed)
724 }
725}
726
727const PARAMS_PER_LAYER: usize = 5; pub struct TrainState {
734 pub layers: Vec<usize>,
735 pub data: Vec<Vec<f32>>,
737 grad: Vec<Vec<f32>>,
738 m1: Vec<Vec<f32>>,
739 m2: Vec<Vec<f32>>,
740 step_t: u64,
741}
742
743impl TrainState {
744 pub fn new(fm: &FcdModel) -> Self {
745 let layers = fm.converted();
746 let mut data = Vec::with_capacity(layers.len() * PARAMS_PER_LAYER);
747 for &li in &layers {
748 let l = &fm.layers[li];
749 let p = format!("model.layers.{li}.");
750 data.push(l.iln.clone());
751 data.push(l.pln.clone());
752 data.push(deq(&fm.src, &format!("{p}mlp.gate_proj.weight")).expect("gate"));
755 data.push(deq(&fm.src, &format!("{p}mlp.up_proj.weight")).expect("up"));
756 data.push(deq(&fm.src, &format!("{p}mlp.down_proj.weight")).expect("down"));
757 }
758 let zeros: Vec<Vec<f32>> = data.iter().map(|d| vec![0f32; d.len()]).collect();
759 Self {
760 layers,
761 grad: zeros.clone(),
762 m1: zeros.clone(),
763 m2: zeros,
764 data,
765 step_t: 0,
766 }
767 }
768
769 fn slot(&self, li: usize) -> Option<usize> {
770 self.layers.iter().position(|&x| x == li)
771 }
772
773 #[doc(hidden)]
775 pub fn grads(&self) -> &[Vec<f32>] {
776 &self.grad
777 }
778
779 fn zero_grad(&mut self) {
780 for g in &mut self.grad {
781 for v in g.iter_mut() {
782 *v = 0.0;
783 }
784 }
785 }
786
787 fn clip_and_step(&mut self, lr: f64) -> f64 {
790 let mut sq = 0f64;
791 for g in &self.grad {
792 for &v in g {
793 sq += (v as f64) * (v as f64);
794 }
795 }
796 let gn = sq.sqrt();
797 let scale = if gn > 1.0 { 1.0 / (gn + 1e-6) } else { 1.0 };
798 self.step_t += 1;
799 let bc1 = 1.0 - ADAM_B1.powi(self.step_t as i32);
800 let bc2 = 1.0 - ADAM_B2.powi(self.step_t as i32);
801 for p in 0..self.data.len() {
802 let (d, g, m, v) = (
803 &mut self.data[p],
804 &self.grad[p],
805 &mut self.m1[p],
806 &mut self.m2[p],
807 );
808 for i in 0..d.len() {
809 let gi = g[i] as f64 * scale;
810 let mi = ADAM_B1 * m[i] as f64 + (1.0 - ADAM_B1) * gi;
811 let vi = ADAM_B2 * v[i] as f64 + (1.0 - ADAM_B2) * gi * gi;
812 m[i] = mi as f32;
813 v[i] = vi as f32;
814 let upd = (mi / bc1) / ((vi / bc2).sqrt() + ADAM_EPS) + ADAM_WD * d[i] as f64;
815 d[i] = (d[i] as f64 - lr * upd) as f32;
816 }
817 }
818 gn
819 }
820}
821
822#[derive(Clone, Copy)]
825pub(crate) struct LnFfn<'a> {
826 pub(crate) iln: &'a [f32],
827 pub(crate) pln: &'a [f32],
828 pub(crate) gate: &'a [f32],
829 pub(crate) up: &'a [f32],
830 pub(crate) down: &'a [f32],
831 pub(crate) gu: Option<&'a [f32]>,
835}
836
837fn ln_ffn<'a>(fm: &'a FcdModel, ts: Option<&'a TrainState>, li: usize, mats: &'a LayerMats) -> LnFfn<'a> {
838 if let Some(t) = ts {
839 if let Some(s) = t.slot(li) {
840 let b = s * PARAMS_PER_LAYER;
841 return LnFfn {
842 iln: &t.data[b],
843 pln: &t.data[b + 1],
844 gate: &t.data[b + 2],
845 up: &t.data[b + 3],
846 down: &t.data[b + 4],
847 gu: None,
848 };
849 }
850 }
851 let l = &fm.layers[li];
852 LnFfn {
853 iln: &l.iln,
854 pln: &l.pln,
855 gate: &[],
858 up: &[],
859 down: &mats.down,
860 gu: Some(&mats.gu),
861 }
862}
863
864enum AttnActs {
868 Full {
869 qpre: Vec<f32>,
870 kpre: Vec<f32>,
871 vproj: Vec<f32>,
872 qrot: Vec<f32>,
873 krot: Vec<f32>,
874 qinv: Vec<f32>,
875 kinv: Vec<f32>,
876 ao: Vec<f32>,
879 gate_pre: Vec<f32>,
881 },
882 Gdn {
885 qkv: Vec<f32>,
886 z: Vec<f32>,
887 a: Vec<f32>,
888 b: Vec<f32>,
889 },
890}
891
892pub(crate) struct LayerActs {
893 inv1: Vec<f32>,
894 attn: AttnActs,
895 pub(crate) h1: Vec<f32>,
896 pub(crate) n2: Vec<f32>,
897 pub(crate) inv2: Vec<f32>,
898 pub(crate) gpre: Vec<f32>,
899 pub(crate) upre: Vec<f32>,
900 pub(crate) act: Vec<f32>,
901}
902
903struct SendMut<T>(*mut T);
905unsafe impl<T> Send for SendMut<T> {}
906unsafe impl<T> Sync for SendMut<T> {}
907impl<T> SendMut<T> {
908 #[inline]
909 unsafe fn at(&self, i: usize) -> *mut T {
910 unsafe { self.0.add(i) }
911 }
912}
913
914impl FcdModel {
915 fn qk_norm_rope(
919 &self,
920 x: &mut [f32],
921 norm: Option<&[f32]>,
922 heads: usize,
923 t: usize,
924 inv_out: &mut [f32],
925 ) {
926 let hd = self.hd;
927 let n = x.len() / (heads * hd);
928 for r in 0..n {
929 let pos = r % t;
930 for hh in 0..heads {
931 let s = (r * heads + hh) * hd;
932 let head = &mut x[s..s + hd];
933 if let Some(w) = norm {
934 let mut inv = [0f32; 1];
935 let mut y = [0f32; 256];
936 debug_assert!(hd <= 256);
937 ops::rmsnorm_fwd(head, w, self.eps, self.gemma, &mut y[..hd], &mut inv);
938 head.copy_from_slice(&y[..hd]);
939 inv_out[r * heads + hh] = inv[0];
940 }
941 ops::rope_fwd(&mut head[..self.rotary_dim], pos, &self.inv_freq);
942 }
943 }
944 }
945
946 #[allow(clippy::too_many_arguments)]
952 fn layer_forward(
953 &self,
954 li: usize,
955 h_in: &[f32],
956 b: usize,
957 t: usize,
958 wts: &LnFfn,
959 nystrom: bool,
960 want_acts: bool,
961 ) -> (Vec<f32>, Option<LayerActs>) {
962 self.layer_forward_scaled(li, h_in, b, t, wts, nystrom, want_acts, None)
963 }
964
965 #[allow(clippy::too_many_arguments)]
969 pub(crate) fn layer_forward_scaled(
970 &self,
971 li: usize,
972 h_in: &[f32],
973 b: usize,
974 t: usize,
975 wts: &LnFfn,
976 nystrom: bool,
977 want_acts: bool,
978 ffn_scale: Option<&[f32]>,
979 ) -> (Vec<f32>, Option<LayerActs>) {
980 let hsz = self.hidden;
981 let n = b * t;
982 let l = &self.layers[li];
983 let pool = self.pool.as_deref();
984
985 let mut n1 = vec![0f32; n * hsz];
986 let mut inv1 = vec![0f32; n];
987 ops::rmsnorm_fwd(h_in, wts.iln, self.eps, self.gemma, &mut n1, &mut inv1);
988
989 let mats = self.mats(li).expect("layer mats");
990 let t_attn = std::time::Instant::now();
991 let (attn_out, attn_acts) = match &l.attn {
992 FcdAttn::Full { .. } => self.full_attn_fwd(&l.attn, &mats, &n1, b, t, nystrom),
993 FcdAttn::Gdn { .. } => self.gdn_attn_fwd(&l.attn, &n1, b, t),
994 };
995 prof::add(&prof::ATTN_FWD, t_attn);
996 let t_ffn = std::time::Instant::now();
997
998 let mut h1 = h_in.to_vec();
999 for (a, &x) in h1.iter_mut().zip(&attn_out) {
1000 *a += x;
1001 }
1002
1003 let mut n2 = vec![0f32; n * hsz];
1004 let mut inv2 = vec![0f32; n];
1005 ops::rmsnorm_fwd(&h1, wts.pln, self.eps, self.gemma, &mut n2, &mut inv2);
1006
1007 let inter = l.inter;
1008 let mut gpre = vec![0f32; n * inter];
1009 let mut upre = vec![0f32; n * inter];
1010 let mut act = vec![0f32; n * inter];
1011 let mut ffn = vec![0f32; n * hsz];
1012 let mut fused = false;
1017 #[cfg(feature = "gpu")]
1018 if let Some(gu) = wts.gu {
1019 if crate::gpu::enabled_here() {
1020 let mut both = want_acts.then(|| vec![0f32; n * 2 * inter]);
1021 if crate::gpu_wgpu::ffn_chain_f32(
1022 &n2,
1023 gu,
1024 wts.down,
1025 ffn_scale,
1026 both.as_deref_mut(),
1027 want_acts.then_some(li),
1028 &mut ffn,
1029 n,
1030 hsz,
1031 inter,
1032 ) {
1033 fused = true;
1034 if let Some(b) = &both {
1035 for r in 0..n {
1036 let row = &b[r * 2 * inter..(r + 1) * 2 * inter];
1037 gpre[r * inter..(r + 1) * inter].copy_from_slice(&row[..inter]);
1038 upre[r * inter..(r + 1) * inter].copy_from_slice(&row[inter..]);
1039 }
1040 for i in 0..n * inter {
1042 act[i] = ops::silu(gpre[i]) * upre[i];
1043 }
1044 }
1045 }
1046 }
1047 }
1048 if !fused {
1049 if let Some(gu) = wts.gu {
1050 let mut both = vec![0f32; n * 2 * inter];
1053 ops::gemm_nt(&n2, gu, &mut both, n, hsz, 2 * inter, pool);
1054 for r in 0..n {
1055 let row = &both[r * 2 * inter..(r + 1) * 2 * inter];
1056 gpre[r * inter..(r + 1) * inter].copy_from_slice(&row[..inter]);
1057 upre[r * inter..(r + 1) * inter].copy_from_slice(&row[inter..]);
1058 }
1059 } else {
1060 ops::gemm_nt(&n2, wts.gate, &mut gpre, n, hsz, inter, pool);
1061 ops::gemm_nt(&n2, wts.up, &mut upre, n, hsz, inter, pool);
1062 }
1063 for i in 0..n * inter {
1064 act[i] = ops::silu(gpre[i]) * upre[i];
1065 }
1066 match ffn_scale {
1067 Some(g) => {
1068 debug_assert_eq!(g.len(), inter);
1069 let mut act2 = act.clone();
1070 for r in 0..n {
1071 for (a, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
1072 *a *= gv;
1073 }
1074 }
1075 ops::gemm_nt(&act2, wts.down, &mut ffn, n, inter, hsz, pool);
1076 }
1077 None => ops::gemm_nt(&act, wts.down, &mut ffn, n, inter, hsz, pool),
1078 }
1079 }
1080 let mut h2 = h1.clone();
1081 for (a, &x) in h2.iter_mut().zip(&ffn) {
1082 *a += x;
1083 }
1084
1085 let acts = want_acts.then_some(LayerActs {
1086 inv1,
1087 attn: attn_acts,
1088 h1,
1089 n2,
1090 inv2,
1091 gpre,
1092 upre,
1093 act,
1094 });
1095 prof::add(&prof::FFN_FWD, t_ffn);
1096 (h2, acts)
1097 }
1098
1099 fn full_attn_fwd(
1103 &self,
1104 attn: &FcdAttn,
1105 mats: &LayerMats,
1106 n1: &[f32],
1107 b: usize,
1108 t: usize,
1109 nystrom: bool,
1110 ) -> (Vec<f32>, AttnActs) {
1111 let (wqkv, wo) = (&mats.wqkv[..], &mats.wo[..]);
1112 let FcdAttn::Full {
1113 qrows: _,
1114 q_norm,
1115 k_norm,
1116 bias,
1117 output_gate,
1118 } = attn
1119 else {
1120 unreachable!("full_attn_fwd on a non-Full layer");
1121 };
1122 let (hsz, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
1123 let n = b * t;
1124 let pool = self.pool.as_deref();
1125 let qdim = nh * hd;
1126 let kvdim = nkv * hd;
1127 let rep = nh / nkv;
1128 let qrows = if *output_gate { 2 * qdim } else { qdim };
1129
1130 let fused = qrows + 2 * kvdim;
1134 let mut qkv = vec![0f32; n * fused];
1135 ops::gemm_nt(n1, wqkv, &mut qkv, n, hsz, fused, pool);
1136 let mut qraw = vec![0f32; n * qrows];
1137 let mut kpre = vec![0f32; n * kvdim];
1138 let mut vproj = vec![0f32; n * kvdim];
1139 for r in 0..n {
1140 let row = &qkv[r * fused..(r + 1) * fused];
1141 qraw[r * qrows..(r + 1) * qrows].copy_from_slice(&row[..qrows]);
1142 kpre[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows..qrows + kvdim]);
1143 vproj[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows + kvdim..]);
1144 }
1145 if let Some((bq, bk, bv)) = bias {
1146 for r in 0..n {
1147 for (x, bb) in qraw[r * qrows..(r + 1) * qrows].iter_mut().zip(bq) {
1148 *x += bb;
1149 }
1150 for (x, bb) in kpre[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bk) {
1151 *x += bb;
1152 }
1153 for (x, bb) in vproj[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bv) {
1154 *x += bb;
1155 }
1156 }
1157 }
1158 let (qpre, gate_pre) = if *output_gate {
1160 let mut qh = vec![0f32; n * qdim];
1161 let mut gp = vec![0f32; n * qdim];
1162 for r in 0..n {
1163 for h in 0..nh {
1164 let src = r * qrows + 2 * h * hd;
1165 let dst = r * qdim + h * hd;
1166 qh[dst..dst + hd].copy_from_slice(&qraw[src..src + hd]);
1167 gp[dst..dst + hd].copy_from_slice(&qraw[src + hd..src + 2 * hd]);
1168 }
1169 }
1170 (qh, gp)
1171 } else {
1172 (qraw, Vec::new())
1173 };
1174
1175 let mut qrot = qpre.clone();
1176 let mut krot = kpre.clone();
1177 let mut qinv = vec![0f32; n * nh];
1178 let mut kinv = vec![0f32; n * nkv];
1179 self.qk_norm_rope(&mut qrot, q_norm.as_deref(), nh, t, &mut qinv);
1180 self.qk_norm_rope(&mut krot, k_norm.as_deref(), nkv, t, &mut kinv);
1181
1182 let mut ao = vec![0f32; n * qdim];
1184 {
1185 let units = b * nh;
1186 let aop = SendMut(ao.as_mut_ptr());
1187 let qr = &qrot;
1188 let kr = &krot;
1189 let vr = &vproj;
1190 let nys = self.nys;
1191 let run_unit = |u: usize| {
1192 let (bi, h) = (u / nh, u % nh);
1193 let g = h / rep;
1194 if nystrom {
1195 let mut q64 = vec![0f64; t * hd];
1197 let mut k64 = vec![0f64; t * hd];
1198 let mut v64 = vec![0f64; t * hd];
1199 for p in 0..t {
1200 let r = bi * t + p;
1201 for c in 0..hd {
1202 q64[p * hd + c] = qr[r * qdim + h * hd + c] as f64;
1203 k64[p * hd + c] = kr[r * kvdim + g * hd + c] as f64;
1204 v64[p * hd + c] = vr[r * kvdim + g * hd + c] as f64;
1205 }
1206 }
1207 let mut o64 = vec![0f64; t * hd];
1208 ops::nystrom_head_fwd(&q64, &k64, &v64, t, hd, hd, &nys, &mut o64);
1209 for p in 0..t {
1210 let r = bi * t + p;
1211 for c in 0..hd {
1212 unsafe {
1214 *aop.at(r * qdim + h * hd + c) = o64[p * hd + c] as f32;
1215 }
1216 }
1217 }
1218 } else {
1219 let mut q32 = vec![0f32; t * hd];
1220 let mut k32 = vec![0f32; t * hd];
1221 let mut v32 = vec![0f32; t * hd];
1222 for p in 0..t {
1223 let r = bi * t + p;
1224 q32[p * hd..(p + 1) * hd]
1225 .copy_from_slice(&qr[r * qdim + h * hd..r * qdim + (h + 1) * hd]);
1226 k32[p * hd..(p + 1) * hd]
1227 .copy_from_slice(&kr[r * kvdim + g * hd..r * kvdim + (g + 1) * hd]);
1228 v32[p * hd..(p + 1) * hd]
1229 .copy_from_slice(&vr[r * kvdim + g * hd..r * kvdim + (g + 1) * hd]);
1230 }
1231 let mut o32 = vec![0f32; t * hd];
1232 ops::attn_head_fwd(&q32, &k32, &v32, t, hd, hd, &mut o32);
1233 for p in 0..t {
1234 let r = bi * t + p;
1235 for c in 0..hd {
1236 unsafe {
1238 *aop.at(r * qdim + h * hd + c) = o32[p * hd + c];
1239 }
1240 }
1241 }
1242 }
1243 };
1244 match pool {
1245 Some(p) if units > 1 => p.run(&|widx, nw| {
1246 for u in (widx..units).step_by(nw) {
1247 run_unit(u);
1248 }
1249 }),
1250 _ => {
1251 for u in 0..units {
1252 run_unit(u);
1253 }
1254 }
1255 }
1256 }
1257
1258 let ao_eff: Vec<f32> = if *output_gate {
1260 ao.iter()
1261 .zip(&gate_pre)
1262 .map(|(&a, &g)| a * (1.0 / (1.0 + (-g).exp())))
1263 .collect()
1264 } else {
1265 ao.clone()
1266 };
1267 let mut attn_out = vec![0f32; n * hsz];
1268 ops::gemm_nt(&ao_eff, wo, &mut attn_out, n, qdim, hsz, pool);
1269 (
1270 attn_out,
1271 AttnActs::Full {
1272 qpre,
1273 kpre,
1274 vproj,
1275 qrot,
1276 krot,
1277 qinv,
1278 kinv,
1279 ao,
1280 gate_pre,
1281 },
1282 )
1283 }
1284
1285 fn gdn_attn_fwd(&self, attn: &FcdAttn, n1: &[f32], b: usize, t: usize) -> (Vec<f32>, AttnActs) {
1290 let FcdAttn::Gdn {
1291 wqkv,
1292 wz,
1293 wa,
1294 wb,
1295 conv,
1296 a_log,
1297 dt_bias,
1298 norm,
1299 wout,
1300 } = attn
1301 else {
1302 unreachable!("gdn_attn_fwd on a non-GDN layer");
1303 };
1304 let d = self.gdn.expect("gdn layer without gdn dims");
1305 let (hsz, n) = (self.hidden, b * t);
1306 let pool = self.pool.as_deref();
1307 let (c_dim, vd, nv) = (d.c_dim(), d.vd(), d.nv);
1308
1309 let mut qkv = vec![0f32; n * c_dim];
1310 ops::gemm_nt(n1, wqkv, &mut qkv, n, hsz, c_dim, pool);
1311 let mut z = vec![0f32; n * vd];
1312 ops::gemm_nt(n1, wz, &mut z, n, hsz, vd, pool);
1313 let mut a = vec![0f32; n * nv];
1314 ops::gemm_nt(n1, wa, &mut a, n, hsz, nv, pool);
1315 let mut bstr = vec![0f32; n * nv];
1316 ops::gemm_nt(n1, wb, &mut bstr, n, hsz, nv, pool);
1317
1318 let cfg = ops::GdnSeqCfg {
1319 nv: d.nv,
1320 nk: d.nk,
1321 dk: d.dk,
1322 dv: d.dv,
1323 kk: d.kk,
1324 rms_eps: self.eps,
1325 conv,
1326 a_log,
1327 dt_bias,
1328 norm,
1329 };
1330 let qkv64: Vec<f64> = qkv.iter().map(|&v| v as f64).collect();
1332 let z64: Vec<f64> = z.iter().map(|&v| v as f64).collect();
1333 let a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
1334 let b64: Vec<f64> = bstr.iter().map(|&v| v as f64).collect();
1335 let mut pre64 = vec![0f64; n * c_dim];
1336 let mut cq64 = vec![0f64; n * c_dim];
1337 for bi in 0..b {
1338 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
1339 ops::gdn_conv_fwd(
1340 &qkv64[r.clone()],
1341 t,
1342 c_dim,
1343 d.kk,
1344 conv,
1345 &mut pre64[r.clone()],
1346 &mut cq64[r],
1347 );
1348 }
1349 let mut of = vec![0f32; n * vd];
1350 {
1351 let units = b * d.nk;
1352 let rep_v = d.nv / d.nk;
1353 let ofp = SendMut(of.as_mut_ptr());
1354 let (cqr, zr, ar, br) = (&cq64, &z64, &a64, &b64);
1355 let cfg_ref = &cfg;
1356 let run_unit = |u: usize| {
1357 let (bi, ko) = (u / d.nk, u % d.nk);
1358 let mut local = vec![0f64; t * vd];
1359 ops::gdn_group_fwd(
1360 &cqr[bi * t * c_dim..(bi + 1) * t * c_dim],
1361 &zr[bi * t * vd..(bi + 1) * t * vd],
1362 &ar[bi * t * nv..(bi + 1) * t * nv],
1363 &br[bi * t * nv..(bi + 1) * t * nv],
1364 t,
1365 cfg_ref,
1366 ko,
1367 &mut local,
1368 );
1369 for hh in 0..rep_v {
1370 let h = ko * rep_v + hh;
1371 for p in 0..t {
1372 for dj in 0..d.dv {
1373 unsafe {
1375 *ofp.at((bi * t + p) * vd + h * d.dv + dj) =
1376 local[p * vd + h * d.dv + dj] as f32;
1377 }
1378 }
1379 }
1380 }
1381 };
1382 match pool {
1383 Some(p) if units > 1 => p.run(&|widx, nw| {
1384 for u in (widx..units).step_by(nw) {
1385 run_unit(u);
1386 }
1387 }),
1388 _ => {
1389 for u in 0..units {
1390 run_unit(u);
1391 }
1392 }
1393 }
1394 }
1395 let mut attn_out = vec![0f32; n * hsz];
1396 ops::gemm_nt(&of, wout, &mut attn_out, n, vd, hsz, pool);
1397 (attn_out, AttnActs::Gdn { qkv, z, a, b: bstr })
1398 }
1399
1400 #[allow(clippy::too_many_arguments)]
1404 fn layer_backward(
1405 &self,
1406 li: usize,
1407 h_in: &[f32],
1408 b: usize,
1409 t: usize,
1410 wts: &LnFfn,
1411 nystrom: bool,
1412 acts: &LayerActs,
1413 dh2: &[f32],
1414 mut grads: Option<&mut [Vec<f32>]>,
1415 ) -> Vec<f32> {
1416 let hsz = self.hidden;
1417 let n = b * t;
1418 let l = &self.layers[li];
1419 let pool = self.pool.as_deref();
1420 let inter = l.inter;
1421
1422 let mut dn2 = vec![0f32; n * hsz];
1424 let mut bwd_fused = false;
1428 #[cfg(feature = "gpu")]
1429 if grads.is_none() {
1430 if let Some(gu) = wts.gu {
1431 if crate::gpu::enabled_here()
1432 && crate::gpu_wgpu::ffn_bwd_chain_f32(
1433 dh2, wts.down, gu, li, &mut dn2, n, hsz, inter,
1434 )
1435 {
1436 bwd_fused = true;
1437 }
1438 }
1439 }
1440 if !bwd_fused {
1441 let mut dact = vec![0f32; n * inter];
1442 ops::gemm_dx(dh2, wts.down, &mut dact, n, inter, hsz, pool);
1443 if let Some(g) = grads.as_deref_mut() {
1444 ops::gemm_dw(dh2, &acts.act, &mut g[4], n, inter, hsz, pool);
1445 }
1446 let mut dg = vec![0f32; n * inter];
1447 let mut du = vec![0f32; n * inter];
1448 for i in 0..n * inter {
1449 dg[i] = dact[i] * acts.upre[i] * ops::silu_bwd(acts.gpre[i]);
1450 du[i] = dact[i] * ops::silu(acts.gpre[i]);
1451 }
1452 if let Some(gu) = wts.gu {
1455 let mut dgu = vec![0f32; n * 2 * inter];
1456 for r in 0..n {
1457 let row = &mut dgu[r * 2 * inter..(r + 1) * 2 * inter];
1458 row[..inter].copy_from_slice(&dg[r * inter..(r + 1) * inter]);
1459 row[inter..].copy_from_slice(&du[r * inter..(r + 1) * inter]);
1460 }
1461 ops::gemm_dx(&dgu, gu, &mut dn2, n, hsz, 2 * inter, pool);
1462 } else {
1463 ops::gemm_dx(&dg, wts.gate, &mut dn2, n, hsz, inter, pool);
1464 ops::gemm_dx(&du, wts.up, &mut dn2, n, hsz, inter, pool);
1465 }
1466 if let Some(g) = grads.as_deref_mut() {
1467 ops::gemm_dw(&dg, &acts.n2, &mut g[2], n, hsz, inter, pool);
1468 ops::gemm_dw(&du, &acts.n2, &mut g[3], n, hsz, inter, pool);
1469 }
1470 }
1471
1472 let mut dh1 = dh2.to_vec();
1473 ops::rmsnorm_bwd(
1474 &acts.h1,
1475 wts.pln,
1476 &acts.inv2,
1477 &dn2,
1478 self.gemma,
1479 &mut dh1,
1480 grads.as_deref_mut().map(|g| &mut g[1][..]),
1481 );
1482
1483 let mats = self.mats(li).expect("layer mats");
1485 let dn1 = match &l.attn {
1486 FcdAttn::Full { .. } => {
1487 self.full_attn_bwd(&l.attn, &mats, &acts.attn, &dh1, b, t, nystrom)
1488 }
1489 FcdAttn::Gdn { .. } => self.gdn_attn_bwd(&l.attn, &acts.attn, &dh1, b, t),
1490 };
1491
1492 let mut dh_in = dh1.clone();
1493 ops::rmsnorm_bwd(
1494 h_in,
1495 wts.iln,
1496 &acts.inv1,
1497 &dn1,
1498 self.gemma,
1499 &mut dh_in,
1500 grads.map(|g| &mut g[0][..]),
1501 );
1502 dh_in
1503 }
1504
1505 fn full_attn_bwd(
1509 &self,
1510 attn: &FcdAttn,
1511 mats: &LayerMats,
1512 acts: &AttnActs,
1513 dattn: &[f32],
1514 b: usize,
1515 t: usize,
1516 nystrom: bool,
1517 ) -> Vec<f32> {
1518 let (wqkv, wo) = (&mats.wqkv[..], &mats.wo[..]);
1519 let FcdAttn::Full {
1520 qrows: _,
1521 q_norm,
1522 k_norm,
1523 output_gate,
1524 ..
1525 } = attn
1526 else {
1527 unreachable!("full_attn_bwd on a non-Full layer");
1528 };
1529 let AttnActs::Full {
1530 qpre,
1531 kpre,
1532 vproj,
1533 qrot,
1534 krot,
1535 qinv,
1536 kinv,
1537 ao,
1538 gate_pre,
1539 } = acts
1540 else {
1541 unreachable!("acts mismatch");
1542 };
1543 let (hsz, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
1544 let n = b * t;
1545 let pool = self.pool.as_deref();
1546 let qdim = nh * hd;
1547 let kvdim = nkv * hd;
1548 let rep = nh / nkv;
1549 let qrows = if *output_gate { 2 * qdim } else { qdim };
1550
1551 let mut dao_eff = vec![0f32; n * qdim];
1552 ops::gemm_dx(dattn, wo, &mut dao_eff, n, qdim, hsz, pool);
1553 let (dao, dgate) = if *output_gate {
1555 let mut dao = vec![0f32; n * qdim];
1556 let mut dgp = vec![0f32; n * qdim];
1557 for i in 0..n * qdim {
1558 let sig = 1.0 / (1.0 + (-gate_pre[i]).exp());
1559 dao[i] = dao_eff[i] * sig;
1560 dgp[i] = dao_eff[i] * ao[i] * sig * (1.0 - sig);
1561 }
1562 (dao, dgp)
1563 } else {
1564 (dao_eff, Vec::new())
1565 };
1566
1567 let mut dqrot = vec![0f32; n * qdim];
1568 let mut dkrot = vec![0f32; n * kvdim];
1569 let mut dvproj = vec![0f32; n * kvdim];
1570 {
1571 let units = b * nkv;
1574 let dqp = SendMut(dqrot.as_mut_ptr());
1575 let dkp = SendMut(dkrot.as_mut_ptr());
1576 let dvp = SendMut(dvproj.as_mut_ptr());
1577 let (qr, kr, vr) = (qrot, krot, vproj);
1578 let daor = &dao;
1579 let nys = self.nys;
1580 let run_unit = |u: usize| {
1581 let (bi, g) = (u / nkv, u % nkv);
1582 let mut k64 = vec![0f64; t * hd];
1583 let mut v64 = vec![0f64; t * hd];
1584 for p in 0..t {
1585 let r = bi * t + p;
1586 for c in 0..hd {
1587 k64[p * hd + c] = kr[r * kvdim + g * hd + c] as f64;
1588 v64[p * hd + c] = vr[r * kvdim + g * hd + c] as f64;
1589 }
1590 }
1591 let mut dk64 = vec![0f64; t * hd];
1592 let mut dv64 = vec![0f64; t * hd];
1593 let mut q64 = vec![0f64; t * hd];
1594 let mut do64 = vec![0f64; t * hd];
1595 let mut dq64 = vec![0f64; t * hd];
1596 for hh in 0..rep {
1597 let h = g * rep + hh;
1598 for p in 0..t {
1599 let r = bi * t + p;
1600 for c in 0..hd {
1601 q64[p * hd + c] = qr[r * qdim + h * hd + c] as f64;
1602 do64[p * hd + c] = daor[r * qdim + h * hd + c] as f64;
1603 }
1604 }
1605 for v in dq64.iter_mut() {
1606 *v = 0.0;
1607 }
1608 if nystrom {
1609 ops::nystrom_head_bwd(
1610 &q64, &k64, &v64, &do64, t, hd, hd, &nys, &mut dq64, &mut dk64,
1611 &mut dv64,
1612 );
1613 } else {
1614 ops::attn_head_bwd(
1615 &q64, &k64, &v64, &do64, t, hd, hd, &mut dq64, &mut dk64, &mut dv64,
1616 );
1617 }
1618 for p in 0..t {
1619 let r = bi * t + p;
1620 for c in 0..hd {
1621 unsafe {
1623 *dqp.at(r * qdim + h * hd + c) = dq64[p * hd + c] as f32;
1624 }
1625 }
1626 }
1627 }
1628 for p in 0..t {
1629 let r = bi * t + p;
1630 for c in 0..hd {
1631 unsafe {
1633 *dkp.at(r * kvdim + g * hd + c) = dk64[p * hd + c] as f32;
1634 *dvp.at(r * kvdim + g * hd + c) = dv64[p * hd + c] as f32;
1635 }
1636 }
1637 }
1638 };
1639 match pool {
1640 Some(p) if units > 1 => p.run(&|widx, nw| {
1641 for u in (widx..units).step_by(nw) {
1642 run_unit(u);
1643 }
1644 }),
1645 _ => {
1646 for u in 0..units {
1647 run_unit(u);
1648 }
1649 }
1650 }
1651 }
1652
1653 let mut dqpre = vec![0f32; n * qdim];
1655 let mut dkpre = vec![0f32; n * kvdim];
1656 for r in 0..n {
1657 let pos = r % t;
1658 for h in 0..nh {
1659 let s = r * qdim + h * hd;
1660 ops::rope_bwd(&mut dqrot[s..s + self.rotary_dim], pos, &self.inv_freq);
1661 match q_norm {
1662 Some(w) => ops::rmsnorm_bwd(
1663 &qpre[s..s + hd],
1664 w,
1665 &qinv[r * nh + h..r * nh + h + 1],
1666 &dqrot[s..s + hd],
1667 self.gemma,
1668 &mut dqpre[s..s + hd],
1669 None,
1670 ),
1671 None => dqpre[s..s + hd].copy_from_slice(&dqrot[s..s + hd]),
1672 }
1673 }
1674 for g in 0..nkv {
1675 let s = r * kvdim + g * hd;
1676 ops::rope_bwd(&mut dkrot[s..s + self.rotary_dim], pos, &self.inv_freq);
1677 match k_norm {
1678 Some(w) => ops::rmsnorm_bwd(
1679 &kpre[s..s + hd],
1680 w,
1681 &kinv[r * nkv + g..r * nkv + g + 1],
1682 &dkrot[s..s + hd],
1683 self.gemma,
1684 &mut dkpre[s..s + hd],
1685 None,
1686 ),
1687 None => dkpre[s..s + hd].copy_from_slice(&dkrot[s..s + hd]),
1688 }
1689 }
1690 }
1691
1692 let dqraw: Vec<f32> = if *output_gate {
1694 let mut dq = vec![0f32; n * qrows];
1695 for r in 0..n {
1696 for h in 0..nh {
1697 let dst = r * qrows + 2 * h * hd;
1698 let src = r * qdim + h * hd;
1699 dq[dst..dst + hd].copy_from_slice(&dqpre[src..src + hd]);
1700 dq[dst + hd..dst + 2 * hd].copy_from_slice(&dgate[src..src + hd]);
1701 }
1702 }
1703 dq
1704 } else {
1705 dqpre
1706 };
1707
1708 let fused = qrows + 2 * kvdim;
1713 let mut dqkv = vec![0f32; n * fused];
1714 for r in 0..n {
1715 let row = &mut dqkv[r * fused..(r + 1) * fused];
1716 row[..qrows].copy_from_slice(&dqraw[r * qrows..(r + 1) * qrows]);
1717 row[qrows..qrows + kvdim].copy_from_slice(&dkpre[r * kvdim..(r + 1) * kvdim]);
1718 row[qrows + kvdim..].copy_from_slice(&dvproj[r * kvdim..(r + 1) * kvdim]);
1719 }
1720 let mut dn1 = vec![0f32; n * hsz];
1721 ops::gemm_dx(&dqkv, wqkv, &mut dn1, n, hsz, fused, pool);
1722 dn1
1723 }
1724
1725 fn gdn_attn_bwd(
1729 &self,
1730 attn: &FcdAttn,
1731 acts: &AttnActs,
1732 dattn: &[f32],
1733 b: usize,
1734 t: usize,
1735 ) -> Vec<f32> {
1736 let FcdAttn::Gdn {
1737 wqkv,
1738 wz,
1739 wa,
1740 wb,
1741 conv,
1742 a_log,
1743 dt_bias,
1744 norm,
1745 wout,
1746 } = attn
1747 else {
1748 unreachable!("gdn_attn_bwd on a non-GDN layer");
1749 };
1750 let AttnActs::Gdn { qkv, z, a, b: bstr } = acts else {
1751 unreachable!("acts mismatch");
1752 };
1753 let d = self.gdn.expect("gdn layer without gdn dims");
1754 let (hsz, n) = (self.hidden, b * t);
1755 let pool = self.pool.as_deref();
1756 let (c_dim, vd, nv) = (d.c_dim(), d.vd(), d.nv);
1757
1758 let mut dof = vec![0f32; n * vd];
1759 ops::gemm_dx(dattn, wout, &mut dof, n, vd, hsz, pool);
1760
1761 let cfg = ops::GdnSeqCfg {
1762 nv: d.nv,
1763 nk: d.nk,
1764 dk: d.dk,
1765 dv: d.dv,
1766 kk: d.kk,
1767 rms_eps: self.eps,
1768 conv,
1769 a_log,
1770 dt_bias,
1771 norm,
1772 };
1773 let qkv64: Vec<f64> = qkv.iter().map(|&v| v as f64).collect();
1774 let z64: Vec<f64> = z.iter().map(|&v| v as f64).collect();
1775 let a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
1776 let b64: Vec<f64> = bstr.iter().map(|&v| v as f64).collect();
1777 let dof64: Vec<f64> = dof.iter().map(|&v| v as f64).collect();
1778 let mut pre64 = vec![0f64; n * c_dim];
1779 let mut cq64 = vec![0f64; n * c_dim];
1780 for bi in 0..b {
1781 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
1782 ops::gdn_conv_fwd(
1783 &qkv64[r.clone()],
1784 t,
1785 c_dim,
1786 d.kk,
1787 conv,
1788 &mut pre64[r.clone()],
1789 &mut cq64[r],
1790 );
1791 }
1792
1793 let mut dcq64 = vec![0f64; n * c_dim];
1794 let mut dz64 = vec![0f64; n * vd];
1795 let mut da64 = vec![0f64; n * nv];
1796 let mut db64 = vec![0f64; n * nv];
1797 {
1798 let units = b * d.nk;
1799 let rep_v = d.nv / d.nk;
1800 let kd = d.nk * d.dk;
1801 let dcqp = SendMut(dcq64.as_mut_ptr());
1802 let dzp = SendMut(dz64.as_mut_ptr());
1803 let dap = SendMut(da64.as_mut_ptr());
1804 let dbp = SendMut(db64.as_mut_ptr());
1805 let (cqr, zr, ar, br, dor) = (&cq64, &z64, &a64, &b64, &dof64);
1806 let cfg_ref = &cfg;
1807 let run_unit = |u: usize| {
1808 let (bi, ko) = (u / d.nk, u % d.nk);
1809 let mut dcq_l = vec![0f64; t * c_dim];
1812 let mut dz_l = vec![0f64; t * vd];
1813 let mut da_l = vec![0f64; t * nv];
1814 let mut db_l = vec![0f64; t * nv];
1815 ops::gdn_group_bwd(
1816 &cqr[bi * t * c_dim..(bi + 1) * t * c_dim],
1817 &zr[bi * t * vd..(bi + 1) * t * vd],
1818 &ar[bi * t * nv..(bi + 1) * t * nv],
1819 &br[bi * t * nv..(bi + 1) * t * nv],
1820 t,
1821 cfg_ref,
1822 ko,
1823 &dor[bi * t * vd..(bi + 1) * t * vd],
1824 &mut dcq_l,
1825 &mut dz_l,
1826 &mut da_l,
1827 &mut db_l,
1828 );
1829 for p in 0..t {
1832 let row = (bi * t + p) * c_dim;
1833 for c in ko * d.dk..(ko + 1) * d.dk {
1834 unsafe {
1835 *dcqp.at(row + c) = dcq_l[p * c_dim + c];
1836 *dcqp.at(row + kd + c) = dcq_l[p * c_dim + kd + c];
1837 }
1838 }
1839 for hh in 0..rep_v {
1840 let h = ko * rep_v + hh;
1841 for dj in 0..d.dv {
1842 unsafe {
1843 *dcqp.at(row + 2 * kd + h * d.dv + dj) =
1844 dcq_l[p * c_dim + 2 * kd + h * d.dv + dj];
1845 *dzp.at((bi * t + p) * vd + h * d.dv + dj) =
1846 dz_l[p * vd + h * d.dv + dj];
1847 }
1848 }
1849 unsafe {
1850 *dap.at((bi * t + p) * nv + h) = da_l[p * nv + h];
1851 *dbp.at((bi * t + p) * nv + h) = db_l[p * nv + h];
1852 }
1853 }
1854 }
1855 };
1856 match pool {
1857 Some(p) if units > 1 => p.run(&|widx, nw| {
1858 for u in (widx..units).step_by(nw) {
1859 run_unit(u);
1860 }
1861 }),
1862 _ => {
1863 for u in 0..units {
1864 run_unit(u);
1865 }
1866 }
1867 }
1868 }
1869
1870 let mut dqkv64 = vec![0f64; n * c_dim];
1871 for bi in 0..b {
1872 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
1873 ops::gdn_conv_bwd(
1874 &pre64[r.clone()],
1875 t,
1876 c_dim,
1877 d.kk,
1878 conv,
1879 &dcq64[r.clone()],
1880 &mut dqkv64[r],
1881 );
1882 }
1883 let to32 = |v: &[f64]| -> Vec<f32> { v.iter().map(|&x| x as f32).collect() };
1884 let (dqkv, dz, da, db) = (to32(&dqkv64), to32(&dz64), to32(&da64), to32(&db64));
1885
1886 let mut dn1 = vec![0f32; n * hsz];
1887 ops::gemm_dx(&dqkv, wqkv, &mut dn1, n, hsz, c_dim, pool);
1888 ops::gemm_dx(&dz, wz, &mut dn1, n, hsz, vd, pool);
1889 ops::gemm_dx(&da, wa, &mut dn1, n, hsz, nv, pool);
1890 ops::gemm_dx(&db, wb, &mut dn1, n, hsz, nv, pool);
1891 dn1
1892 }
1893
1894 fn forward_hidden(
1899 &self,
1900 ids: &[u32],
1901 b: usize,
1902 t: usize,
1903 ts: Option<&TrainState>,
1904 student: bool,
1905 mut keep: Option<&mut Vec<Vec<f32>>>,
1906 ) -> Vec<f32> {
1907 let hsz = self.hidden;
1908 let mut h = vec![0f32; b * t * hsz];
1909 for (r, &id) in ids.iter().enumerate() {
1910 let src = (id as usize).min(self.embed.len() / hsz - 1) * hsz;
1911 h[r * hsz..(r + 1) * hsz].copy_from_slice(&self.embed[src..src + hsz]);
1912 }
1913 for li in 0..self.nl {
1914 if let Some(k) = keep.as_deref_mut() {
1915 k.push(h.clone());
1916 }
1917 let mats = self.mats(li).expect("layer mats");
1918 let wts = ln_ffn(self, if student { ts } else { None }, li, &mats);
1919 let nys = student && self.o1_flags[li];
1920 h = self.layer_forward(li, &h, b, t, &wts, nys, false).0;
1921 }
1922 h
1923 }
1924
1925 fn loss_and_dhidden(
1928 &self,
1929 hs: &[f32],
1930 ht: &[f32],
1931 targets: &[u32],
1932 kl_w: f64,
1933 ) -> (f64, f64, Vec<f32>) {
1934 let hsz = self.hidden;
1935 let n = targets.len();
1936 let pool = self.pool.as_deref();
1937 let wh = self.head_weight();
1938 let vs = self.vocab;
1939
1940 let mut ns = vec![0f32; n * hsz];
1941 let mut invs = vec![0f32; n];
1942 ops::rmsnorm_fwd(
1943 hs,
1944 &self.final_norm,
1945 self.eps,
1946 self.gemma,
1947 &mut ns,
1948 &mut invs,
1949 );
1950 let mut nt = vec![0f32; n * hsz];
1951 let mut invt = vec![0f32; n];
1952 ops::rmsnorm_fwd(
1953 ht,
1954 &self.final_norm,
1955 self.eps,
1956 self.gemma,
1957 &mut nt,
1958 &mut invt,
1959 );
1960
1961 let inv_n = 1.0 / n as f64;
1962 let mut ce_sum = 0f64;
1963 let mut kl_sum = 0f64;
1964 let mut dns = vec![0f32; n * hsz];
1965 let mut ls = vec![0f32; LM_CHUNK * vs];
1966 let mut lt = vec![0f32; LM_CHUNK * vs];
1967 let mut dlg = vec![0f32; LM_CHUNK * vs];
1968 let mut r0 = 0usize;
1969 while r0 < n {
1970 let r1 = (r0 + LM_CHUNK).min(n);
1971 let c = r1 - r0;
1972 ops::gemm_nt(
1973 &ns[r0 * hsz..r1 * hsz],
1974 wh,
1975 &mut ls[..c * vs],
1976 c,
1977 hsz,
1978 vs,
1979 pool,
1980 );
1981 ops::gemm_nt(
1982 &nt[r0 * hsz..r1 * hsz],
1983 wh,
1984 &mut lt[..c * vs],
1985 c,
1986 hsz,
1987 vs,
1988 pool,
1989 );
1990 for r in 0..c {
1991 let (ce, kl) = ops::ce_kl_position(
1992 &ls[r * vs..(r + 1) * vs],
1993 <[r * vs..(r + 1) * vs],
1994 targets[r0 + r] as usize,
1995 kl_w,
1996 inv_n,
1997 &mut dlg[r * vs..(r + 1) * vs],
1998 );
1999 ce_sum += ce;
2000 kl_sum += kl;
2001 }
2002 ops::gemm_dx(
2003 &dlg[..c * vs],
2004 wh,
2005 &mut dns[r0 * hsz..r1 * hsz],
2006 c,
2007 hsz,
2008 vs,
2009 pool,
2010 );
2011 r0 = r1;
2012 }
2013
2014 let mut dhs = vec![0f32; n * hsz];
2015 ops::rmsnorm_bwd(
2016 hs,
2017 &self.final_norm,
2018 &invs,
2019 &dns,
2020 self.gemma,
2021 &mut dhs,
2022 None,
2023 );
2024 (ce_sum * inv_n, kl_sum * inv_n, dhs)
2025 }
2026
2027 fn backward(
2030 &self,
2031 b: usize,
2032 t: usize,
2033 keep: &[Vec<f32>],
2034 dh_last: Vec<f32>,
2035 ts: &mut TrainState,
2036 ) {
2037 let TrainState {
2040 layers, data, grad, ..
2041 } = ts;
2042 let mut dh = dh_last;
2043 for li in (0..self.nl).rev() {
2044 let h_in = &keep[li];
2045 let nys = self.o1_flags[li];
2046 let slot = layers.iter().position(|&x| x == li);
2047 let mats_hold = self.mats(li).expect("layer mats");
2048 let wts = match slot {
2049 Some(s) => {
2050 let bi = s * PARAMS_PER_LAYER;
2051 LnFfn {
2052 iln: &data[bi],
2053 pln: &data[bi + 1],
2054 gate: &data[bi + 2],
2055 up: &data[bi + 3],
2056 down: &data[bi + 4],
2057 gu: None,
2058 }
2059 }
2060 None => {
2061 let l = &self.layers[li];
2062 LnFfn {
2063 iln: &l.iln,
2064 pln: &l.pln,
2065 gate: &[],
2066 up: &[],
2067 down: &mats_hold.down,
2068 gu: Some(&mats_hold.gu),
2069 }
2070 }
2071 };
2072 let (_, acts) = self.layer_forward(li, h_in, b, t, &wts, nys, true);
2073 let acts = acts.expect("want_acts");
2074 dh = match slot {
2075 Some(s) => {
2076 let gb = s * PARAMS_PER_LAYER;
2077 let gr = &mut grad[gb..gb + PARAMS_PER_LAYER];
2078 self.layer_backward(li, h_in, b, t, &wts, nys, &acts, &dh, Some(gr))
2079 }
2080 None => self.layer_backward(li, h_in, b, t, &wts, nys, &acts, &dh, None),
2081 };
2082 }
2083 }
2084
2085 #[doc(hidden)]
2092 pub fn loss_and_grads_for_test(
2093 &self,
2094 ids: &[u32],
2095 tgt: &[u32],
2096 b: usize,
2097 t: usize,
2098 ts: &mut TrainState,
2099 kl_w: f64,
2100 ) -> f64 {
2101 let ht = self.forward_hidden(ids, b, t, None, false, None);
2102 let mut keep = Vec::with_capacity(self.nl);
2103 let hs = self.forward_hidden(ids, b, t, Some(ts), true, Some(&mut keep));
2104 let (ce, kl, dhs) = self.loss_and_dhidden(&hs, &ht, tgt, kl_w);
2105 ts.zero_grad();
2106 self.backward(b, t, &keep, dhs, ts);
2107 (1.0 - kl_w) * ce + kl_w * kl
2108 }
2109
2110 pub fn val_ppl(
2114 &self,
2115 va: &[u32],
2116 ts: Option<&TrainState>,
2117 student: bool,
2118 bs: usize,
2119 nrounds: usize,
2120 seq: usize,
2121 ) -> f64 {
2122 let nwin = nrounds * bs;
2123 if va.len() < seq + 2 || nwin == 0 {
2124 return f64::NAN;
2125 }
2126 let stride = (va.len() - seq - 1) / nwin;
2127 let hsz = self.hidden;
2128 let wh = self.head_weight();
2129 let vs = self.vocab;
2130 let pool = self.pool.as_deref();
2131 let mut nll = 0f64;
2132 let mut cnt = 0usize;
2133 for j in 0..nrounds {
2134 let mut ids = Vec::with_capacity(bs * seq);
2135 let mut tgt = Vec::with_capacity(bs * seq);
2136 for bi in 0..bs {
2137 let off = ((j * bs + bi) * stride.max(1)).min(va.len() - seq - 1);
2138 ids.extend_from_slice(&va[off..off + seq]);
2139 tgt.extend_from_slice(&va[off + 1..off + seq + 1]);
2140 }
2141 let h = self.forward_hidden(&ids, bs, seq, ts, student, None);
2142 let n = bs * seq;
2143 let mut ns = vec![0f32; n * hsz];
2144 let mut inv = vec![0f32; n];
2145 ops::rmsnorm_fwd(
2146 &h,
2147 &self.final_norm,
2148 self.eps,
2149 self.gemma,
2150 &mut ns,
2151 &mut inv,
2152 );
2153 let mut lg = vec![0f32; LM_CHUNK * vs];
2154 let mut r0 = 0usize;
2155 while r0 < n {
2156 let r1 = (r0 + LM_CHUNK).min(n);
2157 let c = r1 - r0;
2158 ops::gemm_nt(
2159 &ns[r0 * hsz..r1 * hsz],
2160 wh,
2161 &mut lg[..c * vs],
2162 c,
2163 hsz,
2164 vs,
2165 pool,
2166 );
2167 for r in 0..c {
2168 let row = &lg[r * vs..(r + 1) * vs];
2169 let target = tgt[r0 + r] as usize;
2170 let mut mx = f64::NEG_INFINITY;
2171 for &v in row {
2172 mx = mx.max(v as f64);
2173 }
2174 let mut s = 0f64;
2175 for &v in row {
2176 s += (v as f64 - mx).exp();
2177 }
2178 nll += mx + s.ln() - row[target.min(vs - 1)] as f64;
2179 cnt += 1;
2180 }
2181 r0 = r1;
2182 }
2183 }
2184 (nll / cnt.max(1) as f64).exp()
2185 }
2186}
2187
2188pub fn run_polish(
2200 model: &Arc<CmfModel>,
2201 o1: &O1Cfg,
2202 hp: &FcdHyper,
2203 tr: &[u32],
2204 va: &[u32],
2205 out: &std::path::Path,
2206 gate: Option<&GenGateCfg>,
2207) -> Result<FcdReport, String> {
2208 if tr.len() < hp.seq + 2 {
2209 return Err(format!(
2210 "train corpus too small: {} tokens < seq+2 = {}",
2211 tr.len(),
2212 hp.seq + 2
2213 ));
2214 }
2215 let fm = FcdModel::from_cmf(model, o1)?;
2216 let converted = fm.converted();
2217 if converted.is_empty() {
2218 return Err("no converted layers under this --o1 spec (nothing to polish)".into());
2219 }
2220 tracing::info!(
2221 "fcd: {} layers converted ({} trainable tensors), m={} w={} sink={}, \
2222 corpus train {} / val {} tokens",
2223 converted.len(),
2224 converted.len() * PARAMS_PER_LAYER,
2225 fm.nys.m,
2226 fm.nys.w,
2227 fm.nys.sink,
2228 tr.len(),
2229 va.len()
2230 );
2231
2232 let mut ts = TrainState::new(&fm);
2233 let teacher_ppl = fm.val_ppl(va, None, false, hp.bs, 2, hp.seq);
2234 let ppl_start = fm.val_ppl(va, Some(&ts), true, hp.bs, 2, hp.seq);
2235 tracing::info!(
2236 "fcd: quick-val teacher ppl {teacher_ppl:.2} | zero-shot o1 student ppl {ppl_start:.2}"
2237 );
2238
2239 let mut gate_state: Option<(Pipeline, Vec<f64>)> = match gate {
2241 Some(g) if !g.prompts.is_empty() => {
2242 let greedy = SamplerConfig {
2243 temperature: 0.0,
2244 top_p: 1.0,
2245 top_k: 0,
2246 repetition_penalty: 1.0,
2247 min_p: 0.0,
2248 seed: Some(0),
2249 suppress_tokens: Vec::new(),
2250 };
2251 let mut pipe = Pipeline::from_model(model, greedy)
2252 .map_err(|e| format!("gen-gate pipeline: {e}"))?;
2253 pipe.set_o1(Some(o1.clone()));
2254 apply_trainables(&mut pipe, &fm, &ts);
2255 let base = gate_gen_scores(&mut pipe, g)?;
2256 tracing::info!("fcd gen-gate baseline loop-scores: {base:?}");
2257 Some((pipe, base))
2258 }
2259 Some(_) => {
2260 tracing::warn!("fcd gen-gate requested but val stream too short — gate off");
2261 None
2262 }
2263 None => None,
2264 };
2265 let init_snapshot: Option<Vec<Vec<f32>>> = gate_state.is_some().then(|| ts.data.clone());
2267 let mut gate_evals: Vec<(usize, f64, Vec<f64>, bool)> = Vec::new();
2268
2269 let mut rng = SplitMix64::new(hp.seed);
2270 let mut best: (f64, Option<Vec<Vec<f32>>>, usize) = (f64::INFINITY, None, 0);
2271 let mut losses: Vec<(f64, f64)> = Vec::with_capacity(hp.steps);
2272 let t0 = std::time::Instant::now();
2273 let n_per_step = hp.bs * hp.seq;
2274 for st in 1..=hp.steps {
2275 let mut ids = Vec::with_capacity(n_per_step);
2278 let mut tgt = Vec::with_capacity(n_per_step);
2279 for _ in 0..hp.bs {
2280 let off = (rng.next_u64() as usize) % (tr.len() - hp.seq - 1);
2281 ids.extend_from_slice(&tr[off..off + hp.seq]);
2282 tgt.extend_from_slice(&tr[off + 1..off + hp.seq + 1]);
2283 }
2284
2285 let ht = fm.forward_hidden(&ids, hp.bs, hp.seq, None, false, None);
2286 let mut keep: Vec<Vec<f32>> = Vec::with_capacity(fm.nl);
2287 let hs = fm.forward_hidden(&ids, hp.bs, hp.seq, Some(&ts), true, Some(&mut keep));
2288 let (ce, kl, dhs) = fm.loss_and_dhidden(&hs, &ht, &tgt, hp.kl_w);
2289 ts.zero_grad();
2290 fm.backward(hp.bs, hp.seq, &keep, dhs, &mut ts);
2291 let gn = ts.clip_and_step(hp.lr);
2292 losses.push((ce, kl));
2293
2294 let el = t0.elapsed().as_secs_f64();
2295 tracing::info!(
2296 "fcd step {st}/{}: ce {ce:.3} kl {kl:.3} |g| {gn:.3} ({:.1}s/step)",
2297 hp.steps,
2298 el / st as f64
2299 );
2300 if hp.eval_every > 0 && st % hp.eval_every == 0 {
2301 let p = fm.val_ppl(va, Some(&ts), true, hp.bs, 2, hp.seq);
2302 match (&mut gate_state, gate) {
2303 (Some((pipe, base)), Some(g)) => {
2304 apply_trainables(pipe, &fm, &ts);
2305 let scores = gate_gen_scores(pipe, g)?;
2306 let pass = gate_pass(&scores, base, g.threshold, g.baseline_slack);
2307 let tag = if pass && p < best.0 {
2308 best = (p, Some(ts.data.clone()), st);
2309 " *best*"
2310 } else {
2311 ""
2312 };
2313 tracing::info!(
2314 "fcd eval step {st}: val ppl {p:.2} | gen-gate {} (loop-scores {scores:?}){tag}",
2315 if pass { "PASS" } else { "FAIL" }
2316 );
2317 gate_evals.push((st, p, scores, pass));
2318 }
2319 _ => {
2320 let tag = if p < best.0 {
2321 best = (p, Some(ts.data.clone()), st);
2322 " *best*"
2323 } else {
2324 ""
2325 };
2326 tracing::info!("fcd eval step {st}: val ppl {p:.2}{tag}");
2327 }
2328 }
2329 }
2330 }
2331
2332 let mut gate_chosen: Option<usize> = None;
2336 if let Some(snap) = best.1.take() {
2337 ts.data = snap;
2338 gate_chosen = Some(best.2);
2339 tracing::info!(
2340 "fcd: restored best checkpoint from step {} (val ppl {:.2})",
2341 best.2,
2342 best.0
2343 );
2344 } else if let Some(init) = init_snapshot {
2345 ts.data = init;
2346 tracing::info!(
2347 "fcd: polish rejected by generation gate — identity artifact (zero-shot state written; claim 13 floor)"
2348 );
2349 }
2350 let ppl_final = fm.val_ppl(va, Some(&ts), true, hp.bs, 6, hp.seq);
2351 let report = FcdReport {
2352 converted: converted.clone(),
2353 teacher_ppl,
2354 ppl_start,
2355 ppl_best: best.0.min(ppl_final),
2356 best_step: best.2,
2357 ppl_final,
2358 steps_run: hp.steps,
2359 sec_per_step: t0.elapsed().as_secs_f64() / hp.steps.max(1) as f64,
2360 losses,
2361 gate: gate_state.map(|(_, base)| GateReport {
2362 baseline: base,
2363 evals: gate_evals,
2364 chosen: gate_chosen,
2365 }),
2366 };
2367 save_polished(model, out, &fm, &ts, o1, hp, &report)?;
2368 Ok(report)
2369}
2370
2371fn apply_trainables(pipe: &mut Pipeline, fm: &FcdModel, ts: &TrainState) {
2375 let hidden = fm.hidden;
2376 for (slot, &li) in ts.layers.iter().enumerate() {
2377 let b = slot * PARAMS_PER_LAYER;
2378 let inter = fm.layers[li].inter;
2379 let lw = &mut pipe.weights.layers[li];
2380 lw.input_norm = ts.data[b].clone();
2381 lw.post_norm = ts.data[b + 1].clone();
2382 lw.ffn = FfnKind::Dense(DenseFfn {
2383 gate_proj: QTensor::from_f32(ts.data[b + 2].clone(), inter, hidden),
2384 up_proj: QTensor::from_f32(ts.data[b + 3].clone(), inter, hidden),
2385 down_proj: QTensor::from_f32(ts.data[b + 4].clone(), hidden, inter),
2386 act: crate::pipeline::Act::Silu,
2387 });
2388 }
2389}
2390
2391fn gate_gen_scores(pipe: &mut Pipeline, g: &GenGateCfg) -> Result<Vec<f64>, String> {
2393 g.prompts
2394 .iter()
2395 .map(|p| {
2396 pipe.generate_from_ids(p, g.gen_tokens, None, None)
2397 .map(|r| loop_score(&r.token_ids))
2398 })
2399 .collect()
2400}
2401
2402fn save_polished(
2407 model: &CmfModel,
2408 out: &std::path::Path,
2409 fm: &FcdModel,
2410 ts: &TrainState,
2411 o1: &O1Cfg,
2412 hp: &FcdHyper,
2413 report: &FcdReport,
2414) -> Result<(), String> {
2415 use cortiq_core::format::TensorSpec;
2416 let mut replace: std::collections::HashMap<String, (usize, usize)> =
2417 std::collections::HashMap::new(); for (s, &li) in ts.layers.iter().enumerate() {
2419 let p = format!("model.layers.{li}.");
2420 for (k, suffix) in [
2421 (0usize, "input_layernorm.weight"),
2422 (1, "post_attention_layernorm.weight"),
2423 (2, "mlp.gate_proj.weight"),
2424 (3, "mlp.up_proj.weight"),
2425 (4, "mlp.down_proj.weight"),
2426 ] {
2427 replace.insert(format!("{p}{suffix}"), (s, k));
2428 }
2429 }
2430 let mut specs = Vec::with_capacity(model.tensors.len());
2431 for t in &model.tensors {
2432 if let Some(&(s, k)) = replace.get(&t.name) {
2433 let data = &ts.data[s * PARAMS_PER_LAYER + k];
2434 let mut bytes = Vec::with_capacity(data.len() * 4);
2435 for v in data {
2436 bytes.extend_from_slice(&v.to_le_bytes());
2437 }
2438 specs.push(TensorSpec {
2439 name: t.name.clone(),
2440 dtype: TensorDtype::F32,
2441 shape: t.shape.clone(),
2442 data: bytes,
2443 });
2444 } else {
2445 specs.push(TensorSpec {
2446 name: t.name.clone(),
2447 dtype: t.dtype,
2448 shape: t.shape.clone(),
2449 data: model.entry_bytes(t).to_vec(),
2450 });
2451 }
2452 }
2453
2454 let mut header = model.header.clone();
2455 let mut prov = match header.provenance.take() {
2456 Some(serde_json::Value::Object(m)) => m,
2457 _ => serde_json::Map::new(),
2458 };
2459 let layers_json = match &o1.layers {
2460 O1Layers::All => serde_json::json!("all"),
2461 O1Layers::Deep(n) => serde_json::json!(format!("deep{n}")),
2462 O1Layers::List(v) => serde_json::json!(v),
2463 };
2464 prov.insert(
2465 "o1_attn".into(),
2466 serde_json::json!({
2467 "layers": layers_json, "m": o1.m, "w": o1.w, "sink": o1.sink
2468 }),
2469 );
2470 prov.insert(
2471 "fcd".into(),
2472 serde_json::json!({
2473 "steps": hp.steps, "lr": hp.lr, "kl_w": hp.kl_w,
2474 "bs": hp.bs, "seq": hp.seq,
2475 "teacher_ppl": report.teacher_ppl,
2476 "ppl_start": report.ppl_start,
2477 "ppl_final": report.ppl_final,
2478 "best_step": report.best_step,
2479 "converted_layers": report.converted,
2480 }),
2481 );
2482 header.provenance = Some(serde_json::Value::Object(prov));
2483 let _ = fm; let masks = if model.masks.masks.is_empty() {
2486 None
2487 } else {
2488 Some(&model.masks)
2489 };
2490 CmfModel::write(out, &header, &specs, masks, model.vocab.as_deref())
2491 .map_err(|e| format!("writing polished cmf: {e}"))
2492}
2493
2494#[cfg(test)]
2495mod tests {
2496 use super::*;
2497
2498 #[test]
2500 fn gate_selects_lowest_ppl_among_passing() {
2501 let base = vec![0.10, 0.00, 0.20];
2502 let evals = vec![
2503 (25usize, 21.0, vec![0.10, 0.05, 0.20]), (50, 18.0, vec![0.40, 0.00, 0.10]), (75, 19.0, vec![0.15, 0.05, 0.25]), (100, 18.5, vec![0.20, 0.30, 0.20]), ];
2508 let sel = select_checkpoint(&evals, &base, 0.35, 0.10);
2509 assert_eq!(sel, Some(2), "step 75 is the lowest-ppl PASSING checkpoint");
2510 }
2511
2512 #[test]
2515 fn gate_all_fail_is_identity() {
2516 let base = vec![0.0, 0.0, 0.0];
2517 let evals = vec![
2518 (25usize, 15.0, vec![0.50, 0.0, 0.0]),
2519 (50, 14.0, vec![0.0, 0.36, 0.0]),
2520 (75, 13.0, vec![0.0, 0.0, 0.11]), ];
2522 assert_eq!(select_checkpoint(&evals, &base, 0.35, 0.10), None);
2523 }
2524
2525 #[test]
2528 fn gate_boundaries_and_tie_break() {
2529 let base = vec![0.25];
2530 assert!(gate_pass(&[0.35], &base, 0.35, 0.10), "== threshold passes");
2531 assert!(
2532 gate_pass(&[0.35], &[0.25], 0.35, 0.10),
2533 "== base+slack passes"
2534 );
2535 assert!(!gate_pass(&[0.351], &base, 0.35, 0.10));
2536 assert!(!gate_pass(&[0.30], &[0.10], 0.35, 0.10), "0.30 > 0.10+0.10");
2537 let evals = vec![(25usize, 20.0, vec![0.10]), (50, 20.0, vec![0.10])];
2538 assert_eq!(
2539 select_checkpoint(&evals, &base, 0.35, 0.10),
2540 Some(0),
2541 "equal ppl → earliest checkpoint"
2542 );
2543 }
2544}