1use crate::fcd_ops::{self as ops, FfnAct, 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 pub polish_only: bool,
61}
62
63impl Default for FcdHyper {
64 fn default() -> Self {
65 Self {
66 steps: 300,
67 lr: 5e-5,
68 kl_w: 0.7,
69 eval_every: 25,
70 bs: 2,
71 seq: 512,
72 seed: 0,
73 polish_only: false,
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug)]
88pub struct FcdStream<'a> {
89 pub ids: &'a [u32],
90 pub weight: Option<&'a [f32]>,
91 pub starts: Option<&'a [usize]>,
92}
93
94impl<'a> FcdStream<'a> {
95 pub fn plain(ids: &'a [u32]) -> Self {
96 Self { ids, weight: None, starts: None }
97 }
98
99 fn is_plain(&self) -> bool {
100 self.weight.is_none() && self.starts.is_none()
101 }
102
103 fn w(&self, i: usize) -> f32 {
104 self.weight.map_or(1.0, |w| w[i])
105 }
106
107 fn scored_records(&self) -> Vec<(usize, usize, usize, usize)> {
110 let n = self.ids.len();
111 let whole = [0usize];
112 let starts = self.starts.unwrap_or(&whole);
113 let mut out = Vec::new();
114 for (r, &st) in starts.iter().enumerate() {
115 let en = starts.get(r + 1).copied().unwrap_or(n).min(n);
116 let mut first = None;
117 let mut last = 0;
118 for i in st + 1..en {
119 if self.w(i) > 0.0 {
120 first.get_or_insert(i);
121 last = i + 1;
122 }
123 }
124 if let Some(f) = first {
125 out.push((st, en, f, last));
126 }
127 }
128 out
129 }
130
131 fn window_start(&self, rec: (usize, usize, usize, usize), p: usize, seq: usize, jitter: u64) -> usize {
135 let (st, en, _, _) = rec;
136 let max_off = self.ids.len() - seq - 1;
137 if en - st <= seq + 1 {
138 return st.min(max_off);
139 }
140 let lo = st.max(p.saturating_sub(seq));
141 let hi = (p - 1).min(max_off).max(lo);
142 (lo + (jitter as usize) % (hi - lo + 1)).min(max_off)
143 }
144
145 fn target_weights(&self, off: usize, seq: usize, rec: (usize, usize, usize, usize)) -> Vec<f32> {
148 let (st, en, _, _) = rec;
149 (0..seq)
150 .map(|t| {
151 let i = off + 1 + t;
152 if i > st && i < en { self.w(i) } else { 0.0 }
153 })
154 .collect()
155 }
156}
157
158enum SpanIn<'a> {
160 Ids(&'a [u32]),
161 Hidden(Vec<f32>),
162}
163
164#[derive(Clone, Debug)]
167pub struct FcdReport {
168 pub converted: Vec<usize>,
169 pub teacher_ppl: f64,
171 pub ppl_start: f64,
173 pub ppl_best: f64,
175 pub best_step: usize,
176 pub ppl_final: f64,
178 pub steps_run: usize,
179 pub sec_per_step: f64,
180 pub losses: Vec<(f64, f64)>,
182 pub gate: Option<GateReport>,
184}
185
186#[derive(Clone, Debug)]
188pub struct GateReport {
189 pub baseline: Vec<f64>,
191 pub evals: Vec<(usize, f64, Vec<f64>, bool)>,
193 pub chosen: Option<usize>,
196}
197
198pub fn loop_score(ids: &[u32]) -> f64 {
203 if ids.len() < 5 {
204 return 0.0;
205 }
206 let grams: std::collections::HashSet<&[u32]> = ids.windows(4).collect();
207 1.0 - grams.len() as f64 / ids.windows(4).count() as f64
208}
209
210#[derive(Clone, Debug)]
214pub struct GenGateCfg {
215 pub prompts: Vec<Vec<u32>>,
218 pub gen_tokens: usize,
219 pub threshold: f64,
221 pub baseline_slack: f64,
223}
224
225impl GenGateCfg {
226 pub fn standard(va: &[u32]) -> Option<Self> {
229 let l = va.len().saturating_sub(500);
230 if l < 400 {
231 return None;
232 }
233 let prompts = [l / 10, l / 2, 8 * l / 10]
234 .iter()
235 .map(|&off| va[off..off + 400].to_vec())
236 .collect();
237 Some(Self {
238 prompts,
239 gen_tokens: 60,
240 threshold: 0.35,
241 baseline_slack: 0.10,
242 })
243 }
244}
245
246pub fn gate_pass(scores: &[f64], baseline: &[f64], threshold: f64, slack: f64) -> bool {
250 scores
251 .iter()
252 .zip(baseline)
253 .all(|(&s, &b)| s <= threshold && s <= b + slack)
254}
255
256pub fn select_checkpoint(
261 evals: &[(usize, f64, Vec<f64>)],
262 baseline: &[f64],
263 threshold: f64,
264 slack: f64,
265) -> Option<usize> {
266 let mut best: Option<usize> = None;
267 for (i, (_, ppl, scores)) in evals.iter().enumerate() {
268 if !gate_pass(scores, baseline, threshold, slack) {
269 continue;
270 }
271 if best.map(|b| *ppl < evals[b].1).unwrap_or(true) {
272 best = Some(i);
273 }
274 }
275 best
276}
277
278pub mod prof {
283 use std::sync::atomic::{AtomicU64, Ordering};
284 pub static ATTN_FWD: AtomicU64 = AtomicU64::new(0);
285 pub static FFN_FWD: AtomicU64 = AtomicU64::new(0);
286 pub static FFN_CHAIN: AtomicU64 = AtomicU64::new(0);
289 pub static FFN_ACT: AtomicU64 = AtomicU64::new(0);
290 pub static FFN_HOST: AtomicU64 = AtomicU64::new(0);
291 pub static FFN_PRE: AtomicU64 = AtomicU64::new(0);
294 pub static FFN_POST: AtomicU64 = AtomicU64::new(0);
295 pub static ATTN_CHAIN: AtomicU64 = AtomicU64::new(0);
297 pub static BWD: AtomicU64 = AtomicU64::new(0);
298 pub static GEMM: AtomicU64 = AtomicU64::new(0);
299 pub static GEMM_CALLS: AtomicU64 = AtomicU64::new(0);
300 #[inline]
301 pub fn add(c: &AtomicU64, t: std::time::Instant) {
302 c.fetch_add(t.elapsed().as_nanos() as u64, Ordering::Relaxed);
303 }
304 pub fn take(c: &AtomicU64) -> f64 {
306 c.swap(0, Ordering::Relaxed) as f64 / 1e9
307 }
308
309 use std::sync::Mutex;
310 pub static SHAPES: Mutex<Vec<((usize, usize, usize), (u64, u64))>> = Mutex::new(Vec::new());
314 pub fn gemm_shape(n: usize, k: usize, m: usize, t0: std::time::Instant) {
315 let ns = t0.elapsed().as_nanos() as u64;
316 let mut g = SHAPES.lock().unwrap();
317 match g.iter_mut().find(|(s, _)| *s == (n, k, m)) {
318 Some((_, (c, tt))) => {
319 *c += 1;
320 *tt += ns;
321 }
322 None => g.push(((n, k, m), (1, ns))),
323 }
324 }
325 pub fn shape_report(top: usize) -> String {
326 let mut g = SHAPES.lock().unwrap();
327 g.sort_by_key(|(_, (_, ns))| std::cmp::Reverse(*ns));
328 let out = g
329 .iter()
330 .take(top)
331 .map(|((n, k, m), (c, ns))| {
332 format!(
333 " [{n}x{k}x{m}] {c} calls, {:.1}s total, {:.1} ms/call",
334 *ns as f64 / 1e9,
335 *ns as f64 / 1e6 / *c as f64
336 )
337 })
338 .collect::<Vec<_>>()
339 .join("\n");
340 g.clear();
341 out
342 }
343}
344
345enum FcdAttn {
348 Full {
349 qrows: usize,
353 q_norm: Option<Vec<f32>>,
354 k_norm: Option<Vec<f32>>,
355 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
356 bias_cat: Option<Vec<f32>>,
360 output_gate: bool,
363 head_gate: Option<Vec<f32>>,
367 window: Option<usize>,
369 local_rope: bool,
372 },
373 Gdn {
376 wqkv: Vec<f32>,
377 wz: Vec<f32>,
378 wa: Vec<f32>,
379 wb: Vec<f32>,
380 conv: Vec<f32>,
381 a_log: Vec<f32>,
382 dt_bias: Vec<f32>,
383 norm: Vec<f32>,
384 wout: Vec<f32>,
385 },
386}
387
388pub(crate) struct FcdLayer {
389 attn: FcdAttn,
390 pub(crate) inter: usize,
391 pub(crate) iln: Vec<f32>,
394 pub(crate) pln: Vec<f32>,
395}
396
397#[derive(Clone, Copy)]
399struct GdnDims {
400 nv: usize,
401 nk: usize,
402 dk: usize,
403 dv: usize,
404 kk: usize,
405}
406
407impl GdnDims {
408 fn c_dim(&self) -> usize {
409 2 * self.nk * self.dk + self.nv * self.dv
410 }
411 fn vd(&self) -> usize {
412 self.nv * self.dv
413 }
414}
415
416pub struct FcdModel {
418 pub hidden: usize,
419 pub nh: usize,
420 pub nkv: usize,
421 pub hd: usize,
422 pub nl: usize,
423 pub vocab: usize,
424 pub(crate) eps: f64,
425 pub(crate) gemma: bool,
426 rotary_dim: usize,
427 inv_freq: Vec<f64>,
428 rotary_dim_local: Option<usize>,
431 inv_freq_local: Option<Vec<f64>>,
432 pub(crate) act: FfnAct,
435 pub(crate) attn_acts: bool,
439 pub(crate) embed: Vec<f32>,
441 pub(crate) lm_head: Option<Vec<f32>>,
442 pub(crate) final_norm: Vec<f32>,
443 pub(crate) layers: Vec<FcdLayer>,
444 pub(crate) src: std::sync::Arc<CmfModel>,
447 mats_cache: Vec<std::sync::Mutex<Option<std::sync::Arc<LayerMats>>>>,
452 rope_tab: std::sync::Mutex<Option<((usize, bool), std::sync::Arc<Vec<f32>>)>>,
456 mats_window: usize,
458 o1_flags: Vec<bool>,
460 nys: NysCfg,
461 gdn: Option<GdnDims>,
463 pub(crate) loops: usize,
470 pub(crate) loop_norm: bool,
471 pub(crate) pool: Option<Arc<Pool>>,
472 pub polish_only: bool,
475}
476
477fn deq(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
478 let e = model
479 .tensor(name)
480 .ok_or_else(|| format!("tensor '{name}' not found"))?;
481 let mut out = vec![0f32; e.n_elems()];
482 cortiq_core::quant::dequant_tensor(e, model.entry_bytes(e), &mut out)?;
483 Ok(out)
484}
485
486#[cfg(target_os = "linux")]
496fn cgroup_limit_bytes() -> Option<u64> {
497 let read =
498 |p: &str| -> Option<u64> { std::fs::read_to_string(p).ok()?.trim().parse::<u64>().ok() };
499 read("/sys/fs/cgroup/memory.max")
502 .or_else(|| read("/sys/fs/cgroup/memory/memory.limit_in_bytes"))
503 .filter(|&v| v < u64::MAX / 2)
504}
505
506pub(crate) fn apply_layer_eligibility(flags: &mut [bool], is_full: &[bool], polish_only: bool) {
515 if polish_only {
516 return;
517 }
518 for (li, f) in flags.iter_mut().enumerate() {
519 if *f && !is_full.get(li).copied().unwrap_or(false) {
520 *f = false;
521 }
522 }
523}
524
525pub(crate) fn effective_total(host: u64, cgroup_limit: Option<u64>) -> u64 {
528 match cgroup_limit {
529 Some(l) => host.min(l),
530 None => host,
531 }
532}
533
534pub(crate) fn effective_available(host: u64, limit: Option<u64>, used: Option<u64>) -> u64 {
537 match (limit, used) {
538 (Some(l), Some(u)) => host.min(l.saturating_sub(u)),
539 (Some(l), None) => host.min(l),
540 _ => host,
541 }
542}
543
544#[cfg(target_os = "linux")]
546fn cgroup_used_bytes() -> Option<u64> {
547 let read =
548 |p: &str| -> Option<u64> { std::fs::read_to_string(p).ok()?.trim().parse::<u64>().ok() };
549 read("/sys/fs/cgroup/memory.current")
550 .or_else(|| read("/sys/fs/cgroup/memory/memory.usage_in_bytes"))
551}
552
553pub fn physical_total_bytes() -> Option<u64> {
556 #[cfg(target_os = "macos")]
557 {
558 let out = std::process::Command::new("sysctl")
559 .args(["-n", "hw.memsize"])
560 .output()
561 .ok()?;
562 return String::from_utf8_lossy(&out.stdout).trim().parse().ok();
563 }
564 #[cfg(target_os = "linux")]
565 {
566 let mem = std::fs::read_to_string("/proc/meminfo").ok()?;
567 let kb: u64 = mem
568 .lines()
569 .find(|l| l.starts_with("MemTotal:"))?
570 .split_whitespace()
571 .nth(1)?
572 .parse()
573 .ok()?;
574 return Some(effective_total(kb * 1024, cgroup_limit_bytes()));
575 }
576 #[allow(unreachable_code)]
577 None
578}
579
580pub(crate) fn available_ram_bytes() -> Option<u64> {
583 #[cfg(target_os = "macos")]
584 {
585 let total: u64 = {
589 let out = std::process::Command::new("sysctl")
590 .args(["-n", "hw.memsize"])
591 .output()
592 .ok()?;
593 String::from_utf8_lossy(&out.stdout).trim().parse().ok()?
594 };
595 let out = std::process::Command::new("memory_pressure")
596 .arg("-Q")
597 .output()
598 .ok()?;
599 let text = String::from_utf8_lossy(&out.stdout);
600 let pct: u64 = text
601 .lines()
602 .find(|l| l.contains("free percentage"))?
603 .split(':')
604 .nth(1)?
605 .trim()
606 .trim_end_matches('%')
607 .parse()
608 .ok()?;
609 return Some(total / 100 * pct);
610 }
611 #[cfg(target_os = "linux")]
612 {
613 let mem = std::fs::read_to_string("/proc/meminfo").ok()?;
614 let kb: u64 = mem
615 .lines()
616 .find(|l| l.starts_with("MemAvailable:"))?
617 .split_whitespace()
618 .nth(1)?
619 .parse()
620 .ok()?;
621 return Some(effective_available(
624 kb * 1024,
625 cgroup_limit_bytes(),
626 cgroup_used_bytes(),
627 ));
628 }
629 #[allow(unreachable_code)]
630 None
631}
632
633pub(crate) fn deq_pub(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
636 deq(model, name)
637}
638
639pub(crate) struct LayerMats {
644 pub(crate) wqkv: Vec<f32>,
645 pub(crate) wo: Vec<f32>,
646 pub(crate) gu: Vec<f32>,
647 pub(crate) down: Vec<f32>,
648}
649
650impl FcdModel {
651 pub(crate) fn mats(&self, li: usize) -> Result<std::sync::Arc<LayerMats>, String> {
656 {
657 let slot = self.mats_cache[li].lock().unwrap();
658 if let Some(m) = slot.as_ref() {
659 return Ok(m.clone());
660 }
661 }
662 let p = format!("model.layers.{li}.");
663 let d = |name: String| -> Result<Vec<f32>, String> { deq(&self.src, &name) };
664 let built = if matches!(self.layers[li].attn, FcdAttn::Full { .. }) {
665 let wq = d(format!("{p}self_attn.q_proj.weight"))?;
666 let wk = d(format!("{p}self_attn.k_proj.weight"))?;
667 let wv = d(format!("{p}self_attn.v_proj.weight"))?;
668 let mut wqkv = Vec::with_capacity(wq.len() + wk.len() + wv.len());
669 wqkv.extend_from_slice(&wq);
670 wqkv.extend_from_slice(&wk);
671 wqkv.extend_from_slice(&wv);
672 let gate = d(format!("{p}mlp.gate_proj.weight"))?;
673 let up = d(format!("{p}mlp.up_proj.weight"))?;
674 let mut gu = Vec::with_capacity(gate.len() + up.len());
675 gu.extend_from_slice(&gate);
676 gu.extend_from_slice(&up);
677 LayerMats {
678 wqkv,
679 wo: d(format!("{p}self_attn.o_proj.weight"))?,
680 gu,
681 down: d(format!("{p}mlp.down_proj.weight"))?,
682 }
683 } else {
684 let gate = d(format!("{p}mlp.gate_proj.weight"))?;
687 let up = d(format!("{p}mlp.up_proj.weight"))?;
688 let mut gu = Vec::with_capacity(gate.len() + up.len());
689 gu.extend_from_slice(&gate);
690 gu.extend_from_slice(&up);
691 LayerMats {
692 wqkv: Vec::new(),
693 wo: Vec::new(),
694 gu,
695 down: d(format!("{p}mlp.down_proj.weight"))?,
696 }
697 };
698 let arc = std::sync::Arc::new(built);
699 *self.mats_cache[li].lock().unwrap() = Some(arc.clone());
700 if self.mats_window > 0 {
703 let w = self.mats_window;
704 for (j, slot) in self.mats_cache.iter().enumerate() {
705 let dist = li.abs_diff(j).min(self.layers.len() - li.abs_diff(j));
706 if dist > w {
707 *slot.lock().unwrap() = None;
708 }
709 }
710 }
711 Ok(arc)
712 }
713}
714
715impl FcdModel {
716 pub fn from_cmf(
719 model: &std::sync::Arc<CmfModel>,
720 o1: &O1Cfg,
721 polish_only: bool,
722 ) -> Result<Self, String> {
723 let arch = model.arch().clone();
724 let act = FfnAct::from_arch(&arch.hidden_act).ok_or_else(|| {
725 format!(
726 "fcd/skill-bake: hidden_act '{}' not supported yet (silu | gelu)",
727 arch.hidden_act
728 )
729 })?;
730 let has_linear = arch
731 .layer_types
732 .iter()
733 .any(|t| matches!(t, LayerType::LinearAttention));
734 let gdn = if has_linear {
735 let lc = arch
736 .linear_core
737 .as_ref()
738 .ok_or_else(|| "model has linear layers but no arch.linear_core".to_string())?;
739 if lc.kind != "gated_delta_net" {
740 return Err(format!(
741 "linear core '{}' has no FCD backward (only gated_delta_net)",
742 lc.kind
743 ));
744 }
745 Some(GdnDims {
746 nv: lc.num_heads,
747 nk: arch
748 .linear_num_key_heads
749 .ok_or("linear core needs arch.linear_num_key_heads")?,
750 dk: arch
751 .linear_key_head_dim
752 .ok_or("linear core needs arch.linear_key_head_dim")?,
753 dv: lc.value_head_dim,
754 kk: arch
755 .linear_conv_kernel_dim
756 .ok_or("linear core needs arch.linear_conv_kernel_dim")?,
757 })
758 } else {
759 None
760 };
761 let (nh, nkv, hd, h) = (
762 arch.num_attention_heads,
763 arch.num_kv_heads,
764 arch.head_dim,
765 arch.hidden_size,
766 );
767 let embed = deq(model, "model.embed_tokens.weight")?;
768 let lm_head = if model.tensor("lm_head.weight").is_some() {
769 Some(deq(model, "lm_head.weight")?)
770 } else if arch.tie_word_embeddings {
771 None
772 } else {
773 return Err("no lm_head.weight and tie_word_embeddings is false".into());
774 };
775 let final_norm = deq(model, "model.norm.weight")?;
776
777 let mut layers = Vec::with_capacity(arch.num_layers);
778 for li in 0..arch.num_layers {
779 let p = format!("model.layers.{li}.");
780 if model.tensor(&format!("{p}mlp.gate.weight")).is_some() {
781 return Err(format!(
782 "layer {li} is MoE — FCD polish supports dense FFN only"
783 ));
784 }
785 let attn = match arch.layer_types.get(li) {
786 Some(LayerType::LinearAttention) => {
787 let la = |n: &str| deq(model, &format!("{p}linear_attn.{n}"));
788 FcdAttn::Gdn {
789 wqkv: la("in_proj_qkv.weight")?,
790 wz: la("in_proj_z.weight")?,
791 wa: la("in_proj_a.weight")?,
792 wb: la("in_proj_b.weight")?,
793 conv: la("conv1d.weight")?,
794 a_log: la("A_log")?,
795 dt_bias: la("dt_bias")?,
796 norm: la("norm.weight")?,
797 wout: la("out_proj.weight")?,
798 }
799 }
800 _ => {
801 let wq = deq(model, &format!("{p}self_attn.q_proj.weight"))?;
802 let output_gate = wq.len() == 2 * nh * hd * h;
803 let opt = |n: &str| -> Option<Vec<f32>> {
804 model
805 .tensor(&format!("{p}self_attn.{n}"))
806 .and_then(|_| deq(model, &format!("{p}self_attn.{n}")).ok())
807 };
808 let bias = match (opt("q_proj.bias"), opt("k_proj.bias"), opt("v_proj.bias")) {
809 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
810 _ => None,
811 };
812 let qrows = wq.len() / h;
813 drop(wq);
814 let bias_cat = bias.as_ref().map(|(bq, bk, bv)| {
815 let mut v = Vec::with_capacity(bq.len() + bk.len() + bv.len());
816 v.extend_from_slice(bq);
817 v.extend_from_slice(bk);
818 v.extend_from_slice(bv);
819 v
820 });
821 let head_gate = match opt("g_proj.weight") {
825 None => None,
826 Some(g) if g.len() == nh * h => Some(g),
827 Some(g) => {
828 return Err(format!(
829 "layer {li}: self_attn.g_proj has {} values; only a per-head gate (nh·hidden = {}) is supported",
830 g.len(),
831 nh * h
832 ));
833 }
834 };
835 let sliding = matches!(arch.layer_types.get(li), Some(LayerType::SlidingAttention));
836 let window = if sliding {
837 Some(arch.sliding_window.ok_or_else(|| {
838 format!("layer {li} is SlidingAttention but arch.sliding_window is unset")
839 })?)
840 } else {
841 None
842 };
843 FcdAttn::Full {
844 qrows,
845 q_norm: opt("q_norm.weight"),
846 k_norm: opt("k_norm.weight"),
847 bias,
848 bias_cat,
849 output_gate,
850 head_gate,
851 window,
852 local_rope: sliding,
853 }
854 }
855 };
856 let gate = deq(model, &format!("{p}mlp.gate_proj.weight"))?;
857 let inter = gate.len() / h;
858 drop(gate);
859 layers.push(FcdLayer {
860 attn,
861 inter,
862 iln: deq(model, &format!("{p}input_layernorm.weight"))?,
863 pln: deq(model, &format!("{p}post_attention_layernorm.weight"))?,
864 });
865 }
866
867 let rotary_dim = ((hd as f32 * arch.partial_rotary_factor) as usize)
868 .max(2)
869 .min(hd);
870 let base = arch.rope_theta;
871 let inv_freq: Vec<f64> = (0..rotary_dim / 2)
872 .map(|i| 1.0 / base.powf(2.0 * i as f64 / rotary_dim as f64))
873 .collect();
874 let any_sliding = arch
878 .layer_types
879 .iter()
880 .any(|t| matches!(t, LayerType::SlidingAttention));
881 let rotary_dim_local = any_sliding.then(|| {
882 ((hd as f32 * arch.local_partial_rotary_factor.unwrap_or(arch.partial_rotary_factor))
883 as usize)
884 .max(2)
885 .min(hd)
886 });
887 let inv_freq_local = match (rotary_dim_local, arch.rope_local_base_freq) {
888 (Some(rd), Some(lb)) => Some(
889 (0..rd / 2)
890 .map(|i| 1.0 / lb.powf(2.0 * i as f64 / rd as f64))
891 .collect::<Vec<f64>>(),
892 ),
893 _ => None,
894 };
895 let loops = arch.num_loops.max(1);
896 let loop_norm = arch.loop_final_norm;
897 let mut flags = o1.layer_flags(arch.num_layers);
898 flags.resize(arch.num_layers, false);
899 {
908 let is_full: Vec<bool> = layers
909 .iter()
910 .map(|l| matches!(l.attn, FcdAttn::Full { .. }))
911 .collect();
912 apply_layer_eligibility(&mut flags, &is_full, polish_only);
913 }
914 let per_layer: u64 = layers
918 .first()
919 .map(|_| {
920 let qrows = match layers[0].attn {
921 FcdAttn::Full { qrows, .. } => qrows,
922 _ => 0,
923 };
924 ((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
928 })
929 .unwrap_or(0);
930 let mats_window = if let Ok(v) = std::env::var("CMF_BAKE_MATS_LAYERS") {
931 v.parse().unwrap_or(0)
932 } else {
933 let avail = available_ram_bytes().unwrap_or(u64::MAX);
934 let cap = physical_total_bytes()
935 .map(|t| t / 5 * 3)
936 .unwrap_or(u64::MAX)
937 .min(avail.saturating_sub(avail / 10));
938 let all = per_layer.saturating_mul(arch.num_layers as u64);
939 if all + 4 * 1024 * 1024 * 1024 <= cap {
940 0 } else {
942 let w = (cap.saturating_sub(3 * 1024 * 1024 * 1024) / per_layer.max(1))
943 .clamp(2, arch.num_layers as u64) as usize;
944 tracing::info!(
945 "fcd replica: streaming {w} of {} layers (~{:.1} GB resident of ~{:.1} GB total)",
946 arch.num_layers,
947 (w as u64 * per_layer) as f64 / 1e9,
948 all as f64 / 1e9,
949 );
950 w
951 }
952 };
953 let mats_cache = (0..arch.num_layers)
954 .map(|_| std::sync::Mutex::new(None))
955 .collect();
956 Ok(Self {
957 src: model.clone(),
958 mats_cache,
959 rope_tab: std::sync::Mutex::new(None),
960 mats_window,
961 hidden: h,
962 nh,
963 nkv,
964 hd,
965 nl: arch.num_layers,
966 vocab: arch.vocab_size.min(embed.len() / h),
967 eps: arch.rms_norm_eps,
968 gemma: matches!(arch.norm_style, NormStyle::Gemma),
969 rotary_dim,
970 inv_freq,
971 rotary_dim_local,
972 inv_freq_local,
973 act,
974 attn_acts: true,
975 embed,
976 lm_head,
977 final_norm,
978 layers,
979 o1_flags: flags,
980 nys: NysCfg {
983 m: o1.m,
984 w: o1.w,
985 sink: o1.sink,
986 prefill: None,
987 },
988 gdn,
989 loops,
990 loop_norm,
991 pool: Pool::from_env(),
992 polish_only: false,
993 })
994 }
995
996 pub fn converted(&self) -> Vec<usize> {
998 (0..self.nl).filter(|&i| self.o1_flags[i]).collect()
999 }
1000
1001 fn head_weight(&self) -> &[f32] {
1002 self.lm_head.as_deref().unwrap_or(&self.embed)
1003 }
1004}
1005
1006const PARAMS_PER_LAYER: usize = 5; pub struct TrainState {
1013 pub layers: Vec<usize>,
1014 pub data: Vec<Vec<f32>>,
1016 grad: Vec<Vec<f32>>,
1017 m1: Vec<Vec<f32>>,
1018 m2: Vec<Vec<f32>>,
1019 step_t: u64,
1020}
1021
1022impl TrainState {
1023 pub fn new(fm: &FcdModel) -> Self {
1024 let layers = fm.converted();
1025 let mut data = Vec::with_capacity(layers.len() * PARAMS_PER_LAYER);
1026 for &li in &layers {
1027 let l = &fm.layers[li];
1028 let p = format!("model.layers.{li}.");
1029 data.push(l.iln.clone());
1030 data.push(l.pln.clone());
1031 data.push(deq(&fm.src, &format!("{p}mlp.gate_proj.weight")).expect("gate"));
1034 data.push(deq(&fm.src, &format!("{p}mlp.up_proj.weight")).expect("up"));
1035 data.push(deq(&fm.src, &format!("{p}mlp.down_proj.weight")).expect("down"));
1036 }
1037 let zeros: Vec<Vec<f32>> = data.iter().map(|d| vec![0f32; d.len()]).collect();
1038 Self {
1039 layers,
1040 grad: zeros.clone(),
1041 m1: zeros.clone(),
1042 m2: zeros,
1043 data,
1044 step_t: 0,
1045 }
1046 }
1047
1048 fn slot(&self, li: usize) -> Option<usize> {
1049 self.layers.iter().position(|&x| x == li)
1050 }
1051
1052 #[doc(hidden)]
1054 pub fn grads(&self) -> &[Vec<f32>] {
1055 &self.grad
1056 }
1057
1058 fn zero_grad(&mut self) {
1059 for g in &mut self.grad {
1060 for v in g.iter_mut() {
1061 *v = 0.0;
1062 }
1063 }
1064 }
1065
1066 fn clip_and_step(&mut self, lr: f64) -> f64 {
1069 let mut sq = 0f64;
1070 for g in &self.grad {
1071 for &v in g {
1072 sq += (v as f64) * (v as f64);
1073 }
1074 }
1075 let gn = sq.sqrt();
1076 let scale = if gn > 1.0 { 1.0 / (gn + 1e-6) } else { 1.0 };
1077 self.step_t += 1;
1078 let bc1 = 1.0 - ADAM_B1.powi(self.step_t as i32);
1079 let bc2 = 1.0 - ADAM_B2.powi(self.step_t as i32);
1080 for p in 0..self.data.len() {
1081 let (d, g, m, v) = (
1082 &mut self.data[p],
1083 &self.grad[p],
1084 &mut self.m1[p],
1085 &mut self.m2[p],
1086 );
1087 for i in 0..d.len() {
1088 let gi = g[i] as f64 * scale;
1089 let mi = ADAM_B1 * m[i] as f64 + (1.0 - ADAM_B1) * gi;
1090 let vi = ADAM_B2 * v[i] as f64 + (1.0 - ADAM_B2) * gi * gi;
1091 m[i] = mi as f32;
1092 v[i] = vi as f32;
1093 let upd = (mi / bc1) / ((vi / bc2).sqrt() + ADAM_EPS) + ADAM_WD * d[i] as f64;
1094 d[i] = (d[i] as f64 - lr * upd) as f32;
1095 }
1096 }
1097 gn
1098 }
1099}
1100
1101#[derive(Clone, Copy)]
1104pub(crate) struct LnFfn<'a> {
1105 pub(crate) iln: &'a [f32],
1106 pub(crate) pln: &'a [f32],
1107 pub(crate) gate: &'a [f32],
1108 pub(crate) up: &'a [f32],
1109 pub(crate) down: &'a [f32],
1110 pub(crate) gu: Option<&'a [f32]>,
1114}
1115
1116fn ln_ffn<'a>(
1117 fm: &'a FcdModel,
1118 ts: Option<&'a TrainState>,
1119 li: usize,
1120 mats: &'a LayerMats,
1121) -> LnFfn<'a> {
1122 if let Some(t) = ts {
1123 if let Some(s) = t.slot(li) {
1124 let b = s * PARAMS_PER_LAYER;
1125 return LnFfn {
1126 iln: &t.data[b],
1127 pln: &t.data[b + 1],
1128 gate: &t.data[b + 2],
1129 up: &t.data[b + 3],
1130 down: &t.data[b + 4],
1131 gu: None,
1132 };
1133 }
1134 }
1135 let l = &fm.layers[li];
1136 LnFfn {
1137 iln: &l.iln,
1138 pln: &l.pln,
1139 gate: &[],
1142 up: &[],
1143 down: &mats.down,
1144 gu: Some(&mats.gu),
1145 }
1146}
1147
1148enum AttnActs {
1152 Full {
1153 qpre: Vec<f32>,
1154 kpre: Vec<f32>,
1155 vproj: Vec<f32>,
1156 qrot: Vec<f32>,
1157 krot: Vec<f32>,
1158 qinv: Vec<f32>,
1159 kinv: Vec<f32>,
1160 ao: Vec<f32>,
1163 gate_pre: Vec<f32>,
1165 head_gain: Vec<f32>,
1169 },
1170 Gdn {
1173 qkv: Vec<f32>,
1174 z: Vec<f32>,
1175 a: Vec<f32>,
1176 b: Vec<f32>,
1177 },
1178}
1179
1180#[derive(Clone, Copy)]
1183pub(crate) struct AttnW<'a> {
1184 pub(crate) wqkv: &'a [f32],
1185 pub(crate) wo: &'a [f32],
1186 pub(crate) head_gate: Option<&'a [f32]>,
1187}
1188
1189pub(crate) struct LayerActs {
1190 pub(crate) inv1: Vec<f32>,
1191 attn: AttnActs,
1192 pub(crate) h1: Vec<f32>,
1193 pub(crate) n2: Vec<f32>,
1194 pub(crate) inv2: Vec<f32>,
1195 pub(crate) gpre: Vec<f32>,
1196 pub(crate) upre: Vec<f32>,
1197 pub(crate) act: Vec<f32>,
1198}
1199
1200impl LayerActs {
1201 pub(crate) fn attn_full(&self) -> Option<(&[f32], &[f32], &[f32], &[f32], &[f32])> {
1205 match &self.attn {
1206 AttnActs::Full {
1207 qrot,
1208 krot,
1209 vproj,
1210 ao,
1211 head_gain,
1212 ..
1213 } if !qrot.is_empty() => Some((qrot, krot, vproj, ao, head_gain)),
1214 _ => None,
1215 }
1216 }
1217}
1218
1219struct SendMut<T>(*mut T);
1221unsafe impl<T> Send for SendMut<T> {}
1222unsafe impl<T> Sync for SendMut<T> {}
1223impl<T> SendMut<T> {
1224 #[inline]
1225 unsafe fn at(&self, i: usize) -> *mut T {
1226 unsafe { self.0.add(i) }
1227 }
1228}
1229
1230
1231pub(crate) fn pooled_rows(n: usize, pool: Option<&Pool>, f: &(dyn Fn(usize, usize) + Sync)) {
1235 match pool {
1236 Some(p) if n >= 64 => p.run(&|w, nw| {
1237 let lo = n * w / nw;
1238 let hi = n * (w + 1) / nw;
1239 if hi > lo {
1240 f(lo, hi);
1241 }
1242 }),
1243 _ => f(0, n),
1244 }
1245}
1246
1247pub(crate) fn rmsnorm_fwd_pooled(
1249 x: &[f32],
1250 w: &[f32],
1251 eps: f64,
1252 gemma: bool,
1253 y: &mut [f32],
1254 inv: &mut [f32],
1255 pool: Option<&Pool>,
1256) {
1257 let d = w.len();
1258 let n = x.len() / d;
1259 let yp = SendMut(y.as_mut_ptr());
1260 let ip = SendMut(inv.as_mut_ptr());
1261 pooled_rows(n, pool, &|lo, hi| {
1262 let (yr, ir) = unsafe {
1264 (
1265 std::slice::from_raw_parts_mut(yp.at(lo * d), (hi - lo) * d),
1266 std::slice::from_raw_parts_mut(ip.at(lo), hi - lo),
1267 )
1268 };
1269 ops::rmsnorm_fwd(&x[lo * d..hi * d], w, eps, gemma, yr, ir);
1270 });
1271}
1272
1273pub(crate) fn rmsnorm_bwd_pooled(
1275 x: &[f32],
1276 w: &[f32],
1277 inv: &[f32],
1278 dy: &[f32],
1279 gemma: bool,
1280 dx: &mut [f32],
1281 pool: Option<&Pool>,
1282) {
1283 let d = w.len();
1284 let n = x.len() / d;
1285 let dp = SendMut(dx.as_mut_ptr());
1286 pooled_rows(n, pool, &|lo, hi| {
1287 let dxr = unsafe { std::slice::from_raw_parts_mut(dp.at(lo * d), (hi - lo) * d) };
1289 ops::rmsnorm_bwd(&x[lo * d..hi * d], w, &inv[lo..hi], &dy[lo * d..hi * d], gemma, dxr, None);
1290 });
1291}
1292
1293pub(crate) fn add_into_pooled(a: &mut [f32], b: &[f32], pool: Option<&Pool>) {
1295 let n = a.len().min(b.len());
1296 let ap = SendMut(a.as_mut_ptr());
1297 const CHUNK: usize = 4096;
1298 let chunks = n.div_ceil(CHUNK);
1299 pooled_rows(chunks, pool, &|lo, hi| {
1300 let (s, e) = (lo * CHUNK, (hi * CHUNK).min(n));
1301 let ar = unsafe { std::slice::from_raw_parts_mut(ap.at(s), e - s) };
1303 for (x, &y) in ar.iter_mut().zip(&b[s..e]) {
1304 *x += y;
1305 }
1306 });
1307}
1308
1309pub(crate) fn act_mul_rows(
1313 f: FfnAct,
1314 gpre: &[f32],
1315 upre: &[f32],
1316 act: &mut [f32],
1317 n: usize,
1318 inter: usize,
1319 pool: Option<&Pool>,
1320) {
1321 let row = |r: usize, out: &mut [f32]| {
1322 let g = &gpre[r * inter..(r + 1) * inter];
1323 let u = &upre[r * inter..(r + 1) * inter];
1324 for ((o, &gv), &uv) in out.iter_mut().zip(g).zip(u) {
1325 *o = f.fwd(gv) * uv;
1326 }
1327 };
1328 match pool {
1329 Some(p) if n > 1 => {
1330 let ap = SendMut(act.as_mut_ptr());
1331 p.run(&|w, nw| {
1332 for r in (w..n).step_by(nw) {
1333 let out = unsafe { std::slice::from_raw_parts_mut(ap.at(r * inter), inter) };
1335 row(r, out);
1336 }
1337 });
1338 }
1339 _ => {
1340 for r in 0..n {
1341 row(r, &mut act[r * inter..(r + 1) * inter]);
1342 }
1343 }
1344 }
1345}
1346
1347#[allow(clippy::too_many_arguments)]
1351pub(crate) fn act_bwd_rows(
1352 f: FfnAct,
1353 gpre: &[f32],
1354 upre: &[f32],
1355 dact: &[f32],
1356 scale: Option<&[f32]>,
1357 dg: &mut [f32],
1358 du: &mut [f32],
1359 n: usize,
1360 inter: usize,
1361 pool: Option<&Pool>,
1362) {
1363 let row = |r: usize, dg: &mut [f32], du: &mut [f32]| {
1364 let base = r * inter;
1365 for j in 0..inter {
1366 let i = base + j;
1367 let da = dact[i] * scale.map(|s| s[j]).unwrap_or(1.0);
1368 dg[j] = da * upre[i] * f.bwd(gpre[i]);
1369 du[j] = da * f.fwd(gpre[i]);
1370 }
1371 };
1372 match pool {
1373 Some(p) if n > 1 => {
1374 let gp = SendMut(dg.as_mut_ptr());
1375 let up = SendMut(du.as_mut_ptr());
1376 p.run(&|w, nw| {
1377 for r in (w..n).step_by(nw) {
1378 let (dgr, dur) = unsafe {
1380 (
1381 std::slice::from_raw_parts_mut(gp.at(r * inter), inter),
1382 std::slice::from_raw_parts_mut(up.at(r * inter), inter),
1383 )
1384 };
1385 row(r, dgr, dur);
1386 }
1387 });
1388 }
1389 _ => {
1390 for r in 0..n {
1391 let (dgr, dur) = (&mut dg[r * inter..(r + 1) * inter], &mut du[r * inter..(r + 1) * inter]);
1392 row(r, dgr, dur);
1393 }
1394 }
1395 }
1396}
1397
1398impl FcdModel {
1399 pub(crate) fn attn_geometry(&self, li: usize) -> Option<(Option<usize>, bool)> {
1404 match &self.layers[li].attn {
1405 FcdAttn::Full {
1406 window, local_rope, ..
1407 } => Some((*window, *local_rope)),
1408 _ => None,
1409 }
1410 }
1411
1412 pub(crate) fn head_gate_weights(&self, li: usize) -> Option<&[f32]> {
1414 match &self.layers[li].attn {
1415 FcdAttn::Full { head_gate, .. } => head_gate.as_deref(),
1416 _ => None,
1417 }
1418 }
1419
1420 pub(crate) fn layer_rope(&self, local: bool) -> (usize, &[f64]) {
1422 if local {
1423 (
1424 self.rotary_dim_local.unwrap_or(self.rotary_dim),
1425 self.inv_freq_local.as_deref().unwrap_or(&self.inv_freq),
1426 )
1427 } else {
1428 (self.rotary_dim, &self.inv_freq)
1429 }
1430 }
1431
1432 fn qk_norm_rope(
1433 &self,
1434 x: &mut [f32],
1435 norm: Option<&[f32]>,
1436 heads: usize,
1437 t: usize,
1438 local: bool,
1439 inv_out: &mut [f32],
1440 ) {
1441 let hd = self.hd;
1442 let (rotary_dim, inv_freq) = self.layer_rope(local);
1443 let n = x.len() / (heads * hd);
1444 for r in 0..n {
1445 let pos = r % t;
1446 for hh in 0..heads {
1447 let s = (r * heads + hh) * hd;
1448 let head = &mut x[s..s + hd];
1449 if let Some(w) = norm {
1450 let mut inv = [0f32; 1];
1451 let mut y = [0f32; 256];
1452 debug_assert!(hd <= 256);
1453 ops::rmsnorm_fwd(head, w, self.eps, self.gemma, &mut y[..hd], &mut inv);
1454 head.copy_from_slice(&y[..hd]);
1455 inv_out[r * heads + hh] = inv[0];
1456 }
1457 ops::rope_fwd(&mut head[..rotary_dim], pos, inv_freq);
1458 }
1459 }
1460 }
1461
1462 #[allow(clippy::too_many_arguments)]
1468 fn layer_forward(
1469 &self,
1470 li: usize,
1471 h_in: &[f32],
1472 b: usize,
1473 t: usize,
1474 wts: &LnFfn,
1475 nystrom: bool,
1476 want_acts: bool,
1477 ) -> (Vec<f32>, Option<LayerActs>) {
1478 self.layer_forward_scaled(li, h_in, b, t, wts, nystrom, want_acts, None, true, None, false)
1479 }
1480
1481 #[allow(clippy::too_many_arguments)]
1485 pub(crate) fn layer_forward_scaled(
1486 &self,
1487 li: usize,
1488 h_in: &[f32],
1489 b: usize,
1490 t: usize,
1491 wts: &LnFfn,
1492 nystrom: bool,
1493 want_acts: bool,
1494 ffn_scale: Option<&[f32]>,
1495 ffn_planes_to_host: bool,
1496 attn_w: Option<AttnW<'_>>,
1497 want_attn_acts: bool,
1498 ) -> (Vec<f32>, Option<LayerActs>) {
1499 let nystrom = nystrom && !self.polish_only;
1501 let hsz = self.hidden;
1502 let n = b * t;
1503 let l = &self.layers[li];
1504 let pool = self.pool.as_deref();
1505
1506 let mut n1 = vec![0f32; n * hsz];
1507 let mut inv1 = vec![0f32; n];
1508 rmsnorm_fwd_pooled(h_in, wts.iln, self.eps, self.gemma, &mut n1, &mut inv1, pool);
1509
1510 let mats = self.mats(li).expect("layer mats");
1511 let t_attn = std::time::Instant::now();
1512 let (attn_out, attn_acts) = match &l.attn {
1513 FcdAttn::Full { .. } => {
1514 self.full_attn_fwd(&l.attn, &mats, &n1, b, t, nystrom, attn_w, want_attn_acts)
1515 }
1516 FcdAttn::Gdn { .. } => self.gdn_attn_fwd(&l.attn, &n1, b, t),
1517 };
1518 prof::add(&prof::ATTN_FWD, t_attn);
1519 let t_ffn = std::time::Instant::now();
1520
1521 let mut h1 = h_in.to_vec();
1522 add_into_pooled(&mut h1, &attn_out, pool);
1523
1524 let mut n2 = vec![0f32; n * hsz];
1525 let mut inv2 = vec![0f32; n];
1526 rmsnorm_fwd_pooled(&h1, wts.pln, self.eps, self.gemma, &mut n2, &mut inv2, pool);
1527
1528 let inter = l.inter;
1529 let mut gpre: Vec<f32> = Vec::new();
1534 let mut upre: Vec<f32> = Vec::new();
1535 let mut act: Vec<f32> = Vec::new();
1536 let mut ffn = vec![0f32; n * hsz];
1537 prof::add(&prof::FFN_PRE, t_ffn);
1538 let mut fused = false;
1543 #[cfg(feature = "gpu")]
1544 if let Some(gu) = wts.gu {
1545 if crate::gpu::enabled_here() {
1546 let mut both = (want_acts && ffn_planes_to_host).then(|| vec![0f32; n * 2 * inter]);
1549 let t_chain = std::time::Instant::now();
1550 let chained = crate::gpu_wgpu::ffn_chain_f32(
1551 &n2,
1552 gu,
1553 wts.down,
1554 ffn_scale,
1555 both.as_deref_mut(),
1556 want_acts.then_some(li),
1557 &mut ffn,
1558 n,
1559 hsz,
1560 inter,
1561 self.act,
1562 );
1563 prof::add(&prof::FFN_CHAIN, t_chain);
1564 if chained {
1565 fused = true;
1566 if let Some(b) = &both {
1567 let t_act = std::time::Instant::now();
1568 gpre.reserve_exact(n * inter);
1569 upre.reserve_exact(n * inter);
1570 for r in 0..n {
1571 let row = &b[r * 2 * inter..(r + 1) * 2 * inter];
1572 gpre.extend_from_slice(&row[..inter]);
1573 upre.extend_from_slice(&row[inter..]);
1574 }
1575 act = vec![0f32; n * inter];
1576 act_mul_rows(self.act, &gpre, &upre, &mut act, n, inter, pool);
1578 prof::add(&prof::FFN_ACT, t_act);
1579 }
1580 }
1581 }
1582 }
1583 let t_host = std::time::Instant::now();
1584 if !fused {
1585 gpre = vec![0f32; n * inter];
1586 upre = vec![0f32; n * inter];
1587 act = vec![0f32; n * inter];
1588 if let Some(gu) = wts.gu {
1589 let mut both = vec![0f32; n * 2 * inter];
1592 ops::gemm_nt(&n2, gu, &mut both, n, hsz, 2 * inter, pool);
1593 for r in 0..n {
1594 let row = &both[r * 2 * inter..(r + 1) * 2 * inter];
1595 gpre[r * inter..(r + 1) * inter].copy_from_slice(&row[..inter]);
1596 upre[r * inter..(r + 1) * inter].copy_from_slice(&row[inter..]);
1597 }
1598 } else {
1599 ops::gemm_nt(&n2, wts.gate, &mut gpre, n, hsz, inter, pool);
1600 ops::gemm_nt(&n2, wts.up, &mut upre, n, hsz, inter, pool);
1601 }
1602 act_mul_rows(self.act, &gpre, &upre, &mut act, n, inter, pool);
1603 match ffn_scale {
1604 Some(g) => {
1605 debug_assert_eq!(g.len(), inter);
1606 let mut act2 = act.clone();
1607 for r in 0..n {
1608 for (a, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
1609 *a *= gv;
1610 }
1611 }
1612 ops::gemm_nt(&act2, wts.down, &mut ffn, n, inter, hsz, pool);
1613 }
1614 None => ops::gemm_nt(&act, wts.down, &mut ffn, n, inter, hsz, pool),
1615 }
1616 }
1617 prof::add(&prof::FFN_HOST, t_host);
1618 let t_post = std::time::Instant::now();
1619 let mut h2 = if want_acts { h1.clone() } else { std::mem::take(&mut h1) };
1621 add_into_pooled(&mut h2, &ffn, pool);
1622 prof::add(&prof::FFN_POST, t_post);
1623
1624 let acts = want_acts.then_some(LayerActs {
1625 inv1,
1626 attn: attn_acts,
1627 h1,
1628 n2,
1629 inv2,
1630 gpre,
1631 upre,
1632 act,
1633 });
1634 prof::add(&prof::FFN_FWD, t_ffn);
1635 (h2, acts)
1636 }
1637
1638 #[allow(clippy::too_many_arguments)]
1642 fn full_attn_fwd(
1643 &self,
1644 attn: &FcdAttn,
1645 mats: &LayerMats,
1646 n1: &[f32],
1647 b: usize,
1648 t: usize,
1649 nystrom: bool,
1650 attn_w: Option<AttnW<'_>>,
1651 want_attn_acts: bool,
1652 ) -> (Vec<f32>, AttnActs) {
1653 let (wqkv, wo) = match attn_w {
1655 Some(w) => (w.wqkv, w.wo),
1656 None => (&mats.wqkv[..], &mats.wo[..]),
1657 };
1658 let FcdAttn::Full {
1659 qrows: _,
1660 q_norm,
1661 k_norm,
1662 bias,
1663 bias_cat,
1664 output_gate,
1665 head_gate,
1666 window,
1667 local_rope,
1668 } = attn
1669 else {
1670 unreachable!("full_attn_fwd on a non-Full layer");
1671 };
1672
1673 let (hsz, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
1674 let n = b * t;
1675 let pool = self.pool.as_deref();
1676 let qdim = nh * hd;
1677 let kvdim = nkv * hd;
1678 let rep = nh / nkv;
1679 let qrows = if *output_gate { 2 * qdim } else { qdim };
1680
1681 let fused = qrows + 2 * kvdim;
1682 let head_gate_w: Option<&[f32]> = attn_w.and_then(|w| w.head_gate).or(head_gate.as_deref());
1687 let head_gain: Vec<f32> = match head_gate_w {
1688 Some(gw) => {
1689 let mut graw = vec![0f32; n * nh];
1690 ops::gemm_nt(n1, gw, &mut graw, n, hsz, nh, pool);
1691 for v in graw.iter_mut() {
1692 *v = 1.0 / (1.0 + (-*v).exp());
1693 }
1694 graw
1695 }
1696 None => Vec::new(),
1697 };
1698 let (rotary_dim, inv_freq) = self.layer_rope(*local_rope);
1699 #[cfg(feature = "gpu")]
1705 if !nystrom && crate::gpu::enabled_here() {
1706 let want_acts = self.attn_acts || want_attn_acts;
1707 let rope = {
1708 let mut rt = self.rope_tab.lock().unwrap();
1709 match rt.as_ref() {
1710 Some((key, arc)) if *key == (t, *local_rope) => arc.clone(),
1711 _ => {
1712 let half = rotary_dim / 2;
1713 let mut tab = Vec::with_capacity(t * half * 2);
1714 for pos in 0..t {
1715 for &freq in inv_freq.iter() {
1716 let a = pos as f64 * freq;
1717 tab.push(a.cos() as f32);
1718 tab.push(a.sin() as f32);
1719 }
1720 }
1721 let arc = std::sync::Arc::new(tab);
1722 *rt = Some(((t, *local_rope), arc.clone()));
1723 arc
1724 }
1725 }
1726 };
1727 let cfg = crate::gpu_wgpu::AttnChainCfg {
1728 wqkv,
1729 wo,
1730 q_norm: q_norm.as_deref(),
1731 k_norm: k_norm.as_deref(),
1732 bias: bias_cat.as_deref(),
1733 output_gate: *output_gate,
1734 head_gain: (!head_gain.is_empty()).then_some(&head_gain[..]),
1735 window: *window,
1736 gemma: self.gemma,
1737 eps: self.eps as f32,
1738 rotary_half: rotary_dim / 2,
1739 rope: &rope,
1740 b,
1741 t,
1742 nh,
1743 nkv,
1744 hd,
1745 hsz: self.hidden,
1746 };
1747 let mut attn_out = vec![0f32; n * self.hidden];
1748 let t_chain = std::time::Instant::now();
1749 let chained = crate::gpu_wgpu::attn_chain_f32(n1, &cfg, &mut attn_out, want_acts);
1750 prof::add(&prof::ATTN_CHAIN, t_chain);
1751 if let Some(ch) = chained {
1752 if !want_acts {
1753 return (
1754 attn_out,
1755 AttnActs::Full {
1756 qpre: Vec::new(),
1757 kpre: Vec::new(),
1758 vproj: Vec::new(),
1759 qrot: Vec::new(),
1760 krot: Vec::new(),
1761 qinv: Vec::new(),
1762 kinv: Vec::new(),
1763 ao: Vec::new(),
1764 gate_pre: Vec::new(),
1765 head_gain,
1766 },
1767 );
1768 }
1769 let mut qraw = vec![0f32; n * qrows];
1772 let mut kpre = vec![0f32; n * kvdim];
1773 let mut vproj = vec![0f32; n * kvdim];
1774 for r in 0..n {
1775 let row = &ch.qkv_plane[r * fused..(r + 1) * fused];
1776 qraw[r * qrows..(r + 1) * qrows].copy_from_slice(&row[..qrows]);
1777 kpre[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows..qrows + kvdim]);
1778 vproj[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows + kvdim..]);
1779 }
1780 if let Some((bq, bk, bv)) = bias {
1781 for r in 0..n {
1782 for (x, bb) in qraw[r * qrows..(r + 1) * qrows].iter_mut().zip(bq) {
1783 *x += bb;
1784 }
1785 for (x, bb) in kpre[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bk) {
1786 *x += bb;
1787 }
1788 for (x, bb) in vproj[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bv) {
1789 *x += bb;
1790 }
1791 }
1792 }
1793 let (qpre, gate_pre) = if *output_gate {
1794 let mut qh = vec![0f32; n * qdim];
1795 let mut gp = vec![0f32; n * qdim];
1796 for r in 0..n {
1797 for h in 0..nh {
1798 let src = r * qrows + 2 * h * hd;
1799 let dst = r * qdim + h * hd;
1800 qh[dst..dst + hd].copy_from_slice(&qraw[src..src + hd]);
1801 gp[dst..dst + hd].copy_from_slice(&qraw[src + hd..src + 2 * hd]);
1802 }
1803 }
1804 (qh, gp)
1805 } else {
1806 (qraw, Vec::new())
1807 };
1808 return (
1809 attn_out,
1810 AttnActs::Full {
1811 qpre,
1812 kpre,
1813 vproj,
1814 qrot: ch.qrot,
1815 krot: ch.krot,
1816 qinv: ch.qinv,
1817 kinv: ch.kinv,
1818 ao: ch.ao,
1819 gate_pre,
1820 head_gain,
1821 },
1822 );
1823 }
1824 }
1825
1826 let mut qkv = vec![0f32; n * fused];
1830 ops::gemm_nt(n1, wqkv, &mut qkv, n, hsz, fused, pool);
1831 let mut qraw = vec![0f32; n * qrows];
1832 let mut kpre = vec![0f32; n * kvdim];
1833 let mut vproj = vec![0f32; n * kvdim];
1834 for r in 0..n {
1835 let row = &qkv[r * fused..(r + 1) * fused];
1836 qraw[r * qrows..(r + 1) * qrows].copy_from_slice(&row[..qrows]);
1837 kpre[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows..qrows + kvdim]);
1838 vproj[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows + kvdim..]);
1839 }
1840 if let Some((bq, bk, bv)) = bias {
1841 for r in 0..n {
1842 for (x, bb) in qraw[r * qrows..(r + 1) * qrows].iter_mut().zip(bq) {
1843 *x += bb;
1844 }
1845 for (x, bb) in kpre[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bk) {
1846 *x += bb;
1847 }
1848 for (x, bb) in vproj[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bv) {
1849 *x += bb;
1850 }
1851 }
1852 }
1853 let (qpre, gate_pre) = if *output_gate {
1855 let mut qh = vec![0f32; n * qdim];
1856 let mut gp = vec![0f32; n * qdim];
1857 for r in 0..n {
1858 for h in 0..nh {
1859 let src = r * qrows + 2 * h * hd;
1860 let dst = r * qdim + h * hd;
1861 qh[dst..dst + hd].copy_from_slice(&qraw[src..src + hd]);
1862 gp[dst..dst + hd].copy_from_slice(&qraw[src + hd..src + 2 * hd]);
1863 }
1864 }
1865 (qh, gp)
1866 } else {
1867 (qraw, Vec::new())
1868 };
1869
1870 let mut qrot = qpre.clone();
1871 let mut krot = kpre.clone();
1872 let mut qinv = vec![0f32; n * nh];
1873 let mut kinv = vec![0f32; n * nkv];
1874 self.qk_norm_rope(&mut qrot, q_norm.as_deref(), nh, t, *local_rope, &mut qinv);
1875 self.qk_norm_rope(&mut krot, k_norm.as_deref(), nkv, t, *local_rope, &mut kinv);
1876
1877 let mut ao = vec![0f32; n * qdim];
1879 {
1880 let units = b * nh;
1881 let aop = SendMut(ao.as_mut_ptr());
1882 let qr = &qrot;
1883 let kr = &krot;
1884 let vr = &vproj;
1885 let nys = self.nys;
1886 let run_unit = |u: usize| {
1887 let (bi, h) = (u / nh, u % nh);
1888 let g = h / rep;
1889 if nystrom {
1890 let mut q64 = vec![0f64; t * hd];
1892 let mut k64 = vec![0f64; t * hd];
1893 let mut v64 = vec![0f64; t * hd];
1894 for p in 0..t {
1895 let r = bi * t + p;
1896 for c in 0..hd {
1897 q64[p * hd + c] = qr[r * qdim + h * hd + c] as f64;
1898 k64[p * hd + c] = kr[r * kvdim + g * hd + c] as f64;
1899 v64[p * hd + c] = vr[r * kvdim + g * hd + c] as f64;
1900 }
1901 }
1902 let mut o64 = vec![0f64; t * hd];
1903 ops::nystrom_head_fwd(&q64, &k64, &v64, t, hd, hd, &nys, &mut o64);
1904 for p in 0..t {
1905 let r = bi * t + p;
1906 for c in 0..hd {
1907 unsafe {
1909 *aop.at(r * qdim + h * hd + c) = o64[p * hd + c] as f32;
1910 }
1911 }
1912 }
1913 } else {
1914 let mut q32 = vec![0f32; t * hd];
1915 let mut k32 = vec![0f32; t * hd];
1916 let mut v32 = vec![0f32; t * hd];
1917 for p in 0..t {
1918 let r = bi * t + p;
1919 q32[p * hd..(p + 1) * hd]
1920 .copy_from_slice(&qr[r * qdim + h * hd..r * qdim + (h + 1) * hd]);
1921 k32[p * hd..(p + 1) * hd]
1922 .copy_from_slice(&kr[r * kvdim + g * hd..r * kvdim + (g + 1) * hd]);
1923 v32[p * hd..(p + 1) * hd]
1924 .copy_from_slice(&vr[r * kvdim + g * hd..r * kvdim + (g + 1) * hd]);
1925 }
1926 let mut o32 = vec![0f32; t * hd];
1927 ops::attn_head_fwd_window(&q32, &k32, &v32, t, hd, hd, *window, &mut o32);
1928 for p in 0..t {
1929 let r = bi * t + p;
1930 for c in 0..hd {
1931 unsafe {
1933 *aop.at(r * qdim + h * hd + c) = o32[p * hd + c];
1934 }
1935 }
1936 }
1937 }
1938 };
1939 match pool {
1940 Some(p) if units > 1 => p.run(&|widx, nw| {
1941 for u in (widx..units).step_by(nw) {
1942 run_unit(u);
1943 }
1944 }),
1945 _ => {
1946 for u in 0..units {
1947 run_unit(u);
1948 }
1949 }
1950 }
1951 }
1952
1953 let mut ao_eff: Vec<f32> = if *output_gate {
1955 ao.iter()
1956 .zip(&gate_pre)
1957 .map(|(&a, &g)| a * (1.0 / (1.0 + (-g).exp())))
1958 .collect()
1959 } else {
1960 ao.clone()
1961 };
1962 if !head_gain.is_empty() {
1964 for r in 0..n {
1965 for h in 0..nh {
1966 let gain = head_gain[r * nh + h];
1967 for a in &mut ao_eff[r * qdim + h * hd..r * qdim + (h + 1) * hd] {
1968 *a *= gain;
1969 }
1970 }
1971 }
1972 }
1973 let mut attn_out = vec![0f32; n * hsz];
1974 ops::gemm_nt(&ao_eff, wo, &mut attn_out, n, qdim, hsz, pool);
1975 (
1976 attn_out,
1977 AttnActs::Full {
1978 qpre,
1979 kpre,
1980 vproj,
1981 qrot,
1982 krot,
1983 qinv,
1984 kinv,
1985 ao,
1986 gate_pre,
1987 head_gain,
1988 },
1989 )
1990 }
1991
1992 fn gdn_attn_fwd(&self, attn: &FcdAttn, n1: &[f32], b: usize, t: usize) -> (Vec<f32>, AttnActs) {
1997 let FcdAttn::Gdn {
1998 wqkv,
1999 wz,
2000 wa,
2001 wb,
2002 conv,
2003 a_log,
2004 dt_bias,
2005 norm,
2006 wout,
2007 } = attn
2008 else {
2009 unreachable!("gdn_attn_fwd on a non-GDN layer");
2010 };
2011 let d = self.gdn.expect("gdn layer without gdn dims");
2012 let (hsz, n) = (self.hidden, b * t);
2013 let pool = self.pool.as_deref();
2014 let (c_dim, vd, nv) = (d.c_dim(), d.vd(), d.nv);
2015
2016 let mut qkv = vec![0f32; n * c_dim];
2017 ops::gemm_nt(n1, wqkv, &mut qkv, n, hsz, c_dim, pool);
2018 let mut z = vec![0f32; n * vd];
2019 ops::gemm_nt(n1, wz, &mut z, n, hsz, vd, pool);
2020 let mut a = vec![0f32; n * nv];
2021 ops::gemm_nt(n1, wa, &mut a, n, hsz, nv, pool);
2022 let mut bstr = vec![0f32; n * nv];
2023 ops::gemm_nt(n1, wb, &mut bstr, n, hsz, nv, pool);
2024
2025 let cfg = ops::GdnSeqCfg {
2026 nv: d.nv,
2027 nk: d.nk,
2028 dk: d.dk,
2029 dv: d.dv,
2030 kk: d.kk,
2031 rms_eps: self.eps,
2032 conv,
2033 a_log,
2034 dt_bias,
2035 norm,
2036 };
2037 let qkv64: Vec<f64> = qkv.iter().map(|&v| v as f64).collect();
2039 let z64: Vec<f64> = z.iter().map(|&v| v as f64).collect();
2040 let a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
2041 let b64: Vec<f64> = bstr.iter().map(|&v| v as f64).collect();
2042 let mut pre64 = vec![0f64; n * c_dim];
2043 let mut cq64 = vec![0f64; n * c_dim];
2044 for bi in 0..b {
2045 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
2046 ops::gdn_conv_fwd(
2047 &qkv64[r.clone()],
2048 t,
2049 c_dim,
2050 d.kk,
2051 conv,
2052 &mut pre64[r.clone()],
2053 &mut cq64[r],
2054 );
2055 }
2056 let mut of = vec![0f32; n * vd];
2057 {
2058 let units = b * d.nk;
2059 let rep_v = d.nv / d.nk;
2060 let ofp = SendMut(of.as_mut_ptr());
2061 let (cqr, zr, ar, br) = (&cq64, &z64, &a64, &b64);
2062 let cfg_ref = &cfg;
2063 let run_unit = |u: usize| {
2064 let (bi, ko) = (u / d.nk, u % d.nk);
2065 let mut local = vec![0f64; t * vd];
2066 ops::gdn_group_fwd(
2067 &cqr[bi * t * c_dim..(bi + 1) * t * c_dim],
2068 &zr[bi * t * vd..(bi + 1) * t * vd],
2069 &ar[bi * t * nv..(bi + 1) * t * nv],
2070 &br[bi * t * nv..(bi + 1) * t * nv],
2071 t,
2072 cfg_ref,
2073 ko,
2074 &mut local,
2075 );
2076 for hh in 0..rep_v {
2077 let h = ko * rep_v + hh;
2078 for p in 0..t {
2079 for dj in 0..d.dv {
2080 unsafe {
2082 *ofp.at((bi * t + p) * vd + h * d.dv + dj) =
2083 local[p * vd + h * d.dv + dj] as f32;
2084 }
2085 }
2086 }
2087 }
2088 };
2089 match pool {
2090 Some(p) if units > 1 => p.run(&|widx, nw| {
2091 for u in (widx..units).step_by(nw) {
2092 run_unit(u);
2093 }
2094 }),
2095 _ => {
2096 for u in 0..units {
2097 run_unit(u);
2098 }
2099 }
2100 }
2101 }
2102 let mut attn_out = vec![0f32; n * hsz];
2103 ops::gemm_nt(&of, wout, &mut attn_out, n, vd, hsz, pool);
2104 (attn_out, AttnActs::Gdn { qkv, z, a, b: bstr })
2105 }
2106
2107 #[allow(clippy::too_many_arguments)]
2111 fn layer_backward(
2112 &self,
2113 li: usize,
2114 h_in: &[f32],
2115 b: usize,
2116 t: usize,
2117 wts: &LnFfn,
2118 nystrom: bool,
2119 acts: &LayerActs,
2120 dh2: &[f32],
2121 mut grads: Option<&mut [Vec<f32>]>,
2122 ) -> Vec<f32> {
2123 let nystrom = nystrom && !self.polish_only;
2124 let hsz = self.hidden;
2125 let n = b * t;
2126 let l = &self.layers[li];
2127 let pool = self.pool.as_deref();
2128 let inter = l.inter;
2129
2130 let mut dn2 = vec![0f32; n * hsz];
2132 let mut bwd_fused = false;
2136 #[cfg(feature = "gpu")]
2137 if grads.is_none() {
2138 if let Some(gu) = wts.gu {
2139 if crate::gpu::enabled_here()
2140 && crate::gpu_wgpu::ffn_bwd_chain_f32(
2141 dh2, wts.down, gu, li, &mut dn2, n, hsz, inter, self.act, None, None,
2142 )
2143 {
2144 bwd_fused = true;
2145 }
2146 }
2147 }
2148 if !bwd_fused {
2149 let mut dact = vec![0f32; n * inter];
2150 ops::gemm_dx(dh2, wts.down, &mut dact, n, inter, hsz, pool);
2151 if let Some(g) = grads.as_deref_mut() {
2152 ops::gemm_dw(dh2, &acts.act, &mut g[4], n, inter, hsz, pool);
2153 }
2154 let mut dg = vec![0f32; n * inter];
2155 let mut du = vec![0f32; n * inter];
2156 act_bwd_rows(self.act, &acts.gpre, &acts.upre, &dact, None, &mut dg, &mut du, n, inter, pool);
2157 if let Some(gu) = wts.gu {
2160 let mut dgu = vec![0f32; n * 2 * inter];
2161 for r in 0..n {
2162 let row = &mut dgu[r * 2 * inter..(r + 1) * 2 * inter];
2163 row[..inter].copy_from_slice(&dg[r * inter..(r + 1) * inter]);
2164 row[inter..].copy_from_slice(&du[r * inter..(r + 1) * inter]);
2165 }
2166 ops::gemm_dx(&dgu, gu, &mut dn2, n, hsz, 2 * inter, pool);
2167 } else {
2168 ops::gemm_dx(&dg, wts.gate, &mut dn2, n, hsz, inter, pool);
2169 ops::gemm_dx(&du, wts.up, &mut dn2, n, hsz, inter, pool);
2170 }
2171 if let Some(g) = grads.as_deref_mut() {
2172 ops::gemm_dw(&dg, &acts.n2, &mut g[2], n, hsz, inter, pool);
2173 ops::gemm_dw(&du, &acts.n2, &mut g[3], n, hsz, inter, pool);
2174 }
2175 }
2176
2177 let mut dh1 = dh2.to_vec();
2178 ops::rmsnorm_bwd(
2179 &acts.h1,
2180 wts.pln,
2181 &acts.inv2,
2182 &dn2,
2183 self.gemma,
2184 &mut dh1,
2185 grads.as_deref_mut().map(|g| &mut g[1][..]),
2186 );
2187
2188 let mats = self.mats(li).expect("layer mats");
2190 let dn1 = match &l.attn {
2191 FcdAttn::Full { .. } => {
2192 self.full_attn_bwd(&l.attn, &mats, &acts.attn, &dh1, b, t, nystrom)
2193 }
2194 FcdAttn::Gdn { .. } => self.gdn_attn_bwd(&l.attn, &acts.attn, &dh1, b, t),
2195 };
2196
2197 let mut dh_in = dh1.clone();
2198 ops::rmsnorm_bwd(
2199 h_in,
2200 wts.iln,
2201 &acts.inv1,
2202 &dn1,
2203 self.gemma,
2204 &mut dh_in,
2205 grads.map(|g| &mut g[0][..]),
2206 );
2207 dh_in
2208 }
2209
2210 fn full_attn_bwd(
2214 &self,
2215 attn: &FcdAttn,
2216 mats: &LayerMats,
2217 acts: &AttnActs,
2218 dattn: &[f32],
2219 b: usize,
2220 t: usize,
2221 nystrom: bool,
2222 ) -> Vec<f32> {
2223 let (wqkv, wo) = (&mats.wqkv[..], &mats.wo[..]);
2224 let FcdAttn::Full {
2225 qrows: _,
2226 q_norm,
2227 k_norm,
2228 output_gate,
2229 local_rope,
2230 window,
2231 ..
2232 } = attn
2233 else {
2234 unreachable!("full_attn_bwd on a non-Full layer");
2235 };
2236 let AttnActs::Full {
2237 qpre,
2238 kpre,
2239 vproj,
2240 qrot,
2241 krot,
2242 qinv,
2243 kinv,
2244 ao,
2245 gate_pre,
2246 head_gain,
2247 } = acts
2248 else {
2249 unreachable!("acts mismatch");
2250 };
2251 let (rotary_dim, inv_freq) = self.layer_rope(*local_rope);
2252 let (hsz, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
2253 let n = b * t;
2254 let pool = self.pool.as_deref();
2255 let qdim = nh * hd;
2256 let kvdim = nkv * hd;
2257 let rep = nh / nkv;
2258 let qrows = if *output_gate { 2 * qdim } else { qdim };
2259
2260 let mut dao_eff = vec![0f32; n * qdim];
2261 ops::gemm_dx(dattn, wo, &mut dao_eff, n, qdim, hsz, pool);
2262 if !head_gain.is_empty() {
2266 for r in 0..n {
2267 for h in 0..nh {
2268 let gain = head_gain[r * nh + h];
2269 for d in &mut dao_eff[r * qdim + h * hd..r * qdim + (h + 1) * hd] {
2270 *d *= gain;
2271 }
2272 }
2273 }
2274 }
2275 let (dao, dgate) = if *output_gate {
2277 let mut dao = vec![0f32; n * qdim];
2278 let mut dgp = vec![0f32; n * qdim];
2279 for i in 0..n * qdim {
2280 let sig = 1.0 / (1.0 + (-gate_pre[i]).exp());
2281 dao[i] = dao_eff[i] * sig;
2282 dgp[i] = dao_eff[i] * ao[i] * sig * (1.0 - sig);
2283 }
2284 (dao, dgp)
2285 } else {
2286 (dao_eff, Vec::new())
2287 };
2288
2289 let mut dqrot = vec![0f32; n * qdim];
2290 let mut dkrot = vec![0f32; n * kvdim];
2291 let mut dvproj = vec![0f32; n * kvdim];
2292 {
2293 let units = b * nkv;
2296 let dqp = SendMut(dqrot.as_mut_ptr());
2297 let dkp = SendMut(dkrot.as_mut_ptr());
2298 let dvp = SendMut(dvproj.as_mut_ptr());
2299 let (qr, kr, vr) = (qrot, krot, vproj);
2300 let daor = &dao;
2301 let nys = self.nys;
2302 let run_unit = |u: usize| {
2303 let (bi, g) = (u / nkv, u % nkv);
2304 let mut k64 = vec![0f64; t * hd];
2305 let mut v64 = vec![0f64; t * hd];
2306 for p in 0..t {
2307 let r = bi * t + p;
2308 for c in 0..hd {
2309 k64[p * hd + c] = kr[r * kvdim + g * hd + c] as f64;
2310 v64[p * hd + c] = vr[r * kvdim + g * hd + c] as f64;
2311 }
2312 }
2313 let mut dk64 = vec![0f64; t * hd];
2314 let mut dv64 = vec![0f64; t * hd];
2315 let mut q64 = vec![0f64; t * hd];
2316 let mut do64 = vec![0f64; t * hd];
2317 let mut dq64 = vec![0f64; t * hd];
2318 for hh in 0..rep {
2319 let h = g * rep + hh;
2320 for p in 0..t {
2321 let r = bi * t + p;
2322 for c in 0..hd {
2323 q64[p * hd + c] = qr[r * qdim + h * hd + c] as f64;
2324 do64[p * hd + c] = daor[r * qdim + h * hd + c] as f64;
2325 }
2326 }
2327 for v in dq64.iter_mut() {
2328 *v = 0.0;
2329 }
2330 if nystrom {
2331 ops::nystrom_head_bwd(
2332 &q64, &k64, &v64, &do64, t, hd, hd, &nys, &mut dq64, &mut dk64,
2333 &mut dv64,
2334 );
2335 } else {
2336 ops::attn_head_bwd_window(
2337 &q64, &k64, &v64, &do64, t, hd, hd, *window, &mut dq64, &mut dk64,
2338 &mut dv64,
2339 );
2340 }
2341 for p in 0..t {
2342 let r = bi * t + p;
2343 for c in 0..hd {
2344 unsafe {
2346 *dqp.at(r * qdim + h * hd + c) = dq64[p * hd + c] as f32;
2347 }
2348 }
2349 }
2350 }
2351 for p in 0..t {
2352 let r = bi * t + p;
2353 for c in 0..hd {
2354 unsafe {
2356 *dkp.at(r * kvdim + g * hd + c) = dk64[p * hd + c] as f32;
2357 *dvp.at(r * kvdim + g * hd + c) = dv64[p * hd + c] as f32;
2358 }
2359 }
2360 }
2361 };
2362 match pool {
2363 Some(p) if units > 1 => p.run(&|widx, nw| {
2364 for u in (widx..units).step_by(nw) {
2365 run_unit(u);
2366 }
2367 }),
2368 _ => {
2369 for u in 0..units {
2370 run_unit(u);
2371 }
2372 }
2373 }
2374 }
2375
2376 let mut dqpre = vec![0f32; n * qdim];
2378 let mut dkpre = vec![0f32; n * kvdim];
2379 for r in 0..n {
2380 let pos = r % t;
2381 for h in 0..nh {
2382 let s = r * qdim + h * hd;
2383 ops::rope_bwd(&mut dqrot[s..s + rotary_dim], pos, inv_freq);
2384 match q_norm {
2385 Some(w) => ops::rmsnorm_bwd(
2386 &qpre[s..s + hd],
2387 w,
2388 &qinv[r * nh + h..r * nh + h + 1],
2389 &dqrot[s..s + hd],
2390 self.gemma,
2391 &mut dqpre[s..s + hd],
2392 None,
2393 ),
2394 None => dqpre[s..s + hd].copy_from_slice(&dqrot[s..s + hd]),
2395 }
2396 }
2397 for g in 0..nkv {
2398 let s = r * kvdim + g * hd;
2399 ops::rope_bwd(&mut dkrot[s..s + rotary_dim], pos, inv_freq);
2400 match k_norm {
2401 Some(w) => ops::rmsnorm_bwd(
2402 &kpre[s..s + hd],
2403 w,
2404 &kinv[r * nkv + g..r * nkv + g + 1],
2405 &dkrot[s..s + hd],
2406 self.gemma,
2407 &mut dkpre[s..s + hd],
2408 None,
2409 ),
2410 None => dkpre[s..s + hd].copy_from_slice(&dkrot[s..s + hd]),
2411 }
2412 }
2413 }
2414
2415 let dqraw: Vec<f32> = if *output_gate {
2417 let mut dq = vec![0f32; n * qrows];
2418 for r in 0..n {
2419 for h in 0..nh {
2420 let dst = r * qrows + 2 * h * hd;
2421 let src = r * qdim + h * hd;
2422 dq[dst..dst + hd].copy_from_slice(&dqpre[src..src + hd]);
2423 dq[dst + hd..dst + 2 * hd].copy_from_slice(&dgate[src..src + hd]);
2424 }
2425 }
2426 dq
2427 } else {
2428 dqpre
2429 };
2430
2431 let fused = qrows + 2 * kvdim;
2436 let mut dqkv = vec![0f32; n * fused];
2437 for r in 0..n {
2438 let row = &mut dqkv[r * fused..(r + 1) * fused];
2439 row[..qrows].copy_from_slice(&dqraw[r * qrows..(r + 1) * qrows]);
2440 row[qrows..qrows + kvdim].copy_from_slice(&dkpre[r * kvdim..(r + 1) * kvdim]);
2441 row[qrows + kvdim..].copy_from_slice(&dvproj[r * kvdim..(r + 1) * kvdim]);
2442 }
2443 let mut dn1 = vec![0f32; n * hsz];
2444 ops::gemm_dx(&dqkv, wqkv, &mut dn1, n, hsz, fused, pool);
2445 dn1
2446 }
2447
2448 fn gdn_attn_bwd(
2452 &self,
2453 attn: &FcdAttn,
2454 acts: &AttnActs,
2455 dattn: &[f32],
2456 b: usize,
2457 t: usize,
2458 ) -> Vec<f32> {
2459 let FcdAttn::Gdn {
2460 wqkv,
2461 wz,
2462 wa,
2463 wb,
2464 conv,
2465 a_log,
2466 dt_bias,
2467 norm,
2468 wout,
2469 } = attn
2470 else {
2471 unreachable!("gdn_attn_bwd on a non-GDN layer");
2472 };
2473 let AttnActs::Gdn { qkv, z, a, b: bstr } = acts else {
2474 unreachable!("acts mismatch");
2475 };
2476 let d = self.gdn.expect("gdn layer without gdn dims");
2477 let (hsz, n) = (self.hidden, b * t);
2478 let pool = self.pool.as_deref();
2479 let (c_dim, vd, nv) = (d.c_dim(), d.vd(), d.nv);
2480
2481 let mut dof = vec![0f32; n * vd];
2482 ops::gemm_dx(dattn, wout, &mut dof, n, vd, hsz, pool);
2483
2484 let cfg = ops::GdnSeqCfg {
2485 nv: d.nv,
2486 nk: d.nk,
2487 dk: d.dk,
2488 dv: d.dv,
2489 kk: d.kk,
2490 rms_eps: self.eps,
2491 conv,
2492 a_log,
2493 dt_bias,
2494 norm,
2495 };
2496 let qkv64: Vec<f64> = qkv.iter().map(|&v| v as f64).collect();
2497 let z64: Vec<f64> = z.iter().map(|&v| v as f64).collect();
2498 let a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
2499 let b64: Vec<f64> = bstr.iter().map(|&v| v as f64).collect();
2500 let dof64: Vec<f64> = dof.iter().map(|&v| v as f64).collect();
2501 let mut pre64 = vec![0f64; n * c_dim];
2502 let mut cq64 = vec![0f64; n * c_dim];
2503 for bi in 0..b {
2504 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
2505 ops::gdn_conv_fwd(
2506 &qkv64[r.clone()],
2507 t,
2508 c_dim,
2509 d.kk,
2510 conv,
2511 &mut pre64[r.clone()],
2512 &mut cq64[r],
2513 );
2514 }
2515
2516 let mut dcq64 = vec![0f64; n * c_dim];
2517 let mut dz64 = vec![0f64; n * vd];
2518 let mut da64 = vec![0f64; n * nv];
2519 let mut db64 = vec![0f64; n * nv];
2520 {
2521 let units = b * d.nk;
2522 let rep_v = d.nv / d.nk;
2523 let kd = d.nk * d.dk;
2524 let dcqp = SendMut(dcq64.as_mut_ptr());
2525 let dzp = SendMut(dz64.as_mut_ptr());
2526 let dap = SendMut(da64.as_mut_ptr());
2527 let dbp = SendMut(db64.as_mut_ptr());
2528 let (cqr, zr, ar, br, dor) = (&cq64, &z64, &a64, &b64, &dof64);
2529 let cfg_ref = &cfg;
2530 let run_unit = |u: usize| {
2531 let (bi, ko) = (u / d.nk, u % d.nk);
2532 let mut dcq_l = vec![0f64; t * c_dim];
2535 let mut dz_l = vec![0f64; t * vd];
2536 let mut da_l = vec![0f64; t * nv];
2537 let mut db_l = vec![0f64; t * nv];
2538 ops::gdn_group_bwd(
2539 &cqr[bi * t * c_dim..(bi + 1) * t * c_dim],
2540 &zr[bi * t * vd..(bi + 1) * t * vd],
2541 &ar[bi * t * nv..(bi + 1) * t * nv],
2542 &br[bi * t * nv..(bi + 1) * t * nv],
2543 t,
2544 cfg_ref,
2545 ko,
2546 &dor[bi * t * vd..(bi + 1) * t * vd],
2547 &mut dcq_l,
2548 &mut dz_l,
2549 &mut da_l,
2550 &mut db_l,
2551 );
2552 for p in 0..t {
2555 let row = (bi * t + p) * c_dim;
2556 for c in ko * d.dk..(ko + 1) * d.dk {
2557 unsafe {
2558 *dcqp.at(row + c) = dcq_l[p * c_dim + c];
2559 *dcqp.at(row + kd + c) = dcq_l[p * c_dim + kd + c];
2560 }
2561 }
2562 for hh in 0..rep_v {
2563 let h = ko * rep_v + hh;
2564 for dj in 0..d.dv {
2565 unsafe {
2566 *dcqp.at(row + 2 * kd + h * d.dv + dj) =
2567 dcq_l[p * c_dim + 2 * kd + h * d.dv + dj];
2568 *dzp.at((bi * t + p) * vd + h * d.dv + dj) =
2569 dz_l[p * vd + h * d.dv + dj];
2570 }
2571 }
2572 unsafe {
2573 *dap.at((bi * t + p) * nv + h) = da_l[p * nv + h];
2574 *dbp.at((bi * t + p) * nv + h) = db_l[p * nv + h];
2575 }
2576 }
2577 }
2578 };
2579 match pool {
2580 Some(p) if units > 1 => p.run(&|widx, nw| {
2581 for u in (widx..units).step_by(nw) {
2582 run_unit(u);
2583 }
2584 }),
2585 _ => {
2586 for u in 0..units {
2587 run_unit(u);
2588 }
2589 }
2590 }
2591 }
2592
2593 let mut dqkv64 = vec![0f64; n * c_dim];
2594 for bi in 0..b {
2595 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
2596 ops::gdn_conv_bwd(
2597 &pre64[r.clone()],
2598 t,
2599 c_dim,
2600 d.kk,
2601 conv,
2602 &dcq64[r.clone()],
2603 &mut dqkv64[r],
2604 );
2605 }
2606 let to32 = |v: &[f64]| -> Vec<f32> { v.iter().map(|&x| x as f32).collect() };
2607 let (dqkv, dz, da, db) = (to32(&dqkv64), to32(&dz64), to32(&da64), to32(&db64));
2608
2609 let mut dn1 = vec![0f32; n * hsz];
2610 ops::gemm_dx(&dqkv, wqkv, &mut dn1, n, hsz, c_dim, pool);
2611 ops::gemm_dx(&dz, wz, &mut dn1, n, hsz, vd, pool);
2612 ops::gemm_dx(&da, wa, &mut dn1, n, hsz, nv, pool);
2613 ops::gemm_dx(&db, wb, &mut dn1, n, hsz, nv, pool);
2614 dn1
2615 }
2616
2617 fn forward_hidden(
2622 &self,
2623 ids: &[u32],
2624 b: usize,
2625 t: usize,
2626 ts: Option<&TrainState>,
2627 student: bool,
2628 keep: Option<&mut Vec<Vec<f32>>>,
2629 ) -> Vec<f32> {
2630 self.forward_span(SpanIn::Ids(ids), 0, self.nl, b, t, ts, student, keep)
2631 }
2632
2633 #[allow(clippy::too_many_arguments)]
2636 fn forward_span(
2637 &self,
2638 input: SpanIn,
2639 from: usize,
2640 to: usize,
2641 b: usize,
2642 t: usize,
2643 ts: Option<&TrainState>,
2644 student: bool,
2645 mut keep: Option<&mut Vec<Vec<f32>>>,
2646 ) -> Vec<f32> {
2647 let hsz = self.hidden;
2648 let mut h = match input {
2649 SpanIn::Hidden(h) => h,
2650 SpanIn::Ids(ids) => {
2651 let mut h = vec![0f32; b * t * hsz];
2652 for (r, &id) in ids.iter().enumerate() {
2653 let src = (id as usize).min(self.embed.len() / hsz - 1) * hsz;
2654 h[r * hsz..(r + 1) * hsz].copy_from_slice(&self.embed[src..src + hsz]);
2655 }
2656 h
2657 }
2658 };
2659 for li in from..to {
2660 if let Some(k) = keep.as_deref_mut() {
2661 k.push(h.clone());
2662 }
2663 let mats = self.mats(li).expect("layer mats");
2664 let wts = ln_ffn(self, if student { ts } else { None }, li, &mats);
2665 let nys = student && self.o1_flags[li];
2666 h = self.layer_forward(li, &h, b, t, &wts, nys, false).0;
2667 }
2668 h
2669 }
2670
2671 #[allow(clippy::too_many_arguments)]
2674 fn head_core(
2675 &self,
2676 ns: &[f32],
2677 nt: &[f32],
2678 targets: &[u32],
2679 kl_w: f64,
2680 weights: Option<&[f32]>,
2681 inv_w: f64,
2682 ) -> (f64, f64, Vec<f32>) {
2683 let hsz = self.hidden;
2684 let n = targets.len();
2685 let pos_w = |i: usize| weights.map_or(1.0, |w| w[i] as f64);
2686 let pool = self.pool.as_deref();
2687 let wh = self.head_weight();
2688 let vs = self.vocab;
2689 let mut ce_sum = 0f64;
2690 let mut kl_sum = 0f64;
2691 let mut dns = vec![0f32; n * hsz];
2692 let mut ls = vec![0f32; LM_CHUNK * vs];
2693 let mut lt = vec![0f32; LM_CHUNK * vs];
2694 let mut dlg = vec![0f32; LM_CHUNK * vs];
2695 let mut r0 = 0usize;
2696 let nth = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4);
2697 while r0 < n {
2698 let r1 = (r0 + LM_CHUNK).min(n);
2699 let c = r1 - r0;
2700 if weights.is_some_and(|w| w[r0..r1].iter().all(|&x| x == 0.0)) {
2702 r0 = r1;
2703 continue;
2704 }
2705 ops::gemm_nt(
2706 &ns[r0 * hsz..r1 * hsz],
2707 wh,
2708 &mut ls[..c * vs],
2709 c,
2710 hsz,
2711 vs,
2712 pool,
2713 );
2714 ops::gemm_nt(
2715 &nt[r0 * hsz..r1 * hsz],
2716 wh,
2717 &mut lt[..c * vs],
2718 c,
2719 hsz,
2720 vs,
2721 pool,
2722 );
2723 let per = c.div_ceil(nth.min(c)).max(1);
2726 let parts: Vec<(f64, f64)> = std::thread::scope(|sc| {
2727 let (ls, lt, pos_w) = (&ls, <, &pos_w);
2728 let hs: Vec<_> = dlg[..c * vs]
2729 .chunks_mut(per * vs)
2730 .enumerate()
2731 .map(|(ti, dchunk)| {
2732 sc.spawn(move || {
2733 let mut acc = (0f64, 0f64);
2734 for (k, drow) in dchunk.chunks_mut(vs).enumerate() {
2735 let r = ti * per + k;
2736 let w = pos_w(r0 + r);
2737 if w == 0.0 {
2738 drow.fill(0.0);
2739 continue;
2740 }
2741 let (ce, kl) = ops::ce_kl_position(
2742 &ls[r * vs..(r + 1) * vs],
2743 <[r * vs..(r + 1) * vs],
2744 targets[r0 + r] as usize,
2745 kl_w,
2746 w * inv_w,
2747 drow,
2748 );
2749 acc.0 += w * ce;
2750 acc.1 += w * kl;
2751 }
2752 acc
2753 })
2754 })
2755 .collect();
2756 hs.into_iter().map(|h| h.join().expect("ce/kl worker")).collect()
2757 });
2758 for (ce, kl) in parts {
2759 ce_sum += ce;
2760 kl_sum += kl;
2761 }
2762 ops::gemm_dx(
2763 &dlg[..c * vs],
2764 wh,
2765 &mut dns[r0 * hsz..r1 * hsz],
2766 c,
2767 hsz,
2768 vs,
2769 pool,
2770 );
2771 r0 = r1;
2772 }
2773
2774 (ce_sum, kl_sum, dns)
2775 }
2776
2777 fn loss_and_dhidden(
2782 &self,
2783 hs: &[f32],
2784 ht: &[f32],
2785 targets: &[u32],
2786 kl_w: f64,
2787 weights: Option<&[f32]>,
2788 ) -> (f64, f64, Vec<f32>) {
2789 let hsz = self.hidden;
2790 let n = targets.len();
2791 let wsum: f64 = weights.map_or(n as f64, |w| w.iter().map(|&v| v as f64).sum());
2792 let inv_w = if wsum > 0.0 { 1.0 / wsum } else { 0.0 };
2793
2794 let mut ns = vec![0f32; n * hsz];
2795 let mut invs = vec![0f32; n];
2796 ops::rmsnorm_fwd(
2797 hs,
2798 &self.final_norm,
2799 self.eps,
2800 self.gemma,
2801 &mut ns,
2802 &mut invs,
2803 );
2804 let mut nt = vec![0f32; n * hsz];
2805 let mut invt = vec![0f32; n];
2806 ops::rmsnorm_fwd(
2807 ht,
2808 &self.final_norm,
2809 self.eps,
2810 self.gemma,
2811 &mut nt,
2812 &mut invt,
2813 );
2814
2815 let (ce_sum, kl_sum, dns) = match weights {
2819 Some(w) if w.iter().any(|&x| x == 0.0) => {
2820 let rows: Vec<usize> = (0..n).filter(|&i| w[i] > 0.0).collect();
2821 let gather = |src: &[f32]| -> Vec<f32> {
2822 rows.iter().flat_map(|&r| src[r * hsz..(r + 1) * hsz].iter().copied()).collect()
2823 };
2824 let (nsc, ntc) = (gather(&ns), gather(&nt));
2825 let tgc: Vec<u32> = rows.iter().map(|&r| targets[r]).collect();
2826 let wc: Vec<f32> = rows.iter().map(|&r| w[r]).collect();
2827 let (ce, kl, dnc) = self.head_core(&nsc, &ntc, &tgc, kl_w, Some(&wc), inv_w);
2828 let mut dns = vec![0f32; n * hsz];
2829 for (k, &r) in rows.iter().enumerate() {
2830 dns[r * hsz..(r + 1) * hsz].copy_from_slice(&dnc[k * hsz..(k + 1) * hsz]);
2831 }
2832 (ce, kl, dns)
2833 }
2834 _ => self.head_core(&ns, &nt, targets, kl_w, weights, inv_w),
2835 };
2836 let mut dhs = vec![0f32; n * hsz];
2837 ops::rmsnorm_bwd(
2838 hs,
2839 &self.final_norm,
2840 &invs,
2841 &dns,
2842 self.gemma,
2843 &mut dhs,
2844 None,
2845 );
2846 (ce_sum * inv_w, kl_sum * inv_w, dhs)
2847 }
2848
2849 fn backward(
2852 &self,
2853 b: usize,
2854 t: usize,
2855 keep: &[Vec<f32>],
2856 dh_last: Vec<f32>,
2857 ts: &mut TrainState,
2858 ) {
2859 self.backward_from(0, b, t, keep, dh_last, ts)
2860 }
2861
2862 fn backward_from(
2866 &self,
2867 from: usize,
2868 b: usize,
2869 t: usize,
2870 keep: &[Vec<f32>],
2871 dh_last: Vec<f32>,
2872 ts: &mut TrainState,
2873 ) {
2874 let TrainState {
2877 layers, data, grad, ..
2878 } = ts;
2879 let mut dh = dh_last;
2880 for li in (from..self.nl).rev() {
2881 let h_in = &keep[li - from];
2882 let nys = self.o1_flags[li];
2883 let slot = layers.iter().position(|&x| x == li);
2884 let mats_hold = self.mats(li).expect("layer mats");
2885 let wts = match slot {
2886 Some(s) => {
2887 let bi = s * PARAMS_PER_LAYER;
2888 LnFfn {
2889 iln: &data[bi],
2890 pln: &data[bi + 1],
2891 gate: &data[bi + 2],
2892 up: &data[bi + 3],
2893 down: &data[bi + 4],
2894 gu: None,
2895 }
2896 }
2897 None => {
2898 let l = &self.layers[li];
2899 LnFfn {
2900 iln: &l.iln,
2901 pln: &l.pln,
2902 gate: &[],
2903 up: &[],
2904 down: &mats_hold.down,
2905 gu: Some(&mats_hold.gu),
2906 }
2907 }
2908 };
2909 let (_, acts) = self.layer_forward(li, h_in, b, t, &wts, nys, true);
2910 let acts = acts.expect("want_acts");
2911 dh = match slot {
2912 Some(s) => {
2913 let gb = s * PARAMS_PER_LAYER;
2914 let gr = &mut grad[gb..gb + PARAMS_PER_LAYER];
2915 self.layer_backward(li, h_in, b, t, &wts, nys, &acts, &dh, Some(gr))
2916 }
2917 None => self.layer_backward(li, h_in, b, t, &wts, nys, &acts, &dh, None),
2918 };
2919 }
2920 }
2921
2922 #[doc(hidden)]
2929 pub fn loss_and_grads_for_test(
2930 &self,
2931 ids: &[u32],
2932 tgt: &[u32],
2933 b: usize,
2934 t: usize,
2935 ts: &mut TrainState,
2936 kl_w: f64,
2937 ) -> f64 {
2938 let ht = self.forward_hidden(ids, b, t, None, false, None);
2939 let mut keep = Vec::with_capacity(self.nl);
2940 let hs = self.forward_hidden(ids, b, t, Some(ts), true, Some(&mut keep));
2941 let (ce, kl, dhs) = self.loss_and_dhidden(&hs, &ht, tgt, kl_w, None);
2942 ts.zero_grad();
2943 self.backward(b, t, &keep, dhs, ts);
2944 (1.0 - kl_w) * ce + kl_w * kl
2945 }
2946
2947 pub fn val_ppl(
2951 &self,
2952 va: &[u32],
2953 ts: Option<&TrainState>,
2954 student: bool,
2955 bs: usize,
2956 nrounds: usize,
2957 seq: usize,
2958 ) -> f64 {
2959 let nwin = nrounds * bs;
2960 if va.len() < seq + 2 || nwin == 0 {
2961 return f64::NAN;
2962 }
2963 let stride = (va.len() - seq - 1) / nwin;
2964 let hsz = self.hidden;
2965 let wh = self.head_weight();
2966 let vs = self.vocab;
2967 let pool = self.pool.as_deref();
2968 let mut nll = 0f64;
2969 let mut cnt = 0usize;
2970 for j in 0..nrounds {
2971 let mut ids = Vec::with_capacity(bs * seq);
2972 let mut tgt = Vec::with_capacity(bs * seq);
2973 for bi in 0..bs {
2974 let off = ((j * bs + bi) * stride.max(1)).min(va.len() - seq - 1);
2975 ids.extend_from_slice(&va[off..off + seq]);
2976 tgt.extend_from_slice(&va[off + 1..off + seq + 1]);
2977 }
2978 let h = self.forward_hidden(&ids, bs, seq, ts, student, None);
2979 let n = bs * seq;
2980 let mut ns = vec![0f32; n * hsz];
2981 let mut inv = vec![0f32; n];
2982 ops::rmsnorm_fwd(
2983 &h,
2984 &self.final_norm,
2985 self.eps,
2986 self.gemma,
2987 &mut ns,
2988 &mut inv,
2989 );
2990 let mut lg = vec![0f32; LM_CHUNK * vs];
2991 let mut r0 = 0usize;
2992 while r0 < n {
2993 let r1 = (r0 + LM_CHUNK).min(n);
2994 let c = r1 - r0;
2995 ops::gemm_nt(
2996 &ns[r0 * hsz..r1 * hsz],
2997 wh,
2998 &mut lg[..c * vs],
2999 c,
3000 hsz,
3001 vs,
3002 pool,
3003 );
3004 for r in 0..c {
3005 let row = &lg[r * vs..(r + 1) * vs];
3006 let target = tgt[r0 + r] as usize;
3007 let mut mx = f64::NEG_INFINITY;
3008 for &v in row {
3009 mx = mx.max(v as f64);
3010 }
3011 let mut s = 0f64;
3012 for &v in row {
3013 s += (v as f64 - mx).exp();
3014 }
3015 nll += mx + s.ln() - row[target.min(vs - 1)] as f64;
3016 cnt += 1;
3017 }
3018 r0 = r1;
3019 }
3020 }
3021 (nll / cnt.max(1) as f64).exp()
3022 }
3023
3024 pub fn val_ppl_stream(
3030 &self,
3031 va: &FcdStream,
3032 ts: Option<&TrainState>,
3033 student: bool,
3034 bs: usize,
3035 nrounds: usize,
3036 seq: usize,
3037 ) -> f64 {
3038 if va.is_plain() {
3039 return self.val_ppl(va.ids, ts, student, bs, nrounds, seq);
3040 }
3041 if va.ids.len() < seq + 2 || bs == 0 {
3042 return f64::NAN;
3043 }
3044 let recs = va.scored_records();
3045 let want = (4 * nrounds * bs).min(recs.len());
3046 if want == 0 {
3047 return f64::NAN;
3048 }
3049 let picks: Vec<(usize, Vec<f32>)> = (0..want)
3050 .map(|j| {
3051 let rec = recs[j * recs.len() / want];
3052 let (st, _, f, _) = rec;
3053 let max_off = va.ids.len() - seq - 1;
3054 let off = if f - st < seq / 2 { st } else { f - seq / 2 }.min(max_off);
3055 (off, va.target_weights(off, seq, rec))
3056 })
3057 .collect();
3058 let hsz = self.hidden;
3059 let wh = self.head_weight();
3060 let vs = self.vocab;
3061 let pool = self.pool.as_deref();
3062 let (mut nll, mut wsum) = (0f64, 0f64);
3063 for group in picks.chunks(bs) {
3064 let b = group.len();
3065 let mut ids = Vec::with_capacity(b * seq);
3066 let mut tgt = Vec::with_capacity(b * seq);
3067 let mut tw = Vec::with_capacity(b * seq);
3068 for (off, w) in group {
3069 ids.extend_from_slice(&va.ids[*off..*off + seq]);
3070 tgt.extend_from_slice(&va.ids[*off + 1..*off + seq + 1]);
3071 tw.extend_from_slice(w);
3072 }
3073 let h = self.forward_hidden(&ids, b, seq, ts, student, None);
3074 let n = b * seq;
3075 let mut ns = vec![0f32; n * hsz];
3076 let mut inv = vec![0f32; n];
3077 ops::rmsnorm_fwd(&h, &self.final_norm, self.eps, self.gemma, &mut ns, &mut inv);
3078 let mut lg = vec![0f32; LM_CHUNK * vs];
3079 let mut r0 = 0usize;
3080 while r0 < n {
3081 let r1 = (r0 + LM_CHUNK).min(n);
3082 let c = r1 - r0;
3083 ops::gemm_nt(&ns[r0 * hsz..r1 * hsz], wh, &mut lg[..c * vs], c, hsz, vs, pool);
3084 for r in 0..c {
3085 let w = tw[r0 + r] as f64;
3086 if w == 0.0 {
3087 continue;
3088 }
3089 let row = &lg[r * vs..(r + 1) * vs];
3090 let target = tgt[r0 + r] as usize;
3091 let mx = row.iter().fold(f64::NEG_INFINITY, |m, &v| m.max(v as f64));
3092 let s: f64 = row.iter().map(|&v| (v as f64 - mx).exp()).sum();
3093 nll += w * (mx + s.ln() - row[target.min(vs - 1)] as f64);
3094 wsum += w;
3095 }
3096 r0 = r1;
3097 }
3098 }
3099 if wsum == 0.0 { f64::NAN } else { (nll / wsum).exp() }
3100 }
3101
3102 fn nll_on(&self, h: &[f32], tgt: &[u32], w: &[f32]) -> (f64, f64) {
3105 let hsz = self.hidden;
3106 let wh = self.head_weight();
3107 let vs = self.vocab;
3108 let pool = self.pool.as_deref();
3109 let n = tgt.len();
3110 let mut ns = vec![0f32; n * hsz];
3111 let mut inv = vec![0f32; n];
3112 ops::rmsnorm_fwd(h, &self.final_norm, self.eps, self.gemma, &mut ns, &mut inv);
3113 let mut lg = vec![0f32; LM_CHUNK * vs];
3114 let (mut nll, mut wsum) = (0f64, 0f64);
3115 let mut r0 = 0usize;
3116 while r0 < n {
3117 let r1 = (r0 + LM_CHUNK).min(n);
3118 let c = r1 - r0;
3119 if w[r0..r1].iter().any(|&x| x > 0.0) {
3120 ops::gemm_nt(&ns[r0 * hsz..r1 * hsz], wh, &mut lg[..c * vs], c, hsz, vs, pool);
3121 let lg = ≶
3122 let parts: Vec<(f64, f64)> = std::thread::scope(|sc| {
3123 let hs: Vec<_> = (0..c)
3124 .map(|r| {
3125 sc.spawn(move || {
3126 let wr = w[r0 + r] as f64;
3127 if wr == 0.0 {
3128 return (0.0, 0.0);
3129 }
3130 let row = &lg[r * vs..(r + 1) * vs];
3131 let mx = row.iter().fold(f64::NEG_INFINITY, |m, &v| m.max(v as f64));
3132 let se: f64 = row.iter().map(|&v| (v as f64 - mx).exp()).sum();
3133 (wr * (mx + se.ln() - row[(tgt[r0 + r] as usize).min(vs - 1)] as f64), wr)
3134 })
3135 })
3136 .collect();
3137 hs.into_iter().map(|h| h.join().expect("nll worker")).collect()
3138 });
3139 for (a, b) in parts {
3140 nll += a;
3141 wsum += b;
3142 }
3143 }
3144 r0 = r1;
3145 }
3146 (nll, wsum)
3147 }
3148}
3149
3150struct Slot {
3152 off: usize,
3153 w: Vec<f32>,
3154 wsum: f64,
3155}
3156
3157fn record_slots(s: &FcdStream, seq: usize, val: bool) -> Vec<Slot> {
3163 let max_off = s.ids.len() - seq - 1;
3164 let mut out = Vec::new();
3165 for rec in s.scored_records() {
3166 let (st, en, f, _) = rec;
3167 let offs: Vec<usize> = if en - st <= seq + 1 {
3168 vec![st.min(max_off)]
3169 } else if val {
3170 vec![if f - st < seq / 2 { st } else { f - seq / 2 }.min(en - seq - 1).min(max_off)]
3172 } else {
3173 let last = (en - seq - 1).min(max_off);
3174 let mut v: Vec<usize> = (st..=last).step_by((seq / 2).max(1)).collect();
3175 if v.last() != Some(&last) {
3176 v.push(last);
3177 }
3178 v
3179 };
3180 for off in offs {
3181 let w = s.target_weights(off, seq, rec);
3182 let wsum: f64 = w.iter().map(|&x| x as f64).sum();
3183 if wsum > 0.0 {
3184 out.push(Slot { off, w, wsum });
3185 }
3186 }
3187 }
3188 out
3189}
3190
3191pub fn run_polish_distilled(
3213 model: &Arc<CmfModel>,
3214 teacher: Option<&Arc<CmfModel>>,
3215 o1: &O1Cfg,
3216 hp: &FcdHyper,
3217 tr: &[u32],
3218 va: &[u32],
3219 out: &std::path::Path,
3220 gate: Option<&GenGateCfg>,
3221) -> Result<FcdReport, String> {
3222 run_polish_stream(
3223 model,
3224 teacher,
3225 o1,
3226 hp,
3227 FcdStream::plain(tr),
3228 FcdStream::plain(va),
3229 out,
3230 gate,
3231 None,
3232 )
3233}
3234
3235#[derive(Clone, Copy)]
3240pub struct CacheModels<'a> {
3241 pub teacher: &'a Arc<CmfModel>,
3242 pub student: &'a Arc<CmfModel>,
3243}
3244
3245#[allow(clippy::too_many_arguments)]
3249pub fn run_polish_stream(
3250 model: &Arc<CmfModel>,
3251 teacher: Option<&Arc<CmfModel>>,
3252 o1: &O1Cfg,
3253 hp: &FcdHyper,
3254 tr: FcdStream,
3255 va: FcdStream,
3256 out: &std::path::Path,
3257 gate: Option<&GenGateCfg>,
3258 cache_models: Option<CacheModels>,
3259) -> Result<FcdReport, String> {
3260 if tr.ids.len() < hp.seq + 2 {
3261 return Err(format!(
3262 "train corpus too small: {} tokens < seq+2 = {}",
3263 tr.ids.len(),
3264 hp.seq + 2
3265 ));
3266 }
3267 let tr_recs = tr.scored_records();
3268 if tr_recs.is_empty() {
3269 return Err("train stream has no scored positions".into());
3270 }
3271 let tr_cum: Vec<f64> = tr_recs
3274 .iter()
3275 .scan(0f64, |acc, &(_, _, f, l)| {
3276 *acc += (l - f) as f64;
3277 Some(*acc)
3278 })
3279 .collect();
3280 let mut fm = FcdModel::from_cmf(model, o1, hp.polish_only)?;
3281 fm.polish_only = hp.polish_only;
3282 let fm = fm;
3283 if !tr.is_plain() {
3284 let converted = fm.converted();
3287 if converted.is_empty() {
3288 return Err("no converted layers under this --o1 spec (nothing to polish)".into());
3289 }
3290 return polish_records(model, fm, teacher, converted, o1, hp, tr, va, out, gate, cache_models);
3291 }
3292 let tfm: Option<FcdModel> = match teacher {
3293 Some(t) => {
3294 let off = O1Cfg {
3295 layers: O1Layers::List(Vec::new()),
3296 m: o1.m,
3297 w: o1.w,
3298 sink: o1.sink,
3299 rect: o1.rect,
3300 };
3301 let mut t = FcdModel::from_cmf(t, &off, true)?;
3302 t.polish_only = true;
3303 if t.hidden != fm.hidden || t.nl != fm.nl {
3304 return Err(format!(
3305 "teacher geometry differs: hidden {}/{} layers {}/{}",
3306 t.hidden, fm.hidden, t.nl, fm.nl
3307 ));
3308 }
3309 tracing::info!("fcd: distilling from a separate teacher file");
3310 Some(t)
3311 }
3312 None => None,
3313 };
3314 let converted = fm.converted();
3315 if converted.is_empty() {
3316 return Err("no converted layers under this --o1 spec (nothing to polish)".into());
3317 }
3318 tracing::info!(
3319 "fcd: {} layers converted ({} trainable tensors), m={} w={} sink={}, \
3320 corpus train {} / val {} tokens",
3321 converted.len(),
3322 converted.len() * PARAMS_PER_LAYER,
3323 fm.nys.m,
3324 fm.nys.w,
3325 fm.nys.sink,
3326 tr.ids.len(),
3327 va.ids.len()
3328 );
3329
3330 let mut ts = TrainState::new(&fm);
3331 let teacher_ppl = match &tfm {
3332 Some(t) => t.val_ppl_stream(&va, None, false, hp.bs, 2, hp.seq),
3333 None => fm.val_ppl_stream(&va, None, false, hp.bs, 2, hp.seq),
3334 };
3335 let ppl_start = fm.val_ppl_stream(&va, Some(&ts), true, hp.bs, 2, hp.seq);
3336 tracing::info!(
3337 "fcd: quick-val teacher ppl {teacher_ppl:.2} | zero-shot o1 student ppl {ppl_start:.2}"
3338 );
3339
3340 let mut gate_state: Option<(Pipeline, Vec<f64>)> = match gate {
3342 Some(g) if !g.prompts.is_empty() => {
3343 let greedy = SamplerConfig {
3344 temperature: 0.0,
3345 top_p: 1.0,
3346 presence_penalty: 0.0,
3347 top_k: 0,
3348 repetition_penalty: 1.0,
3349 min_p: 0.0,
3350 seed: Some(0),
3351 suppress_tokens: Vec::new(),
3352 penalty_window: 0,
3353 };
3354 let mut pipe = Pipeline::from_model(model, greedy)
3355 .map_err(|e| format!("gen-gate pipeline: {e}"))?;
3356 if !hp.polish_only {
3357 pipe.set_o1(Some(o1.clone()));
3358 }
3359 apply_trainables(&mut pipe, &fm, &ts);
3360 let base = gate_gen_scores(&mut pipe, g)?;
3361 tracing::info!("fcd gen-gate baseline loop-scores: {base:?}");
3362 Some((pipe, base))
3363 }
3364 Some(_) => {
3365 tracing::warn!("fcd gen-gate requested but val stream too short — gate off");
3366 None
3367 }
3368 None => None,
3369 };
3370 let init_snapshot: Option<Vec<Vec<f32>>> = gate_state.is_some().then(|| ts.data.clone());
3372 let mut gate_evals: Vec<(usize, f64, Vec<f64>, bool)> = Vec::new();
3373
3374 let mut rng = SplitMix64::new(hp.seed);
3375 let mut best: (f64, Option<Vec<Vec<f32>>>, usize) = (f64::INFINITY, None, 0);
3376 let mut losses: Vec<(f64, f64)> = Vec::with_capacity(hp.steps);
3377 let t0 = std::time::Instant::now();
3378 let n_per_step = hp.bs * hp.seq;
3379 for st in 1..=hp.steps {
3380 let mut ids = Vec::with_capacity(n_per_step);
3383 let mut tgt = Vec::with_capacity(n_per_step);
3384 let mut tw: Vec<f32> = Vec::with_capacity(n_per_step);
3385 for _ in 0..hp.bs {
3386 let off = if tr.is_plain() {
3387 (rng.next_u64() as usize) % (tr.ids.len() - hp.seq - 1)
3388 } else {
3389 let total = *tr_cum.last().unwrap();
3390 let u = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64 * total;
3391 let r = tr_cum.partition_point(|&c| c <= u).min(tr_recs.len() - 1);
3392 let rec = tr_recs[r];
3393 let p = rec.2 + (rng.next_u64() as usize) % (rec.3 - rec.2);
3394 let off = tr.window_start(rec, p, hp.seq, rng.next_u64());
3395 tw.extend(tr.target_weights(off, hp.seq, rec));
3396 off
3397 };
3398 ids.extend_from_slice(&tr.ids[off..off + hp.seq]);
3399 tgt.extend_from_slice(&tr.ids[off + 1..off + hp.seq + 1]);
3400 }
3401
3402 let ht = match &tfm {
3403 Some(t) => t.forward_hidden(&ids, hp.bs, hp.seq, None, false, None),
3404 None => fm.forward_hidden(&ids, hp.bs, hp.seq, None, false, None),
3405 };
3406 let mut keep: Vec<Vec<f32>> = Vec::with_capacity(fm.nl);
3407 let hs = fm.forward_hidden(&ids, hp.bs, hp.seq, Some(&ts), true, Some(&mut keep));
3408 let (ce, kl, dhs) =
3409 fm.loss_and_dhidden(&hs, &ht, &tgt, hp.kl_w, (!tr.is_plain()).then_some(&tw[..]));
3410 ts.zero_grad();
3411 fm.backward(hp.bs, hp.seq, &keep, dhs, &mut ts);
3412 let gn = ts.clip_and_step(hp.lr);
3413 losses.push((ce, kl));
3414
3415 let el = t0.elapsed().as_secs_f64();
3416 tracing::info!(
3417 "fcd step {st}/{}: ce {ce:.3} kl {kl:.3} |g| {gn:.3} ({:.1}s/step)",
3418 hp.steps,
3419 el / st as f64
3420 );
3421 if hp.eval_every > 0 && st % hp.eval_every == 0 {
3422 let p = fm.val_ppl_stream(&va, Some(&ts), true, hp.bs, 2, hp.seq);
3423 match (&mut gate_state, gate) {
3424 (Some((pipe, base)), Some(g)) => {
3425 apply_trainables(pipe, &fm, &ts);
3426 let scores = gate_gen_scores(pipe, g)?;
3427 let pass = gate_pass(&scores, base, g.threshold, g.baseline_slack);
3428 let tag = if pass && p < best.0 {
3429 best = (p, Some(ts.data.clone()), st);
3430 " *best*"
3431 } else {
3432 ""
3433 };
3434 tracing::info!(
3435 "fcd eval step {st}: val ppl {p:.2} | gen-gate {} (loop-scores {scores:?}){tag}",
3436 if pass { "PASS" } else { "FAIL" }
3437 );
3438 gate_evals.push((st, p, scores, pass));
3439 }
3440 _ => {
3441 let tag = if p < best.0 {
3442 best = (p, Some(ts.data.clone()), st);
3443 " *best*"
3444 } else {
3445 ""
3446 };
3447 tracing::info!("fcd eval step {st}: val ppl {p:.2}{tag}");
3448 }
3449 }
3450 }
3451 }
3452
3453 let mut gate_chosen: Option<usize> = None;
3457 if let Some(snap) = best.1.take() {
3458 ts.data = snap;
3459 gate_chosen = Some(best.2);
3460 tracing::info!(
3461 "fcd: restored best checkpoint from step {} (val ppl {:.2})",
3462 best.2,
3463 best.0
3464 );
3465 } else if let Some(init) = init_snapshot {
3466 ts.data = init;
3467 tracing::info!(
3468 "fcd: polish rejected by generation gate — identity artifact (zero-shot state written; claim 13 floor)"
3469 );
3470 }
3471 let ppl_final = fm.val_ppl_stream(&va, Some(&ts), true, hp.bs, 6, hp.seq);
3472 let report = FcdReport {
3473 converted: converted.clone(),
3474 teacher_ppl,
3475 ppl_start,
3476 ppl_best: best.0.min(ppl_final),
3477 best_step: best.2,
3478 ppl_final,
3479 steps_run: hp.steps,
3480 sec_per_step: t0.elapsed().as_secs_f64() / hp.steps.max(1) as f64,
3481 losses,
3482 gate: gate_state.map(|(_, base)| GateReport {
3483 baseline: base,
3484 evals: gate_evals,
3485 chosen: gate_chosen,
3486 }),
3487 };
3488 save_polished(model, out, &fm, &ts, o1, hp, &report)?;
3489 Ok(report)
3490}
3491
3492#[allow(clippy::too_many_arguments)]
3499fn polish_records(
3500 model: &Arc<CmfModel>,
3501 fm: FcdModel,
3502 teacher: Option<&Arc<CmfModel>>,
3503 converted: Vec<usize>,
3504 o1: &O1Cfg,
3505 hp: &FcdHyper,
3506 tr: FcdStream,
3507 va: FcdStream,
3508 out: &std::path::Path,
3509 gate: Option<&GenGateCfg>,
3510 cache_models: Option<CacheModels>,
3511) -> Result<FcdReport, String> {
3512 let (t, hsz) = (hp.seq, fm.hidden);
3513 let mut ts = TrainState::new(&fm);
3514 let from = *ts.layers.iter().min().ok_or("no trainable layers")?;
3515 let tr_slots = record_slots(&tr, t, false);
3516 let mut va_slots = record_slots(&va, t, true);
3517 if va_slots.len() > 64 {
3518 let k = va_slots.len();
3520 let keep: std::collections::HashSet<usize> = (0..64).map(|j| j * k / 64).collect();
3521 va_slots = va_slots.into_iter().enumerate().filter(|(i, _)| keep.contains(i)).map(|(_, s)| s).collect();
3522 }
3523 if tr_slots.is_empty() || va_slots.is_empty() {
3524 return Err("record streams have no scored windows".into());
3525 }
3526 let n_slots = tr_slots.len() + va_slots.len();
3527 let cache_bytes = n_slots as f64 * t as f64 * hsz as f64 * 4.0 * if from > 0 { 2.0 } else { 1.0 };
3528 tracing::info!(
3529 "fcd: {} train + {} val windows of {t}; trainable layers {}..{} — caching the teacher and the frozen prefix ({:.2} GB)",
3530 tr_slots.len(),
3531 va_slots.len(),
3532 from,
3533 fm.nl - 1,
3534 cache_bytes / 1e9
3535 );
3536 let t_cache = std::time::Instant::now();
3538 let runtime = |m: &Arc<CmfModel>| -> Option<Pipeline> {
3544 let mut p = Pipeline::from_model(m, SamplerConfig::default()).ok()?;
3545 p.set_confidence(false);
3546 p.can_prefill_batched().then_some(p)
3547 };
3548 let (t_file, s_file) = match cache_models {
3549 Some(c) => (c.teacher, c.student),
3550 None => (teacher.unwrap_or(model), model),
3551 };
3552 let mut t_rt = runtime(t_file);
3553 let mut s_rt = if from > 0 { runtime(s_file) } else { None };
3554 let fill = |s: &FcdStream, slots: &[Slot], label: &str, t_rt: &mut Option<Pipeline>, s_rt: &mut Option<Pipeline>| -> Result<(Vec<Vec<f32>>, Vec<Vec<f32>>), String> {
3555 let (mut pre, mut th) = (Vec::with_capacity(slots.len()), Vec::with_capacity(slots.len()));
3556 let tc = std::time::Instant::now();
3557 for (k, sl) in slots.iter().enumerate() {
3558 let ids = &s.ids[sl.off..sl.off + t];
3559 let h = match t_rt.as_mut() {
3560 Some(p) => p.final_hidden_rows(ids)?,
3561 None => fm.forward_hidden(ids, 1, t, None, false, None),
3562 };
3563 let p = if from == 0 {
3564 Vec::new()
3565 } else {
3566 match s_rt.as_mut() {
3567 Some(p) => p.calib_window_span(ids, None, 0, from, from)?,
3568 None => fm.forward_span(SpanIn::Ids(ids), 0, from, 1, t, None, true, None),
3569 }
3570 };
3571 if h.len() != t * hsz || (from > 0 && p.len() != t * hsz) {
3572 return Err(format!("window cache: hidden of {} / {} values, expected {}", h.len(), p.len(), t * hsz));
3573 }
3574 pre.push(p);
3575 th.push(h);
3576 if k % 64 == 63 {
3577 tracing::info!("fcd: {label} cache {}/{} windows ({:.0}s)", k + 1, slots.len(), tc.elapsed().as_secs_f64());
3578 }
3579 }
3580 Ok((pre, th))
3581 };
3582 if let (Some(p), Some(sl)) = (t_rt.as_mut(), tr_slots.first()) {
3584 let ids = &tr.ids[sl.off..sl.off + t];
3585 let a = p.final_hidden_rows(ids)?;
3586 let b = p.calib_window_span(ids, None, 0, fm.nl, fm.nl)?;
3587 let num: f64 = a.iter().zip(&b).map(|(x, y)| ((x - y) as f64).powi(2)).sum();
3588 let den: f64 = b.iter().map(|y| (*y as f64).powi(2)).sum();
3589 tracing::info!("fcd: teacher prefill vs span walk, relative difference {:.2e}", (num / den.max(1e-30)).sqrt());
3590 }
3591 let (va_pre, va_th) = fill(&va, &va_slots, "val", &mut t_rt, &mut s_rt)?;
3596 let mut tr_pre: Vec<Option<Vec<f32>>> = vec![None; tr_slots.len()];
3597 let mut tr_th: Vec<Option<Vec<f32>>> = vec![None; tr_slots.len()];
3598 tracing::info!("fcd: val window cache built in {:.0}s; train windows are cached as they are drawn", t_cache.elapsed().as_secs_f64());
3599
3600 let gather = |s: &FcdStream, slots: &[Slot], pre: &[Vec<f32>], th: &[Vec<f32>], pick: &[usize]| {
3601 let mut ids = Vec::with_capacity(pick.len() * t);
3602 let mut tgt = Vec::with_capacity(pick.len() * t);
3603 let mut w = Vec::with_capacity(pick.len() * t);
3604 let mut h0 = Vec::with_capacity(if from > 0 { pick.len() * t * hsz } else { 0 });
3605 let mut ht = Vec::with_capacity(pick.len() * t * hsz);
3606 for &i in pick {
3607 let sl = &slots[i];
3608 ids.extend_from_slice(&s.ids[sl.off..sl.off + t]);
3609 tgt.extend_from_slice(&s.ids[sl.off + 1..sl.off + t + 1]);
3610 w.extend_from_slice(&sl.w);
3611 h0.extend_from_slice(&pre[i]);
3612 ht.extend_from_slice(&th[i]);
3613 }
3614 (ids, tgt, w, h0, ht)
3615 };
3616 let student_hidden = |ids: &[u32], h0: Vec<f32>, b: usize, ts: &TrainState, keep: Option<&mut Vec<Vec<f32>>>| {
3617 if from > 0 {
3618 fm.forward_span(SpanIn::Hidden(h0), from, fm.nl, b, t, Some(ts), true, keep)
3619 } else {
3620 fm.forward_span(SpanIn::Ids(ids), 0, fm.nl, b, t, Some(ts), true, keep)
3621 }
3622 };
3623 let val_ppl = |ts: Option<&TrainState>| -> f64 {
3625 let (mut nll, mut ws) = (0f64, 0f64);
3626 let idx: Vec<usize> = (0..va_slots.len()).collect();
3627 for chunk in idx.chunks(hp.bs.max(1)) {
3628 let (ids, tgt, w, h0, ht) = gather(&va, &va_slots, &va_pre, &va_th, chunk);
3629 let h = match ts {
3630 Some(ts) => student_hidden(&ids, h0, chunk.len(), ts, None),
3631 None => ht,
3632 };
3633 let (a, b) = fm.nll_on(&h, &tgt, &w);
3634 nll += a;
3635 ws += b;
3636 }
3637 if ws == 0.0 { f64::NAN } else { (nll / ws).exp() }
3638 };
3639 let teacher_ppl = val_ppl(None);
3640 let ppl_start = val_ppl(Some(&ts));
3641 tracing::info!("fcd: val ppl (scored tokens) teacher {teacher_ppl:.3} | student before training {ppl_start:.3}");
3642
3643 let mut gate_state: Option<(Pipeline, Vec<f64>)> = match gate {
3644 Some(g) if !g.prompts.is_empty() => {
3645 let greedy = SamplerConfig {
3646 temperature: 0.0,
3647 top_p: 1.0,
3648 presence_penalty: 0.0,
3649 top_k: 0,
3650 repetition_penalty: 1.0,
3651 min_p: 0.0,
3652 seed: Some(0),
3653 suppress_tokens: Vec::new(),
3654 penalty_window: 0,
3655 };
3656 let mut pipe = Pipeline::from_model(model, greedy).map_err(|e| format!("gen-gate pipeline: {e}"))?;
3657 if !hp.polish_only {
3658 pipe.set_o1(Some(o1.clone()));
3659 }
3660 apply_trainables(&mut pipe, &fm, &ts);
3661 let base = gate_gen_scores(&mut pipe, g)?;
3662 tracing::info!("fcd gen-gate baseline loop-scores: {base:?}");
3663 Some((pipe, base))
3664 }
3665 _ => None,
3666 };
3667 let init_snapshot: Vec<Vec<f32>> = ts.data.clone();
3668 let mut gate_evals: Vec<(usize, f64, Vec<f64>, bool)> = Vec::new();
3669 let mut best: (f64, Option<Vec<Vec<f32>>>, usize) = (ppl_start, None, 0);
3671 let cum: Vec<f64> = tr_slots
3672 .iter()
3673 .scan(0f64, |a, s| {
3674 *a += s.wsum;
3675 Some(*a)
3676 })
3677 .collect();
3678 let total = *cum.last().unwrap();
3679 let mut rng = SplitMix64::new(hp.seed);
3680 let mut losses: Vec<(f64, f64)> = Vec::with_capacity(hp.steps);
3681 let t0 = std::time::Instant::now();
3682 for st in 1..=hp.steps {
3683 let pick: Vec<usize> = (0..hp.bs)
3684 .map(|_| {
3685 let u = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64 * total;
3686 cum.partition_point(|&c| c <= u).min(tr_slots.len() - 1)
3687 })
3688 .collect();
3689 for &i in &pick {
3690 if tr_th[i].is_none() {
3691 let (mut p, mut h) = fill(&tr, &tr_slots[i..i + 1], "train", &mut t_rt, &mut s_rt)?;
3692 tr_pre[i] = p.pop();
3693 tr_th[i] = h.pop();
3694 }
3695 }
3696 let pre_v: Vec<Vec<f32>> = pick.iter().map(|&i| tr_pre[i].clone().unwrap_or_default()).collect();
3697 let th_v: Vec<Vec<f32>> = pick.iter().map(|&i| tr_th[i].clone().unwrap_or_default()).collect();
3698 let local: Vec<usize> = (0..pick.len()).collect();
3699 let picked: Vec<Slot> = pick.iter().map(|&i| Slot { off: tr_slots[i].off, w: tr_slots[i].w.clone(), wsum: tr_slots[i].wsum }).collect();
3700 let (ids, tgt, w, h0, ht) = gather(&tr, &picked, &pre_v, &th_v, &local);
3701 let mut keep: Vec<Vec<f32>> = Vec::with_capacity(fm.nl - from);
3702 let t_step = std::time::Instant::now();
3703 let hs = student_hidden(&ids, h0, hp.bs, &ts, Some(&mut keep));
3704 let t_fwd = t_step.elapsed().as_secs_f64();
3705 let (ce, kl, dhs) = fm.loss_and_dhidden(&hs, &ht, &tgt, hp.kl_w, Some(&w));
3706 let t_loss = t_step.elapsed().as_secs_f64() - t_fwd;
3707 ts.zero_grad();
3708 fm.backward_from(from, hp.bs, t, &keep, dhs, &mut ts);
3709 let t_bwd = t_step.elapsed().as_secs_f64() - t_fwd - t_loss;
3710 let gn = ts.clip_and_step(hp.lr);
3711 if std::env::var("CMF_BAKE_PROF").is_ok() && st % 10 == 0 {
3712 tracing::info!(
3713 "fcd prof step {st}: forward {t_fwd:.2}s (attn {:.2} ffn {:.2}) · loss head {t_loss:.2}s · backward {t_bwd:.2}s · adam {:.2}s",
3714 prof::take(&prof::ATTN_FWD),
3715 prof::take(&prof::FFN_FWD),
3716 t_step.elapsed().as_secs_f64() - t_fwd - t_loss - t_bwd
3717 );
3718 let _ = prof::take(&prof::BWD);
3719 }
3720 losses.push((ce, kl));
3721 tracing::info!(
3722 "fcd step {st}/{}: ce {ce:.3} kl {kl:.3} |g| {gn:.3} ({:.1}s/step)",
3723 hp.steps,
3724 t0.elapsed().as_secs_f64() / st as f64
3725 );
3726 if hp.eval_every > 0 && (st % hp.eval_every == 0 || st == hp.steps) {
3727 let p = val_ppl(Some(&ts));
3728 let pass = match (&mut gate_state, gate) {
3729 (Some((pipe, base)), Some(g)) => {
3730 apply_trainables(pipe, &fm, &ts);
3731 let scores = gate_gen_scores(pipe, g)?;
3732 let pass = gate_pass(&scores, base, g.threshold, g.baseline_slack);
3733 gate_evals.push((st, p, scores, pass));
3734 pass
3735 }
3736 _ => true,
3737 };
3738 let tag = if pass && p < best.0 {
3739 best = (p, Some(ts.data.clone()), st);
3740 " *best*"
3741 } else {
3742 ""
3743 };
3744 tracing::info!("fcd eval step {st}: val ppl {p:.3}{}{tag}", if pass { "" } else { " (gen-gate FAIL)" });
3745 }
3746 }
3747 drop((t_rt, s_rt));
3748 tracing::info!(
3749 "fcd: {} of {} train windows were drawn and cached",
3750 tr_th.iter().filter(|x| x.is_some()).count(),
3751 tr_slots.len()
3752 );
3753 let chosen = match best.1.take() {
3754 Some(snap) => {
3755 ts.data = snap;
3756 tracing::info!("fcd: restored best checkpoint from step {} (val ppl {:.3})", best.2, best.0);
3757 Some(best.2)
3758 }
3759 None => {
3760 ts.data = init_snapshot;
3761 tracing::info!("fcd: no checkpoint beat the untrained state on val — identity written");
3762 None
3763 }
3764 };
3765 let ppl_final = val_ppl(Some(&ts));
3766 let report = FcdReport {
3767 converted: converted.clone(),
3768 teacher_ppl,
3769 ppl_start,
3770 ppl_best: best.0.min(ppl_final),
3771 best_step: best.2,
3772 ppl_final,
3773 steps_run: hp.steps,
3774 sec_per_step: t0.elapsed().as_secs_f64() / hp.steps.max(1) as f64,
3775 losses,
3776 gate: gate_state.map(|(_, base)| GateReport { baseline: base, evals: gate_evals, chosen }),
3777 };
3778 save_polished(model, out, &fm, &ts, o1, hp, &report)?;
3779 Ok(report)
3780}
3781
3782fn apply_trainables(pipe: &mut Pipeline, fm: &FcdModel, ts: &TrainState) {
3786 let hidden = fm.hidden;
3787 for (slot, &li) in ts.layers.iter().enumerate() {
3788 let b = slot * PARAMS_PER_LAYER;
3789 let inter = fm.layers[li].inter;
3790 let lw = &mut pipe.weights.layers[li];
3791 lw.input_norm = ts.data[b].clone();
3792 lw.post_norm = ts.data[b + 1].clone();
3793 lw.ffn = FfnKind::Dense(DenseFfn {
3794 gate_proj: QTensor::from_f32(ts.data[b + 2].clone(), inter, hidden),
3795 up_proj: QTensor::from_f32(ts.data[b + 3].clone(), inter, hidden),
3796 down_proj: QTensor::from_f32(ts.data[b + 4].clone(), hidden, inter),
3797 act: crate::pipeline::Act::Silu,
3798 down_t: None,
3799 segs: Vec::new(),
3800 });
3801 }
3802}
3803
3804fn gate_gen_scores(pipe: &mut Pipeline, g: &GenGateCfg) -> Result<Vec<f64>, String> {
3806 g.prompts
3807 .iter()
3808 .map(|p| {
3809 pipe.generate_from_ids(p, g.gen_tokens, None, None)
3810 .map(|r| loop_score(&r.token_ids))
3811 })
3812 .collect()
3813}
3814
3815fn save_polished(
3820 model: &CmfModel,
3821 out: &std::path::Path,
3822 fm: &FcdModel,
3823 ts: &TrainState,
3824 o1: &O1Cfg,
3825 hp: &FcdHyper,
3826 report: &FcdReport,
3827) -> Result<(), String> {
3828 use cortiq_core::format::TensorSpec;
3829 let mut replace: std::collections::HashMap<String, (usize, usize)> =
3830 std::collections::HashMap::new(); for (s, &li) in ts.layers.iter().enumerate() {
3832 let p = format!("model.layers.{li}.");
3833 for (k, suffix) in [
3834 (0usize, "input_layernorm.weight"),
3835 (1, "post_attention_layernorm.weight"),
3836 (2, "mlp.gate_proj.weight"),
3837 (3, "mlp.up_proj.weight"),
3838 (4, "mlp.down_proj.weight"),
3839 ] {
3840 replace.insert(format!("{p}{suffix}"), (s, k));
3841 }
3842 }
3843 let mut specs = Vec::with_capacity(model.tensors.len());
3844 for t in &model.tensors {
3845 if let Some(&(s, k)) = replace.get(&t.name) {
3846 let data = &ts.data[s * PARAMS_PER_LAYER + k];
3847 let mut bytes = Vec::with_capacity(data.len() * 4);
3848 for v in data {
3849 bytes.extend_from_slice(&v.to_le_bytes());
3850 }
3851 specs.push(TensorSpec {
3852 name: t.name.clone(),
3853 dtype: TensorDtype::F32,
3854 shape: t.shape.clone(),
3855 data: bytes,
3856 });
3857 } else {
3858 specs.push(TensorSpec {
3859 name: t.name.clone(),
3860 dtype: t.dtype,
3861 shape: t.shape.clone(),
3862 data: model.entry_bytes(t).to_vec(),
3863 });
3864 }
3865 }
3866
3867 let mut header = model.header.clone();
3868 let mut prov = match header.provenance.take() {
3869 Some(serde_json::Value::Object(m)) => m,
3870 _ => serde_json::Map::new(),
3871 };
3872 let layers_json = match &o1.layers {
3873 O1Layers::All => serde_json::json!("all"),
3874 O1Layers::Deep(n) => serde_json::json!(format!("deep{n}")),
3875 O1Layers::List(v) => serde_json::json!(v),
3876 };
3877 if !hp.polish_only {
3881 prov.insert(
3882 "o1_attn".into(),
3883 serde_json::json!({
3884 "layers": layers_json, "m": o1.m, "w": o1.w, "sink": o1.sink
3885 }),
3886 );
3887 }
3888 prov.insert(
3889 "fcd".into(),
3890 serde_json::json!({
3891 "steps": hp.steps, "lr": hp.lr, "kl_w": hp.kl_w,
3892 "bs": hp.bs, "seq": hp.seq,
3893 "teacher_ppl": report.teacher_ppl,
3894 "ppl_start": report.ppl_start,
3895 "ppl_final": report.ppl_final,
3896 "best_step": report.best_step,
3897 "converted_layers": report.converted,
3898 "polish_only": hp.polish_only,
3899 }),
3900 );
3901 header.provenance = Some(serde_json::Value::Object(prov));
3902 let _ = fm; let masks = if model.masks.masks.is_empty() {
3905 None
3906 } else {
3907 Some(&model.masks)
3908 };
3909 CmfModel::write(out, &header, &specs, masks, model.vocab.as_deref())
3910 .map_err(|e| format!("writing polished cmf: {e}"))
3911}
3912
3913#[cfg(test)]
3914mod tests {
3915 use super::*;
3916
3917 #[test]
3921 fn polish_only_keeps_recurrent_layers_trainable() {
3922 let is_full: Vec<bool> = (0..8).map(|i| i % 4 == 3).collect();
3925
3926 let mut converting = vec![true; 8];
3927 apply_layer_eligibility(&mut converting, &is_full, false);
3928 assert_eq!(
3929 converting.iter().filter(|&&b| b).count(),
3930 2,
3931 "an O(1) conversion may only take the full-attention layers"
3932 );
3933
3934 let mut polishing = vec![true; 8];
3935 apply_layer_eligibility(&mut polishing, &is_full, true);
3936 assert_eq!(
3937 polishing.iter().filter(|&&b| b).count(),
3938 8,
3939 "a polish trains FFN and norms, which every layer has"
3940 );
3941
3942 let mut none = vec![false; 8];
3944 apply_layer_eligibility(&mut none, &is_full, true);
3945 assert!(none.iter().all(|&b| !b));
3946 }
3947
3948 #[test]
3952 fn the_container_ceiling_wins_over_the_host_figure() {
3953 let host = 944 * 1_000_000_000u64;
3954 let cgroup = 117 * 1_000_000_000u64;
3955 assert_eq!(effective_total(host, Some(cgroup)), cgroup);
3956 assert_eq!(effective_total(host, None), host);
3958 assert_eq!(effective_total(host, Some(host * 2)), host);
3960 }
3961
3962 #[test]
3966 fn container_headroom_is_the_limit_minus_what_it_holds() {
3967 let host_free = 800 * 1_000_000_000u64;
3968 let limit = 117 * 1_000_000_000u64;
3969 let used = 79 * 1_000_000_000u64;
3970 assert_eq!(
3971 effective_available(host_free, Some(limit), Some(used)),
3972 38 * 1_000_000_000u64
3973 );
3974 assert_eq!(
3976 effective_available(host_free, Some(limit), Some(limit + 1)),
3977 0
3978 );
3979 assert_eq!(effective_available(host_free, Some(limit), None), limit);
3981 assert_eq!(effective_available(host_free, None, None), host_free);
3983 }
3984
3985 #[test]
3987 fn gate_selects_lowest_ppl_among_passing() {
3988 let base = vec![0.10, 0.00, 0.20];
3989 let evals = vec![
3990 (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]), ];
3995 let sel = select_checkpoint(&evals, &base, 0.35, 0.10);
3996 assert_eq!(sel, Some(2), "step 75 is the lowest-ppl PASSING checkpoint");
3997 }
3998
3999 #[test]
4002 fn gate_all_fail_is_identity() {
4003 let base = vec![0.0, 0.0, 0.0];
4004 let evals = vec![
4005 (25usize, 15.0, vec![0.50, 0.0, 0.0]),
4006 (50, 14.0, vec![0.0, 0.36, 0.0]),
4007 (75, 13.0, vec![0.0, 0.0, 0.11]), ];
4009 assert_eq!(select_checkpoint(&evals, &base, 0.35, 0.10), None);
4010 }
4011
4012 #[test]
4015 fn gate_boundaries_and_tie_break() {
4016 let base = vec![0.25];
4017 assert!(gate_pass(&[0.35], &base, 0.35, 0.10), "== threshold passes");
4018 assert!(
4019 gate_pass(&[0.35], &[0.25], 0.35, 0.10),
4020 "== base+slack passes"
4021 );
4022 assert!(!gate_pass(&[0.351], &base, 0.35, 0.10));
4023 assert!(!gate_pass(&[0.30], &[0.10], 0.35, 0.10), "0.30 > 0.10+0.10");
4024 let evals = vec![(25usize, 20.0, vec![0.10]), (50, 20.0, vec![0.10])];
4025 assert_eq!(
4026 select_checkpoint(&evals, &base, 0.35, 0.10),
4027 Some(0),
4028 "equal ppl → earliest checkpoint"
4029 );
4030 }
4031}
4032
4033pub fn run_polish(
4036 model: &Arc<CmfModel>,
4037 o1: &O1Cfg,
4038 hp: &FcdHyper,
4039 tr: &[u32],
4040 va: &[u32],
4041 out: &std::path::Path,
4042 gate: Option<&GenGateCfg>,
4043) -> Result<FcdReport, String> {
4044 run_polish_distilled(model, None, o1, hp, tr, va, out, gate)
4045}