1use crate::fcd_ops::{self as ops, NysCfg};
25use crate::nystrom::{O1Cfg, O1Layers};
26use crate::pipeline::{DenseFfn, FfnKind, Pipeline};
27use crate::pool::Pool;
28use crate::qtensor::QTensor;
29use crate::sampler::{SamplerConfig, SplitMix64};
30use cortiq_core::{CmfModel, LayerType, NormStyle, TensorDtype};
31use std::sync::Arc;
32
33const LM_CHUNK: usize = 32;
36
37const ADAM_B1: f64 = 0.9;
39const ADAM_B2: f64 = 0.999;
40const ADAM_EPS: f64 = 1e-8;
41const ADAM_WD: f64 = 0.01;
42
43#[derive(Clone, Debug)]
45pub struct FcdHyper {
46 pub steps: usize,
47 pub lr: f64,
48 pub kl_w: f64,
49 pub eval_every: usize,
50 pub bs: usize,
51 pub seq: usize,
52 pub seed: u64,
53 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 BWD: AtomicU64 = AtomicU64::new(0);
201 pub static GEMM: AtomicU64 = AtomicU64::new(0);
202 pub static GEMM_CALLS: AtomicU64 = AtomicU64::new(0);
203 #[inline]
204 pub fn add(c: &AtomicU64, t: std::time::Instant) {
205 c.fetch_add(t.elapsed().as_nanos() as u64, Ordering::Relaxed);
206 }
207 pub fn take(c: &AtomicU64) -> f64 {
209 c.swap(0, Ordering::Relaxed) as f64 / 1e9
210 }
211
212 use std::sync::Mutex;
213 pub static SHAPES: Mutex<Vec<((usize, usize, usize), (u64, u64))>> = Mutex::new(Vec::new());
217 pub fn gemm_shape(n: usize, k: usize, m: usize, t0: std::time::Instant) {
218 let ns = t0.elapsed().as_nanos() as u64;
219 let mut g = SHAPES.lock().unwrap();
220 match g.iter_mut().find(|(s, _)| *s == (n, k, m)) {
221 Some((_, (c, tt))) => {
222 *c += 1;
223 *tt += ns;
224 }
225 None => g.push(((n, k, m), (1, ns))),
226 }
227 }
228 pub fn shape_report(top: usize) -> String {
229 let mut g = SHAPES.lock().unwrap();
230 g.sort_by_key(|(_, (_, ns))| std::cmp::Reverse(*ns));
231 let out = g
232 .iter()
233 .take(top)
234 .map(|((n, k, m), (c, ns))| {
235 format!(
236 " [{n}x{k}x{m}] {c} calls, {:.1}s total, {:.1} ms/call",
237 *ns as f64 / 1e9,
238 *ns as f64 / 1e6 / *c as f64
239 )
240 })
241 .collect::<Vec<_>>()
242 .join("\n");
243 g.clear();
244 out
245 }
246}
247
248enum FcdAttn {
251 Full {
252 qrows: usize,
256 q_norm: Option<Vec<f32>>,
257 k_norm: Option<Vec<f32>>,
258 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
259 bias_cat: Option<Vec<f32>>,
263 output_gate: bool,
266 },
267 Gdn {
270 wqkv: Vec<f32>,
271 wz: Vec<f32>,
272 wa: Vec<f32>,
273 wb: Vec<f32>,
274 conv: Vec<f32>,
275 a_log: Vec<f32>,
276 dt_bias: Vec<f32>,
277 norm: Vec<f32>,
278 wout: Vec<f32>,
279 },
280}
281
282pub(crate) struct FcdLayer {
283 attn: FcdAttn,
284 pub(crate) inter: usize,
285 pub(crate) iln: Vec<f32>,
288 pub(crate) pln: Vec<f32>,
289}
290
291#[derive(Clone, Copy)]
293struct GdnDims {
294 nv: usize,
295 nk: usize,
296 dk: usize,
297 dv: usize,
298 kk: usize,
299}
300
301impl GdnDims {
302 fn c_dim(&self) -> usize {
303 2 * self.nk * self.dk + self.nv * self.dv
304 }
305 fn vd(&self) -> usize {
306 self.nv * self.dv
307 }
308}
309
310pub struct FcdModel {
312 pub hidden: usize,
313 pub nh: usize,
314 pub nkv: usize,
315 pub hd: usize,
316 pub nl: usize,
317 pub vocab: usize,
318 pub(crate) eps: f64,
319 pub(crate) gemma: bool,
320 rotary_dim: usize,
321 inv_freq: Vec<f64>,
322 pub(crate) embed: Vec<f32>,
324 pub(crate) lm_head: Option<Vec<f32>>,
325 pub(crate) final_norm: Vec<f32>,
326 pub(crate) layers: Vec<FcdLayer>,
327 pub(crate) src: std::sync::Arc<CmfModel>,
330 mats_cache: Vec<std::sync::Mutex<Option<std::sync::Arc<LayerMats>>>>,
335 rope_tab: std::sync::Mutex<Option<(usize, std::sync::Arc<Vec<f32>>)>>,
339 mats_window: usize,
341 o1_flags: Vec<bool>,
343 nys: NysCfg,
344 gdn: Option<GdnDims>,
346 pub(crate) loops: usize,
353 pub(crate) loop_norm: bool,
354 pub(crate) pool: Option<Arc<Pool>>,
355 pub polish_only: bool,
358}
359
360fn deq(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
361 let e = model
362 .tensor(name)
363 .ok_or_else(|| format!("tensor '{name}' not found"))?;
364 let mut out = vec![0f32; e.n_elems()];
365 cortiq_core::quant::dequant_tensor(e, model.entry_bytes(e), &mut out)?;
366 Ok(out)
367}
368
369#[cfg(target_os = "linux")]
379fn cgroup_limit_bytes() -> Option<u64> {
380 let read = |p: &str| -> Option<u64> {
381 std::fs::read_to_string(p).ok()?.trim().parse::<u64>().ok()
382 };
383 read("/sys/fs/cgroup/memory.max")
386 .or_else(|| read("/sys/fs/cgroup/memory/memory.limit_in_bytes"))
387 .filter(|&v| v < u64::MAX / 2)
388}
389
390pub(crate) fn apply_layer_eligibility(flags: &mut [bool], is_full: &[bool], polish_only: bool) {
399 if polish_only {
400 return;
401 }
402 for (li, f) in flags.iter_mut().enumerate() {
403 if *f && !is_full.get(li).copied().unwrap_or(false) {
404 *f = false;
405 }
406 }
407}
408
409pub(crate) fn effective_total(host: u64, cgroup_limit: Option<u64>) -> u64 {
412 match cgroup_limit {
413 Some(l) => host.min(l),
414 None => host,
415 }
416}
417
418pub(crate) fn effective_available(host: u64, limit: Option<u64>, used: Option<u64>) -> u64 {
421 match (limit, used) {
422 (Some(l), Some(u)) => host.min(l.saturating_sub(u)),
423 (Some(l), None) => host.min(l),
424 _ => host,
425 }
426}
427
428#[cfg(target_os = "linux")]
430fn cgroup_used_bytes() -> Option<u64> {
431 let read = |p: &str| -> Option<u64> {
432 std::fs::read_to_string(p).ok()?.trim().parse::<u64>().ok()
433 };
434 read("/sys/fs/cgroup/memory.current")
435 .or_else(|| read("/sys/fs/cgroup/memory/memory.usage_in_bytes"))
436}
437
438fn physical_total_bytes() -> Option<u64> {
439 #[cfg(target_os = "macos")]
440 {
441 let out = std::process::Command::new("sysctl")
442 .args(["-n", "hw.memsize"])
443 .output()
444 .ok()?;
445 return String::from_utf8_lossy(&out.stdout).trim().parse().ok();
446 }
447 #[cfg(target_os = "linux")]
448 {
449 let mem = std::fs::read_to_string("/proc/meminfo").ok()?;
450 let kb: u64 = mem
451 .lines()
452 .find(|l| l.starts_with("MemTotal:"))?
453 .split_whitespace()
454 .nth(1)?
455 .parse()
456 .ok()?;
457 return Some(effective_total(kb * 1024, cgroup_limit_bytes()));
458 }
459 #[allow(unreachable_code)]
460 None
461}
462
463fn available_ram_bytes() -> Option<u64> {
466 #[cfg(target_os = "macos")]
467 {
468 let total: u64 = {
472 let out = std::process::Command::new("sysctl")
473 .args(["-n", "hw.memsize"])
474 .output()
475 .ok()?;
476 String::from_utf8_lossy(&out.stdout).trim().parse().ok()?
477 };
478 let out = std::process::Command::new("memory_pressure")
479 .arg("-Q")
480 .output()
481 .ok()?;
482 let text = String::from_utf8_lossy(&out.stdout);
483 let pct: u64 = text
484 .lines()
485 .find(|l| l.contains("free percentage"))?
486 .split(':')
487 .nth(1)?
488 .trim()
489 .trim_end_matches('%')
490 .parse()
491 .ok()?;
492 return Some(total / 100 * pct);
493 }
494 #[cfg(target_os = "linux")]
495 {
496 let mem = std::fs::read_to_string("/proc/meminfo").ok()?;
497 let kb: u64 = mem
498 .lines()
499 .find(|l| l.starts_with("MemAvailable:"))?
500 .split_whitespace()
501 .nth(1)?
502 .parse()
503 .ok()?;
504 return Some(effective_available(
507 kb * 1024,
508 cgroup_limit_bytes(),
509 cgroup_used_bytes(),
510 ));
511 }
512 #[allow(unreachable_code)]
513 None
514}
515
516pub(crate) fn deq_pub(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
519 deq(model, name)
520}
521
522pub(crate) struct LayerMats {
527 pub(crate) wqkv: Vec<f32>,
528 pub(crate) wo: Vec<f32>,
529 pub(crate) gu: Vec<f32>,
530 pub(crate) down: Vec<f32>,
531}
532
533impl FcdModel {
534 pub(crate) fn mats(&self, li: usize) -> Result<std::sync::Arc<LayerMats>, String> {
539 {
540 let slot = self.mats_cache[li].lock().unwrap();
541 if let Some(m) = slot.as_ref() {
542 return Ok(m.clone());
543 }
544 }
545 let p = format!("model.layers.{li}.");
546 let d = |name: String| -> Result<Vec<f32>, String> { deq(&self.src, &name) };
547 let built = if matches!(self.layers[li].attn, FcdAttn::Full { .. }) {
548 let wq = d(format!("{p}self_attn.q_proj.weight"))?;
549 let wk = d(format!("{p}self_attn.k_proj.weight"))?;
550 let wv = d(format!("{p}self_attn.v_proj.weight"))?;
551 let mut wqkv = Vec::with_capacity(wq.len() + wk.len() + wv.len());
552 wqkv.extend_from_slice(&wq);
553 wqkv.extend_from_slice(&wk);
554 wqkv.extend_from_slice(&wv);
555 let gate = d(format!("{p}mlp.gate_proj.weight"))?;
556 let up = d(format!("{p}mlp.up_proj.weight"))?;
557 let mut gu = Vec::with_capacity(gate.len() + up.len());
558 gu.extend_from_slice(&gate);
559 gu.extend_from_slice(&up);
560 LayerMats {
561 wqkv,
562 wo: d(format!("{p}self_attn.o_proj.weight"))?,
563 gu,
564 down: d(format!("{p}mlp.down_proj.weight"))?,
565 }
566 } else {
567 let gate = d(format!("{p}mlp.gate_proj.weight"))?;
570 let up = d(format!("{p}mlp.up_proj.weight"))?;
571 let mut gu = Vec::with_capacity(gate.len() + up.len());
572 gu.extend_from_slice(&gate);
573 gu.extend_from_slice(&up);
574 LayerMats {
575 wqkv: Vec::new(),
576 wo: Vec::new(),
577 gu,
578 down: d(format!("{p}mlp.down_proj.weight"))?,
579 }
580 };
581 let arc = std::sync::Arc::new(built);
582 *self.mats_cache[li].lock().unwrap() = Some(arc.clone());
583 if self.mats_window > 0 {
586 let w = self.mats_window;
587 for (j, slot) in self.mats_cache.iter().enumerate() {
588 let dist = li.abs_diff(j).min(self.layers.len() - li.abs_diff(j));
589 if dist > w {
590 *slot.lock().unwrap() = None;
591 }
592 }
593 }
594 Ok(arc)
595 }
596}
597
598impl FcdModel {
599 pub fn from_cmf(
602 model: &std::sync::Arc<CmfModel>,
603 o1: &O1Cfg,
604 polish_only: bool,
605 ) -> Result<Self, String> {
606 let arch = model.arch().clone();
607 if arch.hidden_act != "silu" {
608 return Err(format!(
609 "fcd/skill-bake: hidden_act '{}' not supported yet (SiLU only)",
610 arch.hidden_act
611 ));
612 }
613 let has_linear = arch
614 .layer_types
615 .iter()
616 .any(|t| matches!(t, LayerType::LinearAttention));
617 let gdn = if has_linear {
618 let lc = arch
619 .linear_core
620 .as_ref()
621 .ok_or_else(|| "model has linear layers but no arch.linear_core".to_string())?;
622 if lc.kind != "gated_delta_net" {
623 return Err(format!(
624 "linear core '{}' has no FCD backward (only gated_delta_net)",
625 lc.kind
626 ));
627 }
628 Some(GdnDims {
629 nv: lc.num_heads,
630 nk: arch
631 .linear_num_key_heads
632 .ok_or("linear core needs arch.linear_num_key_heads")?,
633 dk: arch
634 .linear_key_head_dim
635 .ok_or("linear core needs arch.linear_key_head_dim")?,
636 dv: lc.value_head_dim,
637 kk: arch
638 .linear_conv_kernel_dim
639 .ok_or("linear core needs arch.linear_conv_kernel_dim")?,
640 })
641 } else {
642 None
643 };
644 let (nh, nkv, hd, h) = (
645 arch.num_attention_heads,
646 arch.num_kv_heads,
647 arch.head_dim,
648 arch.hidden_size,
649 );
650 let embed = deq(model, "model.embed_tokens.weight")?;
651 let lm_head = if model.tensor("lm_head.weight").is_some() {
652 Some(deq(model, "lm_head.weight")?)
653 } else if arch.tie_word_embeddings {
654 None
655 } else {
656 return Err("no lm_head.weight and tie_word_embeddings is false".into());
657 };
658 let final_norm = deq(model, "model.norm.weight")?;
659
660 let mut layers = Vec::with_capacity(arch.num_layers);
661 for li in 0..arch.num_layers {
662 let p = format!("model.layers.{li}.");
663 if model.tensor(&format!("{p}mlp.gate.weight")).is_some() {
664 return Err(format!(
665 "layer {li} is MoE — FCD polish supports dense FFN only"
666 ));
667 }
668 let attn = match arch.layer_types.get(li) {
669 Some(LayerType::LinearAttention) => {
670 let la = |n: &str| deq(model, &format!("{p}linear_attn.{n}"));
671 FcdAttn::Gdn {
672 wqkv: la("in_proj_qkv.weight")?,
673 wz: la("in_proj_z.weight")?,
674 wa: la("in_proj_a.weight")?,
675 wb: la("in_proj_b.weight")?,
676 conv: la("conv1d.weight")?,
677 a_log: la("A_log")?,
678 dt_bias: la("dt_bias")?,
679 norm: la("norm.weight")?,
680 wout: la("out_proj.weight")?,
681 }
682 }
683 _ => {
684 let wq = deq(model, &format!("{p}self_attn.q_proj.weight"))?;
685 let output_gate = wq.len() == 2 * nh * hd * h;
686 let opt = |n: &str| -> Option<Vec<f32>> {
687 model
688 .tensor(&format!("{p}self_attn.{n}"))
689 .and_then(|_| deq(model, &format!("{p}self_attn.{n}")).ok())
690 };
691 let bias = match (opt("q_proj.bias"), opt("k_proj.bias"), opt("v_proj.bias")) {
692 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
693 _ => None,
694 };
695 let qrows = wq.len() / h;
696 drop(wq);
697 let bias_cat = bias.as_ref().map(|(bq, bk, bv)| {
698 let mut v = Vec::with_capacity(bq.len() + bk.len() + bv.len());
699 v.extend_from_slice(bq);
700 v.extend_from_slice(bk);
701 v.extend_from_slice(bv);
702 v
703 });
704 FcdAttn::Full {
705 qrows,
706 q_norm: opt("q_norm.weight"),
707 k_norm: opt("k_norm.weight"),
708 bias,
709 bias_cat,
710 output_gate,
711 }
712 }
713 };
714 let gate = deq(model, &format!("{p}mlp.gate_proj.weight"))?;
715 let inter = gate.len() / h;
716 drop(gate);
717 layers.push(FcdLayer {
718 attn,
719 inter,
720 iln: deq(model, &format!("{p}input_layernorm.weight"))?,
721 pln: deq(model, &format!("{p}post_attention_layernorm.weight"))?,
722 });
723 }
724
725 let rotary_dim = ((hd as f32 * arch.partial_rotary_factor) as usize)
726 .max(2)
727 .min(hd);
728 let base = arch.rope_theta;
729 let inv_freq: Vec<f64> = (0..rotary_dim / 2)
730 .map(|i| 1.0 / base.powf(2.0 * i as f64 / rotary_dim as f64))
731 .collect();
732 let loops = arch.num_loops.max(1);
733 let loop_norm = arch.loop_final_norm;
734 let mut flags = o1.layer_flags(arch.num_layers);
735 flags.resize(arch.num_layers, false);
736 {
745 let is_full: Vec<bool> = layers
746 .iter()
747 .map(|l| matches!(l.attn, FcdAttn::Full { .. }))
748 .collect();
749 apply_layer_eligibility(&mut flags, &is_full, polish_only);
750 }
751 let per_layer: u64 = layers
755 .first()
756 .map(|_| {
757 let qrows = match layers[0].attn {
758 FcdAttn::Full { qrows, .. } => qrows,
759 _ => 0,
760 };
761 ((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
765 })
766 .unwrap_or(0);
767 let mats_window = if let Ok(v) = std::env::var("CMF_BAKE_MATS_LAYERS") {
768 v.parse().unwrap_or(0)
769 } else {
770 let avail = available_ram_bytes().unwrap_or(u64::MAX);
771 let cap = physical_total_bytes()
772 .map(|t| t / 5 * 3)
773 .unwrap_or(u64::MAX)
774 .min(avail.saturating_sub(avail / 10));
775 let all = per_layer.saturating_mul(arch.num_layers as u64);
776 if all + 4 * 1024 * 1024 * 1024 <= cap {
777 0 } else {
779 let w = (cap.saturating_sub(3 * 1024 * 1024 * 1024) / per_layer.max(1))
780 .clamp(2, arch.num_layers as u64) as usize;
781 tracing::info!(
782 "fcd replica: streaming {w} of {} layers (~{:.1} GB resident of ~{:.1} GB total)",
783 arch.num_layers,
784 (w as u64 * per_layer) as f64 / 1e9,
785 all as f64 / 1e9,
786 );
787 w
788 }
789 };
790 let mats_cache = (0..arch.num_layers)
791 .map(|_| std::sync::Mutex::new(None))
792 .collect();
793 Ok(Self {
794 src: model.clone(),
795 mats_cache,
796 rope_tab: std::sync::Mutex::new(None),
797 mats_window,
798 hidden: h,
799 nh,
800 nkv,
801 hd,
802 nl: arch.num_layers,
803 vocab: arch.vocab_size.min(embed.len() / h),
804 eps: arch.rms_norm_eps,
805 gemma: matches!(arch.norm_style, NormStyle::Gemma),
806 rotary_dim,
807 inv_freq,
808 embed,
809 lm_head,
810 final_norm,
811 layers,
812 o1_flags: flags,
813 nys: NysCfg {
816 m: o1.m,
817 w: o1.w,
818 sink: o1.sink,
819 prefill: None,
820 },
821 gdn,
822 loops,
823 loop_norm,
824 pool: Pool::from_env(),
825 polish_only: false,
826 })
827 }
828
829 pub fn converted(&self) -> Vec<usize> {
831 (0..self.nl).filter(|&i| self.o1_flags[i]).collect()
832 }
833
834 fn head_weight(&self) -> &[f32] {
835 self.lm_head.as_deref().unwrap_or(&self.embed)
836 }
837}
838
839const PARAMS_PER_LAYER: usize = 5; pub struct TrainState {
846 pub layers: Vec<usize>,
847 pub data: Vec<Vec<f32>>,
849 grad: Vec<Vec<f32>>,
850 m1: Vec<Vec<f32>>,
851 m2: Vec<Vec<f32>>,
852 step_t: u64,
853}
854
855impl TrainState {
856 pub fn new(fm: &FcdModel) -> Self {
857 let layers = fm.converted();
858 let mut data = Vec::with_capacity(layers.len() * PARAMS_PER_LAYER);
859 for &li in &layers {
860 let l = &fm.layers[li];
861 let p = format!("model.layers.{li}.");
862 data.push(l.iln.clone());
863 data.push(l.pln.clone());
864 data.push(deq(&fm.src, &format!("{p}mlp.gate_proj.weight")).expect("gate"));
867 data.push(deq(&fm.src, &format!("{p}mlp.up_proj.weight")).expect("up"));
868 data.push(deq(&fm.src, &format!("{p}mlp.down_proj.weight")).expect("down"));
869 }
870 let zeros: Vec<Vec<f32>> = data.iter().map(|d| vec![0f32; d.len()]).collect();
871 Self {
872 layers,
873 grad: zeros.clone(),
874 m1: zeros.clone(),
875 m2: zeros,
876 data,
877 step_t: 0,
878 }
879 }
880
881 fn slot(&self, li: usize) -> Option<usize> {
882 self.layers.iter().position(|&x| x == li)
883 }
884
885 #[doc(hidden)]
887 pub fn grads(&self) -> &[Vec<f32>] {
888 &self.grad
889 }
890
891 fn zero_grad(&mut self) {
892 for g in &mut self.grad {
893 for v in g.iter_mut() {
894 *v = 0.0;
895 }
896 }
897 }
898
899 fn clip_and_step(&mut self, lr: f64) -> f64 {
902 let mut sq = 0f64;
903 for g in &self.grad {
904 for &v in g {
905 sq += (v as f64) * (v as f64);
906 }
907 }
908 let gn = sq.sqrt();
909 let scale = if gn > 1.0 { 1.0 / (gn + 1e-6) } else { 1.0 };
910 self.step_t += 1;
911 let bc1 = 1.0 - ADAM_B1.powi(self.step_t as i32);
912 let bc2 = 1.0 - ADAM_B2.powi(self.step_t as i32);
913 for p in 0..self.data.len() {
914 let (d, g, m, v) = (
915 &mut self.data[p],
916 &self.grad[p],
917 &mut self.m1[p],
918 &mut self.m2[p],
919 );
920 for i in 0..d.len() {
921 let gi = g[i] as f64 * scale;
922 let mi = ADAM_B1 * m[i] as f64 + (1.0 - ADAM_B1) * gi;
923 let vi = ADAM_B2 * v[i] as f64 + (1.0 - ADAM_B2) * gi * gi;
924 m[i] = mi as f32;
925 v[i] = vi as f32;
926 let upd = (mi / bc1) / ((vi / bc2).sqrt() + ADAM_EPS) + ADAM_WD * d[i] as f64;
927 d[i] = (d[i] as f64 - lr * upd) as f32;
928 }
929 }
930 gn
931 }
932}
933
934#[derive(Clone, Copy)]
937pub(crate) struct LnFfn<'a> {
938 pub(crate) iln: &'a [f32],
939 pub(crate) pln: &'a [f32],
940 pub(crate) gate: &'a [f32],
941 pub(crate) up: &'a [f32],
942 pub(crate) down: &'a [f32],
943 pub(crate) gu: Option<&'a [f32]>,
947}
948
949fn ln_ffn<'a>(
950 fm: &'a FcdModel,
951 ts: Option<&'a TrainState>,
952 li: usize,
953 mats: &'a LayerMats,
954) -> LnFfn<'a> {
955 if let Some(t) = ts {
956 if let Some(s) = t.slot(li) {
957 let b = s * PARAMS_PER_LAYER;
958 return LnFfn {
959 iln: &t.data[b],
960 pln: &t.data[b + 1],
961 gate: &t.data[b + 2],
962 up: &t.data[b + 3],
963 down: &t.data[b + 4],
964 gu: None,
965 };
966 }
967 }
968 let l = &fm.layers[li];
969 LnFfn {
970 iln: &l.iln,
971 pln: &l.pln,
972 gate: &[],
975 up: &[],
976 down: &mats.down,
977 gu: Some(&mats.gu),
978 }
979}
980
981enum AttnActs {
985 Full {
986 qpre: Vec<f32>,
987 kpre: Vec<f32>,
988 vproj: Vec<f32>,
989 qrot: Vec<f32>,
990 krot: Vec<f32>,
991 qinv: Vec<f32>,
992 kinv: Vec<f32>,
993 ao: Vec<f32>,
996 gate_pre: Vec<f32>,
998 },
999 Gdn {
1002 qkv: Vec<f32>,
1003 z: Vec<f32>,
1004 a: Vec<f32>,
1005 b: Vec<f32>,
1006 },
1007}
1008
1009pub(crate) struct LayerActs {
1010 inv1: Vec<f32>,
1011 attn: AttnActs,
1012 pub(crate) h1: Vec<f32>,
1013 pub(crate) n2: Vec<f32>,
1014 pub(crate) inv2: Vec<f32>,
1015 pub(crate) gpre: Vec<f32>,
1016 pub(crate) upre: Vec<f32>,
1017 pub(crate) act: Vec<f32>,
1018}
1019
1020struct SendMut<T>(*mut T);
1022unsafe impl<T> Send for SendMut<T> {}
1023unsafe impl<T> Sync for SendMut<T> {}
1024impl<T> SendMut<T> {
1025 #[inline]
1026 unsafe fn at(&self, i: usize) -> *mut T {
1027 unsafe { self.0.add(i) }
1028 }
1029}
1030
1031impl FcdModel {
1032 fn qk_norm_rope(
1036 &self,
1037 x: &mut [f32],
1038 norm: Option<&[f32]>,
1039 heads: usize,
1040 t: usize,
1041 inv_out: &mut [f32],
1042 ) {
1043 let hd = self.hd;
1044 let n = x.len() / (heads * hd);
1045 for r in 0..n {
1046 let pos = r % t;
1047 for hh in 0..heads {
1048 let s = (r * heads + hh) * hd;
1049 let head = &mut x[s..s + hd];
1050 if let Some(w) = norm {
1051 let mut inv = [0f32; 1];
1052 let mut y = [0f32; 256];
1053 debug_assert!(hd <= 256);
1054 ops::rmsnorm_fwd(head, w, self.eps, self.gemma, &mut y[..hd], &mut inv);
1055 head.copy_from_slice(&y[..hd]);
1056 inv_out[r * heads + hh] = inv[0];
1057 }
1058 ops::rope_fwd(&mut head[..self.rotary_dim], pos, &self.inv_freq);
1059 }
1060 }
1061 }
1062
1063 #[allow(clippy::too_many_arguments)]
1069 fn layer_forward(
1070 &self,
1071 li: usize,
1072 h_in: &[f32],
1073 b: usize,
1074 t: usize,
1075 wts: &LnFfn,
1076 nystrom: bool,
1077 want_acts: bool,
1078 ) -> (Vec<f32>, Option<LayerActs>) {
1079 self.layer_forward_scaled(li, h_in, b, t, wts, nystrom, want_acts, None)
1080 }
1081
1082 #[allow(clippy::too_many_arguments)]
1086 pub(crate) fn layer_forward_scaled(
1087 &self,
1088 li: usize,
1089 h_in: &[f32],
1090 b: usize,
1091 t: usize,
1092 wts: &LnFfn,
1093 nystrom: bool,
1094 want_acts: bool,
1095 ffn_scale: Option<&[f32]>,
1096 ) -> (Vec<f32>, Option<LayerActs>) {
1097 let nystrom = nystrom && !self.polish_only;
1099 let hsz = self.hidden;
1100 let n = b * t;
1101 let l = &self.layers[li];
1102 let pool = self.pool.as_deref();
1103
1104 let mut n1 = vec![0f32; n * hsz];
1105 let mut inv1 = vec![0f32; n];
1106 ops::rmsnorm_fwd(h_in, wts.iln, self.eps, self.gemma, &mut n1, &mut inv1);
1107
1108 let mats = self.mats(li).expect("layer mats");
1109 let t_attn = std::time::Instant::now();
1110 let (attn_out, attn_acts) = match &l.attn {
1111 FcdAttn::Full { .. } => self.full_attn_fwd(&l.attn, &mats, &n1, b, t, nystrom),
1112 FcdAttn::Gdn { .. } => self.gdn_attn_fwd(&l.attn, &n1, b, t),
1113 };
1114 prof::add(&prof::ATTN_FWD, t_attn);
1115 let t_ffn = std::time::Instant::now();
1116
1117 let mut h1 = h_in.to_vec();
1118 for (a, &x) in h1.iter_mut().zip(&attn_out) {
1119 *a += x;
1120 }
1121
1122 let mut n2 = vec![0f32; n * hsz];
1123 let mut inv2 = vec![0f32; n];
1124 ops::rmsnorm_fwd(&h1, wts.pln, self.eps, self.gemma, &mut n2, &mut inv2);
1125
1126 let inter = l.inter;
1127 let mut gpre = vec![0f32; n * inter];
1128 let mut upre = vec![0f32; n * inter];
1129 let mut act = vec![0f32; n * inter];
1130 let mut ffn = vec![0f32; n * hsz];
1131 let mut fused = false;
1136 #[cfg(feature = "gpu")]
1137 if let Some(gu) = wts.gu {
1138 if crate::gpu::enabled_here() {
1139 let mut both = want_acts.then(|| vec![0f32; n * 2 * inter]);
1140 if crate::gpu_wgpu::ffn_chain_f32(
1141 &n2,
1142 gu,
1143 wts.down,
1144 ffn_scale,
1145 both.as_deref_mut(),
1146 want_acts.then_some(li),
1147 &mut ffn,
1148 n,
1149 hsz,
1150 inter,
1151 ) {
1152 fused = true;
1153 if let Some(b) = &both {
1154 for r in 0..n {
1155 let row = &b[r * 2 * inter..(r + 1) * 2 * inter];
1156 gpre[r * inter..(r + 1) * inter].copy_from_slice(&row[..inter]);
1157 upre[r * inter..(r + 1) * inter].copy_from_slice(&row[inter..]);
1158 }
1159 for i in 0..n * inter {
1161 act[i] = ops::silu(gpre[i]) * upre[i];
1162 }
1163 }
1164 }
1165 }
1166 }
1167 if !fused {
1168 if let Some(gu) = wts.gu {
1169 let mut both = vec![0f32; n * 2 * inter];
1172 ops::gemm_nt(&n2, gu, &mut both, n, hsz, 2 * inter, pool);
1173 for r in 0..n {
1174 let row = &both[r * 2 * inter..(r + 1) * 2 * inter];
1175 gpre[r * inter..(r + 1) * inter].copy_from_slice(&row[..inter]);
1176 upre[r * inter..(r + 1) * inter].copy_from_slice(&row[inter..]);
1177 }
1178 } else {
1179 ops::gemm_nt(&n2, wts.gate, &mut gpre, n, hsz, inter, pool);
1180 ops::gemm_nt(&n2, wts.up, &mut upre, n, hsz, inter, pool);
1181 }
1182 for i in 0..n * inter {
1183 act[i] = ops::silu(gpre[i]) * upre[i];
1184 }
1185 match ffn_scale {
1186 Some(g) => {
1187 debug_assert_eq!(g.len(), inter);
1188 let mut act2 = act.clone();
1189 for r in 0..n {
1190 for (a, &gv) in act2[r * inter..(r + 1) * inter].iter_mut().zip(g) {
1191 *a *= gv;
1192 }
1193 }
1194 ops::gemm_nt(&act2, wts.down, &mut ffn, n, inter, hsz, pool);
1195 }
1196 None => ops::gemm_nt(&act, wts.down, &mut ffn, n, inter, hsz, pool),
1197 }
1198 }
1199 let mut h2 = h1.clone();
1200 for (a, &x) in h2.iter_mut().zip(&ffn) {
1201 *a += x;
1202 }
1203
1204 let acts = want_acts.then_some(LayerActs {
1205 inv1,
1206 attn: attn_acts,
1207 h1,
1208 n2,
1209 inv2,
1210 gpre,
1211 upre,
1212 act,
1213 });
1214 prof::add(&prof::FFN_FWD, t_ffn);
1215 (h2, acts)
1216 }
1217
1218 fn full_attn_fwd(
1222 &self,
1223 attn: &FcdAttn,
1224 mats: &LayerMats,
1225 n1: &[f32],
1226 b: usize,
1227 t: usize,
1228 nystrom: bool,
1229 ) -> (Vec<f32>, AttnActs) {
1230 let (wqkv, wo) = (&mats.wqkv[..], &mats.wo[..]);
1231 let FcdAttn::Full {
1232 qrows: _,
1233 q_norm,
1234 k_norm,
1235 bias,
1236 bias_cat,
1237 output_gate,
1238 } = attn
1239 else {
1240 unreachable!("full_attn_fwd on a non-Full layer");
1241 };
1242 let (hsz, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
1243 let n = b * t;
1244 let pool = self.pool.as_deref();
1245 let qdim = nh * hd;
1246 let kvdim = nkv * hd;
1247 let rep = nh / nkv;
1248 let qrows = if *output_gate { 2 * qdim } else { qdim };
1249
1250 let fused = qrows + 2 * kvdim;
1251 #[cfg(feature = "gpu")]
1257 if !nystrom && crate::gpu::enabled_here() {
1258 let want_acts = true; let rope = {
1260 let mut rt = self.rope_tab.lock().unwrap();
1261 match rt.as_ref() {
1262 Some((tt, arc)) if *tt == t => arc.clone(),
1263 _ => {
1264 let half = self.rotary_dim / 2;
1265 let mut tab = Vec::with_capacity(t * half * 2);
1266 for pos in 0..t {
1267 for &freq in self.inv_freq.iter() {
1268 let a = pos as f64 * freq;
1269 tab.push(a.cos() as f32);
1270 tab.push(a.sin() as f32);
1271 }
1272 }
1273 let arc = std::sync::Arc::new(tab);
1274 *rt = Some((t, arc.clone()));
1275 arc
1276 }
1277 }
1278 };
1279 let cfg = crate::gpu_wgpu::AttnChainCfg {
1280 wqkv,
1281 wo,
1282 q_norm: q_norm.as_deref(),
1283 k_norm: k_norm.as_deref(),
1284 bias: bias_cat.as_deref(),
1285 output_gate: *output_gate,
1286 gemma: self.gemma,
1287 eps: self.eps as f32,
1288 rotary_half: self.rotary_dim / 2,
1289 rope: &rope,
1290 b,
1291 t,
1292 nh,
1293 nkv,
1294 hd,
1295 hsz: self.hidden,
1296 };
1297 let mut attn_out = vec![0f32; n * self.hidden];
1298 if let Some(ch) = crate::gpu_wgpu::attn_chain_f32(n1, &cfg, &mut attn_out, want_acts) {
1299 let mut qraw = vec![0f32; n * qrows];
1302 let mut kpre = vec![0f32; n * kvdim];
1303 let mut vproj = vec![0f32; n * kvdim];
1304 for r in 0..n {
1305 let row = &ch.qkv_plane[r * fused..(r + 1) * fused];
1306 qraw[r * qrows..(r + 1) * qrows].copy_from_slice(&row[..qrows]);
1307 kpre[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows..qrows + kvdim]);
1308 vproj[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows + kvdim..]);
1309 }
1310 if let Some((bq, bk, bv)) = bias {
1311 for r in 0..n {
1312 for (x, bb) in qraw[r * qrows..(r + 1) * qrows].iter_mut().zip(bq) {
1313 *x += bb;
1314 }
1315 for (x, bb) in kpre[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bk) {
1316 *x += bb;
1317 }
1318 for (x, bb) in vproj[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bv) {
1319 *x += bb;
1320 }
1321 }
1322 }
1323 let (qpre, gate_pre) = if *output_gate {
1324 let mut qh = vec![0f32; n * qdim];
1325 let mut gp = vec![0f32; n * qdim];
1326 for r in 0..n {
1327 for h in 0..nh {
1328 let src = r * qrows + 2 * h * hd;
1329 let dst = r * qdim + h * hd;
1330 qh[dst..dst + hd].copy_from_slice(&qraw[src..src + hd]);
1331 gp[dst..dst + hd].copy_from_slice(&qraw[src + hd..src + 2 * hd]);
1332 }
1333 }
1334 (qh, gp)
1335 } else {
1336 (qraw, Vec::new())
1337 };
1338 return (
1339 attn_out,
1340 AttnActs::Full {
1341 qpre,
1342 kpre,
1343 vproj,
1344 qrot: ch.qrot,
1345 krot: ch.krot,
1346 qinv: ch.qinv,
1347 kinv: ch.kinv,
1348 ao: ch.ao,
1349 gate_pre,
1350 },
1351 );
1352 }
1353 }
1354
1355 let mut qkv = vec![0f32; n * fused];
1359 ops::gemm_nt(n1, wqkv, &mut qkv, n, hsz, fused, pool);
1360 let mut qraw = vec![0f32; n * qrows];
1361 let mut kpre = vec![0f32; n * kvdim];
1362 let mut vproj = vec![0f32; n * kvdim];
1363 for r in 0..n {
1364 let row = &qkv[r * fused..(r + 1) * fused];
1365 qraw[r * qrows..(r + 1) * qrows].copy_from_slice(&row[..qrows]);
1366 kpre[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows..qrows + kvdim]);
1367 vproj[r * kvdim..(r + 1) * kvdim].copy_from_slice(&row[qrows + kvdim..]);
1368 }
1369 if let Some((bq, bk, bv)) = bias {
1370 for r in 0..n {
1371 for (x, bb) in qraw[r * qrows..(r + 1) * qrows].iter_mut().zip(bq) {
1372 *x += bb;
1373 }
1374 for (x, bb) in kpre[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bk) {
1375 *x += bb;
1376 }
1377 for (x, bb) in vproj[r * kvdim..(r + 1) * kvdim].iter_mut().zip(bv) {
1378 *x += bb;
1379 }
1380 }
1381 }
1382 let (qpre, gate_pre) = if *output_gate {
1384 let mut qh = vec![0f32; n * qdim];
1385 let mut gp = vec![0f32; n * qdim];
1386 for r in 0..n {
1387 for h in 0..nh {
1388 let src = r * qrows + 2 * h * hd;
1389 let dst = r * qdim + h * hd;
1390 qh[dst..dst + hd].copy_from_slice(&qraw[src..src + hd]);
1391 gp[dst..dst + hd].copy_from_slice(&qraw[src + hd..src + 2 * hd]);
1392 }
1393 }
1394 (qh, gp)
1395 } else {
1396 (qraw, Vec::new())
1397 };
1398
1399 let mut qrot = qpre.clone();
1400 let mut krot = kpre.clone();
1401 let mut qinv = vec![0f32; n * nh];
1402 let mut kinv = vec![0f32; n * nkv];
1403 self.qk_norm_rope(&mut qrot, q_norm.as_deref(), nh, t, &mut qinv);
1404 self.qk_norm_rope(&mut krot, k_norm.as_deref(), nkv, t, &mut kinv);
1405
1406 let mut ao = vec![0f32; n * qdim];
1408 {
1409 let units = b * nh;
1410 let aop = SendMut(ao.as_mut_ptr());
1411 let qr = &qrot;
1412 let kr = &krot;
1413 let vr = &vproj;
1414 let nys = self.nys;
1415 let run_unit = |u: usize| {
1416 let (bi, h) = (u / nh, u % nh);
1417 let g = h / rep;
1418 if nystrom {
1419 let mut q64 = vec![0f64; t * hd];
1421 let mut k64 = vec![0f64; t * hd];
1422 let mut v64 = vec![0f64; t * hd];
1423 for p in 0..t {
1424 let r = bi * t + p;
1425 for c in 0..hd {
1426 q64[p * hd + c] = qr[r * qdim + h * hd + c] as f64;
1427 k64[p * hd + c] = kr[r * kvdim + g * hd + c] as f64;
1428 v64[p * hd + c] = vr[r * kvdim + g * hd + c] as f64;
1429 }
1430 }
1431 let mut o64 = vec![0f64; t * hd];
1432 ops::nystrom_head_fwd(&q64, &k64, &v64, t, hd, hd, &nys, &mut o64);
1433 for p in 0..t {
1434 let r = bi * t + p;
1435 for c in 0..hd {
1436 unsafe {
1438 *aop.at(r * qdim + h * hd + c) = o64[p * hd + c] as f32;
1439 }
1440 }
1441 }
1442 } else {
1443 let mut q32 = vec![0f32; t * hd];
1444 let mut k32 = vec![0f32; t * hd];
1445 let mut v32 = vec![0f32; t * hd];
1446 for p in 0..t {
1447 let r = bi * t + p;
1448 q32[p * hd..(p + 1) * hd]
1449 .copy_from_slice(&qr[r * qdim + h * hd..r * qdim + (h + 1) * hd]);
1450 k32[p * hd..(p + 1) * hd]
1451 .copy_from_slice(&kr[r * kvdim + g * hd..r * kvdim + (g + 1) * hd]);
1452 v32[p * hd..(p + 1) * hd]
1453 .copy_from_slice(&vr[r * kvdim + g * hd..r * kvdim + (g + 1) * hd]);
1454 }
1455 let mut o32 = vec![0f32; t * hd];
1456 ops::attn_head_fwd(&q32, &k32, &v32, t, hd, hd, &mut o32);
1457 for p in 0..t {
1458 let r = bi * t + p;
1459 for c in 0..hd {
1460 unsafe {
1462 *aop.at(r * qdim + h * hd + c) = o32[p * hd + c];
1463 }
1464 }
1465 }
1466 }
1467 };
1468 match pool {
1469 Some(p) if units > 1 => p.run(&|widx, nw| {
1470 for u in (widx..units).step_by(nw) {
1471 run_unit(u);
1472 }
1473 }),
1474 _ => {
1475 for u in 0..units {
1476 run_unit(u);
1477 }
1478 }
1479 }
1480 }
1481
1482 let ao_eff: Vec<f32> = if *output_gate {
1484 ao.iter()
1485 .zip(&gate_pre)
1486 .map(|(&a, &g)| a * (1.0 / (1.0 + (-g).exp())))
1487 .collect()
1488 } else {
1489 ao.clone()
1490 };
1491 let mut attn_out = vec![0f32; n * hsz];
1492 ops::gemm_nt(&ao_eff, wo, &mut attn_out, n, qdim, hsz, pool);
1493 (
1494 attn_out,
1495 AttnActs::Full {
1496 qpre,
1497 kpre,
1498 vproj,
1499 qrot,
1500 krot,
1501 qinv,
1502 kinv,
1503 ao,
1504 gate_pre,
1505 },
1506 )
1507 }
1508
1509 fn gdn_attn_fwd(&self, attn: &FcdAttn, n1: &[f32], b: usize, t: usize) -> (Vec<f32>, AttnActs) {
1514 let FcdAttn::Gdn {
1515 wqkv,
1516 wz,
1517 wa,
1518 wb,
1519 conv,
1520 a_log,
1521 dt_bias,
1522 norm,
1523 wout,
1524 } = attn
1525 else {
1526 unreachable!("gdn_attn_fwd on a non-GDN layer");
1527 };
1528 let d = self.gdn.expect("gdn layer without gdn dims");
1529 let (hsz, n) = (self.hidden, b * t);
1530 let pool = self.pool.as_deref();
1531 let (c_dim, vd, nv) = (d.c_dim(), d.vd(), d.nv);
1532
1533 let mut qkv = vec![0f32; n * c_dim];
1534 ops::gemm_nt(n1, wqkv, &mut qkv, n, hsz, c_dim, pool);
1535 let mut z = vec![0f32; n * vd];
1536 ops::gemm_nt(n1, wz, &mut z, n, hsz, vd, pool);
1537 let mut a = vec![0f32; n * nv];
1538 ops::gemm_nt(n1, wa, &mut a, n, hsz, nv, pool);
1539 let mut bstr = vec![0f32; n * nv];
1540 ops::gemm_nt(n1, wb, &mut bstr, n, hsz, nv, pool);
1541
1542 let cfg = ops::GdnSeqCfg {
1543 nv: d.nv,
1544 nk: d.nk,
1545 dk: d.dk,
1546 dv: d.dv,
1547 kk: d.kk,
1548 rms_eps: self.eps,
1549 conv,
1550 a_log,
1551 dt_bias,
1552 norm,
1553 };
1554 let qkv64: Vec<f64> = qkv.iter().map(|&v| v as f64).collect();
1556 let z64: Vec<f64> = z.iter().map(|&v| v as f64).collect();
1557 let a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
1558 let b64: Vec<f64> = bstr.iter().map(|&v| v as f64).collect();
1559 let mut pre64 = vec![0f64; n * c_dim];
1560 let mut cq64 = vec![0f64; n * c_dim];
1561 for bi in 0..b {
1562 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
1563 ops::gdn_conv_fwd(
1564 &qkv64[r.clone()],
1565 t,
1566 c_dim,
1567 d.kk,
1568 conv,
1569 &mut pre64[r.clone()],
1570 &mut cq64[r],
1571 );
1572 }
1573 let mut of = vec![0f32; n * vd];
1574 {
1575 let units = b * d.nk;
1576 let rep_v = d.nv / d.nk;
1577 let ofp = SendMut(of.as_mut_ptr());
1578 let (cqr, zr, ar, br) = (&cq64, &z64, &a64, &b64);
1579 let cfg_ref = &cfg;
1580 let run_unit = |u: usize| {
1581 let (bi, ko) = (u / d.nk, u % d.nk);
1582 let mut local = vec![0f64; t * vd];
1583 ops::gdn_group_fwd(
1584 &cqr[bi * t * c_dim..(bi + 1) * t * c_dim],
1585 &zr[bi * t * vd..(bi + 1) * t * vd],
1586 &ar[bi * t * nv..(bi + 1) * t * nv],
1587 &br[bi * t * nv..(bi + 1) * t * nv],
1588 t,
1589 cfg_ref,
1590 ko,
1591 &mut local,
1592 );
1593 for hh in 0..rep_v {
1594 let h = ko * rep_v + hh;
1595 for p in 0..t {
1596 for dj in 0..d.dv {
1597 unsafe {
1599 *ofp.at((bi * t + p) * vd + h * d.dv + dj) =
1600 local[p * vd + h * d.dv + dj] as f32;
1601 }
1602 }
1603 }
1604 }
1605 };
1606 match pool {
1607 Some(p) if units > 1 => p.run(&|widx, nw| {
1608 for u in (widx..units).step_by(nw) {
1609 run_unit(u);
1610 }
1611 }),
1612 _ => {
1613 for u in 0..units {
1614 run_unit(u);
1615 }
1616 }
1617 }
1618 }
1619 let mut attn_out = vec![0f32; n * hsz];
1620 ops::gemm_nt(&of, wout, &mut attn_out, n, vd, hsz, pool);
1621 (attn_out, AttnActs::Gdn { qkv, z, a, b: bstr })
1622 }
1623
1624 #[allow(clippy::too_many_arguments)]
1628 fn layer_backward(
1629 &self,
1630 li: usize,
1631 h_in: &[f32],
1632 b: usize,
1633 t: usize,
1634 wts: &LnFfn,
1635 nystrom: bool,
1636 acts: &LayerActs,
1637 dh2: &[f32],
1638 mut grads: Option<&mut [Vec<f32>]>,
1639 ) -> Vec<f32> {
1640 let nystrom = nystrom && !self.polish_only;
1641 let hsz = self.hidden;
1642 let n = b * t;
1643 let l = &self.layers[li];
1644 let pool = self.pool.as_deref();
1645 let inter = l.inter;
1646
1647 let mut dn2 = vec![0f32; n * hsz];
1649 let mut bwd_fused = false;
1653 #[cfg(feature = "gpu")]
1654 if grads.is_none() {
1655 if let Some(gu) = wts.gu {
1656 if crate::gpu::enabled_here()
1657 && crate::gpu_wgpu::ffn_bwd_chain_f32(
1658 dh2, wts.down, gu, li, &mut dn2, n, hsz, inter,
1659 )
1660 {
1661 bwd_fused = true;
1662 }
1663 }
1664 }
1665 if !bwd_fused {
1666 let mut dact = vec![0f32; n * inter];
1667 ops::gemm_dx(dh2, wts.down, &mut dact, n, inter, hsz, pool);
1668 if let Some(g) = grads.as_deref_mut() {
1669 ops::gemm_dw(dh2, &acts.act, &mut g[4], n, inter, hsz, pool);
1670 }
1671 let mut dg = vec![0f32; n * inter];
1672 let mut du = vec![0f32; n * inter];
1673 for i in 0..n * inter {
1674 dg[i] = dact[i] * acts.upre[i] * ops::silu_bwd(acts.gpre[i]);
1675 du[i] = dact[i] * ops::silu(acts.gpre[i]);
1676 }
1677 if let Some(gu) = wts.gu {
1680 let mut dgu = vec![0f32; n * 2 * inter];
1681 for r in 0..n {
1682 let row = &mut dgu[r * 2 * inter..(r + 1) * 2 * inter];
1683 row[..inter].copy_from_slice(&dg[r * inter..(r + 1) * inter]);
1684 row[inter..].copy_from_slice(&du[r * inter..(r + 1) * inter]);
1685 }
1686 ops::gemm_dx(&dgu, gu, &mut dn2, n, hsz, 2 * inter, pool);
1687 } else {
1688 ops::gemm_dx(&dg, wts.gate, &mut dn2, n, hsz, inter, pool);
1689 ops::gemm_dx(&du, wts.up, &mut dn2, n, hsz, inter, pool);
1690 }
1691 if let Some(g) = grads.as_deref_mut() {
1692 ops::gemm_dw(&dg, &acts.n2, &mut g[2], n, hsz, inter, pool);
1693 ops::gemm_dw(&du, &acts.n2, &mut g[3], n, hsz, inter, pool);
1694 }
1695 }
1696
1697 let mut dh1 = dh2.to_vec();
1698 ops::rmsnorm_bwd(
1699 &acts.h1,
1700 wts.pln,
1701 &acts.inv2,
1702 &dn2,
1703 self.gemma,
1704 &mut dh1,
1705 grads.as_deref_mut().map(|g| &mut g[1][..]),
1706 );
1707
1708 let mats = self.mats(li).expect("layer mats");
1710 let dn1 = match &l.attn {
1711 FcdAttn::Full { .. } => {
1712 self.full_attn_bwd(&l.attn, &mats, &acts.attn, &dh1, b, t, nystrom)
1713 }
1714 FcdAttn::Gdn { .. } => self.gdn_attn_bwd(&l.attn, &acts.attn, &dh1, b, t),
1715 };
1716
1717 let mut dh_in = dh1.clone();
1718 ops::rmsnorm_bwd(
1719 h_in,
1720 wts.iln,
1721 &acts.inv1,
1722 &dn1,
1723 self.gemma,
1724 &mut dh_in,
1725 grads.map(|g| &mut g[0][..]),
1726 );
1727 dh_in
1728 }
1729
1730 fn full_attn_bwd(
1734 &self,
1735 attn: &FcdAttn,
1736 mats: &LayerMats,
1737 acts: &AttnActs,
1738 dattn: &[f32],
1739 b: usize,
1740 t: usize,
1741 nystrom: bool,
1742 ) -> Vec<f32> {
1743 let (wqkv, wo) = (&mats.wqkv[..], &mats.wo[..]);
1744 let FcdAttn::Full {
1745 qrows: _,
1746 q_norm,
1747 k_norm,
1748 output_gate,
1749 ..
1750 } = attn
1751 else {
1752 unreachable!("full_attn_bwd on a non-Full layer");
1753 };
1754 let AttnActs::Full {
1755 qpre,
1756 kpre,
1757 vproj,
1758 qrot,
1759 krot,
1760 qinv,
1761 kinv,
1762 ao,
1763 gate_pre,
1764 } = acts
1765 else {
1766 unreachable!("acts mismatch");
1767 };
1768 let (hsz, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
1769 let n = b * t;
1770 let pool = self.pool.as_deref();
1771 let qdim = nh * hd;
1772 let kvdim = nkv * hd;
1773 let rep = nh / nkv;
1774 let qrows = if *output_gate { 2 * qdim } else { qdim };
1775
1776 let mut dao_eff = vec![0f32; n * qdim];
1777 ops::gemm_dx(dattn, wo, &mut dao_eff, n, qdim, hsz, pool);
1778 let (dao, dgate) = if *output_gate {
1780 let mut dao = vec![0f32; n * qdim];
1781 let mut dgp = vec![0f32; n * qdim];
1782 for i in 0..n * qdim {
1783 let sig = 1.0 / (1.0 + (-gate_pre[i]).exp());
1784 dao[i] = dao_eff[i] * sig;
1785 dgp[i] = dao_eff[i] * ao[i] * sig * (1.0 - sig);
1786 }
1787 (dao, dgp)
1788 } else {
1789 (dao_eff, Vec::new())
1790 };
1791
1792 let mut dqrot = vec![0f32; n * qdim];
1793 let mut dkrot = vec![0f32; n * kvdim];
1794 let mut dvproj = vec![0f32; n * kvdim];
1795 {
1796 let units = b * nkv;
1799 let dqp = SendMut(dqrot.as_mut_ptr());
1800 let dkp = SendMut(dkrot.as_mut_ptr());
1801 let dvp = SendMut(dvproj.as_mut_ptr());
1802 let (qr, kr, vr) = (qrot, krot, vproj);
1803 let daor = &dao;
1804 let nys = self.nys;
1805 let run_unit = |u: usize| {
1806 let (bi, g) = (u / nkv, u % nkv);
1807 let mut k64 = vec![0f64; t * hd];
1808 let mut v64 = vec![0f64; t * hd];
1809 for p in 0..t {
1810 let r = bi * t + p;
1811 for c in 0..hd {
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 dk64 = vec![0f64; t * hd];
1817 let mut dv64 = vec![0f64; t * hd];
1818 let mut q64 = vec![0f64; t * hd];
1819 let mut do64 = vec![0f64; t * hd];
1820 let mut dq64 = vec![0f64; t * hd];
1821 for hh in 0..rep {
1822 let h = g * rep + hh;
1823 for p in 0..t {
1824 let r = bi * t + p;
1825 for c in 0..hd {
1826 q64[p * hd + c] = qr[r * qdim + h * hd + c] as f64;
1827 do64[p * hd + c] = daor[r * qdim + h * hd + c] as f64;
1828 }
1829 }
1830 for v in dq64.iter_mut() {
1831 *v = 0.0;
1832 }
1833 if nystrom {
1834 ops::nystrom_head_bwd(
1835 &q64, &k64, &v64, &do64, t, hd, hd, &nys, &mut dq64, &mut dk64,
1836 &mut dv64,
1837 );
1838 } else {
1839 ops::attn_head_bwd(
1840 &q64, &k64, &v64, &do64, t, hd, hd, &mut dq64, &mut dk64, &mut dv64,
1841 );
1842 }
1843 for p in 0..t {
1844 let r = bi * t + p;
1845 for c in 0..hd {
1846 unsafe {
1848 *dqp.at(r * qdim + h * hd + c) = dq64[p * hd + c] as f32;
1849 }
1850 }
1851 }
1852 }
1853 for p in 0..t {
1854 let r = bi * t + p;
1855 for c in 0..hd {
1856 unsafe {
1858 *dkp.at(r * kvdim + g * hd + c) = dk64[p * hd + c] as f32;
1859 *dvp.at(r * kvdim + g * hd + c) = dv64[p * hd + c] as f32;
1860 }
1861 }
1862 }
1863 };
1864 match pool {
1865 Some(p) if units > 1 => p.run(&|widx, nw| {
1866 for u in (widx..units).step_by(nw) {
1867 run_unit(u);
1868 }
1869 }),
1870 _ => {
1871 for u in 0..units {
1872 run_unit(u);
1873 }
1874 }
1875 }
1876 }
1877
1878 let mut dqpre = vec![0f32; n * qdim];
1880 let mut dkpre = vec![0f32; n * kvdim];
1881 for r in 0..n {
1882 let pos = r % t;
1883 for h in 0..nh {
1884 let s = r * qdim + h * hd;
1885 ops::rope_bwd(&mut dqrot[s..s + self.rotary_dim], pos, &self.inv_freq);
1886 match q_norm {
1887 Some(w) => ops::rmsnorm_bwd(
1888 &qpre[s..s + hd],
1889 w,
1890 &qinv[r * nh + h..r * nh + h + 1],
1891 &dqrot[s..s + hd],
1892 self.gemma,
1893 &mut dqpre[s..s + hd],
1894 None,
1895 ),
1896 None => dqpre[s..s + hd].copy_from_slice(&dqrot[s..s + hd]),
1897 }
1898 }
1899 for g in 0..nkv {
1900 let s = r * kvdim + g * hd;
1901 ops::rope_bwd(&mut dkrot[s..s + self.rotary_dim], pos, &self.inv_freq);
1902 match k_norm {
1903 Some(w) => ops::rmsnorm_bwd(
1904 &kpre[s..s + hd],
1905 w,
1906 &kinv[r * nkv + g..r * nkv + g + 1],
1907 &dkrot[s..s + hd],
1908 self.gemma,
1909 &mut dkpre[s..s + hd],
1910 None,
1911 ),
1912 None => dkpre[s..s + hd].copy_from_slice(&dkrot[s..s + hd]),
1913 }
1914 }
1915 }
1916
1917 let dqraw: Vec<f32> = if *output_gate {
1919 let mut dq = vec![0f32; n * qrows];
1920 for r in 0..n {
1921 for h in 0..nh {
1922 let dst = r * qrows + 2 * h * hd;
1923 let src = r * qdim + h * hd;
1924 dq[dst..dst + hd].copy_from_slice(&dqpre[src..src + hd]);
1925 dq[dst + hd..dst + 2 * hd].copy_from_slice(&dgate[src..src + hd]);
1926 }
1927 }
1928 dq
1929 } else {
1930 dqpre
1931 };
1932
1933 let fused = qrows + 2 * kvdim;
1938 let mut dqkv = vec![0f32; n * fused];
1939 for r in 0..n {
1940 let row = &mut dqkv[r * fused..(r + 1) * fused];
1941 row[..qrows].copy_from_slice(&dqraw[r * qrows..(r + 1) * qrows]);
1942 row[qrows..qrows + kvdim].copy_from_slice(&dkpre[r * kvdim..(r + 1) * kvdim]);
1943 row[qrows + kvdim..].copy_from_slice(&dvproj[r * kvdim..(r + 1) * kvdim]);
1944 }
1945 let mut dn1 = vec![0f32; n * hsz];
1946 ops::gemm_dx(&dqkv, wqkv, &mut dn1, n, hsz, fused, pool);
1947 dn1
1948 }
1949
1950 fn gdn_attn_bwd(
1954 &self,
1955 attn: &FcdAttn,
1956 acts: &AttnActs,
1957 dattn: &[f32],
1958 b: usize,
1959 t: usize,
1960 ) -> Vec<f32> {
1961 let FcdAttn::Gdn {
1962 wqkv,
1963 wz,
1964 wa,
1965 wb,
1966 conv,
1967 a_log,
1968 dt_bias,
1969 norm,
1970 wout,
1971 } = attn
1972 else {
1973 unreachable!("gdn_attn_bwd on a non-GDN layer");
1974 };
1975 let AttnActs::Gdn { qkv, z, a, b: bstr } = acts else {
1976 unreachable!("acts mismatch");
1977 };
1978 let d = self.gdn.expect("gdn layer without gdn dims");
1979 let (hsz, n) = (self.hidden, b * t);
1980 let pool = self.pool.as_deref();
1981 let (c_dim, vd, nv) = (d.c_dim(), d.vd(), d.nv);
1982
1983 let mut dof = vec![0f32; n * vd];
1984 ops::gemm_dx(dattn, wout, &mut dof, n, vd, hsz, pool);
1985
1986 let cfg = ops::GdnSeqCfg {
1987 nv: d.nv,
1988 nk: d.nk,
1989 dk: d.dk,
1990 dv: d.dv,
1991 kk: d.kk,
1992 rms_eps: self.eps,
1993 conv,
1994 a_log,
1995 dt_bias,
1996 norm,
1997 };
1998 let qkv64: Vec<f64> = qkv.iter().map(|&v| v as f64).collect();
1999 let z64: Vec<f64> = z.iter().map(|&v| v as f64).collect();
2000 let a64: Vec<f64> = a.iter().map(|&v| v as f64).collect();
2001 let b64: Vec<f64> = bstr.iter().map(|&v| v as f64).collect();
2002 let dof64: Vec<f64> = dof.iter().map(|&v| v as f64).collect();
2003 let mut pre64 = vec![0f64; n * c_dim];
2004 let mut cq64 = vec![0f64; n * c_dim];
2005 for bi in 0..b {
2006 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
2007 ops::gdn_conv_fwd(
2008 &qkv64[r.clone()],
2009 t,
2010 c_dim,
2011 d.kk,
2012 conv,
2013 &mut pre64[r.clone()],
2014 &mut cq64[r],
2015 );
2016 }
2017
2018 let mut dcq64 = vec![0f64; n * c_dim];
2019 let mut dz64 = vec![0f64; n * vd];
2020 let mut da64 = vec![0f64; n * nv];
2021 let mut db64 = vec![0f64; n * nv];
2022 {
2023 let units = b * d.nk;
2024 let rep_v = d.nv / d.nk;
2025 let kd = d.nk * d.dk;
2026 let dcqp = SendMut(dcq64.as_mut_ptr());
2027 let dzp = SendMut(dz64.as_mut_ptr());
2028 let dap = SendMut(da64.as_mut_ptr());
2029 let dbp = SendMut(db64.as_mut_ptr());
2030 let (cqr, zr, ar, br, dor) = (&cq64, &z64, &a64, &b64, &dof64);
2031 let cfg_ref = &cfg;
2032 let run_unit = |u: usize| {
2033 let (bi, ko) = (u / d.nk, u % d.nk);
2034 let mut dcq_l = vec![0f64; t * c_dim];
2037 let mut dz_l = vec![0f64; t * vd];
2038 let mut da_l = vec![0f64; t * nv];
2039 let mut db_l = vec![0f64; t * nv];
2040 ops::gdn_group_bwd(
2041 &cqr[bi * t * c_dim..(bi + 1) * t * c_dim],
2042 &zr[bi * t * vd..(bi + 1) * t * vd],
2043 &ar[bi * t * nv..(bi + 1) * t * nv],
2044 &br[bi * t * nv..(bi + 1) * t * nv],
2045 t,
2046 cfg_ref,
2047 ko,
2048 &dor[bi * t * vd..(bi + 1) * t * vd],
2049 &mut dcq_l,
2050 &mut dz_l,
2051 &mut da_l,
2052 &mut db_l,
2053 );
2054 for p in 0..t {
2057 let row = (bi * t + p) * c_dim;
2058 for c in ko * d.dk..(ko + 1) * d.dk {
2059 unsafe {
2060 *dcqp.at(row + c) = dcq_l[p * c_dim + c];
2061 *dcqp.at(row + kd + c) = dcq_l[p * c_dim + kd + c];
2062 }
2063 }
2064 for hh in 0..rep_v {
2065 let h = ko * rep_v + hh;
2066 for dj in 0..d.dv {
2067 unsafe {
2068 *dcqp.at(row + 2 * kd + h * d.dv + dj) =
2069 dcq_l[p * c_dim + 2 * kd + h * d.dv + dj];
2070 *dzp.at((bi * t + p) * vd + h * d.dv + dj) =
2071 dz_l[p * vd + h * d.dv + dj];
2072 }
2073 }
2074 unsafe {
2075 *dap.at((bi * t + p) * nv + h) = da_l[p * nv + h];
2076 *dbp.at((bi * t + p) * nv + h) = db_l[p * nv + h];
2077 }
2078 }
2079 }
2080 };
2081 match pool {
2082 Some(p) if units > 1 => p.run(&|widx, nw| {
2083 for u in (widx..units).step_by(nw) {
2084 run_unit(u);
2085 }
2086 }),
2087 _ => {
2088 for u in 0..units {
2089 run_unit(u);
2090 }
2091 }
2092 }
2093 }
2094
2095 let mut dqkv64 = vec![0f64; n * c_dim];
2096 for bi in 0..b {
2097 let r = bi * t * c_dim..(bi + 1) * t * c_dim;
2098 ops::gdn_conv_bwd(
2099 &pre64[r.clone()],
2100 t,
2101 c_dim,
2102 d.kk,
2103 conv,
2104 &dcq64[r.clone()],
2105 &mut dqkv64[r],
2106 );
2107 }
2108 let to32 = |v: &[f64]| -> Vec<f32> { v.iter().map(|&x| x as f32).collect() };
2109 let (dqkv, dz, da, db) = (to32(&dqkv64), to32(&dz64), to32(&da64), to32(&db64));
2110
2111 let mut dn1 = vec![0f32; n * hsz];
2112 ops::gemm_dx(&dqkv, wqkv, &mut dn1, n, hsz, c_dim, pool);
2113 ops::gemm_dx(&dz, wz, &mut dn1, n, hsz, vd, pool);
2114 ops::gemm_dx(&da, wa, &mut dn1, n, hsz, nv, pool);
2115 ops::gemm_dx(&db, wb, &mut dn1, n, hsz, nv, pool);
2116 dn1
2117 }
2118
2119 fn forward_hidden(
2124 &self,
2125 ids: &[u32],
2126 b: usize,
2127 t: usize,
2128 ts: Option<&TrainState>,
2129 student: bool,
2130 mut keep: Option<&mut Vec<Vec<f32>>>,
2131 ) -> Vec<f32> {
2132 let hsz = self.hidden;
2133 let mut h = vec![0f32; b * t * hsz];
2134 for (r, &id) in ids.iter().enumerate() {
2135 let src = (id as usize).min(self.embed.len() / hsz - 1) * hsz;
2136 h[r * hsz..(r + 1) * hsz].copy_from_slice(&self.embed[src..src + hsz]);
2137 }
2138 for li in 0..self.nl {
2139 if let Some(k) = keep.as_deref_mut() {
2140 k.push(h.clone());
2141 }
2142 let mats = self.mats(li).expect("layer mats");
2143 let wts = ln_ffn(self, if student { ts } else { None }, li, &mats);
2144 let nys = student && self.o1_flags[li];
2145 h = self.layer_forward(li, &h, b, t, &wts, nys, false).0;
2146 }
2147 h
2148 }
2149
2150 fn loss_and_dhidden(
2153 &self,
2154 hs: &[f32],
2155 ht: &[f32],
2156 targets: &[u32],
2157 kl_w: f64,
2158 ) -> (f64, f64, Vec<f32>) {
2159 let hsz = self.hidden;
2160 let n = targets.len();
2161 let pool = self.pool.as_deref();
2162 let wh = self.head_weight();
2163 let vs = self.vocab;
2164
2165 let mut ns = vec![0f32; n * hsz];
2166 let mut invs = vec![0f32; n];
2167 ops::rmsnorm_fwd(
2168 hs,
2169 &self.final_norm,
2170 self.eps,
2171 self.gemma,
2172 &mut ns,
2173 &mut invs,
2174 );
2175 let mut nt = vec![0f32; n * hsz];
2176 let mut invt = vec![0f32; n];
2177 ops::rmsnorm_fwd(
2178 ht,
2179 &self.final_norm,
2180 self.eps,
2181 self.gemma,
2182 &mut nt,
2183 &mut invt,
2184 );
2185
2186 let inv_n = 1.0 / n as f64;
2187 let mut ce_sum = 0f64;
2188 let mut kl_sum = 0f64;
2189 let mut dns = vec![0f32; n * hsz];
2190 let mut ls = vec![0f32; LM_CHUNK * vs];
2191 let mut lt = vec![0f32; LM_CHUNK * vs];
2192 let mut dlg = vec![0f32; LM_CHUNK * vs];
2193 let mut r0 = 0usize;
2194 while r0 < n {
2195 let r1 = (r0 + LM_CHUNK).min(n);
2196 let c = r1 - r0;
2197 ops::gemm_nt(
2198 &ns[r0 * hsz..r1 * hsz],
2199 wh,
2200 &mut ls[..c * vs],
2201 c,
2202 hsz,
2203 vs,
2204 pool,
2205 );
2206 ops::gemm_nt(
2207 &nt[r0 * hsz..r1 * hsz],
2208 wh,
2209 &mut lt[..c * vs],
2210 c,
2211 hsz,
2212 vs,
2213 pool,
2214 );
2215 for r in 0..c {
2216 let (ce, kl) = ops::ce_kl_position(
2217 &ls[r * vs..(r + 1) * vs],
2218 <[r * vs..(r + 1) * vs],
2219 targets[r0 + r] as usize,
2220 kl_w,
2221 inv_n,
2222 &mut dlg[r * vs..(r + 1) * vs],
2223 );
2224 ce_sum += ce;
2225 kl_sum += kl;
2226 }
2227 ops::gemm_dx(
2228 &dlg[..c * vs],
2229 wh,
2230 &mut dns[r0 * hsz..r1 * hsz],
2231 c,
2232 hsz,
2233 vs,
2234 pool,
2235 );
2236 r0 = r1;
2237 }
2238
2239 let mut dhs = vec![0f32; n * hsz];
2240 ops::rmsnorm_bwd(
2241 hs,
2242 &self.final_norm,
2243 &invs,
2244 &dns,
2245 self.gemma,
2246 &mut dhs,
2247 None,
2248 );
2249 (ce_sum * inv_n, kl_sum * inv_n, dhs)
2250 }
2251
2252 fn backward(
2255 &self,
2256 b: usize,
2257 t: usize,
2258 keep: &[Vec<f32>],
2259 dh_last: Vec<f32>,
2260 ts: &mut TrainState,
2261 ) {
2262 let TrainState {
2265 layers, data, grad, ..
2266 } = ts;
2267 let mut dh = dh_last;
2268 for li in (0..self.nl).rev() {
2269 let h_in = &keep[li];
2270 let nys = self.o1_flags[li];
2271 let slot = layers.iter().position(|&x| x == li);
2272 let mats_hold = self.mats(li).expect("layer mats");
2273 let wts = match slot {
2274 Some(s) => {
2275 let bi = s * PARAMS_PER_LAYER;
2276 LnFfn {
2277 iln: &data[bi],
2278 pln: &data[bi + 1],
2279 gate: &data[bi + 2],
2280 up: &data[bi + 3],
2281 down: &data[bi + 4],
2282 gu: None,
2283 }
2284 }
2285 None => {
2286 let l = &self.layers[li];
2287 LnFfn {
2288 iln: &l.iln,
2289 pln: &l.pln,
2290 gate: &[],
2291 up: &[],
2292 down: &mats_hold.down,
2293 gu: Some(&mats_hold.gu),
2294 }
2295 }
2296 };
2297 let (_, acts) = self.layer_forward(li, h_in, b, t, &wts, nys, true);
2298 let acts = acts.expect("want_acts");
2299 dh = match slot {
2300 Some(s) => {
2301 let gb = s * PARAMS_PER_LAYER;
2302 let gr = &mut grad[gb..gb + PARAMS_PER_LAYER];
2303 self.layer_backward(li, h_in, b, t, &wts, nys, &acts, &dh, Some(gr))
2304 }
2305 None => self.layer_backward(li, h_in, b, t, &wts, nys, &acts, &dh, None),
2306 };
2307 }
2308 }
2309
2310 #[doc(hidden)]
2317 pub fn loss_and_grads_for_test(
2318 &self,
2319 ids: &[u32],
2320 tgt: &[u32],
2321 b: usize,
2322 t: usize,
2323 ts: &mut TrainState,
2324 kl_w: f64,
2325 ) -> f64 {
2326 let ht = self.forward_hidden(ids, b, t, None, false, None);
2327 let mut keep = Vec::with_capacity(self.nl);
2328 let hs = self.forward_hidden(ids, b, t, Some(ts), true, Some(&mut keep));
2329 let (ce, kl, dhs) = self.loss_and_dhidden(&hs, &ht, tgt, kl_w);
2330 ts.zero_grad();
2331 self.backward(b, t, &keep, dhs, ts);
2332 (1.0 - kl_w) * ce + kl_w * kl
2333 }
2334
2335 pub fn val_ppl(
2339 &self,
2340 va: &[u32],
2341 ts: Option<&TrainState>,
2342 student: bool,
2343 bs: usize,
2344 nrounds: usize,
2345 seq: usize,
2346 ) -> f64 {
2347 let nwin = nrounds * bs;
2348 if va.len() < seq + 2 || nwin == 0 {
2349 return f64::NAN;
2350 }
2351 let stride = (va.len() - seq - 1) / nwin;
2352 let hsz = self.hidden;
2353 let wh = self.head_weight();
2354 let vs = self.vocab;
2355 let pool = self.pool.as_deref();
2356 let mut nll = 0f64;
2357 let mut cnt = 0usize;
2358 for j in 0..nrounds {
2359 let mut ids = Vec::with_capacity(bs * seq);
2360 let mut tgt = Vec::with_capacity(bs * seq);
2361 for bi in 0..bs {
2362 let off = ((j * bs + bi) * stride.max(1)).min(va.len() - seq - 1);
2363 ids.extend_from_slice(&va[off..off + seq]);
2364 tgt.extend_from_slice(&va[off + 1..off + seq + 1]);
2365 }
2366 let h = self.forward_hidden(&ids, bs, seq, ts, student, None);
2367 let n = bs * seq;
2368 let mut ns = vec![0f32; n * hsz];
2369 let mut inv = vec![0f32; n];
2370 ops::rmsnorm_fwd(
2371 &h,
2372 &self.final_norm,
2373 self.eps,
2374 self.gemma,
2375 &mut ns,
2376 &mut inv,
2377 );
2378 let mut lg = vec![0f32; LM_CHUNK * vs];
2379 let mut r0 = 0usize;
2380 while r0 < n {
2381 let r1 = (r0 + LM_CHUNK).min(n);
2382 let c = r1 - r0;
2383 ops::gemm_nt(
2384 &ns[r0 * hsz..r1 * hsz],
2385 wh,
2386 &mut lg[..c * vs],
2387 c,
2388 hsz,
2389 vs,
2390 pool,
2391 );
2392 for r in 0..c {
2393 let row = &lg[r * vs..(r + 1) * vs];
2394 let target = tgt[r0 + r] as usize;
2395 let mut mx = f64::NEG_INFINITY;
2396 for &v in row {
2397 mx = mx.max(v as f64);
2398 }
2399 let mut s = 0f64;
2400 for &v in row {
2401 s += (v as f64 - mx).exp();
2402 }
2403 nll += mx + s.ln() - row[target.min(vs - 1)] as f64;
2404 cnt += 1;
2405 }
2406 r0 = r1;
2407 }
2408 }
2409 (nll / cnt.max(1) as f64).exp()
2410 }
2411}
2412
2413pub fn run_polish_distilled(
2435 model: &Arc<CmfModel>,
2436 teacher: Option<&Arc<CmfModel>>,
2437 o1: &O1Cfg,
2438 hp: &FcdHyper,
2439 tr: &[u32],
2440 va: &[u32],
2441 out: &std::path::Path,
2442 gate: Option<&GenGateCfg>,
2443) -> Result<FcdReport, String> {
2444 if tr.len() < hp.seq + 2 {
2445 return Err(format!(
2446 "train corpus too small: {} tokens < seq+2 = {}",
2447 tr.len(),
2448 hp.seq + 2
2449 ));
2450 }
2451 let mut fm = FcdModel::from_cmf(model, o1, hp.polish_only)?;
2452 fm.polish_only = hp.polish_only;
2453 let fm = fm;
2454 let tfm: Option<FcdModel> = match teacher {
2455 Some(t) => {
2456 let off = O1Cfg {
2457 layers: O1Layers::List(Vec::new()),
2458 m: o1.m,
2459 w: o1.w,
2460 sink: o1.sink,
2461 rect: o1.rect,
2462 };
2463 let mut t = FcdModel::from_cmf(t, &off, true)?;
2464 t.polish_only = true;
2465 if t.hidden != fm.hidden || t.nl != fm.nl {
2466 return Err(format!(
2467 "teacher geometry differs: hidden {}/{} layers {}/{}",
2468 t.hidden, fm.hidden, t.nl, fm.nl
2469 ));
2470 }
2471 tracing::info!("fcd: distilling from a separate teacher file");
2472 Some(t)
2473 }
2474 None => None,
2475 };
2476 let converted = fm.converted();
2477 if converted.is_empty() {
2478 return Err("no converted layers under this --o1 spec (nothing to polish)".into());
2479 }
2480 tracing::info!(
2481 "fcd: {} layers converted ({} trainable tensors), m={} w={} sink={}, \
2482 corpus train {} / val {} tokens",
2483 converted.len(),
2484 converted.len() * PARAMS_PER_LAYER,
2485 fm.nys.m,
2486 fm.nys.w,
2487 fm.nys.sink,
2488 tr.len(),
2489 va.len()
2490 );
2491
2492 let mut ts = TrainState::new(&fm);
2493 let teacher_ppl = match &tfm {
2494 Some(t) => t.val_ppl(va, None, false, hp.bs, 2, hp.seq),
2495 None => fm.val_ppl(va, None, false, hp.bs, 2, hp.seq),
2496 };
2497 let ppl_start = fm.val_ppl(va, Some(&ts), true, hp.bs, 2, hp.seq);
2498 tracing::info!(
2499 "fcd: quick-val teacher ppl {teacher_ppl:.2} | zero-shot o1 student ppl {ppl_start:.2}"
2500 );
2501
2502 let mut gate_state: Option<(Pipeline, Vec<f64>)> = match gate {
2504 Some(g) if !g.prompts.is_empty() => {
2505 let greedy = SamplerConfig {
2506 temperature: 0.0,
2507 top_p: 1.0,
2508 presence_penalty: 0.0,
2509 top_k: 0,
2510 repetition_penalty: 1.0,
2511 min_p: 0.0,
2512 seed: Some(0),
2513 suppress_tokens: Vec::new(),
2514 };
2515 let mut pipe = Pipeline::from_model(model, greedy)
2516 .map_err(|e| format!("gen-gate pipeline: {e}"))?;
2517 if !hp.polish_only {
2518 pipe.set_o1(Some(o1.clone()));
2519 }
2520 apply_trainables(&mut pipe, &fm, &ts);
2521 let base = gate_gen_scores(&mut pipe, g)?;
2522 tracing::info!("fcd gen-gate baseline loop-scores: {base:?}");
2523 Some((pipe, base))
2524 }
2525 Some(_) => {
2526 tracing::warn!("fcd gen-gate requested but val stream too short — gate off");
2527 None
2528 }
2529 None => None,
2530 };
2531 let init_snapshot: Option<Vec<Vec<f32>>> = gate_state.is_some().then(|| ts.data.clone());
2533 let mut gate_evals: Vec<(usize, f64, Vec<f64>, bool)> = Vec::new();
2534
2535 let mut rng = SplitMix64::new(hp.seed);
2536 let mut best: (f64, Option<Vec<Vec<f32>>>, usize) = (f64::INFINITY, None, 0);
2537 let mut losses: Vec<(f64, f64)> = Vec::with_capacity(hp.steps);
2538 let t0 = std::time::Instant::now();
2539 let n_per_step = hp.bs * hp.seq;
2540 for st in 1..=hp.steps {
2541 let mut ids = Vec::with_capacity(n_per_step);
2544 let mut tgt = Vec::with_capacity(n_per_step);
2545 for _ in 0..hp.bs {
2546 let off = (rng.next_u64() as usize) % (tr.len() - hp.seq - 1);
2547 ids.extend_from_slice(&tr[off..off + hp.seq]);
2548 tgt.extend_from_slice(&tr[off + 1..off + hp.seq + 1]);
2549 }
2550
2551 let ht = match &tfm {
2552 Some(t) => t.forward_hidden(&ids, hp.bs, hp.seq, None, false, None),
2553 None => fm.forward_hidden(&ids, hp.bs, hp.seq, None, false, None),
2554 };
2555 let mut keep: Vec<Vec<f32>> = Vec::with_capacity(fm.nl);
2556 let hs = fm.forward_hidden(&ids, hp.bs, hp.seq, Some(&ts), true, Some(&mut keep));
2557 let (ce, kl, dhs) = fm.loss_and_dhidden(&hs, &ht, &tgt, hp.kl_w);
2558 ts.zero_grad();
2559 fm.backward(hp.bs, hp.seq, &keep, dhs, &mut ts);
2560 let gn = ts.clip_and_step(hp.lr);
2561 losses.push((ce, kl));
2562
2563 let el = t0.elapsed().as_secs_f64();
2564 tracing::info!(
2565 "fcd step {st}/{}: ce {ce:.3} kl {kl:.3} |g| {gn:.3} ({:.1}s/step)",
2566 hp.steps,
2567 el / st as f64
2568 );
2569 if hp.eval_every > 0 && st % hp.eval_every == 0 {
2570 let p = fm.val_ppl(va, Some(&ts), true, hp.bs, 2, hp.seq);
2571 match (&mut gate_state, gate) {
2572 (Some((pipe, base)), Some(g)) => {
2573 apply_trainables(pipe, &fm, &ts);
2574 let scores = gate_gen_scores(pipe, g)?;
2575 let pass = gate_pass(&scores, base, g.threshold, g.baseline_slack);
2576 let tag = if pass && p < best.0 {
2577 best = (p, Some(ts.data.clone()), st);
2578 " *best*"
2579 } else {
2580 ""
2581 };
2582 tracing::info!(
2583 "fcd eval step {st}: val ppl {p:.2} | gen-gate {} (loop-scores {scores:?}){tag}",
2584 if pass { "PASS" } else { "FAIL" }
2585 );
2586 gate_evals.push((st, p, scores, pass));
2587 }
2588 _ => {
2589 let tag = if p < best.0 {
2590 best = (p, Some(ts.data.clone()), st);
2591 " *best*"
2592 } else {
2593 ""
2594 };
2595 tracing::info!("fcd eval step {st}: val ppl {p:.2}{tag}");
2596 }
2597 }
2598 }
2599 }
2600
2601 let mut gate_chosen: Option<usize> = None;
2605 if let Some(snap) = best.1.take() {
2606 ts.data = snap;
2607 gate_chosen = Some(best.2);
2608 tracing::info!(
2609 "fcd: restored best checkpoint from step {} (val ppl {:.2})",
2610 best.2,
2611 best.0
2612 );
2613 } else if let Some(init) = init_snapshot {
2614 ts.data = init;
2615 tracing::info!(
2616 "fcd: polish rejected by generation gate — identity artifact (zero-shot state written; claim 13 floor)"
2617 );
2618 }
2619 let ppl_final = fm.val_ppl(va, Some(&ts), true, hp.bs, 6, hp.seq);
2620 let report = FcdReport {
2621 converted: converted.clone(),
2622 teacher_ppl,
2623 ppl_start,
2624 ppl_best: best.0.min(ppl_final),
2625 best_step: best.2,
2626 ppl_final,
2627 steps_run: hp.steps,
2628 sec_per_step: t0.elapsed().as_secs_f64() / hp.steps.max(1) as f64,
2629 losses,
2630 gate: gate_state.map(|(_, base)| GateReport {
2631 baseline: base,
2632 evals: gate_evals,
2633 chosen: gate_chosen,
2634 }),
2635 };
2636 save_polished(model, out, &fm, &ts, o1, hp, &report)?;
2637 Ok(report)
2638}
2639
2640fn apply_trainables(pipe: &mut Pipeline, fm: &FcdModel, ts: &TrainState) {
2644 let hidden = fm.hidden;
2645 for (slot, &li) in ts.layers.iter().enumerate() {
2646 let b = slot * PARAMS_PER_LAYER;
2647 let inter = fm.layers[li].inter;
2648 let lw = &mut pipe.weights.layers[li];
2649 lw.input_norm = ts.data[b].clone();
2650 lw.post_norm = ts.data[b + 1].clone();
2651 lw.ffn = FfnKind::Dense(DenseFfn {
2652 gate_proj: QTensor::from_f32(ts.data[b + 2].clone(), inter, hidden),
2653 up_proj: QTensor::from_f32(ts.data[b + 3].clone(), inter, hidden),
2654 down_proj: QTensor::from_f32(ts.data[b + 4].clone(), hidden, inter),
2655 act: crate::pipeline::Act::Silu,
2656 down_t: None,
2657 segs: Vec::new(),
2658 });
2659 }
2660}
2661
2662fn gate_gen_scores(pipe: &mut Pipeline, g: &GenGateCfg) -> Result<Vec<f64>, String> {
2664 g.prompts
2665 .iter()
2666 .map(|p| {
2667 pipe.generate_from_ids(p, g.gen_tokens, None, None)
2668 .map(|r| loop_score(&r.token_ids))
2669 })
2670 .collect()
2671}
2672
2673fn save_polished(
2678 model: &CmfModel,
2679 out: &std::path::Path,
2680 fm: &FcdModel,
2681 ts: &TrainState,
2682 o1: &O1Cfg,
2683 hp: &FcdHyper,
2684 report: &FcdReport,
2685) -> Result<(), String> {
2686 use cortiq_core::format::TensorSpec;
2687 let mut replace: std::collections::HashMap<String, (usize, usize)> =
2688 std::collections::HashMap::new(); for (s, &li) in ts.layers.iter().enumerate() {
2690 let p = format!("model.layers.{li}.");
2691 for (k, suffix) in [
2692 (0usize, "input_layernorm.weight"),
2693 (1, "post_attention_layernorm.weight"),
2694 (2, "mlp.gate_proj.weight"),
2695 (3, "mlp.up_proj.weight"),
2696 (4, "mlp.down_proj.weight"),
2697 ] {
2698 replace.insert(format!("{p}{suffix}"), (s, k));
2699 }
2700 }
2701 let mut specs = Vec::with_capacity(model.tensors.len());
2702 for t in &model.tensors {
2703 if let Some(&(s, k)) = replace.get(&t.name) {
2704 let data = &ts.data[s * PARAMS_PER_LAYER + k];
2705 let mut bytes = Vec::with_capacity(data.len() * 4);
2706 for v in data {
2707 bytes.extend_from_slice(&v.to_le_bytes());
2708 }
2709 specs.push(TensorSpec {
2710 name: t.name.clone(),
2711 dtype: TensorDtype::F32,
2712 shape: t.shape.clone(),
2713 data: bytes,
2714 });
2715 } else {
2716 specs.push(TensorSpec {
2717 name: t.name.clone(),
2718 dtype: t.dtype,
2719 shape: t.shape.clone(),
2720 data: model.entry_bytes(t).to_vec(),
2721 });
2722 }
2723 }
2724
2725 let mut header = model.header.clone();
2726 let mut prov = match header.provenance.take() {
2727 Some(serde_json::Value::Object(m)) => m,
2728 _ => serde_json::Map::new(),
2729 };
2730 let layers_json = match &o1.layers {
2731 O1Layers::All => serde_json::json!("all"),
2732 O1Layers::Deep(n) => serde_json::json!(format!("deep{n}")),
2733 O1Layers::List(v) => serde_json::json!(v),
2734 };
2735 prov.insert(
2736 "o1_attn".into(),
2737 serde_json::json!({
2738 "layers": layers_json, "m": o1.m, "w": o1.w, "sink": o1.sink
2739 }),
2740 );
2741 prov.insert(
2742 "fcd".into(),
2743 serde_json::json!({
2744 "steps": hp.steps, "lr": hp.lr, "kl_w": hp.kl_w,
2745 "bs": hp.bs, "seq": hp.seq,
2746 "teacher_ppl": report.teacher_ppl,
2747 "ppl_start": report.ppl_start,
2748 "ppl_final": report.ppl_final,
2749 "best_step": report.best_step,
2750 "converted_layers": report.converted,
2751 }),
2752 );
2753 header.provenance = Some(serde_json::Value::Object(prov));
2754 let _ = fm; let masks = if model.masks.masks.is_empty() {
2757 None
2758 } else {
2759 Some(&model.masks)
2760 };
2761 CmfModel::write(out, &header, &specs, masks, model.vocab.as_deref())
2762 .map_err(|e| format!("writing polished cmf: {e}"))
2763}
2764
2765#[cfg(test)]
2766mod tests {
2767 use super::*;
2768
2769 #[test]
2773 fn polish_only_keeps_recurrent_layers_trainable() {
2774 let is_full: Vec<bool> = (0..8).map(|i| i % 4 == 3).collect();
2777
2778 let mut converting = vec![true; 8];
2779 apply_layer_eligibility(&mut converting, &is_full, false);
2780 assert_eq!(
2781 converting.iter().filter(|&&b| b).count(),
2782 2,
2783 "an O(1) conversion may only take the full-attention layers"
2784 );
2785
2786 let mut polishing = vec![true; 8];
2787 apply_layer_eligibility(&mut polishing, &is_full, true);
2788 assert_eq!(
2789 polishing.iter().filter(|&&b| b).count(),
2790 8,
2791 "a polish trains FFN and norms, which every layer has"
2792 );
2793
2794 let mut none = vec![false; 8];
2796 apply_layer_eligibility(&mut none, &is_full, true);
2797 assert!(none.iter().all(|&b| !b));
2798 }
2799
2800 #[test]
2804 fn the_container_ceiling_wins_over_the_host_figure() {
2805 let host = 944 * 1_000_000_000u64;
2806 let cgroup = 117 * 1_000_000_000u64;
2807 assert_eq!(effective_total(host, Some(cgroup)), cgroup);
2808 assert_eq!(effective_total(host, None), host);
2810 assert_eq!(effective_total(host, Some(host * 2)), host);
2812 }
2813
2814 #[test]
2818 fn container_headroom_is_the_limit_minus_what_it_holds() {
2819 let host_free = 800 * 1_000_000_000u64;
2820 let limit = 117 * 1_000_000_000u64;
2821 let used = 79 * 1_000_000_000u64;
2822 assert_eq!(
2823 effective_available(host_free, Some(limit), Some(used)),
2824 38 * 1_000_000_000u64
2825 );
2826 assert_eq!(
2828 effective_available(host_free, Some(limit), Some(limit + 1)),
2829 0
2830 );
2831 assert_eq!(effective_available(host_free, Some(limit), None), limit);
2833 assert_eq!(effective_available(host_free, None, None), host_free);
2835 }
2836
2837 #[test]
2839 fn gate_selects_lowest_ppl_among_passing() {
2840 let base = vec![0.10, 0.00, 0.20];
2841 let evals = vec![
2842 (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]), ];
2847 let sel = select_checkpoint(&evals, &base, 0.35, 0.10);
2848 assert_eq!(sel, Some(2), "step 75 is the lowest-ppl PASSING checkpoint");
2849 }
2850
2851 #[test]
2854 fn gate_all_fail_is_identity() {
2855 let base = vec![0.0, 0.0, 0.0];
2856 let evals = vec![
2857 (25usize, 15.0, vec![0.50, 0.0, 0.0]),
2858 (50, 14.0, vec![0.0, 0.36, 0.0]),
2859 (75, 13.0, vec![0.0, 0.0, 0.11]), ];
2861 assert_eq!(select_checkpoint(&evals, &base, 0.35, 0.10), None);
2862 }
2863
2864 #[test]
2867 fn gate_boundaries_and_tie_break() {
2868 let base = vec![0.25];
2869 assert!(gate_pass(&[0.35], &base, 0.35, 0.10), "== threshold passes");
2870 assert!(
2871 gate_pass(&[0.35], &[0.25], 0.35, 0.10),
2872 "== base+slack passes"
2873 );
2874 assert!(!gate_pass(&[0.351], &base, 0.35, 0.10));
2875 assert!(!gate_pass(&[0.30], &[0.10], 0.35, 0.10), "0.30 > 0.10+0.10");
2876 let evals = vec![(25usize, 20.0, vec![0.10]), (50, 20.0, vec![0.10])];
2877 assert_eq!(
2878 select_checkpoint(&evals, &base, 0.35, 0.10),
2879 Some(0),
2880 "equal ppl → earliest checkpoint"
2881 );
2882 }
2883}
2884
2885
2886pub fn run_polish(
2889 model: &Arc<CmfModel>,
2890 o1: &O1Cfg,
2891 hp: &FcdHyper,
2892 tr: &[u32],
2893 va: &[u32],
2894 out: &std::path::Path,
2895 gate: Option<&GenGateCfg>,
2896) -> Result<FcdReport, String> {
2897 run_polish_distilled(model, None, o1, hp, tr, va, out, gate)
2898}