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