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