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, Debug)]
81pub struct FcdReport {
82 pub converted: Vec<usize>,
83 pub teacher_ppl: f64,
85 pub ppl_start: f64,
87 pub ppl_best: f64,
89 pub best_step: usize,
90 pub ppl_final: f64,
92 pub steps_run: usize,
93 pub sec_per_step: f64,
94 pub losses: Vec<(f64, f64)>,
96 pub gate: Option<GateReport>,
98}
99
100#[derive(Clone, Debug)]
102pub struct GateReport {
103 pub baseline: Vec<f64>,
105 pub evals: Vec<(usize, f64, Vec<f64>, bool)>,
107 pub chosen: Option<usize>,
110}
111
112pub fn loop_score(ids: &[u32]) -> f64 {
117 if ids.len() < 5 {
118 return 0.0;
119 }
120 let grams: std::collections::HashSet<&[u32]> = ids.windows(4).collect();
121 1.0 - grams.len() as f64 / ids.windows(4).count() as f64
122}
123
124#[derive(Clone, Debug)]
128pub struct GenGateCfg {
129 pub prompts: Vec<Vec<u32>>,
132 pub gen_tokens: usize,
133 pub threshold: f64,
135 pub baseline_slack: f64,
137}
138
139impl GenGateCfg {
140 pub fn standard(va: &[u32]) -> Option<Self> {
143 let l = va.len().saturating_sub(500);
144 if l < 400 {
145 return None;
146 }
147 let prompts = [l / 10, l / 2, 8 * l / 10]
148 .iter()
149 .map(|&off| va[off..off + 400].to_vec())
150 .collect();
151 Some(Self {
152 prompts,
153 gen_tokens: 60,
154 threshold: 0.35,
155 baseline_slack: 0.10,
156 })
157 }
158}
159
160pub fn gate_pass(scores: &[f64], baseline: &[f64], threshold: f64, slack: f64) -> bool {
164 scores
165 .iter()
166 .zip(baseline)
167 .all(|(&s, &b)| s <= threshold && s <= b + slack)
168}
169
170pub fn select_checkpoint(
175 evals: &[(usize, f64, Vec<f64>)],
176 baseline: &[f64],
177 threshold: f64,
178 slack: f64,
179) -> Option<usize> {
180 let mut best: Option<usize> = None;
181 for (i, (_, ppl, scores)) in evals.iter().enumerate() {
182 if !gate_pass(scores, baseline, threshold, slack) {
183 continue;
184 }
185 if best.map(|b| *ppl < evals[b].1).unwrap_or(true) {
186 best = Some(i);
187 }
188 }
189 best
190}
191
192pub mod prof {
197 use std::sync::atomic::{AtomicU64, Ordering};
198 pub static ATTN_FWD: AtomicU64 = AtomicU64::new(0);
199 pub static FFN_FWD: AtomicU64 = AtomicU64::new(0);
200 pub static FFN_CHAIN: AtomicU64 = AtomicU64::new(0);
203 pub static FFN_ACT: AtomicU64 = AtomicU64::new(0);
204 pub static FFN_HOST: AtomicU64 = AtomicU64::new(0);
205 pub static FFN_PRE: AtomicU64 = AtomicU64::new(0);
208 pub static FFN_POST: AtomicU64 = AtomicU64::new(0);
209 pub static ATTN_CHAIN: AtomicU64 = AtomicU64::new(0);
211 pub static BWD: AtomicU64 = AtomicU64::new(0);
212 pub static GEMM: AtomicU64 = AtomicU64::new(0);
213 pub static GEMM_CALLS: AtomicU64 = AtomicU64::new(0);
214 #[inline]
215 pub fn add(c: &AtomicU64, t: std::time::Instant) {
216 c.fetch_add(t.elapsed().as_nanos() as u64, Ordering::Relaxed);
217 }
218 pub fn take(c: &AtomicU64) -> f64 {
220 c.swap(0, Ordering::Relaxed) as f64 / 1e9
221 }
222
223 use std::sync::Mutex;
224 pub static SHAPES: Mutex<Vec<((usize, usize, usize), (u64, u64))>> = Mutex::new(Vec::new());
228 pub fn gemm_shape(n: usize, k: usize, m: usize, t0: std::time::Instant) {
229 let ns = t0.elapsed().as_nanos() as u64;
230 let mut g = SHAPES.lock().unwrap();
231 match g.iter_mut().find(|(s, _)| *s == (n, k, m)) {
232 Some((_, (c, tt))) => {
233 *c += 1;
234 *tt += ns;
235 }
236 None => g.push(((n, k, m), (1, ns))),
237 }
238 }
239 pub fn shape_report(top: usize) -> String {
240 let mut g = SHAPES.lock().unwrap();
241 g.sort_by_key(|(_, (_, ns))| std::cmp::Reverse(*ns));
242 let out = g
243 .iter()
244 .take(top)
245 .map(|((n, k, m), (c, ns))| {
246 format!(
247 " [{n}x{k}x{m}] {c} calls, {:.1}s total, {:.1} ms/call",
248 *ns as f64 / 1e9,
249 *ns as f64 / 1e6 / *c as f64
250 )
251 })
252 .collect::<Vec<_>>()
253 .join("\n");
254 g.clear();
255 out
256 }
257}
258
259enum FcdAttn {
262 Full {
263 qrows: usize,
267 q_norm: Option<Vec<f32>>,
268 k_norm: Option<Vec<f32>>,
269 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
270 bias_cat: Option<Vec<f32>>,
274 output_gate: bool,
277 head_gate: Option<Vec<f32>>,
281 window: Option<usize>,
283 local_rope: bool,
286 },
287 Gdn {
290 wqkv: Vec<f32>,
291 wz: Vec<f32>,
292 wa: Vec<f32>,
293 wb: Vec<f32>,
294 conv: Vec<f32>,
295 a_log: Vec<f32>,
296 dt_bias: Vec<f32>,
297 norm: Vec<f32>,
298 wout: Vec<f32>,
299 },
300}
301
302pub(crate) struct FcdLayer {
303 attn: FcdAttn,
304 pub(crate) inter: usize,
305 pub(crate) iln: Vec<f32>,
308 pub(crate) pln: Vec<f32>,
309}
310
311#[derive(Clone, Copy)]
313struct GdnDims {
314 nv: usize,
315 nk: usize,
316 dk: usize,
317 dv: usize,
318 kk: usize,
319}
320
321impl GdnDims {
322 fn c_dim(&self) -> usize {
323 2 * self.nk * self.dk + self.nv * self.dv
324 }
325 fn vd(&self) -> usize {
326 self.nv * self.dv
327 }
328}
329
330pub struct FcdModel {
332 pub hidden: usize,
333 pub nh: usize,
334 pub nkv: usize,
335 pub hd: usize,
336 pub nl: usize,
337 pub vocab: usize,
338 pub(crate) eps: f64,
339 pub(crate) gemma: bool,
340 rotary_dim: usize,
341 inv_freq: Vec<f64>,
342 rotary_dim_local: Option<usize>,
345 inv_freq_local: Option<Vec<f64>>,
346 pub(crate) act: FfnAct,
349 pub(crate) attn_acts: bool,
353 pub(crate) embed: Vec<f32>,
355 pub(crate) lm_head: Option<Vec<f32>>,
356 pub(crate) final_norm: Vec<f32>,
357 pub(crate) layers: Vec<FcdLayer>,
358 pub(crate) src: std::sync::Arc<CmfModel>,
361 mats_cache: Vec<std::sync::Mutex<Option<std::sync::Arc<LayerMats>>>>,
366 rope_tab: std::sync::Mutex<Option<((usize, bool), std::sync::Arc<Vec<f32>>)>>,
370 mats_window: usize,
372 o1_flags: Vec<bool>,
374 nys: NysCfg,
375 gdn: Option<GdnDims>,
377 pub(crate) loops: usize,
384 pub(crate) loop_norm: bool,
385 pub(crate) pool: Option<Arc<Pool>>,
386 pub polish_only: bool,
389}
390
391fn deq(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
392 let e = model
393 .tensor(name)
394 .ok_or_else(|| format!("tensor '{name}' not found"))?;
395 let mut out = vec![0f32; e.n_elems()];
396 cortiq_core::quant::dequant_tensor(e, model.entry_bytes(e), &mut out)?;
397 Ok(out)
398}
399
400#[cfg(target_os = "linux")]
410fn cgroup_limit_bytes() -> Option<u64> {
411 let read =
412 |p: &str| -> Option<u64> { std::fs::read_to_string(p).ok()?.trim().parse::<u64>().ok() };
413 read("/sys/fs/cgroup/memory.max")
416 .or_else(|| read("/sys/fs/cgroup/memory/memory.limit_in_bytes"))
417 .filter(|&v| v < u64::MAX / 2)
418}
419
420pub(crate) fn apply_layer_eligibility(flags: &mut [bool], is_full: &[bool], polish_only: bool) {
429 if polish_only {
430 return;
431 }
432 for (li, f) in flags.iter_mut().enumerate() {
433 if *f && !is_full.get(li).copied().unwrap_or(false) {
434 *f = false;
435 }
436 }
437}
438
439pub(crate) fn effective_total(host: u64, cgroup_limit: Option<u64>) -> u64 {
442 match cgroup_limit {
443 Some(l) => host.min(l),
444 None => host,
445 }
446}
447
448pub(crate) fn effective_available(host: u64, limit: Option<u64>, used: Option<u64>) -> u64 {
451 match (limit, used) {
452 (Some(l), Some(u)) => host.min(l.saturating_sub(u)),
453 (Some(l), None) => host.min(l),
454 _ => host,
455 }
456}
457
458#[cfg(target_os = "linux")]
460fn cgroup_used_bytes() -> Option<u64> {
461 let read =
462 |p: &str| -> Option<u64> { std::fs::read_to_string(p).ok()?.trim().parse::<u64>().ok() };
463 read("/sys/fs/cgroup/memory.current")
464 .or_else(|| read("/sys/fs/cgroup/memory/memory.usage_in_bytes"))
465}
466
467fn physical_total_bytes() -> Option<u64> {
468 #[cfg(target_os = "macos")]
469 {
470 let out = std::process::Command::new("sysctl")
471 .args(["-n", "hw.memsize"])
472 .output()
473 .ok()?;
474 return String::from_utf8_lossy(&out.stdout).trim().parse().ok();
475 }
476 #[cfg(target_os = "linux")]
477 {
478 let mem = std::fs::read_to_string("/proc/meminfo").ok()?;
479 let kb: u64 = mem
480 .lines()
481 .find(|l| l.starts_with("MemTotal:"))?
482 .split_whitespace()
483 .nth(1)?
484 .parse()
485 .ok()?;
486 return Some(effective_total(kb * 1024, cgroup_limit_bytes()));
487 }
488 #[allow(unreachable_code)]
489 None
490}
491
492pub(crate) fn available_ram_bytes() -> Option<u64> {
495 #[cfg(target_os = "macos")]
496 {
497 let total: u64 = {
501 let out = std::process::Command::new("sysctl")
502 .args(["-n", "hw.memsize"])
503 .output()
504 .ok()?;
505 String::from_utf8_lossy(&out.stdout).trim().parse().ok()?
506 };
507 let out = std::process::Command::new("memory_pressure")
508 .arg("-Q")
509 .output()
510 .ok()?;
511 let text = String::from_utf8_lossy(&out.stdout);
512 let pct: u64 = text
513 .lines()
514 .find(|l| l.contains("free percentage"))?
515 .split(':')
516 .nth(1)?
517 .trim()
518 .trim_end_matches('%')
519 .parse()
520 .ok()?;
521 return Some(total / 100 * pct);
522 }
523 #[cfg(target_os = "linux")]
524 {
525 let mem = std::fs::read_to_string("/proc/meminfo").ok()?;
526 let kb: u64 = mem
527 .lines()
528 .find(|l| l.starts_with("MemAvailable:"))?
529 .split_whitespace()
530 .nth(1)?
531 .parse()
532 .ok()?;
533 return Some(effective_available(
536 kb * 1024,
537 cgroup_limit_bytes(),
538 cgroup_used_bytes(),
539 ));
540 }
541 #[allow(unreachable_code)]
542 None
543}
544
545pub(crate) fn deq_pub(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
548 deq(model, name)
549}
550
551pub(crate) struct LayerMats {
556 pub(crate) wqkv: Vec<f32>,
557 pub(crate) wo: Vec<f32>,
558 pub(crate) gu: Vec<f32>,
559 pub(crate) down: Vec<f32>,
560}
561
562impl FcdModel {
563 pub(crate) fn mats(&self, li: usize) -> Result<std::sync::Arc<LayerMats>, String> {
568 {
569 let slot = self.mats_cache[li].lock().unwrap();
570 if let Some(m) = slot.as_ref() {
571 return Ok(m.clone());
572 }
573 }
574 let p = format!("model.layers.{li}.");
575 let d = |name: String| -> Result<Vec<f32>, String> { deq(&self.src, &name) };
576 let built = if matches!(self.layers[li].attn, FcdAttn::Full { .. }) {
577 let wq = d(format!("{p}self_attn.q_proj.weight"))?;
578 let wk = d(format!("{p}self_attn.k_proj.weight"))?;
579 let wv = d(format!("{p}self_attn.v_proj.weight"))?;
580 let mut wqkv = Vec::with_capacity(wq.len() + wk.len() + wv.len());
581 wqkv.extend_from_slice(&wq);
582 wqkv.extend_from_slice(&wk);
583 wqkv.extend_from_slice(&wv);
584 let gate = d(format!("{p}mlp.gate_proj.weight"))?;
585 let up = d(format!("{p}mlp.up_proj.weight"))?;
586 let mut gu = Vec::with_capacity(gate.len() + up.len());
587 gu.extend_from_slice(&gate);
588 gu.extend_from_slice(&up);
589 LayerMats {
590 wqkv,
591 wo: d(format!("{p}self_attn.o_proj.weight"))?,
592 gu,
593 down: d(format!("{p}mlp.down_proj.weight"))?,
594 }
595 } else {
596 let gate = d(format!("{p}mlp.gate_proj.weight"))?;
599 let up = d(format!("{p}mlp.up_proj.weight"))?;
600 let mut gu = Vec::with_capacity(gate.len() + up.len());
601 gu.extend_from_slice(&gate);
602 gu.extend_from_slice(&up);
603 LayerMats {
604 wqkv: Vec::new(),
605 wo: Vec::new(),
606 gu,
607 down: d(format!("{p}mlp.down_proj.weight"))?,
608 }
609 };
610 let arc = std::sync::Arc::new(built);
611 *self.mats_cache[li].lock().unwrap() = Some(arc.clone());
612 if self.mats_window > 0 {
615 let w = self.mats_window;
616 for (j, slot) in self.mats_cache.iter().enumerate() {
617 let dist = li.abs_diff(j).min(self.layers.len() - li.abs_diff(j));
618 if dist > w {
619 *slot.lock().unwrap() = None;
620 }
621 }
622 }
623 Ok(arc)
624 }
625}
626
627impl FcdModel {
628 pub fn from_cmf(
631 model: &std::sync::Arc<CmfModel>,
632 o1: &O1Cfg,
633 polish_only: bool,
634 ) -> Result<Self, String> {
635 let arch = model.arch().clone();
636 let act = FfnAct::from_arch(&arch.hidden_act).ok_or_else(|| {
637 format!(
638 "fcd/skill-bake: hidden_act '{}' not supported yet (silu | gelu)",
639 arch.hidden_act
640 )
641 })?;
642 let has_linear = arch
643 .layer_types
644 .iter()
645 .any(|t| matches!(t, LayerType::LinearAttention));
646 let gdn = if has_linear {
647 let lc = arch
648 .linear_core
649 .as_ref()
650 .ok_or_else(|| "model has linear layers but no arch.linear_core".to_string())?;
651 if lc.kind != "gated_delta_net" {
652 return Err(format!(
653 "linear core '{}' has no FCD backward (only gated_delta_net)",
654 lc.kind
655 ));
656 }
657 Some(GdnDims {
658 nv: lc.num_heads,
659 nk: arch
660 .linear_num_key_heads
661 .ok_or("linear core needs arch.linear_num_key_heads")?,
662 dk: arch
663 .linear_key_head_dim
664 .ok_or("linear core needs arch.linear_key_head_dim")?,
665 dv: lc.value_head_dim,
666 kk: arch
667 .linear_conv_kernel_dim
668 .ok_or("linear core needs arch.linear_conv_kernel_dim")?,
669 })
670 } else {
671 None
672 };
673 let (nh, nkv, hd, h) = (
674 arch.num_attention_heads,
675 arch.num_kv_heads,
676 arch.head_dim,
677 arch.hidden_size,
678 );
679 let embed = deq(model, "model.embed_tokens.weight")?;
680 let lm_head = if model.tensor("lm_head.weight").is_some() {
681 Some(deq(model, "lm_head.weight")?)
682 } else if arch.tie_word_embeddings {
683 None
684 } else {
685 return Err("no lm_head.weight and tie_word_embeddings is false".into());
686 };
687 let final_norm = deq(model, "model.norm.weight")?;
688
689 let mut layers = Vec::with_capacity(arch.num_layers);
690 for li in 0..arch.num_layers {
691 let p = format!("model.layers.{li}.");
692 if model.tensor(&format!("{p}mlp.gate.weight")).is_some() {
693 return Err(format!(
694 "layer {li} is MoE — FCD polish supports dense FFN only"
695 ));
696 }
697 let attn = match arch.layer_types.get(li) {
698 Some(LayerType::LinearAttention) => {
699 let la = |n: &str| deq(model, &format!("{p}linear_attn.{n}"));
700 FcdAttn::Gdn {
701 wqkv: la("in_proj_qkv.weight")?,
702 wz: la("in_proj_z.weight")?,
703 wa: la("in_proj_a.weight")?,
704 wb: la("in_proj_b.weight")?,
705 conv: la("conv1d.weight")?,
706 a_log: la("A_log")?,
707 dt_bias: la("dt_bias")?,
708 norm: la("norm.weight")?,
709 wout: la("out_proj.weight")?,
710 }
711 }
712 _ => {
713 let wq = deq(model, &format!("{p}self_attn.q_proj.weight"))?;
714 let output_gate = wq.len() == 2 * nh * hd * h;
715 let opt = |n: &str| -> Option<Vec<f32>> {
716 model
717 .tensor(&format!("{p}self_attn.{n}"))
718 .and_then(|_| deq(model, &format!("{p}self_attn.{n}")).ok())
719 };
720 let bias = match (opt("q_proj.bias"), opt("k_proj.bias"), opt("v_proj.bias")) {
721 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
722 _ => None,
723 };
724 let qrows = wq.len() / h;
725 drop(wq);
726 let bias_cat = bias.as_ref().map(|(bq, bk, bv)| {
727 let mut v = Vec::with_capacity(bq.len() + bk.len() + bv.len());
728 v.extend_from_slice(bq);
729 v.extend_from_slice(bk);
730 v.extend_from_slice(bv);
731 v
732 });
733 let head_gate = match opt("g_proj.weight") {
737 None => None,
738 Some(g) if g.len() == nh * h => Some(g),
739 Some(g) => {
740 return Err(format!(
741 "layer {li}: self_attn.g_proj has {} values; only a per-head gate (nh·hidden = {}) is supported",
742 g.len(),
743 nh * h
744 ));
745 }
746 };
747 let sliding = matches!(arch.layer_types.get(li), Some(LayerType::SlidingAttention));
748 let window = if sliding {
749 Some(arch.sliding_window.ok_or_else(|| {
750 format!("layer {li} is SlidingAttention but arch.sliding_window is unset")
751 })?)
752 } else {
753 None
754 };
755 FcdAttn::Full {
756 qrows,
757 q_norm: opt("q_norm.weight"),
758 k_norm: opt("k_norm.weight"),
759 bias,
760 bias_cat,
761 output_gate,
762 head_gate,
763 window,
764 local_rope: sliding,
765 }
766 }
767 };
768 let gate = deq(model, &format!("{p}mlp.gate_proj.weight"))?;
769 let inter = gate.len() / h;
770 drop(gate);
771 layers.push(FcdLayer {
772 attn,
773 inter,
774 iln: deq(model, &format!("{p}input_layernorm.weight"))?,
775 pln: deq(model, &format!("{p}post_attention_layernorm.weight"))?,
776 });
777 }
778
779 let rotary_dim = ((hd as f32 * arch.partial_rotary_factor) as usize)
780 .max(2)
781 .min(hd);
782 let base = arch.rope_theta;
783 let inv_freq: Vec<f64> = (0..rotary_dim / 2)
784 .map(|i| 1.0 / base.powf(2.0 * i as f64 / rotary_dim as f64))
785 .collect();
786 let any_sliding = arch
790 .layer_types
791 .iter()
792 .any(|t| matches!(t, LayerType::SlidingAttention));
793 let rotary_dim_local = any_sliding.then(|| {
794 ((hd as f32 * arch.local_partial_rotary_factor.unwrap_or(arch.partial_rotary_factor))
795 as usize)
796 .max(2)
797 .min(hd)
798 });
799 let inv_freq_local = match (rotary_dim_local, arch.rope_local_base_freq) {
800 (Some(rd), Some(lb)) => Some(
801 (0..rd / 2)
802 .map(|i| 1.0 / lb.powf(2.0 * i as f64 / rd as f64))
803 .collect::<Vec<f64>>(),
804 ),
805 _ => None,
806 };
807 let loops = arch.num_loops.max(1);
808 let loop_norm = arch.loop_final_norm;
809 let mut flags = o1.layer_flags(arch.num_layers);
810 flags.resize(arch.num_layers, false);
811 {
820 let is_full: Vec<bool> = layers
821 .iter()
822 .map(|l| matches!(l.attn, FcdAttn::Full { .. }))
823 .collect();
824 apply_layer_eligibility(&mut flags, &is_full, polish_only);
825 }
826 let per_layer: u64 = layers
830 .first()
831 .map(|_| {
832 let qrows = match layers[0].attn {
833 FcdAttn::Full { qrows, .. } => qrows,
834 _ => 0,
835 };
836 ((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
840 })
841 .unwrap_or(0);
842 let mats_window = if let Ok(v) = std::env::var("CMF_BAKE_MATS_LAYERS") {
843 v.parse().unwrap_or(0)
844 } else {
845 let avail = available_ram_bytes().unwrap_or(u64::MAX);
846 let cap = physical_total_bytes()
847 .map(|t| t / 5 * 3)
848 .unwrap_or(u64::MAX)
849 .min(avail.saturating_sub(avail / 10));
850 let all = per_layer.saturating_mul(arch.num_layers as u64);
851 if all + 4 * 1024 * 1024 * 1024 <= cap {
852 0 } else {
854 let w = (cap.saturating_sub(3 * 1024 * 1024 * 1024) / per_layer.max(1))
855 .clamp(2, arch.num_layers as u64) as usize;
856 tracing::info!(
857 "fcd replica: streaming {w} of {} layers (~{:.1} GB resident of ~{:.1} GB total)",
858 arch.num_layers,
859 (w as u64 * per_layer) as f64 / 1e9,
860 all as f64 / 1e9,
861 );
862 w
863 }
864 };
865 let mats_cache = (0..arch.num_layers)
866 .map(|_| std::sync::Mutex::new(None))
867 .collect();
868 Ok(Self {
869 src: model.clone(),
870 mats_cache,
871 rope_tab: std::sync::Mutex::new(None),
872 mats_window,
873 hidden: h,
874 nh,
875 nkv,
876 hd,
877 nl: arch.num_layers,
878 vocab: arch.vocab_size.min(embed.len() / h),
879 eps: arch.rms_norm_eps,
880 gemma: matches!(arch.norm_style, NormStyle::Gemma),
881 rotary_dim,
882 inv_freq,
883 rotary_dim_local,
884 inv_freq_local,
885 act,
886 attn_acts: true,
887 embed,
888 lm_head,
889 final_norm,
890 layers,
891 o1_flags: flags,
892 nys: NysCfg {
895 m: o1.m,
896 w: o1.w,
897 sink: o1.sink,
898 prefill: None,
899 },
900 gdn,
901 loops,
902 loop_norm,
903 pool: Pool::from_env(),
904 polish_only: false,
905 })
906 }
907
908 pub fn converted(&self) -> Vec<usize> {
910 (0..self.nl).filter(|&i| self.o1_flags[i]).collect()
911 }
912
913 fn head_weight(&self) -> &[f32] {
914 self.lm_head.as_deref().unwrap_or(&self.embed)
915 }
916}
917
918const PARAMS_PER_LAYER: usize = 5; pub struct TrainState {
925 pub layers: Vec<usize>,
926 pub data: Vec<Vec<f32>>,
928 grad: Vec<Vec<f32>>,
929 m1: Vec<Vec<f32>>,
930 m2: Vec<Vec<f32>>,
931 step_t: u64,
932}
933
934impl TrainState {
935 pub fn new(fm: &FcdModel) -> Self {
936 let layers = fm.converted();
937 let mut data = Vec::with_capacity(layers.len() * PARAMS_PER_LAYER);
938 for &li in &layers {
939 let l = &fm.layers[li];
940 let p = format!("model.layers.{li}.");
941 data.push(l.iln.clone());
942 data.push(l.pln.clone());
943 data.push(deq(&fm.src, &format!("{p}mlp.gate_proj.weight")).expect("gate"));
946 data.push(deq(&fm.src, &format!("{p}mlp.up_proj.weight")).expect("up"));
947 data.push(deq(&fm.src, &format!("{p}mlp.down_proj.weight")).expect("down"));
948 }
949 let zeros: Vec<Vec<f32>> = data.iter().map(|d| vec![0f32; d.len()]).collect();
950 Self {
951 layers,
952 grad: zeros.clone(),
953 m1: zeros.clone(),
954 m2: zeros,
955 data,
956 step_t: 0,
957 }
958 }
959
960 fn slot(&self, li: usize) -> Option<usize> {
961 self.layers.iter().position(|&x| x == li)
962 }
963
964 #[doc(hidden)]
966 pub fn grads(&self) -> &[Vec<f32>] {
967 &self.grad
968 }
969
970 fn zero_grad(&mut self) {
971 for g in &mut self.grad {
972 for v in g.iter_mut() {
973 *v = 0.0;
974 }
975 }
976 }
977
978 fn clip_and_step(&mut self, lr: f64) -> f64 {
981 let mut sq = 0f64;
982 for g in &self.grad {
983 for &v in g {
984 sq += (v as f64) * (v as f64);
985 }
986 }
987 let gn = sq.sqrt();
988 let scale = if gn > 1.0 { 1.0 / (gn + 1e-6) } else { 1.0 };
989 self.step_t += 1;
990 let bc1 = 1.0 - ADAM_B1.powi(self.step_t as i32);
991 let bc2 = 1.0 - ADAM_B2.powi(self.step_t as i32);
992 for p in 0..self.data.len() {
993 let (d, g, m, v) = (
994 &mut self.data[p],
995 &self.grad[p],
996 &mut self.m1[p],
997 &mut self.m2[p],
998 );
999 for i in 0..d.len() {
1000 let gi = g[i] as f64 * scale;
1001 let mi = ADAM_B1 * m[i] as f64 + (1.0 - ADAM_B1) * gi;
1002 let vi = ADAM_B2 * v[i] as f64 + (1.0 - ADAM_B2) * gi * gi;
1003 m[i] = mi as f32;
1004 v[i] = vi as f32;
1005 let upd = (mi / bc1) / ((vi / bc2).sqrt() + ADAM_EPS) + ADAM_WD * d[i] as f64;
1006 d[i] = (d[i] as f64 - lr * upd) as f32;
1007 }
1008 }
1009 gn
1010 }
1011}
1012
1013#[derive(Clone, Copy)]
1016pub(crate) struct LnFfn<'a> {
1017 pub(crate) iln: &'a [f32],
1018 pub(crate) pln: &'a [f32],
1019 pub(crate) gate: &'a [f32],
1020 pub(crate) up: &'a [f32],
1021 pub(crate) down: &'a [f32],
1022 pub(crate) gu: Option<&'a [f32]>,
1026}
1027
1028fn ln_ffn<'a>(
1029 fm: &'a FcdModel,
1030 ts: Option<&'a TrainState>,
1031 li: usize,
1032 mats: &'a LayerMats,
1033) -> LnFfn<'a> {
1034 if let Some(t) = ts {
1035 if let Some(s) = t.slot(li) {
1036 let b = s * PARAMS_PER_LAYER;
1037 return LnFfn {
1038 iln: &t.data[b],
1039 pln: &t.data[b + 1],
1040 gate: &t.data[b + 2],
1041 up: &t.data[b + 3],
1042 down: &t.data[b + 4],
1043 gu: None,
1044 };
1045 }
1046 }
1047 let l = &fm.layers[li];
1048 LnFfn {
1049 iln: &l.iln,
1050 pln: &l.pln,
1051 gate: &[],
1054 up: &[],
1055 down: &mats.down,
1056 gu: Some(&mats.gu),
1057 }
1058}
1059
1060enum AttnActs {
1064 Full {
1065 qpre: Vec<f32>,
1066 kpre: Vec<f32>,
1067 vproj: Vec<f32>,
1068 qrot: Vec<f32>,
1069 krot: Vec<f32>,
1070 qinv: Vec<f32>,
1071 kinv: Vec<f32>,
1072 ao: Vec<f32>,
1075 gate_pre: Vec<f32>,
1077 head_gain: Vec<f32>,
1081 },
1082 Gdn {
1085 qkv: Vec<f32>,
1086 z: Vec<f32>,
1087 a: Vec<f32>,
1088 b: Vec<f32>,
1089 },
1090}
1091
1092#[derive(Clone, Copy)]
1095pub(crate) struct AttnW<'a> {
1096 pub(crate) wqkv: &'a [f32],
1097 pub(crate) wo: &'a [f32],
1098 pub(crate) head_gate: Option<&'a [f32]>,
1099}
1100
1101pub(crate) struct LayerActs {
1102 pub(crate) inv1: Vec<f32>,
1103 attn: AttnActs,
1104 pub(crate) h1: Vec<f32>,
1105 pub(crate) n2: Vec<f32>,
1106 pub(crate) inv2: Vec<f32>,
1107 pub(crate) gpre: Vec<f32>,
1108 pub(crate) upre: Vec<f32>,
1109 pub(crate) act: Vec<f32>,
1110}
1111
1112impl LayerActs {
1113 pub(crate) fn attn_full(&self) -> Option<(&[f32], &[f32], &[f32], &[f32], &[f32])> {
1117 match &self.attn {
1118 AttnActs::Full {
1119 qrot,
1120 krot,
1121 vproj,
1122 ao,
1123 head_gain,
1124 ..
1125 } if !qrot.is_empty() => Some((qrot, krot, vproj, ao, head_gain)),
1126 _ => None,
1127 }
1128 }
1129}
1130
1131struct SendMut<T>(*mut T);
1133unsafe impl<T> Send for SendMut<T> {}
1134unsafe impl<T> Sync for SendMut<T> {}
1135impl<T> SendMut<T> {
1136 #[inline]
1137 unsafe fn at(&self, i: usize) -> *mut T {
1138 unsafe { self.0.add(i) }
1139 }
1140}
1141
1142
1143pub(crate) fn pooled_rows(n: usize, pool: Option<&Pool>, f: &(dyn Fn(usize, usize) + Sync)) {
1147 match pool {
1148 Some(p) if n >= 64 => p.run(&|w, nw| {
1149 let lo = n * w / nw;
1150 let hi = n * (w + 1) / nw;
1151 if hi > lo {
1152 f(lo, hi);
1153 }
1154 }),
1155 _ => f(0, n),
1156 }
1157}
1158
1159pub(crate) fn rmsnorm_fwd_pooled(
1161 x: &[f32],
1162 w: &[f32],
1163 eps: f64,
1164 gemma: bool,
1165 y: &mut [f32],
1166 inv: &mut [f32],
1167 pool: Option<&Pool>,
1168) {
1169 let d = w.len();
1170 let n = x.len() / d;
1171 let yp = SendMut(y.as_mut_ptr());
1172 let ip = SendMut(inv.as_mut_ptr());
1173 pooled_rows(n, pool, &|lo, hi| {
1174 let (yr, ir) = unsafe {
1176 (
1177 std::slice::from_raw_parts_mut(yp.at(lo * d), (hi - lo) * d),
1178 std::slice::from_raw_parts_mut(ip.at(lo), hi - lo),
1179 )
1180 };
1181 ops::rmsnorm_fwd(&x[lo * d..hi * d], w, eps, gemma, yr, ir);
1182 });
1183}
1184
1185pub(crate) fn rmsnorm_bwd_pooled(
1187 x: &[f32],
1188 w: &[f32],
1189 inv: &[f32],
1190 dy: &[f32],
1191 gemma: bool,
1192 dx: &mut [f32],
1193 pool: Option<&Pool>,
1194) {
1195 let d = w.len();
1196 let n = x.len() / d;
1197 let dp = SendMut(dx.as_mut_ptr());
1198 pooled_rows(n, pool, &|lo, hi| {
1199 let dxr = unsafe { std::slice::from_raw_parts_mut(dp.at(lo * d), (hi - lo) * d) };
1201 ops::rmsnorm_bwd(&x[lo * d..hi * d], w, &inv[lo..hi], &dy[lo * d..hi * d], gemma, dxr, None);
1202 });
1203}
1204
1205pub(crate) fn add_into_pooled(a: &mut [f32], b: &[f32], pool: Option<&Pool>) {
1207 let n = a.len().min(b.len());
1208 let ap = SendMut(a.as_mut_ptr());
1209 const CHUNK: usize = 4096;
1210 let chunks = n.div_ceil(CHUNK);
1211 pooled_rows(chunks, pool, &|lo, hi| {
1212 let (s, e) = (lo * CHUNK, (hi * CHUNK).min(n));
1213 let ar = unsafe { std::slice::from_raw_parts_mut(ap.at(s), e - s) };
1215 for (x, &y) in ar.iter_mut().zip(&b[s..e]) {
1216 *x += y;
1217 }
1218 });
1219}
1220
1221pub(crate) fn act_mul_rows(
1225 f: FfnAct,
1226 gpre: &[f32],
1227 upre: &[f32],
1228 act: &mut [f32],
1229 n: usize,
1230 inter: usize,
1231 pool: Option<&Pool>,
1232) {
1233 let row = |r: usize, out: &mut [f32]| {
1234 let g = &gpre[r * inter..(r + 1) * inter];
1235 let u = &upre[r * inter..(r + 1) * inter];
1236 for ((o, &gv), &uv) in out.iter_mut().zip(g).zip(u) {
1237 *o = f.fwd(gv) * uv;
1238 }
1239 };
1240 match pool {
1241 Some(p) if n > 1 => {
1242 let ap = SendMut(act.as_mut_ptr());
1243 p.run(&|w, nw| {
1244 for r in (w..n).step_by(nw) {
1245 let out = unsafe { std::slice::from_raw_parts_mut(ap.at(r * inter), inter) };
1247 row(r, out);
1248 }
1249 });
1250 }
1251 _ => {
1252 for r in 0..n {
1253 row(r, &mut act[r * inter..(r + 1) * inter]);
1254 }
1255 }
1256 }
1257}
1258
1259#[allow(clippy::too_many_arguments)]
1263pub(crate) fn act_bwd_rows(
1264 f: FfnAct,
1265 gpre: &[f32],
1266 upre: &[f32],
1267 dact: &[f32],
1268 scale: Option<&[f32]>,
1269 dg: &mut [f32],
1270 du: &mut [f32],
1271 n: usize,
1272 inter: usize,
1273 pool: Option<&Pool>,
1274) {
1275 let row = |r: usize, dg: &mut [f32], du: &mut [f32]| {
1276 let base = r * inter;
1277 for j in 0..inter {
1278 let i = base + j;
1279 let da = dact[i] * scale.map(|s| s[j]).unwrap_or(1.0);
1280 dg[j] = da * upre[i] * f.bwd(gpre[i]);
1281 du[j] = da * f.fwd(gpre[i]);
1282 }
1283 };
1284 match pool {
1285 Some(p) if n > 1 => {
1286 let gp = SendMut(dg.as_mut_ptr());
1287 let up = SendMut(du.as_mut_ptr());
1288 p.run(&|w, nw| {
1289 for r in (w..n).step_by(nw) {
1290 let (dgr, dur) = unsafe {
1292 (
1293 std::slice::from_raw_parts_mut(gp.at(r * inter), inter),
1294 std::slice::from_raw_parts_mut(up.at(r * inter), inter),
1295 )
1296 };
1297 row(r, dgr, dur);
1298 }
1299 });
1300 }
1301 _ => {
1302 for r in 0..n {
1303 let (dgr, dur) = (&mut dg[r * inter..(r + 1) * inter], &mut du[r * inter..(r + 1) * inter]);
1304 row(r, dgr, dur);
1305 }
1306 }
1307 }
1308}
1309
1310impl FcdModel {
1311 pub(crate) fn attn_geometry(&self, li: usize) -> Option<(Option<usize>, bool)> {
1316 match &self.layers[li].attn {
1317 FcdAttn::Full {
1318 window, local_rope, ..
1319 } => Some((*window, *local_rope)),
1320 _ => None,
1321 }
1322 }
1323
1324 pub(crate) fn head_gate_weights(&self, li: usize) -> Option<&[f32]> {
1326 match &self.layers[li].attn {
1327 FcdAttn::Full { head_gate, .. } => head_gate.as_deref(),
1328 _ => None,
1329 }
1330 }
1331
1332 pub(crate) fn layer_rope(&self, local: bool) -> (usize, &[f64]) {
1334 if local {
1335 (
1336 self.rotary_dim_local.unwrap_or(self.rotary_dim),
1337 self.inv_freq_local.as_deref().unwrap_or(&self.inv_freq),
1338 )
1339 } else {
1340 (self.rotary_dim, &self.inv_freq)
1341 }
1342 }
1343
1344 fn qk_norm_rope(
1345 &self,
1346 x: &mut [f32],
1347 norm: Option<&[f32]>,
1348 heads: usize,
1349 t: usize,
1350 local: bool,
1351 inv_out: &mut [f32],
1352 ) {
1353 let hd = self.hd;
1354 let (rotary_dim, inv_freq) = self.layer_rope(local);
1355 let n = x.len() / (heads * hd);
1356 for r in 0..n {
1357 let pos = r % t;
1358 for hh in 0..heads {
1359 let s = (r * heads + hh) * hd;
1360 let head = &mut x[s..s + hd];
1361 if let Some(w) = norm {
1362 let mut inv = [0f32; 1];
1363 let mut y = [0f32; 256];
1364 debug_assert!(hd <= 256);
1365 ops::rmsnorm_fwd(head, w, self.eps, self.gemma, &mut y[..hd], &mut inv);
1366 head.copy_from_slice(&y[..hd]);
1367 inv_out[r * heads + hh] = inv[0];
1368 }
1369 ops::rope_fwd(&mut head[..rotary_dim], pos, inv_freq);
1370 }
1371 }
1372 }
1373
1374 #[allow(clippy::too_many_arguments)]
1380 fn layer_forward(
1381 &self,
1382 li: usize,
1383 h_in: &[f32],
1384 b: usize,
1385 t: usize,
1386 wts: &LnFfn,
1387 nystrom: bool,
1388 want_acts: bool,
1389 ) -> (Vec<f32>, Option<LayerActs>) {
1390 self.layer_forward_scaled(li, h_in, b, t, wts, nystrom, want_acts, None, true, None, false)
1391 }
1392
1393 #[allow(clippy::too_many_arguments)]
1397 pub(crate) fn layer_forward_scaled(
1398 &self,
1399 li: usize,
1400 h_in: &[f32],
1401 b: usize,
1402 t: usize,
1403 wts: &LnFfn,
1404 nystrom: bool,
1405 want_acts: bool,
1406 ffn_scale: Option<&[f32]>,
1407 ffn_planes_to_host: bool,
1408 attn_w: Option<AttnW<'_>>,
1409 want_attn_acts: bool,
1410 ) -> (Vec<f32>, Option<LayerActs>) {
1411 let nystrom = nystrom && !self.polish_only;
1413 let hsz = self.hidden;
1414 let n = b * t;
1415 let l = &self.layers[li];
1416 let pool = self.pool.as_deref();
1417
1418 let mut n1 = vec![0f32; n * hsz];
1419 let mut inv1 = vec![0f32; n];
1420 rmsnorm_fwd_pooled(h_in, wts.iln, self.eps, self.gemma, &mut n1, &mut inv1, pool);
1421
1422 let mats = self.mats(li).expect("layer mats");
1423 let t_attn = std::time::Instant::now();
1424 let (attn_out, attn_acts) = match &l.attn {
1425 FcdAttn::Full { .. } => {
1426 self.full_attn_fwd(&l.attn, &mats, &n1, b, t, nystrom, attn_w, want_attn_acts)
1427 }
1428 FcdAttn::Gdn { .. } => self.gdn_attn_fwd(&l.attn, &n1, b, t),
1429 };
1430 prof::add(&prof::ATTN_FWD, t_attn);
1431 let t_ffn = std::time::Instant::now();
1432
1433 let mut h1 = h_in.to_vec();
1434 add_into_pooled(&mut h1, &attn_out, pool);
1435
1436 let mut n2 = vec![0f32; n * hsz];
1437 let mut inv2 = vec![0f32; n];
1438 rmsnorm_fwd_pooled(&h1, wts.pln, self.eps, self.gemma, &mut n2, &mut inv2, pool);
1439
1440 let inter = l.inter;
1441 let mut gpre: Vec<f32> = Vec::new();
1446 let mut upre: Vec<f32> = Vec::new();
1447 let mut act: Vec<f32> = Vec::new();
1448 let mut ffn = vec![0f32; n * hsz];
1449 prof::add(&prof::FFN_PRE, t_ffn);
1450 let mut fused = false;
1455 #[cfg(feature = "gpu")]
1456 if let Some(gu) = wts.gu {
1457 if crate::gpu::enabled_here() {
1458 let mut both = (want_acts && ffn_planes_to_host).then(|| vec![0f32; n * 2 * inter]);
1461 let t_chain = std::time::Instant::now();
1462 let chained = crate::gpu_wgpu::ffn_chain_f32(
1463 &n2,
1464 gu,
1465 wts.down,
1466 ffn_scale,
1467 both.as_deref_mut(),
1468 want_acts.then_some(li),
1469 &mut ffn,
1470 n,
1471 hsz,
1472 inter,
1473 self.act,
1474 );
1475 prof::add(&prof::FFN_CHAIN, t_chain);
1476 if chained {
1477 fused = true;
1478 if let Some(b) = &both {
1479 let t_act = std::time::Instant::now();
1480 gpre.reserve_exact(n * inter);
1481 upre.reserve_exact(n * inter);
1482 for r in 0..n {
1483 let row = &b[r * 2 * inter..(r + 1) * 2 * inter];
1484 gpre.extend_from_slice(&row[..inter]);
1485 upre.extend_from_slice(&row[inter..]);
1486 }
1487 act = vec![0f32; n * inter];
1488 act_mul_rows(self.act, &gpre, &upre, &mut act, n, inter, pool);
1490 prof::add(&prof::FFN_ACT, t_act);
1491 }
1492 }
1493 }
1494 }
1495 let t_host = std::time::Instant::now();
1496 if !fused {
1497 gpre = vec![0f32; n * inter];
1498 upre = vec![0f32; n * inter];
1499 act = vec![0f32; n * inter];
1500 if let Some(gu) = wts.gu {
1501 let mut both = vec![0f32; n * 2 * inter];
1504 ops::gemm_nt(&n2, gu, &mut both, n, hsz, 2 * inter, pool);
1505 for r in 0..n {
1506 let row = &both[r * 2 * inter..(r + 1) * 2 * inter];
1507 gpre[r * inter..(r + 1) * inter].copy_from_slice(&row[..inter]);
1508 upre[r * inter..(r + 1) * inter].copy_from_slice(&row[inter..]);
1509 }
1510 } else {
1511 ops::gemm_nt(&n2, wts.gate, &mut gpre, n, hsz, inter, pool);
1512 ops::gemm_nt(&n2, wts.up, &mut upre, n, hsz, inter, pool);
1513 }
1514 act_mul_rows(self.act, &gpre, &upre, &mut act, n, inter, pool);
1515 match ffn_scale {
1516 Some(g) => {
1517 debug_assert_eq!(g.len(), inter);
1518 let mut act2 = act.clone();
1519 for r in 0..n {
1520 for (a, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
1521 *a *= gv;
1522 }
1523 }
1524 ops::gemm_nt(&act2, wts.down, &mut ffn, n, inter, hsz, pool);
1525 }
1526 None => ops::gemm_nt(&act, wts.down, &mut ffn, n, inter, hsz, pool),
1527 }
1528 }
1529 prof::add(&prof::FFN_HOST, t_host);
1530 let t_post = std::time::Instant::now();
1531 let mut h2 = if want_acts { h1.clone() } else { std::mem::take(&mut h1) };
1533 add_into_pooled(&mut h2, &ffn, pool);
1534 prof::add(&prof::FFN_POST, t_post);
1535
1536 let acts = want_acts.then_some(LayerActs {
1537 inv1,
1538 attn: attn_acts,
1539 h1,
1540 n2,
1541 inv2,
1542 gpre,
1543 upre,
1544 act,
1545 });
1546 prof::add(&prof::FFN_FWD, t_ffn);
1547 (h2, acts)
1548 }
1549
1550 #[allow(clippy::too_many_arguments)]
1554 fn full_attn_fwd(
1555 &self,
1556 attn: &FcdAttn,
1557 mats: &LayerMats,
1558 n1: &[f32],
1559 b: usize,
1560 t: usize,
1561 nystrom: bool,
1562 attn_w: Option<AttnW<'_>>,
1563 want_attn_acts: bool,
1564 ) -> (Vec<f32>, AttnActs) {
1565 let (wqkv, wo) = match attn_w {
1567 Some(w) => (w.wqkv, w.wo),
1568 None => (&mats.wqkv[..], &mats.wo[..]),
1569 };
1570 let FcdAttn::Full {
1571 qrows: _,
1572 q_norm,
1573 k_norm,
1574 bias,
1575 bias_cat,
1576 output_gate,
1577 head_gate,
1578 window,
1579 local_rope,
1580 } = attn
1581 else {
1582 unreachable!("full_attn_fwd on a non-Full layer");
1583 };
1584
1585 let (hsz, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
1586 let n = b * t;
1587 let pool = self.pool.as_deref();
1588 let qdim = nh * hd;
1589 let kvdim = nkv * hd;
1590 let rep = nh / nkv;
1591 let qrows = if *output_gate { 2 * qdim } else { qdim };
1592
1593 let fused = qrows + 2 * kvdim;
1594 let head_gate_w: Option<&[f32]> = attn_w.and_then(|w| w.head_gate).or(head_gate.as_deref());
1599 let head_gain: Vec<f32> = match head_gate_w {
1600 Some(gw) => {
1601 let mut graw = vec![0f32; n * nh];
1602 ops::gemm_nt(n1, gw, &mut graw, n, hsz, nh, pool);
1603 for v in graw.iter_mut() {
1604 *v = 1.0 / (1.0 + (-*v).exp());
1605 }
1606 graw
1607 }
1608 None => Vec::new(),
1609 };
1610 let (rotary_dim, inv_freq) = self.layer_rope(*local_rope);
1611 #[cfg(feature = "gpu")]
1617 if !nystrom && crate::gpu::enabled_here() {
1618 let want_acts = self.attn_acts || want_attn_acts;
1619 let rope = {
1620 let mut rt = self.rope_tab.lock().unwrap();
1621 match rt.as_ref() {
1622 Some((key, arc)) if *key == (t, *local_rope) => arc.clone(),
1623 _ => {
1624 let half = rotary_dim / 2;
1625 let mut tab = Vec::with_capacity(t * half * 2);
1626 for pos in 0..t {
1627 for &freq in inv_freq.iter() {
1628 let a = pos as f64 * freq;
1629 tab.push(a.cos() as f32);
1630 tab.push(a.sin() as f32);
1631 }
1632 }
1633 let arc = std::sync::Arc::new(tab);
1634 *rt = Some(((t, *local_rope), arc.clone()));
1635 arc
1636 }
1637 }
1638 };
1639 let cfg = crate::gpu_wgpu::AttnChainCfg {
1640 wqkv,
1641 wo,
1642 q_norm: q_norm.as_deref(),
1643 k_norm: k_norm.as_deref(),
1644 bias: bias_cat.as_deref(),
1645 output_gate: *output_gate,
1646 head_gain: (!head_gain.is_empty()).then_some(&head_gain[..]),
1647 window: *window,
1648 gemma: self.gemma,
1649 eps: self.eps as f32,
1650 rotary_half: rotary_dim / 2,
1651 rope: &rope,
1652 b,
1653 t,
1654 nh,
1655 nkv,
1656 hd,
1657 hsz: self.hidden,
1658 };
1659 let mut attn_out = vec![0f32; n * self.hidden];
1660 let t_chain = std::time::Instant::now();
1661 let chained = crate::gpu_wgpu::attn_chain_f32(n1, &cfg, &mut attn_out, want_acts);
1662 prof::add(&prof::ATTN_CHAIN, t_chain);
1663 if let Some(ch) = chained {
1664 if !want_acts {
1665 return (
1666 attn_out,
1667 AttnActs::Full {
1668 qpre: Vec::new(),
1669 kpre: Vec::new(),
1670 vproj: Vec::new(),
1671 qrot: Vec::new(),
1672 krot: Vec::new(),
1673 qinv: Vec::new(),
1674 kinv: Vec::new(),
1675 ao: Vec::new(),
1676 gate_pre: Vec::new(),
1677 head_gain,
1678 },
1679 );
1680 }
1681 let mut qraw = vec![0f32; n * qrows];
1684 let mut kpre = vec![0f32; n * kvdim];
1685 let mut vproj = vec![0f32; n * kvdim];
1686 for r in 0..n {
1687 let row = &ch.qkv_plane[r * fused..(r + 1) * fused];
1688 qraw[r * qrows..(r + 1) * qrows].copy_from_slice(&row[..qrows]);
1689 kpre[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows..qrows + kvdim]);
1690 vproj[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows + kvdim..]);
1691 }
1692 if let Some((bq, bk, bv)) = bias {
1693 for r in 0..n {
1694 for (x, bb) in qraw[r * qrows..(r + 1) * qrows].iter_mut().zip(bq) {
1695 *x += bb;
1696 }
1697 for (x, bb) in kpre[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bk) {
1698 *x += bb;
1699 }
1700 for (x, bb) in vproj[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bv) {
1701 *x += bb;
1702 }
1703 }
1704 }
1705 let (qpre, gate_pre) = if *output_gate {
1706 let mut qh = vec![0f32; n * qdim];
1707 let mut gp = vec![0f32; n * qdim];
1708 for r in 0..n {
1709 for h in 0..nh {
1710 let src = r * qrows + 2 * h * hd;
1711 let dst = r * qdim + h * hd;
1712 qh[dst..dst + hd].copy_from_slice(&qraw[src..src + hd]);
1713 gp[dst..dst + hd].copy_from_slice(&qraw[src + hd..src + 2 * hd]);
1714 }
1715 }
1716 (qh, gp)
1717 } else {
1718 (qraw, Vec::new())
1719 };
1720 return (
1721 attn_out,
1722 AttnActs::Full {
1723 qpre,
1724 kpre,
1725 vproj,
1726 qrot: ch.qrot,
1727 krot: ch.krot,
1728 qinv: ch.qinv,
1729 kinv: ch.kinv,
1730 ao: ch.ao,
1731 gate_pre,
1732 head_gain,
1733 },
1734 );
1735 }
1736 }
1737
1738 let mut qkv = vec![0f32; n * fused];
1742 ops::gemm_nt(n1, wqkv, &mut qkv, n, hsz, fused, pool);
1743 let mut qraw = vec![0f32; n * qrows];
1744 let mut kpre = vec![0f32; n * kvdim];
1745 let mut vproj = vec![0f32; n * kvdim];
1746 for r in 0..n {
1747 let row = &qkv[r * fused..(r + 1) * fused];
1748 qraw[r * qrows..(r + 1) * qrows].copy_from_slice(&row[..qrows]);
1749 kpre[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows..qrows + kvdim]);
1750 vproj[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows + kvdim..]);
1751 }
1752 if let Some((bq, bk, bv)) = bias {
1753 for r in 0..n {
1754 for (x, bb) in qraw[r * qrows..(r + 1) * qrows].iter_mut().zip(bq) {
1755 *x += bb;
1756 }
1757 for (x, bb) in kpre[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bk) {
1758 *x += bb;
1759 }
1760 for (x, bb) in vproj[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bv) {
1761 *x += bb;
1762 }
1763 }
1764 }
1765 let (qpre, gate_pre) = if *output_gate {
1767 let mut qh = vec![0f32; n * qdim];
1768 let mut gp = vec![0f32; n * qdim];
1769 for r in 0..n {
1770 for h in 0..nh {
1771 let src = r * qrows + 2 * h * hd;
1772 let dst = r * qdim + h * hd;
1773 qh[dst..dst + hd].copy_from_slice(&qraw[src..src + hd]);
1774 gp[dst..dst + hd].copy_from_slice(&qraw[src + hd..src + 2 * hd]);
1775 }
1776 }
1777 (qh, gp)
1778 } else {
1779 (qraw, Vec::new())
1780 };
1781
1782 let mut qrot = qpre.clone();
1783 let mut krot = kpre.clone();
1784 let mut qinv = vec![0f32; n * nh];
1785 let mut kinv = vec![0f32; n * nkv];
1786 self.qk_norm_rope(&mut qrot, q_norm.as_deref(), nh, t, *local_rope, &mut qinv);
1787 self.qk_norm_rope(&mut krot, k_norm.as_deref(), nkv, t, *local_rope, &mut kinv);
1788
1789 let mut ao = vec![0f32; n * qdim];
1791 {
1792 let units = b * nh;
1793 let aop = SendMut(ao.as_mut_ptr());
1794 let qr = &qrot;
1795 let kr = &krot;
1796 let vr = &vproj;
1797 let nys = self.nys;
1798 let run_unit = |u: usize| {
1799 let (bi, h) = (u / nh, u % nh);
1800 let g = h / rep;
1801 if nystrom {
1802 let mut q64 = vec![0f64; t * hd];
1804 let mut k64 = vec![0f64; t * hd];
1805 let mut v64 = vec![0f64; t * hd];
1806 for p in 0..t {
1807 let r = bi * t + p;
1808 for c in 0..hd {
1809 q64[p * hd + c] = qr[r * qdim + h * hd + c] as f64;
1810 k64[p * hd + c] = kr[r * kvdim + g * hd + c] as f64;
1811 v64[p * hd + c] = vr[r * kvdim + g * hd + c] as f64;
1812 }
1813 }
1814 let mut o64 = vec![0f64; t * hd];
1815 ops::nystrom_head_fwd(&q64, &k64, &v64, t, hd, hd, &nys, &mut o64);
1816 for p in 0..t {
1817 let r = bi * t + p;
1818 for c in 0..hd {
1819 unsafe {
1821 *aop.at(r * qdim + h * hd + c) = o64[p * hd + c] as f32;
1822 }
1823 }
1824 }
1825 } else {
1826 let mut q32 = vec![0f32; t * hd];
1827 let mut k32 = vec![0f32; t * hd];
1828 let mut v32 = vec![0f32; t * hd];
1829 for p in 0..t {
1830 let r = bi * t + p;
1831 q32[p * hd..(p + 1) * hd]
1832 .copy_from_slice(&qr[r * qdim + h * hd..r * qdim + (h + 1) * hd]);
1833 k32[p * hd..(p + 1) * hd]
1834 .copy_from_slice(&kr[r * kvdim + g * hd..r * kvdim + (g + 1) * hd]);
1835 v32[p * hd..(p + 1) * hd]
1836 .copy_from_slice(&vr[r * kvdim + g * hd..r * kvdim + (g + 1) * hd]);
1837 }
1838 let mut o32 = vec![0f32; t * hd];
1839 ops::attn_head_fwd_window(&q32, &k32, &v32, t, hd, hd, *window, &mut o32);
1840 for p in 0..t {
1841 let r = bi * t + p;
1842 for c in 0..hd {
1843 unsafe {
1845 *aop.at(r * qdim + h * hd + c) = o32[p * hd + c];
1846 }
1847 }
1848 }
1849 }
1850 };
1851 match pool {
1852 Some(p) if units > 1 => p.run(&|widx, nw| {
1853 for u in (widx..units).step_by(nw) {
1854 run_unit(u);
1855 }
1856 }),
1857 _ => {
1858 for u in 0..units {
1859 run_unit(u);
1860 }
1861 }
1862 }
1863 }
1864
1865 let mut ao_eff: Vec<f32> = if *output_gate {
1867 ao.iter()
1868 .zip(&gate_pre)
1869 .map(|(&a, &g)| a * (1.0 / (1.0 + (-g).exp())))
1870 .collect()
1871 } else {
1872 ao.clone()
1873 };
1874 if !head_gain.is_empty() {
1876 for r in 0..n {
1877 for h in 0..nh {
1878 let gain = head_gain[r * nh + h];
1879 for a in &mut ao_eff[r * qdim + h * hd..r * qdim + (h + 1) * hd] {
1880 *a *= gain;
1881 }
1882 }
1883 }
1884 }
1885 let mut attn_out = vec![0f32; n * hsz];
1886 ops::gemm_nt(&ao_eff, wo, &mut attn_out, n, qdim, hsz, pool);
1887 (
1888 attn_out,
1889 AttnActs::Full {
1890 qpre,
1891 kpre,
1892 vproj,
1893 qrot,
1894 krot,
1895 qinv,
1896 kinv,
1897 ao,
1898 gate_pre,
1899 head_gain,
1900 },
1901 )
1902 }
1903
1904 fn gdn_attn_fwd(&self, attn: &FcdAttn, n1: &[f32], b: usize, t: usize) -> (Vec<f32>, AttnActs) {
1909 let FcdAttn::Gdn {
1910 wqkv,
1911 wz,
1912 wa,
1913 wb,
1914 conv,
1915 a_log,
1916 dt_bias,
1917 norm,
1918 wout,
1919 } = attn
1920 else {
1921 unreachable!("gdn_attn_fwd on a non-GDN layer");
1922 };
1923 let d = self.gdn.expect("gdn layer without gdn dims");
1924 let (hsz, n) = (self.hidden, b * t);
1925 let pool = self.pool.as_deref();
1926 let (c_dim, vd, nv) = (d.c_dim(), d.vd(), d.nv);
1927
1928 let mut qkv = vec![0f32; n * c_dim];
1929 ops::gemm_nt(n1, wqkv, &mut qkv, n, hsz, c_dim, pool);
1930 let mut z = vec![0f32; n * vd];
1931 ops::gemm_nt(n1, wz, &mut z, n, hsz, vd, pool);
1932 let mut a = vec![0f32; n * nv];
1933 ops::gemm_nt(n1, wa, &mut a, n, hsz, nv, pool);
1934 let mut bstr = vec![0f32; n * nv];
1935 ops::gemm_nt(n1, wb, &mut bstr, n, hsz, nv, pool);
1936
1937 let cfg = ops::GdnSeqCfg {
1938 nv: d.nv,
1939 nk: d.nk,
1940 dk: d.dk,
1941 dv: d.dv,
1942 kk: d.kk,
1943 rms_eps: self.eps,
1944 conv,
1945 a_log,
1946 dt_bias,
1947 norm,
1948 };
1949 let qkv64: Vec<f64> = qkv.iter().map(|&v| v as f64).collect();
1951 let z64: Vec<f64> = z.iter().map(|&v| v as f64).collect();
1952 let a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
1953 let b64: Vec<f64> = bstr.iter().map(|&v| v as f64).collect();
1954 let mut pre64 = vec![0f64; n * c_dim];
1955 let mut cq64 = vec![0f64; n * c_dim];
1956 for bi in 0..b {
1957 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
1958 ops::gdn_conv_fwd(
1959 &qkv64[r.clone()],
1960 t,
1961 c_dim,
1962 d.kk,
1963 conv,
1964 &mut pre64[r.clone()],
1965 &mut cq64[r],
1966 );
1967 }
1968 let mut of = vec![0f32; n * vd];
1969 {
1970 let units = b * d.nk;
1971 let rep_v = d.nv / d.nk;
1972 let ofp = SendMut(of.as_mut_ptr());
1973 let (cqr, zr, ar, br) = (&cq64, &z64, &a64, &b64);
1974 let cfg_ref = &cfg;
1975 let run_unit = |u: usize| {
1976 let (bi, ko) = (u / d.nk, u % d.nk);
1977 let mut local = vec![0f64; t * vd];
1978 ops::gdn_group_fwd(
1979 &cqr[bi * t * c_dim..(bi + 1) * t * c_dim],
1980 &zr[bi * t * vd..(bi + 1) * t * vd],
1981 &ar[bi * t * nv..(bi + 1) * t * nv],
1982 &br[bi * t * nv..(bi + 1) * t * nv],
1983 t,
1984 cfg_ref,
1985 ko,
1986 &mut local,
1987 );
1988 for hh in 0..rep_v {
1989 let h = ko * rep_v + hh;
1990 for p in 0..t {
1991 for dj in 0..d.dv {
1992 unsafe {
1994 *ofp.at((bi * t + p) * vd + h * d.dv + dj) =
1995 local[p * vd + h * d.dv + dj] as f32;
1996 }
1997 }
1998 }
1999 }
2000 };
2001 match pool {
2002 Some(p) if units > 1 => p.run(&|widx, nw| {
2003 for u in (widx..units).step_by(nw) {
2004 run_unit(u);
2005 }
2006 }),
2007 _ => {
2008 for u in 0..units {
2009 run_unit(u);
2010 }
2011 }
2012 }
2013 }
2014 let mut attn_out = vec![0f32; n * hsz];
2015 ops::gemm_nt(&of, wout, &mut attn_out, n, vd, hsz, pool);
2016 (attn_out, AttnActs::Gdn { qkv, z, a, b: bstr })
2017 }
2018
2019 #[allow(clippy::too_many_arguments)]
2023 fn layer_backward(
2024 &self,
2025 li: usize,
2026 h_in: &[f32],
2027 b: usize,
2028 t: usize,
2029 wts: &LnFfn,
2030 nystrom: bool,
2031 acts: &LayerActs,
2032 dh2: &[f32],
2033 mut grads: Option<&mut [Vec<f32>]>,
2034 ) -> Vec<f32> {
2035 let nystrom = nystrom && !self.polish_only;
2036 let hsz = self.hidden;
2037 let n = b * t;
2038 let l = &self.layers[li];
2039 let pool = self.pool.as_deref();
2040 let inter = l.inter;
2041
2042 let mut dn2 = vec![0f32; n * hsz];
2044 let mut bwd_fused = false;
2048 #[cfg(feature = "gpu")]
2049 if grads.is_none() {
2050 if let Some(gu) = wts.gu {
2051 if crate::gpu::enabled_here()
2052 && crate::gpu_wgpu::ffn_bwd_chain_f32(
2053 dh2, wts.down, gu, li, &mut dn2, n, hsz, inter, self.act, None, None,
2054 )
2055 {
2056 bwd_fused = true;
2057 }
2058 }
2059 }
2060 if !bwd_fused {
2061 let mut dact = vec![0f32; n * inter];
2062 ops::gemm_dx(dh2, wts.down, &mut dact, n, inter, hsz, pool);
2063 if let Some(g) = grads.as_deref_mut() {
2064 ops::gemm_dw(dh2, &acts.act, &mut g[4], n, inter, hsz, pool);
2065 }
2066 let mut dg = vec![0f32; n * inter];
2067 let mut du = vec![0f32; n * inter];
2068 act_bwd_rows(self.act, &acts.gpre, &acts.upre, &dact, None, &mut dg, &mut du, n, inter, pool);
2069 if let Some(gu) = wts.gu {
2072 let mut dgu = vec![0f32; n * 2 * inter];
2073 for r in 0..n {
2074 let row = &mut dgu[r * 2 * inter..(r + 1) * 2 * inter];
2075 row[..inter].copy_from_slice(&dg[r * inter..(r + 1) * inter]);
2076 row[inter..].copy_from_slice(&du[r * inter..(r + 1) * inter]);
2077 }
2078 ops::gemm_dx(&dgu, gu, &mut dn2, n, hsz, 2 * inter, pool);
2079 } else {
2080 ops::gemm_dx(&dg, wts.gate, &mut dn2, n, hsz, inter, pool);
2081 ops::gemm_dx(&du, wts.up, &mut dn2, n, hsz, inter, pool);
2082 }
2083 if let Some(g) = grads.as_deref_mut() {
2084 ops::gemm_dw(&dg, &acts.n2, &mut g[2], n, hsz, inter, pool);
2085 ops::gemm_dw(&du, &acts.n2, &mut g[3], n, hsz, inter, pool);
2086 }
2087 }
2088
2089 let mut dh1 = dh2.to_vec();
2090 ops::rmsnorm_bwd(
2091 &acts.h1,
2092 wts.pln,
2093 &acts.inv2,
2094 &dn2,
2095 self.gemma,
2096 &mut dh1,
2097 grads.as_deref_mut().map(|g| &mut g[1][..]),
2098 );
2099
2100 let mats = self.mats(li).expect("layer mats");
2102 let dn1 = match &l.attn {
2103 FcdAttn::Full { .. } => {
2104 self.full_attn_bwd(&l.attn, &mats, &acts.attn, &dh1, b, t, nystrom)
2105 }
2106 FcdAttn::Gdn { .. } => self.gdn_attn_bwd(&l.attn, &acts.attn, &dh1, b, t),
2107 };
2108
2109 let mut dh_in = dh1.clone();
2110 ops::rmsnorm_bwd(
2111 h_in,
2112 wts.iln,
2113 &acts.inv1,
2114 &dn1,
2115 self.gemma,
2116 &mut dh_in,
2117 grads.map(|g| &mut g[0][..]),
2118 );
2119 dh_in
2120 }
2121
2122 fn full_attn_bwd(
2126 &self,
2127 attn: &FcdAttn,
2128 mats: &LayerMats,
2129 acts: &AttnActs,
2130 dattn: &[f32],
2131 b: usize,
2132 t: usize,
2133 nystrom: bool,
2134 ) -> Vec<f32> {
2135 let (wqkv, wo) = (&mats.wqkv[..], &mats.wo[..]);
2136 let FcdAttn::Full {
2137 qrows: _,
2138 q_norm,
2139 k_norm,
2140 output_gate,
2141 local_rope,
2142 window,
2143 ..
2144 } = attn
2145 else {
2146 unreachable!("full_attn_bwd on a non-Full layer");
2147 };
2148 let AttnActs::Full {
2149 qpre,
2150 kpre,
2151 vproj,
2152 qrot,
2153 krot,
2154 qinv,
2155 kinv,
2156 ao,
2157 gate_pre,
2158 head_gain,
2159 } = acts
2160 else {
2161 unreachable!("acts mismatch");
2162 };
2163 let (rotary_dim, inv_freq) = self.layer_rope(*local_rope);
2164 let (hsz, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
2165 let n = b * t;
2166 let pool = self.pool.as_deref();
2167 let qdim = nh * hd;
2168 let kvdim = nkv * hd;
2169 let rep = nh / nkv;
2170 let qrows = if *output_gate { 2 * qdim } else { qdim };
2171
2172 let mut dao_eff = vec![0f32; n * qdim];
2173 ops::gemm_dx(dattn, wo, &mut dao_eff, n, qdim, hsz, pool);
2174 if !head_gain.is_empty() {
2178 for r in 0..n {
2179 for h in 0..nh {
2180 let gain = head_gain[r * nh + h];
2181 for d in &mut dao_eff[r * qdim + h * hd..r * qdim + (h + 1) * hd] {
2182 *d *= gain;
2183 }
2184 }
2185 }
2186 }
2187 let (dao, dgate) = if *output_gate {
2189 let mut dao = vec![0f32; n * qdim];
2190 let mut dgp = vec![0f32; n * qdim];
2191 for i in 0..n * qdim {
2192 let sig = 1.0 / (1.0 + (-gate_pre[i]).exp());
2193 dao[i] = dao_eff[i] * sig;
2194 dgp[i] = dao_eff[i] * ao[i] * sig * (1.0 - sig);
2195 }
2196 (dao, dgp)
2197 } else {
2198 (dao_eff, Vec::new())
2199 };
2200
2201 let mut dqrot = vec![0f32; n * qdim];
2202 let mut dkrot = vec![0f32; n * kvdim];
2203 let mut dvproj = vec![0f32; n * kvdim];
2204 {
2205 let units = b * nkv;
2208 let dqp = SendMut(dqrot.as_mut_ptr());
2209 let dkp = SendMut(dkrot.as_mut_ptr());
2210 let dvp = SendMut(dvproj.as_mut_ptr());
2211 let (qr, kr, vr) = (qrot, krot, vproj);
2212 let daor = &dao;
2213 let nys = self.nys;
2214 let run_unit = |u: usize| {
2215 let (bi, g) = (u / nkv, u % nkv);
2216 let mut k64 = vec![0f64; t * hd];
2217 let mut v64 = vec![0f64; t * hd];
2218 for p in 0..t {
2219 let r = bi * t + p;
2220 for c in 0..hd {
2221 k64[p * hd + c] = kr[r * kvdim + g * hd + c] as f64;
2222 v64[p * hd + c] = vr[r * kvdim + g * hd + c] as f64;
2223 }
2224 }
2225 let mut dk64 = vec![0f64; t * hd];
2226 let mut dv64 = vec![0f64; t * hd];
2227 let mut q64 = vec![0f64; t * hd];
2228 let mut do64 = vec![0f64; t * hd];
2229 let mut dq64 = vec![0f64; t * hd];
2230 for hh in 0..rep {
2231 let h = g * rep + hh;
2232 for p in 0..t {
2233 let r = bi * t + p;
2234 for c in 0..hd {
2235 q64[p * hd + c] = qr[r * qdim + h * hd + c] as f64;
2236 do64[p * hd + c] = daor[r * qdim + h * hd + c] as f64;
2237 }
2238 }
2239 for v in dq64.iter_mut() {
2240 *v = 0.0;
2241 }
2242 if nystrom {
2243 ops::nystrom_head_bwd(
2244 &q64, &k64, &v64, &do64, t, hd, hd, &nys, &mut dq64, &mut dk64,
2245 &mut dv64,
2246 );
2247 } else {
2248 ops::attn_head_bwd_window(
2249 &q64, &k64, &v64, &do64, t, hd, hd, *window, &mut dq64, &mut dk64,
2250 &mut dv64,
2251 );
2252 }
2253 for p in 0..t {
2254 let r = bi * t + p;
2255 for c in 0..hd {
2256 unsafe {
2258 *dqp.at(r * qdim + h * hd + c) = dq64[p * hd + c] as f32;
2259 }
2260 }
2261 }
2262 }
2263 for p in 0..t {
2264 let r = bi * t + p;
2265 for c in 0..hd {
2266 unsafe {
2268 *dkp.at(r * kvdim + g * hd + c) = dk64[p * hd + c] as f32;
2269 *dvp.at(r * kvdim + g * hd + c) = dv64[p * hd + c] as f32;
2270 }
2271 }
2272 }
2273 };
2274 match pool {
2275 Some(p) if units > 1 => p.run(&|widx, nw| {
2276 for u in (widx..units).step_by(nw) {
2277 run_unit(u);
2278 }
2279 }),
2280 _ => {
2281 for u in 0..units {
2282 run_unit(u);
2283 }
2284 }
2285 }
2286 }
2287
2288 let mut dqpre = vec![0f32; n * qdim];
2290 let mut dkpre = vec![0f32; n * kvdim];
2291 for r in 0..n {
2292 let pos = r % t;
2293 for h in 0..nh {
2294 let s = r * qdim + h * hd;
2295 ops::rope_bwd(&mut dqrot[s..s + rotary_dim], pos, inv_freq);
2296 match q_norm {
2297 Some(w) => ops::rmsnorm_bwd(
2298 &qpre[s..s + hd],
2299 w,
2300 &qinv[r * nh + h..r * nh + h + 1],
2301 &dqrot[s..s + hd],
2302 self.gemma,
2303 &mut dqpre[s..s + hd],
2304 None,
2305 ),
2306 None => dqpre[s..s + hd].copy_from_slice(&dqrot[s..s + hd]),
2307 }
2308 }
2309 for g in 0..nkv {
2310 let s = r * kvdim + g * hd;
2311 ops::rope_bwd(&mut dkrot[s..s + rotary_dim], pos, inv_freq);
2312 match k_norm {
2313 Some(w) => ops::rmsnorm_bwd(
2314 &kpre[s..s + hd],
2315 w,
2316 &kinv[r * nkv + g..r * nkv + g + 1],
2317 &dkrot[s..s + hd],
2318 self.gemma,
2319 &mut dkpre[s..s + hd],
2320 None,
2321 ),
2322 None => dkpre[s..s + hd].copy_from_slice(&dkrot[s..s + hd]),
2323 }
2324 }
2325 }
2326
2327 let dqraw: Vec<f32> = if *output_gate {
2329 let mut dq = vec![0f32; n * qrows];
2330 for r in 0..n {
2331 for h in 0..nh {
2332 let dst = r * qrows + 2 * h * hd;
2333 let src = r * qdim + h * hd;
2334 dq[dst..dst + hd].copy_from_slice(&dqpre[src..src + hd]);
2335 dq[dst + hd..dst + 2 * hd].copy_from_slice(&dgate[src..src + hd]);
2336 }
2337 }
2338 dq
2339 } else {
2340 dqpre
2341 };
2342
2343 let fused = qrows + 2 * kvdim;
2348 let mut dqkv = vec![0f32; n * fused];
2349 for r in 0..n {
2350 let row = &mut dqkv[r * fused..(r + 1) * fused];
2351 row[..qrows].copy_from_slice(&dqraw[r * qrows..(r + 1) * qrows]);
2352 row[qrows..qrows + kvdim].copy_from_slice(&dkpre[r * kvdim..(r + 1) * kvdim]);
2353 row[qrows + kvdim..].copy_from_slice(&dvproj[r * kvdim..(r + 1) * kvdim]);
2354 }
2355 let mut dn1 = vec![0f32; n * hsz];
2356 ops::gemm_dx(&dqkv, wqkv, &mut dn1, n, hsz, fused, pool);
2357 dn1
2358 }
2359
2360 fn gdn_attn_bwd(
2364 &self,
2365 attn: &FcdAttn,
2366 acts: &AttnActs,
2367 dattn: &[f32],
2368 b: usize,
2369 t: usize,
2370 ) -> Vec<f32> {
2371 let FcdAttn::Gdn {
2372 wqkv,
2373 wz,
2374 wa,
2375 wb,
2376 conv,
2377 a_log,
2378 dt_bias,
2379 norm,
2380 wout,
2381 } = attn
2382 else {
2383 unreachable!("gdn_attn_bwd on a non-GDN layer");
2384 };
2385 let AttnActs::Gdn { qkv, z, a, b: bstr } = acts else {
2386 unreachable!("acts mismatch");
2387 };
2388 let d = self.gdn.expect("gdn layer without gdn dims");
2389 let (hsz, n) = (self.hidden, b * t);
2390 let pool = self.pool.as_deref();
2391 let (c_dim, vd, nv) = (d.c_dim(), d.vd(), d.nv);
2392
2393 let mut dof = vec![0f32; n * vd];
2394 ops::gemm_dx(dattn, wout, &mut dof, n, vd, hsz, pool);
2395
2396 let cfg = ops::GdnSeqCfg {
2397 nv: d.nv,
2398 nk: d.nk,
2399 dk: d.dk,
2400 dv: d.dv,
2401 kk: d.kk,
2402 rms_eps: self.eps,
2403 conv,
2404 a_log,
2405 dt_bias,
2406 norm,
2407 };
2408 let qkv64: Vec<f64> = qkv.iter().map(|&v| v as f64).collect();
2409 let z64: Vec<f64> = z.iter().map(|&v| v as f64).collect();
2410 let a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
2411 let b64: Vec<f64> = bstr.iter().map(|&v| v as f64).collect();
2412 let dof64: Vec<f64> = dof.iter().map(|&v| v as f64).collect();
2413 let mut pre64 = vec![0f64; n * c_dim];
2414 let mut cq64 = vec![0f64; n * c_dim];
2415 for bi in 0..b {
2416 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
2417 ops::gdn_conv_fwd(
2418 &qkv64[r.clone()],
2419 t,
2420 c_dim,
2421 d.kk,
2422 conv,
2423 &mut pre64[r.clone()],
2424 &mut cq64[r],
2425 );
2426 }
2427
2428 let mut dcq64 = vec![0f64; n * c_dim];
2429 let mut dz64 = vec![0f64; n * vd];
2430 let mut da64 = vec![0f64; n * nv];
2431 let mut db64 = vec![0f64; n * nv];
2432 {
2433 let units = b * d.nk;
2434 let rep_v = d.nv / d.nk;
2435 let kd = d.nk * d.dk;
2436 let dcqp = SendMut(dcq64.as_mut_ptr());
2437 let dzp = SendMut(dz64.as_mut_ptr());
2438 let dap = SendMut(da64.as_mut_ptr());
2439 let dbp = SendMut(db64.as_mut_ptr());
2440 let (cqr, zr, ar, br, dor) = (&cq64, &z64, &a64, &b64, &dof64);
2441 let cfg_ref = &cfg;
2442 let run_unit = |u: usize| {
2443 let (bi, ko) = (u / d.nk, u % d.nk);
2444 let mut dcq_l = vec![0f64; t * c_dim];
2447 let mut dz_l = vec![0f64; t * vd];
2448 let mut da_l = vec![0f64; t * nv];
2449 let mut db_l = vec![0f64; t * nv];
2450 ops::gdn_group_bwd(
2451 &cqr[bi * t * c_dim..(bi + 1) * t * c_dim],
2452 &zr[bi * t * vd..(bi + 1) * t * vd],
2453 &ar[bi * t * nv..(bi + 1) * t * nv],
2454 &br[bi * t * nv..(bi + 1) * t * nv],
2455 t,
2456 cfg_ref,
2457 ko,
2458 &dor[bi * t * vd..(bi + 1) * t * vd],
2459 &mut dcq_l,
2460 &mut dz_l,
2461 &mut da_l,
2462 &mut db_l,
2463 );
2464 for p in 0..t {
2467 let row = (bi * t + p) * c_dim;
2468 for c in ko * d.dk..(ko + 1) * d.dk {
2469 unsafe {
2470 *dcqp.at(row + c) = dcq_l[p * c_dim + c];
2471 *dcqp.at(row + kd + c) = dcq_l[p * c_dim + kd + c];
2472 }
2473 }
2474 for hh in 0..rep_v {
2475 let h = ko * rep_v + hh;
2476 for dj in 0..d.dv {
2477 unsafe {
2478 *dcqp.at(row + 2 * kd + h * d.dv + dj) =
2479 dcq_l[p * c_dim + 2 * kd + h * d.dv + dj];
2480 *dzp.at((bi * t + p) * vd + h * d.dv + dj) =
2481 dz_l[p * vd + h * d.dv + dj];
2482 }
2483 }
2484 unsafe {
2485 *dap.at((bi * t + p) * nv + h) = da_l[p * nv + h];
2486 *dbp.at((bi * t + p) * nv + h) = db_l[p * nv + h];
2487 }
2488 }
2489 }
2490 };
2491 match pool {
2492 Some(p) if units > 1 => p.run(&|widx, nw| {
2493 for u in (widx..units).step_by(nw) {
2494 run_unit(u);
2495 }
2496 }),
2497 _ => {
2498 for u in 0..units {
2499 run_unit(u);
2500 }
2501 }
2502 }
2503 }
2504
2505 let mut dqkv64 = vec![0f64; n * c_dim];
2506 for bi in 0..b {
2507 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
2508 ops::gdn_conv_bwd(
2509 &pre64[r.clone()],
2510 t,
2511 c_dim,
2512 d.kk,
2513 conv,
2514 &dcq64[r.clone()],
2515 &mut dqkv64[r],
2516 );
2517 }
2518 let to32 = |v: &[f64]| -> Vec<f32> { v.iter().map(|&x| x as f32).collect() };
2519 let (dqkv, dz, da, db) = (to32(&dqkv64), to32(&dz64), to32(&da64), to32(&db64));
2520
2521 let mut dn1 = vec![0f32; n * hsz];
2522 ops::gemm_dx(&dqkv, wqkv, &mut dn1, n, hsz, c_dim, pool);
2523 ops::gemm_dx(&dz, wz, &mut dn1, n, hsz, vd, pool);
2524 ops::gemm_dx(&da, wa, &mut dn1, n, hsz, nv, pool);
2525 ops::gemm_dx(&db, wb, &mut dn1, n, hsz, nv, pool);
2526 dn1
2527 }
2528
2529 fn forward_hidden(
2534 &self,
2535 ids: &[u32],
2536 b: usize,
2537 t: usize,
2538 ts: Option<&TrainState>,
2539 student: bool,
2540 mut keep: Option<&mut Vec<Vec<f32>>>,
2541 ) -> Vec<f32> {
2542 let hsz = self.hidden;
2543 let mut h = vec![0f32; b * t * hsz];
2544 for (r, &id) in ids.iter().enumerate() {
2545 let src = (id as usize).min(self.embed.len() / hsz - 1) * hsz;
2546 h[r * hsz..(r + 1) * hsz].copy_from_slice(&self.embed[src..src + hsz]);
2547 }
2548 for li in 0..self.nl {
2549 if let Some(k) = keep.as_deref_mut() {
2550 k.push(h.clone());
2551 }
2552 let mats = self.mats(li).expect("layer mats");
2553 let wts = ln_ffn(self, if student { ts } else { None }, li, &mats);
2554 let nys = student && self.o1_flags[li];
2555 h = self.layer_forward(li, &h, b, t, &wts, nys, false).0;
2556 }
2557 h
2558 }
2559
2560 fn loss_and_dhidden(
2563 &self,
2564 hs: &[f32],
2565 ht: &[f32],
2566 targets: &[u32],
2567 kl_w: f64,
2568 ) -> (f64, f64, Vec<f32>) {
2569 let hsz = self.hidden;
2570 let n = targets.len();
2571 let pool = self.pool.as_deref();
2572 let wh = self.head_weight();
2573 let vs = self.vocab;
2574
2575 let mut ns = vec![0f32; n * hsz];
2576 let mut invs = vec![0f32; n];
2577 ops::rmsnorm_fwd(
2578 hs,
2579 &self.final_norm,
2580 self.eps,
2581 self.gemma,
2582 &mut ns,
2583 &mut invs,
2584 );
2585 let mut nt = vec![0f32; n * hsz];
2586 let mut invt = vec![0f32; n];
2587 ops::rmsnorm_fwd(
2588 ht,
2589 &self.final_norm,
2590 self.eps,
2591 self.gemma,
2592 &mut nt,
2593 &mut invt,
2594 );
2595
2596 let inv_n = 1.0 / n as f64;
2597 let mut ce_sum = 0f64;
2598 let mut kl_sum = 0f64;
2599 let mut dns = vec![0f32; n * hsz];
2600 let mut ls = vec![0f32; LM_CHUNK * vs];
2601 let mut lt = vec![0f32; LM_CHUNK * vs];
2602 let mut dlg = vec![0f32; LM_CHUNK * vs];
2603 let mut r0 = 0usize;
2604 while r0 < n {
2605 let r1 = (r0 + LM_CHUNK).min(n);
2606 let c = r1 - r0;
2607 ops::gemm_nt(
2608 &ns[r0 * hsz..r1 * hsz],
2609 wh,
2610 &mut ls[..c * vs],
2611 c,
2612 hsz,
2613 vs,
2614 pool,
2615 );
2616 ops::gemm_nt(
2617 &nt[r0 * hsz..r1 * hsz],
2618 wh,
2619 &mut lt[..c * vs],
2620 c,
2621 hsz,
2622 vs,
2623 pool,
2624 );
2625 for r in 0..c {
2626 let (ce, kl) = ops::ce_kl_position(
2627 &ls[r * vs..(r + 1) * vs],
2628 <[r * vs..(r + 1) * vs],
2629 targets[r0 + r] as usize,
2630 kl_w,
2631 inv_n,
2632 &mut dlg[r * vs..(r + 1) * vs],
2633 );
2634 ce_sum += ce;
2635 kl_sum += kl;
2636 }
2637 ops::gemm_dx(
2638 &dlg[..c * vs],
2639 wh,
2640 &mut dns[r0 * hsz..r1 * hsz],
2641 c,
2642 hsz,
2643 vs,
2644 pool,
2645 );
2646 r0 = r1;
2647 }
2648
2649 let mut dhs = vec![0f32; n * hsz];
2650 ops::rmsnorm_bwd(
2651 hs,
2652 &self.final_norm,
2653 &invs,
2654 &dns,
2655 self.gemma,
2656 &mut dhs,
2657 None,
2658 );
2659 (ce_sum * inv_n, kl_sum * inv_n, dhs)
2660 }
2661
2662 fn backward(
2665 &self,
2666 b: usize,
2667 t: usize,
2668 keep: &[Vec<f32>],
2669 dh_last: Vec<f32>,
2670 ts: &mut TrainState,
2671 ) {
2672 let TrainState {
2675 layers, data, grad, ..
2676 } = ts;
2677 let mut dh = dh_last;
2678 for li in (0..self.nl).rev() {
2679 let h_in = &keep[li];
2680 let nys = self.o1_flags[li];
2681 let slot = layers.iter().position(|&x| x == li);
2682 let mats_hold = self.mats(li).expect("layer mats");
2683 let wts = match slot {
2684 Some(s) => {
2685 let bi = s * PARAMS_PER_LAYER;
2686 LnFfn {
2687 iln: &data[bi],
2688 pln: &data[bi + 1],
2689 gate: &data[bi + 2],
2690 up: &data[bi + 3],
2691 down: &data[bi + 4],
2692 gu: None,
2693 }
2694 }
2695 None => {
2696 let l = &self.layers[li];
2697 LnFfn {
2698 iln: &l.iln,
2699 pln: &l.pln,
2700 gate: &[],
2701 up: &[],
2702 down: &mats_hold.down,
2703 gu: Some(&mats_hold.gu),
2704 }
2705 }
2706 };
2707 let (_, acts) = self.layer_forward(li, h_in, b, t, &wts, nys, true);
2708 let acts = acts.expect("want_acts");
2709 dh = match slot {
2710 Some(s) => {
2711 let gb = s * PARAMS_PER_LAYER;
2712 let gr = &mut grad[gb..gb + PARAMS_PER_LAYER];
2713 self.layer_backward(li, h_in, b, t, &wts, nys, &acts, &dh, Some(gr))
2714 }
2715 None => self.layer_backward(li, h_in, b, t, &wts, nys, &acts, &dh, None),
2716 };
2717 }
2718 }
2719
2720 #[doc(hidden)]
2727 pub fn loss_and_grads_for_test(
2728 &self,
2729 ids: &[u32],
2730 tgt: &[u32],
2731 b: usize,
2732 t: usize,
2733 ts: &mut TrainState,
2734 kl_w: f64,
2735 ) -> f64 {
2736 let ht = self.forward_hidden(ids, b, t, None, false, None);
2737 let mut keep = Vec::with_capacity(self.nl);
2738 let hs = self.forward_hidden(ids, b, t, Some(ts), true, Some(&mut keep));
2739 let (ce, kl, dhs) = self.loss_and_dhidden(&hs, &ht, tgt, kl_w);
2740 ts.zero_grad();
2741 self.backward(b, t, &keep, dhs, ts);
2742 (1.0 - kl_w) * ce + kl_w * kl
2743 }
2744
2745 pub fn val_ppl(
2749 &self,
2750 va: &[u32],
2751 ts: Option<&TrainState>,
2752 student: bool,
2753 bs: usize,
2754 nrounds: usize,
2755 seq: usize,
2756 ) -> f64 {
2757 let nwin = nrounds * bs;
2758 if va.len() < seq + 2 || nwin == 0 {
2759 return f64::NAN;
2760 }
2761 let stride = (va.len() - seq - 1) / nwin;
2762 let hsz = self.hidden;
2763 let wh = self.head_weight();
2764 let vs = self.vocab;
2765 let pool = self.pool.as_deref();
2766 let mut nll = 0f64;
2767 let mut cnt = 0usize;
2768 for j in 0..nrounds {
2769 let mut ids = Vec::with_capacity(bs * seq);
2770 let mut tgt = Vec::with_capacity(bs * seq);
2771 for bi in 0..bs {
2772 let off = ((j * bs + bi) * stride.max(1)).min(va.len() - seq - 1);
2773 ids.extend_from_slice(&va[off..off + seq]);
2774 tgt.extend_from_slice(&va[off + 1..off + seq + 1]);
2775 }
2776 let h = self.forward_hidden(&ids, bs, seq, ts, student, None);
2777 let n = bs * seq;
2778 let mut ns = vec![0f32; n * hsz];
2779 let mut inv = vec![0f32; n];
2780 ops::rmsnorm_fwd(
2781 &h,
2782 &self.final_norm,
2783 self.eps,
2784 self.gemma,
2785 &mut ns,
2786 &mut inv,
2787 );
2788 let mut lg = vec![0f32; LM_CHUNK * vs];
2789 let mut r0 = 0usize;
2790 while r0 < n {
2791 let r1 = (r0 + LM_CHUNK).min(n);
2792 let c = r1 - r0;
2793 ops::gemm_nt(
2794 &ns[r0 * hsz..r1 * hsz],
2795 wh,
2796 &mut lg[..c * vs],
2797 c,
2798 hsz,
2799 vs,
2800 pool,
2801 );
2802 for r in 0..c {
2803 let row = &lg[r * vs..(r + 1) * vs];
2804 let target = tgt[r0 + r] as usize;
2805 let mut mx = f64::NEG_INFINITY;
2806 for &v in row {
2807 mx = mx.max(v as f64);
2808 }
2809 let mut s = 0f64;
2810 for &v in row {
2811 s += (v as f64 - mx).exp();
2812 }
2813 nll += mx + s.ln() - row[target.min(vs - 1)] as f64;
2814 cnt += 1;
2815 }
2816 r0 = r1;
2817 }
2818 }
2819 (nll / cnt.max(1) as f64).exp()
2820 }
2821}
2822
2823pub fn run_polish_distilled(
2845 model: &Arc<CmfModel>,
2846 teacher: Option<&Arc<CmfModel>>,
2847 o1: &O1Cfg,
2848 hp: &FcdHyper,
2849 tr: &[u32],
2850 va: &[u32],
2851 out: &std::path::Path,
2852 gate: Option<&GenGateCfg>,
2853) -> Result<FcdReport, String> {
2854 if tr.len() < hp.seq + 2 {
2855 return Err(format!(
2856 "train corpus too small: {} tokens < seq+2 = {}",
2857 tr.len(),
2858 hp.seq + 2
2859 ));
2860 }
2861 let mut fm = FcdModel::from_cmf(model, o1, hp.polish_only)?;
2862 fm.polish_only = hp.polish_only;
2863 let fm = fm;
2864 let tfm: Option<FcdModel> = match teacher {
2865 Some(t) => {
2866 let off = O1Cfg {
2867 layers: O1Layers::List(Vec::new()),
2868 m: o1.m,
2869 w: o1.w,
2870 sink: o1.sink,
2871 rect: o1.rect,
2872 };
2873 let mut t = FcdModel::from_cmf(t, &off, true)?;
2874 t.polish_only = true;
2875 if t.hidden != fm.hidden || t.nl != fm.nl {
2876 return Err(format!(
2877 "teacher geometry differs: hidden {}/{} layers {}/{}",
2878 t.hidden, fm.hidden, t.nl, fm.nl
2879 ));
2880 }
2881 tracing::info!("fcd: distilling from a separate teacher file");
2882 Some(t)
2883 }
2884 None => None,
2885 };
2886 let converted = fm.converted();
2887 if converted.is_empty() {
2888 return Err("no converted layers under this --o1 spec (nothing to polish)".into());
2889 }
2890 tracing::info!(
2891 "fcd: {} layers converted ({} trainable tensors), m={} w={} sink={}, \
2892 corpus train {} / val {} tokens",
2893 converted.len(),
2894 converted.len() * PARAMS_PER_LAYER,
2895 fm.nys.m,
2896 fm.nys.w,
2897 fm.nys.sink,
2898 tr.len(),
2899 va.len()
2900 );
2901
2902 let mut ts = TrainState::new(&fm);
2903 let teacher_ppl = match &tfm {
2904 Some(t) => t.val_ppl(va, None, false, hp.bs, 2, hp.seq),
2905 None => fm.val_ppl(va, None, false, hp.bs, 2, hp.seq),
2906 };
2907 let ppl_start = fm.val_ppl(va, Some(&ts), true, hp.bs, 2, hp.seq);
2908 tracing::info!(
2909 "fcd: quick-val teacher ppl {teacher_ppl:.2} | zero-shot o1 student ppl {ppl_start:.2}"
2910 );
2911
2912 let mut gate_state: Option<(Pipeline, Vec<f64>)> = match gate {
2914 Some(g) if !g.prompts.is_empty() => {
2915 let greedy = SamplerConfig {
2916 temperature: 0.0,
2917 top_p: 1.0,
2918 presence_penalty: 0.0,
2919 top_k: 0,
2920 repetition_penalty: 1.0,
2921 min_p: 0.0,
2922 seed: Some(0),
2923 suppress_tokens: Vec::new(),
2924 penalty_window: 0,
2925 };
2926 let mut pipe = Pipeline::from_model(model, greedy)
2927 .map_err(|e| format!("gen-gate pipeline: {e}"))?;
2928 if !hp.polish_only {
2929 pipe.set_o1(Some(o1.clone()));
2930 }
2931 apply_trainables(&mut pipe, &fm, &ts);
2932 let base = gate_gen_scores(&mut pipe, g)?;
2933 tracing::info!("fcd gen-gate baseline loop-scores: {base:?}");
2934 Some((pipe, base))
2935 }
2936 Some(_) => {
2937 tracing::warn!("fcd gen-gate requested but val stream too short — gate off");
2938 None
2939 }
2940 None => None,
2941 };
2942 let init_snapshot: Option<Vec<Vec<f32>>> = gate_state.is_some().then(|| ts.data.clone());
2944 let mut gate_evals: Vec<(usize, f64, Vec<f64>, bool)> = Vec::new();
2945
2946 let mut rng = SplitMix64::new(hp.seed);
2947 let mut best: (f64, Option<Vec<Vec<f32>>>, usize) = (f64::INFINITY, None, 0);
2948 let mut losses: Vec<(f64, f64)> = Vec::with_capacity(hp.steps);
2949 let t0 = std::time::Instant::now();
2950 let n_per_step = hp.bs * hp.seq;
2951 for st in 1..=hp.steps {
2952 let mut ids = Vec::with_capacity(n_per_step);
2955 let mut tgt = Vec::with_capacity(n_per_step);
2956 for _ in 0..hp.bs {
2957 let off = (rng.next_u64() as usize) % (tr.len() - hp.seq - 1);
2958 ids.extend_from_slice(&tr[off..off + hp.seq]);
2959 tgt.extend_from_slice(&tr[off + 1..off + hp.seq + 1]);
2960 }
2961
2962 let ht = match &tfm {
2963 Some(t) => t.forward_hidden(&ids, hp.bs, hp.seq, None, false, None),
2964 None => fm.forward_hidden(&ids, hp.bs, hp.seq, None, false, None),
2965 };
2966 let mut keep: Vec<Vec<f32>> = Vec::with_capacity(fm.nl);
2967 let hs = fm.forward_hidden(&ids, hp.bs, hp.seq, Some(&ts), true, Some(&mut keep));
2968 let (ce, kl, dhs) = fm.loss_and_dhidden(&hs, &ht, &tgt, hp.kl_w);
2969 ts.zero_grad();
2970 fm.backward(hp.bs, hp.seq, &keep, dhs, &mut ts);
2971 let gn = ts.clip_and_step(hp.lr);
2972 losses.push((ce, kl));
2973
2974 let el = t0.elapsed().as_secs_f64();
2975 tracing::info!(
2976 "fcd step {st}/{}: ce {ce:.3} kl {kl:.3} |g| {gn:.3} ({:.1}s/step)",
2977 hp.steps,
2978 el / st as f64
2979 );
2980 if hp.eval_every > 0 && st % hp.eval_every == 0 {
2981 let p = fm.val_ppl(va, Some(&ts), true, hp.bs, 2, hp.seq);
2982 match (&mut gate_state, gate) {
2983 (Some((pipe, base)), Some(g)) => {
2984 apply_trainables(pipe, &fm, &ts);
2985 let scores = gate_gen_scores(pipe, g)?;
2986 let pass = gate_pass(&scores, base, g.threshold, g.baseline_slack);
2987 let tag = if pass && p < best.0 {
2988 best = (p, Some(ts.data.clone()), st);
2989 " *best*"
2990 } else {
2991 ""
2992 };
2993 tracing::info!(
2994 "fcd eval step {st}: val ppl {p:.2} | gen-gate {} (loop-scores {scores:?}){tag}",
2995 if pass { "PASS" } else { "FAIL" }
2996 );
2997 gate_evals.push((st, p, scores, pass));
2998 }
2999 _ => {
3000 let tag = if p < best.0 {
3001 best = (p, Some(ts.data.clone()), st);
3002 " *best*"
3003 } else {
3004 ""
3005 };
3006 tracing::info!("fcd eval step {st}: val ppl {p:.2}{tag}");
3007 }
3008 }
3009 }
3010 }
3011
3012 let mut gate_chosen: Option<usize> = None;
3016 if let Some(snap) = best.1.take() {
3017 ts.data = snap;
3018 gate_chosen = Some(best.2);
3019 tracing::info!(
3020 "fcd: restored best checkpoint from step {} (val ppl {:.2})",
3021 best.2,
3022 best.0
3023 );
3024 } else if let Some(init) = init_snapshot {
3025 ts.data = init;
3026 tracing::info!(
3027 "fcd: polish rejected by generation gate — identity artifact (zero-shot state written; claim 13 floor)"
3028 );
3029 }
3030 let ppl_final = fm.val_ppl(va, Some(&ts), true, hp.bs, 6, hp.seq);
3031 let report = FcdReport {
3032 converted: converted.clone(),
3033 teacher_ppl,
3034 ppl_start,
3035 ppl_best: best.0.min(ppl_final),
3036 best_step: best.2,
3037 ppl_final,
3038 steps_run: hp.steps,
3039 sec_per_step: t0.elapsed().as_secs_f64() / hp.steps.max(1) as f64,
3040 losses,
3041 gate: gate_state.map(|(_, base)| GateReport {
3042 baseline: base,
3043 evals: gate_evals,
3044 chosen: gate_chosen,
3045 }),
3046 };
3047 save_polished(model, out, &fm, &ts, o1, hp, &report)?;
3048 Ok(report)
3049}
3050
3051fn apply_trainables(pipe: &mut Pipeline, fm: &FcdModel, ts: &TrainState) {
3055 let hidden = fm.hidden;
3056 for (slot, &li) in ts.layers.iter().enumerate() {
3057 let b = slot * PARAMS_PER_LAYER;
3058 let inter = fm.layers[li].inter;
3059 let lw = &mut pipe.weights.layers[li];
3060 lw.input_norm = ts.data[b].clone();
3061 lw.post_norm = ts.data[b + 1].clone();
3062 lw.ffn = FfnKind::Dense(DenseFfn {
3063 gate_proj: QTensor::from_f32(ts.data[b + 2].clone(), inter, hidden),
3064 up_proj: QTensor::from_f32(ts.data[b + 3].clone(), inter, hidden),
3065 down_proj: QTensor::from_f32(ts.data[b + 4].clone(), hidden, inter),
3066 act: crate::pipeline::Act::Silu,
3067 down_t: None,
3068 segs: Vec::new(),
3069 });
3070 }
3071}
3072
3073fn gate_gen_scores(pipe: &mut Pipeline, g: &GenGateCfg) -> Result<Vec<f64>, String> {
3075 g.prompts
3076 .iter()
3077 .map(|p| {
3078 pipe.generate_from_ids(p, g.gen_tokens, None, None)
3079 .map(|r| loop_score(&r.token_ids))
3080 })
3081 .collect()
3082}
3083
3084fn save_polished(
3089 model: &CmfModel,
3090 out: &std::path::Path,
3091 fm: &FcdModel,
3092 ts: &TrainState,
3093 o1: &O1Cfg,
3094 hp: &FcdHyper,
3095 report: &FcdReport,
3096) -> Result<(), String> {
3097 use cortiq_core::format::TensorSpec;
3098 let mut replace: std::collections::HashMap<String, (usize, usize)> =
3099 std::collections::HashMap::new(); for (s, &li) in ts.layers.iter().enumerate() {
3101 let p = format!("model.layers.{li}.");
3102 for (k, suffix) in [
3103 (0usize, "input_layernorm.weight"),
3104 (1, "post_attention_layernorm.weight"),
3105 (2, "mlp.gate_proj.weight"),
3106 (3, "mlp.up_proj.weight"),
3107 (4, "mlp.down_proj.weight"),
3108 ] {
3109 replace.insert(format!("{p}{suffix}"), (s, k));
3110 }
3111 }
3112 let mut specs = Vec::with_capacity(model.tensors.len());
3113 for t in &model.tensors {
3114 if let Some(&(s, k)) = replace.get(&t.name) {
3115 let data = &ts.data[s * PARAMS_PER_LAYER + k];
3116 let mut bytes = Vec::with_capacity(data.len() * 4);
3117 for v in data {
3118 bytes.extend_from_slice(&v.to_le_bytes());
3119 }
3120 specs.push(TensorSpec {
3121 name: t.name.clone(),
3122 dtype: TensorDtype::F32,
3123 shape: t.shape.clone(),
3124 data: bytes,
3125 });
3126 } else {
3127 specs.push(TensorSpec {
3128 name: t.name.clone(),
3129 dtype: t.dtype,
3130 shape: t.shape.clone(),
3131 data: model.entry_bytes(t).to_vec(),
3132 });
3133 }
3134 }
3135
3136 let mut header = model.header.clone();
3137 let mut prov = match header.provenance.take() {
3138 Some(serde_json::Value::Object(m)) => m,
3139 _ => serde_json::Map::new(),
3140 };
3141 let layers_json = match &o1.layers {
3142 O1Layers::All => serde_json::json!("all"),
3143 O1Layers::Deep(n) => serde_json::json!(format!("deep{n}")),
3144 O1Layers::List(v) => serde_json::json!(v),
3145 };
3146 prov.insert(
3147 "o1_attn".into(),
3148 serde_json::json!({
3149 "layers": layers_json, "m": o1.m, "w": o1.w, "sink": o1.sink
3150 }),
3151 );
3152 prov.insert(
3153 "fcd".into(),
3154 serde_json::json!({
3155 "steps": hp.steps, "lr": hp.lr, "kl_w": hp.kl_w,
3156 "bs": hp.bs, "seq": hp.seq,
3157 "teacher_ppl": report.teacher_ppl,
3158 "ppl_start": report.ppl_start,
3159 "ppl_final": report.ppl_final,
3160 "best_step": report.best_step,
3161 "converted_layers": report.converted,
3162 }),
3163 );
3164 header.provenance = Some(serde_json::Value::Object(prov));
3165 let _ = fm; let masks = if model.masks.masks.is_empty() {
3168 None
3169 } else {
3170 Some(&model.masks)
3171 };
3172 CmfModel::write(out, &header, &specs, masks, model.vocab.as_deref())
3173 .map_err(|e| format!("writing polished cmf: {e}"))
3174}
3175
3176#[cfg(test)]
3177mod tests {
3178 use super::*;
3179
3180 #[test]
3184 fn polish_only_keeps_recurrent_layers_trainable() {
3185 let is_full: Vec<bool> = (0..8).map(|i| i % 4 == 3).collect();
3188
3189 let mut converting = vec![true; 8];
3190 apply_layer_eligibility(&mut converting, &is_full, false);
3191 assert_eq!(
3192 converting.iter().filter(|&&b| b).count(),
3193 2,
3194 "an O(1) conversion may only take the full-attention layers"
3195 );
3196
3197 let mut polishing = vec![true; 8];
3198 apply_layer_eligibility(&mut polishing, &is_full, true);
3199 assert_eq!(
3200 polishing.iter().filter(|&&b| b).count(),
3201 8,
3202 "a polish trains FFN and norms, which every layer has"
3203 );
3204
3205 let mut none = vec![false; 8];
3207 apply_layer_eligibility(&mut none, &is_full, true);
3208 assert!(none.iter().all(|&b| !b));
3209 }
3210
3211 #[test]
3215 fn the_container_ceiling_wins_over_the_host_figure() {
3216 let host = 944 * 1_000_000_000u64;
3217 let cgroup = 117 * 1_000_000_000u64;
3218 assert_eq!(effective_total(host, Some(cgroup)), cgroup);
3219 assert_eq!(effective_total(host, None), host);
3221 assert_eq!(effective_total(host, Some(host * 2)), host);
3223 }
3224
3225 #[test]
3229 fn container_headroom_is_the_limit_minus_what_it_holds() {
3230 let host_free = 800 * 1_000_000_000u64;
3231 let limit = 117 * 1_000_000_000u64;
3232 let used = 79 * 1_000_000_000u64;
3233 assert_eq!(
3234 effective_available(host_free, Some(limit), Some(used)),
3235 38 * 1_000_000_000u64
3236 );
3237 assert_eq!(
3239 effective_available(host_free, Some(limit), Some(limit + 1)),
3240 0
3241 );
3242 assert_eq!(effective_available(host_free, Some(limit), None), limit);
3244 assert_eq!(effective_available(host_free, None, None), host_free);
3246 }
3247
3248 #[test]
3250 fn gate_selects_lowest_ppl_among_passing() {
3251 let base = vec![0.10, 0.00, 0.20];
3252 let evals = vec![
3253 (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]), ];
3258 let sel = select_checkpoint(&evals, &base, 0.35, 0.10);
3259 assert_eq!(sel, Some(2), "step 75 is the lowest-ppl PASSING checkpoint");
3260 }
3261
3262 #[test]
3265 fn gate_all_fail_is_identity() {
3266 let base = vec![0.0, 0.0, 0.0];
3267 let evals = vec![
3268 (25usize, 15.0, vec![0.50, 0.0, 0.0]),
3269 (50, 14.0, vec![0.0, 0.36, 0.0]),
3270 (75, 13.0, vec![0.0, 0.0, 0.11]), ];
3272 assert_eq!(select_checkpoint(&evals, &base, 0.35, 0.10), None);
3273 }
3274
3275 #[test]
3278 fn gate_boundaries_and_tie_break() {
3279 let base = vec![0.25];
3280 assert!(gate_pass(&[0.35], &base, 0.35, 0.10), "== threshold passes");
3281 assert!(
3282 gate_pass(&[0.35], &[0.25], 0.35, 0.10),
3283 "== base+slack passes"
3284 );
3285 assert!(!gate_pass(&[0.351], &base, 0.35, 0.10));
3286 assert!(!gate_pass(&[0.30], &[0.10], 0.35, 0.10), "0.30 > 0.10+0.10");
3287 let evals = vec![(25usize, 20.0, vec![0.10]), (50, 20.0, vec![0.10])];
3288 assert_eq!(
3289 select_checkpoint(&evals, &base, 0.35, 0.10),
3290 Some(0),
3291 "equal ppl → earliest checkpoint"
3292 );
3293 }
3294}
3295
3296pub fn run_polish(
3299 model: &Arc<CmfModel>,
3300 o1: &O1Cfg,
3301 hp: &FcdHyper,
3302 tr: &[u32],
3303 va: &[u32],
3304 out: &std::path::Path,
3305 gate: Option<&GenGateCfg>,
3306) -> Result<FcdReport, String> {
3307 run_polish_distilled(model, None, o1, hp, tr, va, out, gate)
3308}