1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11use crate::kv_cache::KvCache;
12use crate::linear_core::{
13 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
14 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
15 vmf_phase_pair,
16};
17use crate::pool::Pool;
18use crate::qtensor::QTensor;
19use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
20use crate::tokenizer::Tokenizer;
21use cortiq_core::mask::TaskMask;
22use cortiq_core::types::NormStyle;
23
24pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
25 std::sync::atomic::AtomicBool::new(false);
26
27struct ForwardScratch {
31 n1: Vec<f32>,
32 n2: Vec<f32>,
33 p1: Vec<f32>,
34 p2: Vec<f32>,
35}
36
37impl ForwardScratch {
38 fn new(hidden: usize) -> Self {
39 Self {
40 n1: vec![0.0; hidden],
41 n2: vec![0.0; hidden],
42 p1: vec![0.0; hidden],
43 p2: vec![0.0; hidden],
44 }
45 }
46}
47
48pub struct Pipeline {
50 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
55 pub tokenizer: std::sync::Arc<Tokenizer>,
58 pub kv_cache: KvCache,
59 pub sampler_config: SamplerConfig,
60 pub weights: PipelineWeights,
61 pub hidden_size: usize,
62 pub intermediate_size: usize,
63 pub num_heads: usize,
64 pub num_kv_heads: usize,
65 pub head_dim: usize,
66 pub num_layers: usize,
68 pub physical_layers: usize,
70 pub loop_final_norm: bool,
72 pub vocab_size: usize,
73 pub rms_eps: f64,
74 pub rope_base: f32,
75 pub norm_style: NormStyle,
76 pub rotary_dim: usize,
78 pub attention_heads_per_layer: Option<Vec<usize>>,
80 pub vmf_cfg: Option<VmfPhaseCfg>,
82 pub gdn_cfg: Option<GdnCfg>,
84 pub logit_multiplier: Option<f32>,
86 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
91 graph_failed: std::sync::atomic::AtomicBool,
96 pub kv_history: Vec<u32>,
101 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
103 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
106 pub dsv4: Option<
110 Box<(
111 crate::dsv4::Dsv4Globals,
112 Vec<crate::dsv4::Dsv4Layer>,
113 crate::dsv4::Dsv4Cfg,
114 crate::dsv4::Dsv4State,
115 )>,
116 >,
117 pub qwen4_exp: Option<
120 Box<(
121 crate::qwen4_exp::Globals,
122 Vec<crate::qwen4_exp::Layer>,
123 crate::qwen4_exp::Cfg,
124 crate::qwen4_exp::State,
125 )>,
126 >,
127 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
131 pub dspark: Option<crate::dsv4::DsparkState>,
133 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
136 pub dspark_hist: Vec<usize>,
138 pub dspark_real: Vec<u32>,
142 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
146 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
148 pub dspark_draft_ns: u128,
152 pub short_conv_cfg: Option<ShortConvCfg>,
155 pub mtp: Option<MtpModule>,
157 pub speculative: bool,
159 rng: SplitMix64,
160 sampler_scratch: SamplerScratch,
161 spec_forced: Option<u32>,
167 spec_q: Vec<Vec<f32>>,
168 spec_p: Vec<f32>,
169 spec_res: Vec<f32>,
170 spec_qs: Vec<sampler::Sparse>,
172 spec_ps: sampler::Sparse,
173 spec_ress: sampler::Sparse,
174 mtp_graph_mode: Option<bool>,
181 #[cfg(target_os = "macos")]
184 metal_verify: Option<MetalVerifyPending>,
185 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
189 ws: ForwardScratch,
193 pool: Option<std::sync::Arc<Pool>>,
195 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
199 pub(crate) dyn_force_f32: bool,
201 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
206 pub(crate) dyn_active: Option<usize>,
212 pub(crate) dyn_blend_loaded: bool,
216 pub(crate) dyn_phi_layer: Option<usize>,
219 dyn_phi_ema: Vec<f32>,
221 dyn_phi_seen: usize,
222 pub dyn_router: Option<crate::swarm::DynRouter>,
225 o1_cfg: Option<crate::nystrom::O1Cfg>,
228 o1_epoch: u64,
231 o1_flags: Vec<bool>,
233 trace: bool,
236 calib_temp: f32,
239 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
241 graph_kv_id: u64,
242 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
245 graph_want_logits: bool,
246 graph_logits: Option<Vec<f32>>,
249 pub embed_multiplier: f32,
251 pub attn_scale: f32,
254 pub swa: Option<(usize, usize)>,
257 pub sliding_layers: Option<Vec<bool>>,
260 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
263 pub rotary_dim_local: Option<usize>,
264 pub rope_scale: f32,
265 pub rope_scale_local: f32,
266 pub global_attn: Option<(usize, usize)>,
269 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
272 pub attn_v_norm: bool,
274 pub final_softcap: Option<f32>,
276 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
280 pub attn_softcap: f32,
282 confidence_on: bool,
286 #[cfg(test)]
289 nll_test_fail_at: Option<usize>,
290 #[cfg(test)]
293 nll_test_force_serial: bool,
294}
295
296#[cfg(target_os = "macos")]
297impl Drop for Pipeline {
298 fn drop(&mut self) {
299 crate::gpu::kv_mirror_drop(self.graph_kv_id);
300 }
301}
302
303pub struct PipelineWeights {
308 pub embed_tokens: QTensor,
310 pub layers: Vec<LayerWeights>,
312 pub lm_head: QTensor,
314 pub final_norm: Vec<f32>,
316}
317
318pub struct LayerWeights {
320 pub input_norm: Vec<f32>,
321 pub post_norm: Vec<f32>,
324 pub attn_out_norm: Option<Vec<f32>>,
327 pub layer_scale: Option<f32>,
329 pub ffn_out_norm: Option<Vec<f32>>,
332 pub ffn: FfnKind,
333 pub attn: AttnKind,
334}
335
336#[derive(Clone, Copy, PartialEq, Debug, Default)]
339pub enum Act {
340 #[default]
341 Silu,
342 GeluTanh,
343 Situ {
346 beta: f32,
347 linear_beta: f32,
348 },
349}
350
351impl Act {
352 pub fn from_arch(name: &str) -> Self {
353 if name == "gelu_tanh" {
354 Self::GeluTanh
355 } else {
356 Self::Silu
357 }
358 }
359
360 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
362 match arch.hidden_act.as_str() {
363 "situ" => Self::Situ {
364 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
365 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
366 },
367 other => Self::from_arch(other),
368 }
369 }
370
371 #[inline]
372 pub fn apply(self, x: f32) -> f32 {
373 match self {
374 Self::Silu => inference::silu(x),
375 Self::GeluTanh => inference::gelu_tanh(x),
376 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
377 }
378 }
379
380 #[inline]
383 pub fn combine(self, g: f32, u: f32) -> f32 {
384 match self {
385 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
386 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
387 }
388 _ => self.apply(g) * u,
389 }
390 }
391}
392
393pub struct DenseFfn {
395 pub gate_proj: QTensor,
396 pub up_proj: QTensor,
397 pub down_proj: QTensor,
398 pub act: Act,
400 pub down_t: Option<QTensor>,
406 pub segs: Vec<FfnSeg>,
413}
414
415pub struct FfnSeg {
420 pub gate: QTensor,
421 pub up: QTensor,
422 pub down: QTensor,
423 pub start: usize,
424 pub width: usize,
425}
426
427pub enum FfnKind {
430 Dense(DenseFfn),
431 Moe(MoeFfn),
435 DenseMoe(Box<DenseMoeFfn>),
442}
443
444pub struct DenseMoeFfn {
446 pub dense: DenseFfn,
447 pub moe: MoeFfn,
448 pub post_norm_1: Vec<f32>,
450 pub pre_norm_2: Vec<f32>,
453 pub post_norm_2: Vec<f32>,
455}
456
457pub struct MoeFfn {
458 pub router: QTensor,
460 pub experts: Vec<DenseFfn>,
461 pub top_k: usize,
462 pub norm_topk_prob: bool,
463 pub router_sigmoid: bool,
466 pub expert_bias: Option<Vec<f32>>,
470 pub routed_scaling: f32,
473 pub route_tau: Option<f32>,
479 pub shared: Option<(DenseFfn, Option<QTensor>)>,
482 pub stats: std::cell::RefCell<Vec<u64>>,
486 pub act_sq: std::cell::RefCell<Vec<f64>>,
493 pub act_rows: std::cell::RefCell<Vec<f32>>,
499 pub mask: Option<Vec<bool>>,
504 pub per_expert_scale: Option<Vec<f32>>,
507 pub router_input_norm: bool,
511 pub resonance: Option<Resonance>,
515}
516
517pub struct Resonance {
519 pub mu: Vec<f32>,
521 pub u: Vec<f32>,
523 pub k: usize,
524 pub bias: Vec<f32>,
526}
527
528impl Resonance {
529 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
531 let h = x.len();
532 let ne = out.len();
533 for e in 0..ne {
534 let mu = &self.mu[e * h..(e + 1) * h];
535 let mut d2 = 0.0f32;
536 for j in 0..h {
537 let d = x[j] - mu[j];
538 d2 += d * d;
539 }
540 let mut proj = 0.0f32;
541 for i in 0..self.k {
542 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
543 let mut p = 0.0f32;
544 for j in 0..h {
545 p += (x[j] - mu[j]) * u[j];
546 }
547 proj += p * p;
548 }
549 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
550 }
551 }
552}
553
554pub enum AttnKind {
557 Full {
559 wq: QTensor,
560 wk: QTensor,
561 wv: QTensor,
562 wo: QTensor,
563 q_norm: Option<Vec<f32>>,
564 k_norm: Option<Vec<f32>>,
565 output_gate: bool,
566 softplus_gate: Option<(QTensor, bool)>,
570 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
572 },
573 Linear(VmfPhaseWeights),
575 LinearGdn(GdnWeights),
577 ShortConv(ShortConvWeights),
580 Mla(Box<MlaWeights>),
588 Kda(Box<crate::linear_core::KdaWeights>),
592}
593
594pub struct MlaWeights {
596 pub q_proj: QTensor,
600 pub q_a: Option<QTensor>,
603 pub q_a_norm: Option<Vec<f32>>,
604 pub kv_a: QTensor,
606 pub kv_a_norm: Vec<f32>,
608 pub kv_b: QTensor,
610 pub o_proj: QTensor,
612 pub nh: usize,
613 pub qk_rope: usize,
614 pub qk_nope: usize,
615 pub v_dim: usize,
616 pub lora: usize,
617 pub scale: f32,
619 pub nope: bool,
621}
622
623pub struct MtpModule {
628 pub enorm: Vec<f32>,
629 pub hnorm: Vec<f32>,
630 pub eh_proj: QTensor,
632 pub layer: LayerWeights,
633 pub final_norm: Vec<f32>,
634 pub kv: crate::kv_cache::LayerKvCache,
635}
636
637#[cfg(target_os = "macos")]
644enum MetalRowsItem<'a> {
645 Gdn {
646 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
647 first: usize,
648 },
649 Attn {
650 l: crate::gpu_metal::AttnGpuLayer<'a>,
651 li: usize,
652 q_norm: Option<&'a [f32]>,
653 k_norm: Option<&'a [f32]>,
654 output_gate: bool,
655 },
656}
657
658#[cfg(target_os = "macos")]
659struct MetalVerifyPending {
660 graph: crate::gpu_metal::VerifyGraph,
661 gdn_layers: Vec<usize>,
662 attn_layers: Vec<(usize, usize)>,
663}
664
665#[derive(Clone, Copy)]
669enum SpecTrial {
670 Spec {
671 t0: std::time::Instant,
672 gen0: usize,
673 rounds: usize,
674 },
675 Plain {
676 t0: std::time::Instant,
677 gen0: usize,
678 },
679 Decided {
680 spec: bool,
681 recheck_at: usize,
682 },
683}
684
685#[derive(Default, Clone, Copy)]
696struct SpecMon {
697 round_ms: f64,
698 tokens: f64,
699 plain_ms: f64,
700 n: u32,
701 fails: u32,
702}
703
704impl SpecMon {
705 fn round(&mut self, dt_ms: f64, produced: usize) {
706 self.n += 1;
707 if self.n == 1 {
708 return; }
710 let a = if self.n == 2 { 1.0 } else { 0.3 };
711 self.round_ms += a * (dt_ms - self.round_ms);
712 self.tokens += a * (produced as f64 - self.tokens);
713 }
714 fn pays(&self) -> bool {
715 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
716 }
717}
718
719pub struct GenerateResult {
721 pub text: String,
722 pub token_ids: Vec<u32>,
723 pub prompt_tokens: usize,
724 pub tokens_generated: usize,
725 pub finish_reason: String,
726 pub mtp_drafted: usize,
728 pub mtp_accepted: usize,
729 pub token_confidence: Vec<f32>,
734 pub traces: Vec<TokenTrace>,
737}
738
739#[derive(Clone, Debug)]
744pub struct TokenTrace {
745 pub t: usize,
747 pub token_id: u32,
749 pub confidence: f32,
751 pub active_skill: Option<String>,
753 pub recon: Option<f32>,
757 pub switched: bool,
760}
761
762#[cfg_attr(not(test), allow(dead_code))]
767fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
768 let t = if temp > 1e-3 { temp } else { 1.0 };
769 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
770 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
771 if sum > 0.0 {
772 (((logits[id as usize] - max) / t).exp()) / sum
773 } else {
774 0.0
775 }
776}
777
778fn prefill_batched() -> bool {
781 std::env::var("CMF_PREFILL")
782 .map(|v| v != "seq")
783 .unwrap_or(true)
784}
785
786#[derive(Clone, Copy)]
790enum PrefillIn<'a> {
791 Ids(&'a [u32]),
792 Hidden(&'a [f32]),
793}
794
795impl Pipeline {
802 fn can_prefill_batched(&self) -> bool {
803 #[cfg(test)]
804 let force_serial = self.nll_test_force_serial;
805 #[cfg(not(test))]
806 let force_serial = false;
807 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
808 }
809
810 fn automatic_gpu_prefix(&self) -> Option<usize> {
813 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
814 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
815 }
816}
817
818pub fn prefill_chunk() -> usize {
825 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
826 .ok()
827 .and_then(|v| v.parse::<usize>().ok())
828 {
829 return n.max(1);
830 }
831 if cfg!(target_os = "macos") {
832 512
833 } else if cfg!(target_arch = "aarch64") {
834 256
837 } else {
838 48
839 }
840}
841
842#[inline]
848fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
849 if end <= start || start >= input_len {
850 return 0;
851 }
852 let rows = (end.min(input_len) - start).min(input_len - start);
853 if end < input_len {
854 rows
855 } else {
856 rows.saturating_sub(1)
857 }
858}
859
860pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
862
863impl Pipeline {
864 fn clear_sequence_state(&mut self) {
872 self.kv_cache.clear();
873 self.kv_history.clear();
874 crate::gpu::graph_kv_reset(self.graph_kv_id);
875 crate::gpu::graph_kv_reset(self.mtp_kv_id());
880 }
881
882 fn finish_generation(
888 &mut self,
889 mtp: &mut Option<MtpModule>,
890 router: &mut Option<crate::swarm::DynRouter>,
891 clear_sequence: bool,
892 ) {
893 if router.is_some() {
897 let _ = self.set_active_skill(None);
898 }
899 if clear_sequence {
900 self.clear_sequence_state();
901 if let Some(m) = mtp.as_mut() {
902 m.kv.clear();
908 }
909 if let Some(m) = self.mtp.as_mut() {
910 m.kv.clear();
914 }
915 }
916 self.graph_want_logits = false;
917 self.graph_logits = None;
918 self.graph_failed
919 .store(false, std::sync::atomic::Ordering::Relaxed);
920 self.cancel
921 .store(false, std::sync::atomic::Ordering::Relaxed);
922 self.dyn_router = router.take().or(self.dyn_router.take());
923 self.mtp = mtp.take().or(self.mtp.take());
924 self.mtp_graph_mode = None;
925 self.spec_forced = None;
926 }
927
928 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
932 if self
933 .graph_failed
934 .swap(false, std::sync::atomic::Ordering::Relaxed)
935 {
936 self.cancel
937 .store(false, std::sync::atomic::Ordering::Relaxed);
938 self.clear_sequence_state();
939 self.graph_logits = None;
940 self.graph_want_logits = false;
941 return Err(format!("GPU graph failed during {phase} at position {pos}"));
942 }
943 Ok(())
944 }
945
946 fn nll_begin(&mut self) -> Result<(), String> {
951 if self
952 .graph_failed
953 .swap(false, std::sync::atomic::Ordering::Relaxed)
954 {
955 self.cancel
956 .store(false, std::sync::atomic::Ordering::Relaxed);
957 self.clear_sequence_state();
958 self.graph_logits = None;
959 self.graph_want_logits = false;
960 return Err("GPU graph failed before NLL scoring".to_string());
961 }
962 self.clear_sequence_state();
963 self.graph_logits = None;
964 self.graph_want_logits = false;
965 Ok(())
966 }
967
968 fn nll_end(&mut self) {
972 self.clear_sequence_state();
973 self.graph_logits = None;
974 self.graph_want_logits = false;
975 self.graph_failed
976 .store(false, std::sync::atomic::Ordering::Relaxed);
977 }
978
979 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
982 #[cfg(test)]
983 if self.nll_test_fail_at == Some(pos) {
984 self.nll_test_fail_at = None;
985 self.graph_failed
986 .store(true, std::sync::atomic::Ordering::Relaxed);
987 self.cancel
988 .store(true, std::sync::atomic::Ordering::Relaxed);
989 }
990 if self
991 .graph_failed
992 .swap(false, std::sync::atomic::Ordering::Relaxed)
993 {
994 self.cancel
995 .store(false, std::sync::atomic::Ordering::Relaxed);
996 self.clear_sequence_state();
997 self.graph_logits = None;
998 self.graph_want_logits = false;
999 return Err(format!(
1000 "GPU graph failed during NLL {phase} at position {pos}"
1001 ));
1002 }
1003 Ok(())
1004 }
1005
1006 #[inline]
1010 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1011 virtual_idx % self.physical_layers
1012 }
1013
1014 #[inline]
1017 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1018 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1019 }
1020
1021 #[allow(clippy::too_many_arguments)]
1023
1024 #[cfg(target_os = "macos")]
1043 fn graph_prefill_preferred(&self) -> bool {
1044 if !crate::gpu::enabled_here()
1045 || !crate::gpu::q1_force()
1046 || std::env::var("CMF_GPU_BLOCK")
1047 .map(|v| v == "0")
1048 .unwrap_or(false)
1049 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1052 {
1053 return false;
1054 }
1055 self.weights
1056 .layers
1057 .iter()
1058 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
1059 }
1060
1061 #[cfg(not(target_os = "macos"))]
1062 fn graph_prefill_preferred(&self) -> bool {
1063 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1071 if !graph_on || !crate::gpu::enabled_here() {
1072 return false;
1073 }
1074 if self.o1_active() {
1083 return false;
1084 }
1085 self.weights
1086 .layers
1087 .iter()
1088 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1089 }
1090
1091 #[cfg(target_os = "macos")]
1092 fn q1_graph_gpu(
1093 &mut self,
1094 start: usize,
1095 upto: Option<usize>,
1096 position: usize,
1097 h: &mut [f32],
1098 ) -> usize {
1099 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1101 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
1103 || !crate::gpu::q1_force()
1104 || std::env::var("CMF_GPU_BLOCK")
1105 .map(|v| v == "0")
1106 .unwrap_or(false)
1107 {
1108 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1109 eprintln!(
1110 "block-graph: front gate (softcap={} enabled_here={} q1_force={})",
1111 self.attn_softcap > 0.0,
1112 crate::gpu::enabled_here(),
1113 crate::gpu::q1_force(),
1114 );
1115 }
1116 return start;
1117 }
1118 if self.swa.is_some()
1122 || self.global_attn.is_some()
1123 || self.attention_heads_per_layer.is_some()
1124 || self.attn_v_norm
1125 || self.weights.layers.iter().any(|lw| {
1126 lw.attn_out_norm.is_some()
1127 || lw.ffn_out_norm.is_some()
1128 || lw.layer_scale.is_some()
1129 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1130 })
1131 {
1132 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1133 eprintln!(
1134 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1135 self.swa.is_some(),
1136 self.global_attn.is_some(),
1137 self.attention_heads_per_layer.is_some(),
1138 self.attn_v_norm,
1139 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1140 );
1141 }
1142 return start;
1143 }
1144 let limit = upto
1147 .map(|u| u + 1)
1148 .unwrap_or(self.num_layers)
1149 .min(self.num_layers);
1150
1151 enum Item<'a> {
1152 Gdn {
1153 run: Vec<GdnGpuLayer<'a>>,
1154 first: usize,
1155 },
1156 Attn {
1157 l: AttnGpuLayer<'a>,
1158 li: usize,
1159 q_norm: Option<&'a [f32]>,
1160 k_norm: Option<&'a [f32]>,
1161 output_gate: bool,
1162 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1163 full_gpu: bool,
1166 },
1167 }
1168
1169 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1176 let attend_contract = attend_mode != "0"
1177 && attend_mode != "off"
1178 && self.head_dim % 4 == 0
1179 && self.head_dim <= 256
1180 && self.rotary_dim >= 2
1181 && self.rotary_dim <= self.head_dim
1182 && (self.rotary_dim / 2) % 32 == 0
1183 && self.num_kv_heads > 0
1184 && self.num_heads % self.num_kv_heads == 0;
1185
1186 let mut plan: Vec<Item> = Vec::new();
1187 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1188 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1190 let mut scan = start;
1191 while scan < limit {
1192 let lw = &self.weights.layers[self.phys_layer(scan)];
1193 let ffn = match &lw.ffn {
1194 FfnKind::Dense(d) if d.segs.is_empty() => {
1195 let (Some(g), Some(u), Some(dn)) = (
1196 d.gate_proj.q1_parts(),
1197 d.up_proj.q1_parts(),
1198 d.down_proj.q1_parts(),
1199 ) else {
1200 if block_diag {
1201 eprintln!(
1202 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1203 );
1204 }
1205 break;
1206 };
1207 MetalFfn::Dense {
1208 gate: g,
1209 up: u,
1210 down: dn,
1211 }
1212 }
1213 FfnKind::Moe(m) => {
1214 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1215 if block_diag {
1216 eprintln!(
1217 "block-graph: L{scan} MoE outside the graph contract — run ends"
1218 );
1219 }
1220 break;
1221 };
1222 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1223 model_ref.get_or_insert_with(|| model.clone());
1224 }
1225 MetalFfn::Moe(moe)
1226 }
1227 _ => {
1228 if block_diag {
1229 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1230 }
1231 break;
1232 }
1233 };
1234 match &lw.attn {
1235 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1236 let parts = (
1237 w.in_proj_qkv.q1_parts(),
1238 w.in_proj_z.q1_parts(),
1239 w.in_proj_a.f32_parts(),
1240 w.in_proj_b.f32_parts(),
1241 w.out_proj.q1_parts(),
1242 );
1243 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1244 if block_diag {
1245 eprintln!(
1246 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1247 w.in_proj_qkv.q1_parts().is_some(),
1248 w.in_proj_z.q1_parts().is_some(),
1249 w.in_proj_a.f32_parts().is_some(),
1250 w.in_proj_b.f32_parts().is_some(),
1251 w.out_proj.q1_parts().is_some(),
1252 );
1253 }
1254 break;
1255 };
1256 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1257 model_ref.get_or_insert_with(|| model.clone());
1258 }
1259 let gl = GdnGpuLayer {
1260 attn_norm: &lw.input_norm,
1261 post_norm: &lw.post_norm,
1262 qkv,
1263 z,
1264 a,
1265 b,
1266 out,
1267 ffn,
1268 conv1d: &w.conv1d,
1269 a_log: &w.a_log,
1270 dt_bias: &w.dt_bias,
1271 gnorm: &w.norm,
1272 };
1273 match plan.last_mut() {
1274 Some(Item::Gdn { run, .. }) => run.push(gl),
1275 _ => plan.push(Item::Gdn {
1276 run: vec![gl],
1277 first: scan,
1278 }),
1279 }
1280 }
1281 AttnKind::Full {
1282 wq,
1283 wk,
1284 wv,
1285 wo,
1286 q_norm,
1287 k_norm,
1288 output_gate,
1289 softplus_gate: None,
1290 bias,
1291 } if !self.kv_cache.layers[scan].o1_sealed()
1292 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1297 {
1298 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
1299 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1300 break;
1301 };
1302 if let QTensor::Mapped { model, .. } = wq {
1303 model_ref.get_or_insert_with(|| model.clone());
1304 }
1305 let cache = &self.kv_cache.layers[scan];
1306 let o1_metal = cache.o1.is_some()
1310 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1311 && cache.o1_views().is_some();
1312 let full_gpu = attend_contract
1313 && cache.mode == crate::kv_cache::KvMode::F32
1314 && (cache.o1.is_none() || o1_metal)
1315 && bias.is_none()
1316 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1317 && pk.1 == self.num_kv_heads * self.head_dim
1318 && pv.1 == self.num_kv_heads * self.head_dim
1319 && po.2 == self.num_heads * self.head_dim;
1320 plan.push(Item::Attn {
1321 l: AttnGpuLayer {
1322 attn_norm: &lw.input_norm,
1323 post_norm: &lw.post_norm,
1324 wq: pq,
1325 wk: pk,
1326 wv: pv,
1327 wo: po,
1328 ffn,
1329 },
1330 li: scan,
1331 q_norm: q_norm.as_deref(),
1332 k_norm: k_norm.as_deref(),
1333 output_gate: *output_gate,
1334 bias: bias
1335 .as_ref()
1336 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1337 full_gpu,
1338 });
1339 }
1340 _ => break,
1341 }
1342 scan += 1;
1343 }
1344 let Some(model) = model_ref else {
1345 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1346 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1347 }
1348 return start;
1349 };
1350 if plan.is_empty() {
1351 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1352 eprintln!("q1-graph: empty plan at layer {start}");
1353 }
1354 return start;
1355 }
1356 let has_moe = plan.iter().any(|it| match it {
1357 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1358 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1359 });
1360 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1361 let dev_attend = attend_contract
1362 && (self.head_dim <= 128
1363 || has_moe
1364 || (self.head_dim <= 256 && has_gdn)
1370 || attend_mode == "force"
1371 || attend_mode == "256");
1372 if !dev_attend {
1373 for it in &mut plan {
1374 if let Item::Attn { li, full_gpu, .. } = it {
1375 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1378 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1379 if !keep_o1 {
1380 *full_gpu = false;
1381 }
1382 }
1383 }
1384 }
1385 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1386 use std::sync::atomic::{AtomicBool, Ordering};
1387 static SAID: AtomicBool = AtomicBool::new(false);
1388 if !SAID.swap(true, Ordering::Relaxed) {
1389 let fg = plan
1390 .iter()
1391 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1392 .count();
1393 let att = plan
1394 .iter()
1395 .filter(|it| matches!(it, Item::Attn { .. }))
1396 .count();
1397 eprintln!(
1398 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1399 plan.len(),
1400 self.head_dim,
1401 self.rotary_dim,
1402 self.num_kv_heads,
1403 self.num_heads,
1404 );
1405 }
1406 }
1407 let dims = GraphDims {
1408 hidden: self.hidden_size,
1409 eps: self.rms_eps as f32,
1410 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1411 };
1412 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1413 return start;
1414 };
1415 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1416 nv: cfg.num_v_heads,
1417 nk: cfg.num_k_heads,
1418 dk: cfg.key_head_dim,
1419 dv: cfg.value_head_dim,
1420 kk: cfg.conv_kernel,
1421 hidden: self.hidden_size,
1422 inter: self.intermediate_size,
1423 c_dim: cfg.conv_dim(),
1424 eps: cfg.rms_eps as f32,
1425 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1426 });
1427 let mut valid = 0usize;
1431 let mut end = start;
1432 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1434 static ONCE: std::sync::Once = std::sync::Once::new();
1435 ONCE.call_once(|| {
1436 for it in &plan {
1437 match it {
1438 Item::Gdn { first, run } => {
1439 eprintln!("plan: Gdn first={first} len={}", run.len())
1440 }
1441 Item::Attn { li, full_gpu, .. } => {
1442 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1443 }
1444 }
1445 }
1446 });
1447 }
1448 for item in &plan {
1449 let ok = match item {
1450 Item::Gdn { run, .. } => gcfg
1451 .as_ref()
1452 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1453 .unwrap_or(false),
1454 Item::Attn { l, .. } => graph.attn_ok(l),
1455 };
1456 if !ok {
1457 if block_diag {
1458 eprintln!(
1459 "block-graph: plan item {} ({}) failed graph preflight",
1460 valid,
1461 match item {
1462 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1463 Item::Attn { li, .. } => format!("Attn L{li}"),
1464 }
1465 );
1466 }
1467 break;
1468 }
1469 valid += 1;
1470 end += match item {
1471 Item::Gdn { run, .. } => run.len(),
1472 Item::Attn { .. } => 1,
1473 };
1474 }
1475 plan.truncate(valid);
1476 if plan.is_empty() {
1477 return start;
1478 }
1479
1480 let inv_freq = self.inv_freq.clone();
1481 let pool = self.pool.clone();
1482 let (nh, nkv, hd, hs, rd, eps) = (
1483 self.num_heads,
1484 self.num_kv_heads,
1485 self.head_dim,
1486 self.hidden_size,
1487 self.rotary_dim,
1488 self.rms_eps,
1489 );
1490 let norm_style = self.norm_style;
1491 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1492 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1493 let kv_id = self.graph_kv_id;
1494 let mut pending: Vec<(usize, usize)> = Vec::new();
1497 let mut dev_attn: Vec<usize> = Vec::new();
1500 for item in &plan {
1501 let _xt0 = std::time::Instant::now();
1502 let _xkind: u32 = match item {
1503 Item::Gdn { .. } => 2,
1504 Item::Attn { .. } => 3,
1505 };
1506 if self.loop_final_norm {
1508 let item_start = match item {
1509 Item::Gdn { first, .. } => *first,
1510 Item::Attn { li, .. } => *li,
1511 };
1512 if item_start > start && self.is_loop_end(item_start - 1) {
1513 graph.encode_loop_norm(&self.weights.final_norm);
1514 }
1515 }
1516 match item {
1517 Item::Gdn { run, first } => {
1518 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1519 if l.linear_state.len() != want {
1520 l.linear_state = vec![0f32; want];
1521 }
1522 }
1523 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1524 .iter()
1525 .map(|l| l.linear_state.as_slice())
1526 .collect();
1527 let _ig = std::time::Instant::now();
1528 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1529 tracing::error!("q1 graph: GDN run refused after validation");
1531 return start;
1532 }
1533 graph.commit_kind = 2;
1536 graph.commit();
1537 crate::gpu::stageprof(0, _ig.elapsed());
1538 pending.push((*first, run.len()));
1539 }
1540 Item::Attn {
1541 l,
1542 li,
1543 q_norm,
1544 k_norm,
1545 output_gate,
1546 bias,
1547 full_gpu,
1548 } => {
1549 let _ia = std::time::Instant::now();
1550 if *full_gpu {
1552 let cache = &self.kv_cache.layers[*li];
1553 let o1p = if cache.o1.is_some() {
1554 match cache.o1_views() {
1555 Some(views) => Some(crate::gpu::O1AttnParams {
1556 views,
1557 epoch: self.o1_epoch,
1558 }),
1559 None => None,
1561 }
1562 } else {
1563 None
1564 };
1565 let o1_layer = cache.o1.is_some();
1566 if o1_layer && o1p.is_none() {
1567 }
1569 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1570 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1571 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1572 let p = crate::gpu::AttnDeviceParams {
1573 kv_id,
1574 layer: *li,
1575 nh,
1576 nkv,
1577 hd,
1578 rd,
1579 position,
1580 scale: self.attn_scale,
1581 eps: eps as f32,
1582 gemma,
1583 output_gate: *output_gate,
1584 q_norm: *q_norm,
1585 k_norm: *k_norm,
1586 inv_freq: &inv_freq,
1587 cpu_k,
1588 cpu_v,
1589 cpu_stored,
1590 o1: o1p,
1591 };
1592 let o1_bad = o1_layer && p.o1.is_none();
1593 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1594 {
1595 if p.o1.is_none() {
1597 dev_attn.push(*li);
1598 }
1599 graph.commit_kind = 3;
1600 graph.commit();
1601 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1605 continue;
1606 }
1607 }
1609 graph.encode_attn_prefix(l);
1610 graph.sync();
1611 if !pending.is_empty() {
1612 let idxs: Vec<usize> =
1613 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1614 let mut outs: Vec<&mut [f32]> = self
1615 .kv_cache
1616 .layers
1617 .iter_mut()
1618 .enumerate()
1619 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1620 .map(|(_, s)| s.linear_state.as_mut_slice())
1621 .collect();
1622 graph.read_states(&mut outs);
1623 }
1624 let mut q_raw = attention::take_buf(l.wq.1);
1625 let mut k = attention::take_buf(l.wk.1);
1626 let mut v = attention::take_buf(l.wv.1);
1627 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1628 let cfg = QwenAttnCfg {
1629 num_heads: nh,
1630 num_kv_heads: nkv,
1631 head_dim: hd,
1632 hidden_size: hs,
1633 position,
1634 inv_freq: &inv_freq,
1635 rotary_dim: rd,
1636 scale: self.attn_scale,
1637 softcap: self.attn_softcap,
1638 window: None,
1639 v_norm: false,
1640 q_norm: *q_norm,
1641 k_norm: *k_norm,
1642 output_gate: *output_gate,
1643 softplus_gate: None,
1644 rope_scale: 1.0,
1645 bias: *bias,
1646 rms_eps: eps,
1647 norm_style,
1648 pool: pool.as_deref(),
1649 };
1650 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1653 || std::env::var("CMF_ATTN_DUMP").is_ok();
1654 let _ = full_gpu;
1655 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1656 let mut ao = attention::qwen_attention_core(
1657 q_raw,
1658 k,
1659 v,
1660 &mut self.kv_cache.layers[*li],
1661 &cfg,
1662 );
1663 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1667 if let Some((qr0, k0, v0)) = oracle_in.clone() {
1668 let (cq, _cg, _ck, _cv) =
1669 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1670 let cache = &self.kv_cache.layers[*li];
1671 let n = cache.head_keys(0).len() / hd;
1672 let mut bytes: Vec<u8> = Vec::new();
1673 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1674 bytes.extend_from_slice(&v.to_le_bytes());
1675 }
1676 for v in &cq {
1677 bytes.extend_from_slice(&v.to_le_bytes());
1678 }
1679 for g in 0..nkv {
1680 for v in cache.head_keys(g) {
1681 bytes.extend_from_slice(&v.to_le_bytes());
1682 }
1683 }
1684 for g in 0..nkv {
1685 for v in cache.head_values(g) {
1686 bytes.extend_from_slice(&v.to_le_bytes());
1687 }
1688 }
1689 let _ =
1690 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1691 }
1692 }
1693 if let Some((qr0, k0, v0)) =
1694 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1695 {
1696 let (cq, _cg, ck, cv) =
1697 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1698 let mut h_now = vec![0f32; hs];
1699 graph.read_h(&mut h_now);
1700 let cache = &self.kv_cache.layers[*li];
1701 let n_after = cache.head_keys(0).len() / hd;
1702 let stored = n_after.saturating_sub(1);
1706 let cpu_k: Vec<&[f32]> = (0..nkv)
1707 .map(|g| &cache.head_keys(g)[..stored * hd])
1708 .collect();
1709 let cpu_v: Vec<&[f32]> = (0..nkv)
1710 .map(|g| &cache.head_values(g)[..stored * hd])
1711 .collect();
1712 let p = crate::gpu::AttnDeviceParams {
1713 kv_id,
1714 layer: *li,
1715 nh,
1716 nkv,
1717 hd,
1718 rd,
1719 position,
1720 scale: self.attn_scale,
1721 eps: eps as f32,
1722 gemma,
1723 output_gate: *output_gate,
1724 q_norm: *q_norm,
1725 k_norm: *k_norm,
1726 inv_freq: &inv_freq,
1727 cpu_k,
1728 cpu_v,
1729 cpu_stored: stored,
1730 o1: None,
1731 };
1732 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1733 let md = |a: &[f32], b: &[f32]| {
1734 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
1735 };
1736 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1737 eprintln!(
1738 "attn-oracle L{li} pos {position}: |q| {:.2} max|dq| {:.4} | |k| {:.2} max|dk| {:.4} | |v| {:.2} max|dv| {:.4} | |ao| {:.2} max|dao| {:.4}",
1739 nn(&cq),
1740 md(&cq, &dq),
1741 nn(&ck),
1742 md(&ck, &dk),
1743 nn(&cv),
1744 md(&cv, &dv),
1745 nn(&ao),
1746 md(&ao, &dao)
1747 );
1748 } else {
1749 eprintln!("attn-oracle L{li}: device probe declined");
1750 }
1751 }
1752 graph.encode_attn_suffix(l, &ao);
1753 graph.commit();
1756 attention::recycle_buf(&mut ao);
1757 }
1758 }
1759
1760 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1761 }
1762 let mut lm_rows = None;
1767 if self.graph_want_logits
1768 && upto.is_none()
1769 && end == self.num_layers
1770 && std::env::var("CMF_GPU_LMHEAD")
1771 .map(|v| v != "0")
1772 .unwrap_or(true)
1773 {
1774 if let Some(lm) = self.weights.lm_head.q1_parts() {
1775 if graph.lm_head_ok(lm) {
1776 graph.encode_lm_head(&self.weights.final_norm, lm);
1777 lm_rows = Some(lm.1);
1778 }
1779 }
1780 }
1781 let _sy0 = std::time::Instant::now();
1782 graph.sync();
1783 let _rs0 = std::time::Instant::now();
1784 if !pending.is_empty() {
1785 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1786 let mut outs: Vec<&mut [f32]> = self
1787 .kv_cache
1788 .layers
1789 .iter_mut()
1790 .enumerate()
1791 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1792 .map(|(_, s)| s.linear_state.as_mut_slice())
1793 .collect();
1794 graph.read_states(&mut outs);
1795 }
1796 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1797 use std::sync::atomic::{AtomicU64, Ordering};
1798 static SY: AtomicU64 = AtomicU64::new(0);
1799 static RS: AtomicU64 = AtomicU64::new(0);
1800 static N: AtomicU64 = AtomicU64::new(0);
1801 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1802 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1803 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1804 if n % 100 == 0 {
1805 eprintln!(
1806 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1807 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1808 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1809 );
1810 }
1811 }
1812 if let Some(rows) = lm_rows {
1813 crate::gpu::hostprof_encode_done(_mt0);
1814 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1815 graph.read_logits(&mut lg);
1816 crate::gpu::hostprof_total(_mt0);
1817 lg.resize(self.vocab_size, 0.0);
1818 if let Some(c) = self.final_softcap {
1819 for l in lg.iter_mut() {
1820 *l = c * (*l / c).tanh();
1821 }
1822 }
1823 self.graph_logits = Some(lg);
1824 }
1825 graph.finish(h);
1826 for li in dev_attn {
1830 let mut krow = attention::take_buf(nkv * hd);
1831 let mut vrow = attention::take_buf(nkv * hd);
1832 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1833 let cache = &mut self.kv_cache.layers[li];
1834 cache.append(&krow, &vrow, &[]);
1835 let n = cache.seq_len;
1836 let mut imp = attention::take_buf(n);
1837 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1838 cache.accumulate_imp(&imp);
1839 attention::recycle_buf(&mut imp);
1840 }
1841 attention::recycle_buf(&mut krow);
1842 attention::recycle_buf(&mut vrow);
1843 }
1844 end
1845 }
1846
1847 pub fn new(
1848 tokenizer: Tokenizer,
1849 weights: PipelineWeights,
1850 hidden_size: usize,
1851 intermediate_size: usize,
1852 num_heads: usize,
1853 num_kv_heads: usize,
1854 head_dim: usize,
1855 num_layers: usize,
1856 physical_layers: usize,
1857 loop_final_norm: bool,
1858 vocab_size: usize,
1859 rms_eps: f64,
1860 rope_base: f32,
1861 norm_style: NormStyle,
1862 max_seq_len: usize,
1863 sampler_config: SamplerConfig,
1864 ) -> Self {
1865 let rng = match sampler_config.seed {
1866 Some(s) => SplitMix64::new(s),
1867 None => SplitMix64::from_entropy(),
1868 };
1869 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1870 let pool = Pool::from_env();
1871 if let Some(p) = &pool {
1872 tracing::info!("worker pool: {} threads", p.n_workers());
1873 }
1874 Self {
1875 gpu_plan: None,
1876 tokenizer: std::sync::Arc::new(tokenizer),
1877 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1878 sampler_config,
1879 weights,
1880 hidden_size,
1881 intermediate_size,
1882 num_heads,
1883 num_kv_heads,
1884 head_dim,
1885 num_layers,
1886 physical_layers,
1887 loop_final_norm,
1888 vocab_size,
1889 rms_eps,
1890 rope_base,
1891 norm_style,
1892 rotary_dim: head_dim,
1893 attention_heads_per_layer: None,
1894 vmf_cfg: None,
1895 gdn_cfg: None,
1896 kda_cfg: None,
1897 g3n: None,
1898 dsv4: None,
1899 qwen4_exp: None,
1900 dsv4_mtp: Vec::new(),
1901 dspark: None,
1902 dspark_pending: Vec::new(),
1903 dspark_hist: Vec::new(),
1904 dspark_real: Vec::new(),
1905 dspark_trunk_picks: Vec::new(),
1906 dspark_exp: Vec::new(),
1907 dspark_draft_ns: 0,
1908 logit_multiplier: None,
1909 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1910 graph_failed: std::sync::atomic::AtomicBool::new(false),
1911 kv_history: Vec::new(),
1912 short_conv_cfg: None,
1913 mtp: None,
1914 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1915 rng,
1916 sampler_scratch: SamplerScratch::default(),
1917 spec_forced: None,
1918 spec_q: Vec::new(),
1919 spec_p: Vec::new(),
1920 spec_res: Vec::new(),
1921 spec_qs: Vec::new(),
1922 spec_ps: Vec::new(),
1923 spec_ress: Vec::new(),
1924 mtp_graph_mode: None,
1925 #[cfg(target_os = "macos")]
1926 metal_verify: None,
1927 inv_freq,
1928 ws: ForwardScratch::new(hidden_size),
1929 pool,
1930 model: None,
1931 dyn_force_f32: false,
1932 dyn_skill_layers: Vec::new(),
1933 dyn_active: None,
1934 dyn_blend_loaded: false,
1935 dyn_phi_layer: None,
1936 dyn_phi_ema: Vec::new(),
1937 dyn_phi_seen: 0,
1938 dyn_router: None,
1939 o1_cfg: None,
1940 o1_epoch: 0,
1941 o1_flags: Vec::new(),
1942 trace: false,
1943 calib_temp: 1.0,
1944 confidence_on: true,
1945 embed_multiplier: 1.0,
1946 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1947 swa: None,
1948 sliding_layers: None,
1949 inv_freq_local: None,
1950 rotary_dim_local: None,
1951 rope_scale: 1.0,
1952 rope_scale_local: 1.0,
1953 global_attn: None,
1954 inv_freq_global: None,
1955 attn_v_norm: false,
1956 final_softcap: None,
1957 head_clusters: None,
1958 attn_softcap: 0.0,
1959 graph_want_logits: false,
1960 graph_logits: None,
1961 graph_kv_id: {
1962 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1963 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1964 },
1965 #[cfg(test)]
1966 nll_test_fail_at: None,
1967 #[cfg(test)]
1968 nll_test_force_serial: false,
1969 }
1970 }
1971
1972 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1980 if let Some(c) = &cfg {
1981 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
1982 tracing::error!(
1983 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
1984 c.w,
1985 c.sink
1986 );
1987 self.o1_flags.clear();
1988 self.o1_cfg = None;
1989 return;
1990 }
1991 }
1992 self.o1_flags = match &cfg {
1993 Some(c) => {
1994 let mut flags = c.layer_flags(self.num_layers);
1995 for (li, f) in flags.iter_mut().enumerate() {
1996 if *f
1997 && !matches!(
1998 self.weights.layers[self.phys_layer(li)].attn,
1999 AttnKind::Full { .. }
2000 )
2001 {
2002 *f = false;
2003 }
2004 }
2005 flags
2006 }
2007 None => Vec::new(),
2008 };
2009 if let Some(c) = &cfg {
2010 let n = self.o1_flags.iter().filter(|&&f| f).count();
2011 tracing::info!(
2012 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2013 self.num_layers,
2014 c.m,
2015 c.w,
2016 c.sink,
2017 c.rect
2018 );
2019 }
2020 self.o1_cfg = cfg;
2021 }
2022
2023 pub fn o1_active(&self) -> bool {
2025 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2026 }
2027
2028 pub fn generation_graph_prefill(&self) -> bool {
2033 let graph = self.graph_prefill_preferred();
2034 #[cfg(not(target_os = "macos"))]
2042 if graph
2043 && self.mtp.is_some()
2044 && std::env::var("CMF_BATCH_K")
2045 .ok()
2046 .and_then(|v| v.parse::<usize>().ok())
2047 .is_some_and(|k| k > 0)
2048 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2049 {
2050 return false;
2051 }
2052 graph
2053 }
2054
2055 pub fn o1_device_stats(&self) -> (usize, u64) {
2060 crate::gpu::o1_device_stats(self.graph_kv_id)
2061 }
2062
2063 pub fn o1_begin(&mut self) {
2068 self.o1_begin_with_prefix(None);
2069 }
2070
2071 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2075 if let Some(c) = &self.o1_cfg {
2076 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2077 let boundary = requested_prefix.map(|p| {
2078 p.max(
2079 crate::nystrom::o1_deferred_boundary(w, sink)
2080 .expect("o1 config boundary validated in set_o1"),
2081 )
2082 });
2083 for (li, &f) in self.o1_flags.iter().enumerate() {
2084 if f {
2085 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2086 }
2087 }
2088 }
2089 }
2090
2091 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2093 self.o1_cfg.as_ref().and_then(|c| {
2094 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2095 .map(|floor| requested_prefix.max(floor))
2096 })
2097 }
2098
2099 fn o1_note_transition(&mut self) {
2100 let mut transitioned = false;
2104 for (li, &flagged) in self.o1_flags.iter().enumerate() {
2105 if flagged {
2106 transitioned |= self.kv_cache.layers[li].take_o1_transition();
2107 }
2108 }
2109 if transitioned {
2110 self.o1_epoch = self.o1_epoch.wrapping_add(1);
2111 }
2112 }
2113
2114 fn o1_pending(&self) -> bool {
2115 self.o1_flags.iter().enumerate().any(|(li, &f)| {
2116 f && self.kv_cache.layers[li].seq_len > 0
2117 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2118 })
2119 }
2120
2121 fn o1_fail(&mut self, err: String) {
2122 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2123 self.clear_sequence_state();
2124 self.graph_failed
2125 .store(true, std::sync::atomic::Ordering::Relaxed);
2126 self.cancel
2127 .store(true, std::sync::atomic::Ordering::Relaxed);
2128 }
2129
2130 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2135 if self.o1_cfg.is_none() {
2136 return Ok(false);
2137 }
2138 let mut participating = false;
2139 for li in 0..self.num_layers {
2140 if !self.o1_flags.get(li).copied().unwrap_or(false) {
2141 continue;
2142 }
2143 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2144 return Err(err);
2145 }
2146 if self.kv_cache.layers[li].seq_len == 0 {
2147 continue;
2148 }
2149 participating = true;
2150 let num_heads = self.layer_num_heads(li);
2151 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2152 }
2153 self.o1_note_transition();
2154 for li in 0..self.num_layers {
2155 if self.o1_flags.get(li).copied().unwrap_or(false) {
2156 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2157 return Err(err);
2158 }
2159 }
2160 }
2161 Ok(participating
2162 && (0..self.num_layers).all(|li| {
2163 !self.o1_flags.get(li).copied().unwrap_or(false)
2164 || self.kv_cache.layers[li].seq_len == 0
2165 || self.kv_cache.layers[li].o1_sealed()
2166 }))
2167 }
2168
2169 fn o1_progress(&mut self) {
2172 if !self.o1_active() {
2173 return;
2174 }
2175 for li in 0..self.num_layers {
2176 if self.o1_flags.get(li).copied().unwrap_or(false) {
2177 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2178 self.o1_fail(err);
2179 return;
2180 }
2181 }
2182 }
2183 self.o1_note_transition();
2187 if !self.o1_pending() {
2188 return;
2189 }
2190 if let Err(err) = self.o1_seal_checked() {
2191 self.o1_fail(err);
2192 }
2193 }
2194
2195 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2200 if self
2201 .graph_failed
2202 .swap(false, std::sync::atomic::Ordering::Relaxed)
2203 {
2204 self.cancel
2205 .store(false, std::sync::atomic::Ordering::Relaxed);
2206 self.clear_sequence_state();
2207 return Err(format!("{phase}: deferred O(1) transition failed"));
2208 }
2209 Ok(())
2210 }
2211
2212 pub fn o1_seal(&mut self) {
2216 if let Err(err) = self.o1_seal_checked() {
2217 self.o1_fail(err);
2218 }
2219 }
2220
2221 pub fn set_trace(&mut self, on: bool) {
2223 self.trace = on;
2224 }
2225
2226 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2229 self.rng = match config.seed {
2230 Some(seed) => SplitMix64::new(seed),
2231 None => SplitMix64::from_entropy(),
2232 };
2233 self.sampler_config = config;
2234 }
2235
2236 pub fn set_confidence(&mut self, on: bool) {
2241 self.confidence_on = on;
2242 }
2243
2244 pub fn set_calib_temp(&mut self, t: f32) {
2247 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2248 }
2249
2250 pub fn calib_temp(&self) -> f32 {
2252 self.calib_temp
2253 }
2254
2255 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2258 self.rotary_dim = rotary_dim.min(self.head_dim);
2259 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2260 }
2261
2262 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2263 QwenAttnCfg {
2264 num_heads: self.num_heads,
2265 num_kv_heads: self.num_kv_heads,
2266 head_dim: self.head_dim,
2267 hidden_size: self.hidden_size,
2268 position,
2269 inv_freq: &self.inv_freq,
2270 rotary_dim: self.rotary_dim,
2271 scale: self.attn_scale,
2272 softcap: self.attn_softcap,
2273 window: None,
2274 v_norm: false,
2275 q_norm: None,
2276 k_norm: None,
2277 output_gate: false,
2278 softplus_gate: None,
2279 rope_scale: self.rope_scale,
2280 bias: None,
2281 rms_eps: self.rms_eps,
2282 norm_style: self.norm_style,
2283 pool: self.pool.as_deref(),
2284 }
2285 }
2286
2287 pub fn generate(
2289 &mut self,
2290 prompt: &str,
2291 max_tokens: usize,
2292 task_mask: Option<&TaskMask>,
2293 on_token: Option<TokenCallback>,
2294 ) -> Result<GenerateResult, String> {
2295 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2296 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2297 }
2298
2299 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2301 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2302 }
2303
2304 pub fn generate_from_ids(
2312 &mut self,
2313 input_ids: &[u32],
2314 max_tokens: usize,
2315 task_mask: Option<&TaskMask>,
2316 mut on_token: Option<TokenCallback>,
2317 ) -> Result<GenerateResult, String> {
2318 if std::env::var("CMF_TRACE_H").is_ok() {
2319 eprintln!("input_ids: {input_ids:?}");
2320 }
2321 if input_ids.is_empty() {
2322 return Err("empty prompt: nothing to generate from".to_string());
2323 }
2324 self.graph_failed
2328 .store(false, std::sync::atomic::Ordering::Relaxed);
2329 let task_mask = self.drop_open_mask(task_mask);
2334
2335 let reuse_from = {
2343 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2344 let h = &self.kv_history;
2345 if on
2346 && task_mask.is_none()
2347 && self.mtp.is_none()
2348 && self.o1_cfg.is_none()
2349 && !h.is_empty()
2350 && h.len() < input_ids.len()
2351 && input_ids[..h.len()] == h[..]
2352 {
2353 h.len()
2354 } else {
2355 0
2356 }
2357 };
2358 if reuse_from == 0 {
2359 self.clear_sequence_state();
2361 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2362 eprintln!(
2363 "kv-reuse: {} of {} prompt positions already cached",
2364 reuse_from,
2365 input_ids.len()
2366 );
2367 }
2368 crate::gpu::graph_race_begin_generation();
2369 let o1_prefill = if self.o1_active() && task_mask.is_none() {
2373 std::env::var("CMF_O1_PREFILL")
2374 .ok()
2375 .and_then(|v| v.parse::<usize>().ok())
2376 .filter(|&p| p > 0)
2377 } else {
2378 None
2379 };
2380 if task_mask.is_none() {
2381 self.o1_begin_with_prefix(o1_prefill);
2382 }
2383
2384 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2390 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2417 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2418 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2434 for lw in &self.weights.layers {
2435 if let FfnKind::Dense(d) = &lw.ffn {
2436 dense_n += 1;
2437 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2438 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2439 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2440 {
2441 dense_q4tp += 1;
2442 }
2443 }
2444 }
2445 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2446 let penalized = self.sampler_config.repetition_penalty != 1.0
2450 || self.sampler_config.presence_penalty != 0.0
2451 || !self.sampler_config.suppress_tokens.is_empty();
2452 #[cfg(feature = "gpu")]
2457 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2458 #[cfg(not(feature = "gpu"))]
2459 let metal_wgpu = false;
2460 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2461 let spec_wanted = match spec_env.as_deref() {
2462 Some("0") => false,
2463 Some(_) => {
2464 if metal_wgpu {
2465 tracing::warn!(
2466 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2467 verified on this backend (garbage measured on Qwen3.5-0.8B)"
2468 );
2469 }
2470 true
2471 }
2472 None => spec_default_ok && !penalized && !metal_wgpu,
2473 };
2474 #[cfg(target_os = "macos")]
2477 let metal_graph = crate::gpu::q1_force()
2478 && crate::gpu::enabled_here()
2479 && std::env::var("CMF_GPU_BLOCK")
2480 .map(|v| v != "0")
2481 .unwrap_or(true);
2482 #[cfg(not(target_os = "macos"))]
2483 let metal_graph = false;
2484 let graph_spec = self.speculative
2485 && (graph_on || metal_graph)
2486 && self.mtp.is_some()
2487 && task_mask.is_none()
2488 && !self.o1_active()
2489 && spec_sampling_ok
2490 && spec_wanted;
2491 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2498 let spec_active = self.speculative
2499 && self.mtp.is_some()
2500 && task_mask.is_none()
2501 && !self.o1_active()
2502 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2503 let mut mtp = if spec_active { self.mtp.take() } else { None };
2506 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2507 eprintln!(
2508 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2509 mtp.is_some(),
2510 self.speculative,
2511 self.sampler_config.temperature < 1e-6,
2512 );
2513 }
2514 if let Some(m) = &mut mtp {
2515 m.kv.clear();
2516 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2518 self.mtp_graph_mode = None;
2519 }
2520 let mut router = if mtp.is_none() {
2524 self.dyn_router.take()
2525 } else {
2526 None
2527 };
2528 if let Some(r) = &mut router {
2529 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2532 }
2533
2534 let mut all_ids = input_ids.to_vec();
2535 let mut generated = 0usize;
2536 let mut finish_reason = "max_tokens".to_string();
2537 let mut drafted = 0usize;
2538 let mut accepted = 0usize;
2539 let mut dsv4_spec_bad = 0usize;
2546 let mut dsv4_spec_retry_at = 0usize;
2547 let mut confidence: Vec<f32> = Vec::new();
2548 let trace_on = self.trace;
2549 let calib_temp = self.calib_temp;
2550 let mut traces: Vec<TokenTrace> = Vec::new();
2551
2552 let mut hidden = vec![0.0f32; self.hidden_size];
2558 let mut pos = reuse_from;
2559 let fuse_lm = mtp.is_none()
2568 && router.is_none()
2569 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2570 self.graph_logits = None;
2571 self.graph_want_logits = false;
2572 let _tpf = std::time::Instant::now();
2573 let batch_k = std::env::var("CMF_BATCH_K")
2574 .ok()
2575 .and_then(|v| v.parse::<usize>().ok())
2576 .unwrap_or(0);
2577 while self.qwen4_exp.is_some()
2588 && mtp.is_none()
2589 && pos < input_ids.len()
2590 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2591 {
2592 let token_id = input_ids[pos];
2593 let want_logits = pos + 1 == input_ids.len();
2594 let mut lg = Vec::new();
2595 if let Some(b) = &mut self.qwen4_exp {
2596 crate::qwen4_exp::forward_token(
2597 &b.0,
2598 &b.1,
2599 &b.2,
2600 &mut b.3,
2601 token_id,
2602 pos,
2603 &self.inv_freq,
2604 self.pool.as_deref(),
2605 &mut lg,
2606 want_logits,
2607 );
2608 }
2609 if want_logits {
2610 self.graph_logits = Some(lg);
2611 }
2612 pos += 1;
2613 hidden.fill(0.0);
2614 }
2615 while self.dsv4.is_some()
2616 && mtp.is_none()
2617 && pos < input_ids.len()
2618 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2619 {
2620 let end = (pos + prefill_chunk()).min(input_ids.len());
2621 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2622 let mut lg = Vec::new();
2623 if let Some(b) = &mut self.dsv4 {
2624 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2625 crate::dsv4::forward_chunk(
2626 g,
2627 layers,
2628 &cfg,
2629 st,
2630 &ids,
2631 pos,
2632 &self.inv_freq,
2633 self.pool.as_deref(),
2634 &mut lg,
2635 end == input_ids.len(),
2636 );
2637 }
2638 if end == input_ids.len() {
2639 self.graph_logits = Some(lg);
2640 }
2641 pos = end;
2642 hidden = vec![0.0; self.hidden_size];
2643 }
2644 let dyn_prefill = router.is_some();
2649 let o1_prefill_limit = o1_prefill
2657 .and_then(|requested| self.o1_effective_boundary(requested))
2658 .map(|boundary| boundary.min(input_ids.len()));
2659 let mut o1_sealed = false;
2660 if let Some(limit) = o1_prefill_limit {
2661 if self.can_prefill_batched() && limit > 2 {
2664 let chunk = prefill_chunk();
2665 let hs = self.hidden_size;
2666 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2667 let end = (pos + chunk).min(limit);
2668 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2669 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2670 pos = end;
2671 }
2672 } else {
2673 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2674 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
2675 pos += 1;
2676 }
2677 }
2678 if pos >= limit {
2679 o1_sealed = match self.o1_seal_checked() {
2680 Ok(sealed) => sealed,
2681 Err(err) => {
2682 self.finish_generation(&mut mtp, &mut router, true);
2683 return Err(err);
2684 }
2685 };
2686 tracing::info!(
2687 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
2688 o1_prefill.unwrap_or(0),
2689 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
2690 .unwrap_or(limit),
2691 limit,
2692 input_ids.len()
2693 );
2694 }
2695 }
2696 let graph_prefill = self.graph_prefill_preferred();
2702 #[cfg(target_os = "macos")]
2710 if task_mask.is_none()
2711 && !dyn_prefill
2712 && crate::gpu::q1_force()
2713 && crate::gpu::enabled_here()
2714 && self.gdn_cfg.is_some()
2715 && self.g3n.is_none()
2716 && input_ids.len() > 8
2717 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2718 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2719 {
2720 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2721 .ok()
2722 .and_then(|v| v.parse().ok())
2723 .filter(|&v| (16..=512).contains(&v))
2724 .unwrap_or(256);
2725 let hs = self.hidden_size;
2726 let _tp = std::time::Instant::now();
2727 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2728 let end = (pos + chunk).min(input_ids.len());
2729 let Some(hb) = self.prefill_batch_metal(&input_ids[pos..end], pos) else {
2730 break;
2731 };
2732 if let Some(m) = &mut mtp {
2733 let n_pairs = if end < input_ids.len() {
2734 end - pos
2735 } else {
2736 end - pos - 1
2737 };
2738 if n_pairs > 0 {
2739 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
2740 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
2741 .collect();
2742 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
2743 for (j, (h, t)) in pairs.iter().enumerate() {
2744 let h = h.to_vec();
2745 let _ = self.mtp_step(m, &h, *t, pos + j);
2746 }
2747 }
2748 }
2749 }
2750 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2751 pos = end;
2752 }
2753 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2754 eprintln!(
2755 "metal-prefill: {} of {} tokens in {:.1} ms",
2756 pos,
2757 input_ids.len(),
2758 _tp.elapsed().as_secs_f64() * 1e3
2759 );
2760 }
2761 }
2762 if task_mask.is_none()
2763 && !dyn_prefill
2764 && !graph_prefill
2765 && self.can_prefill_batched()
2766 && self.g3n.is_none()
2767 && o1_prefill.is_none()
2768 && input_ids.len() > 2
2769 {
2770 let chunk = prefill_chunk();
2776 let hs = self.hidden_size;
2777 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2778 let end = (pos + chunk).min(input_ids.len());
2779 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2780 if let Some(m) = &mut mtp {
2781 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2782 .ok()
2783 .and_then(|v| v.parse().ok())
2784 .unwrap_or(0);
2785 for p in pos..end {
2786 if p + 1 < input_ids.len() {
2787 if probe >= 1 && p + 2 < input_ids.len() {
2788 let (d1, mut hx) = self.mtp_step_h(
2792 m,
2793 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2794 input_ids[p + 1],
2795 p,
2796 );
2797 let mut ok = d1 == input_ids[p + 2];
2798 Self::chain_probe_note(0, ok);
2799 let mut d_prev = d1;
2800 let mut extra = 0usize;
2801 for j in 1..probe {
2802 if p + 2 + j >= input_ids.len() {
2803 break;
2804 }
2805 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
2806 extra += 1;
2807 ok = ok && dj == input_ids[p + 2 + j];
2808 Self::chain_probe_note(j, ok);
2809 d_prev = dj;
2810 hx = hj;
2811 }
2812 m.kv.truncate_last(extra);
2813 } else {
2814 let _ = self.mtp_step(
2815 m,
2816 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2817 input_ids[p + 1],
2818 p,
2819 );
2820 }
2821 }
2822 }
2823 }
2824 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2825 pos = end;
2826 }
2827 }
2828 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
2829 if task_mask.is_none()
2830 && !dyn_prefill
2831 && !graph_prefill
2832 && !pair_off
2833 && self.pair_supported()
2834 && o1_prefill.is_none()
2835 {
2836 while pos + 1 < input_ids.len()
2837 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2838 {
2839 let e1 = self.embed_single(input_ids[pos]);
2840 let e2 = self.embed_single(input_ids[pos + 1]);
2841 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
2842 self.commit_linear_scratch();
2844 if let Some(m) = &mut mtp {
2845 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
2846 if pos + 2 < input_ids.len() {
2847 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2848 .ok()
2849 .and_then(|v| v.parse().ok())
2850 .unwrap_or(0);
2851 if probe >= 1 && pos + 3 < input_ids.len() {
2852 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
2856 let mut ok = d1 == input_ids[pos + 3];
2857 Self::chain_probe_note(0, ok);
2858 let mut d_prev = d1;
2859 let mut extra = 0usize;
2860 for j in 1..probe {
2861 if pos + 3 + j >= input_ids.len() {
2862 break;
2863 }
2864 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
2865 extra += 1;
2866 ok = ok && dj == input_ids[pos + 3 + j];
2867 Self::chain_probe_note(j, ok);
2868 d_prev = dj;
2869 hx = hj;
2870 }
2871 m.kv.truncate_last(extra);
2872 } else {
2873 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
2874 }
2875 }
2876 }
2877 hidden = h2;
2878 pos += 2;
2879 }
2880 }
2881 let o1_batch_ready = o1_sealed
2894 && o1_prefill.is_some()
2895 && mtp.is_none()
2896 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
2897 && (0..self.num_layers).all(|li| {
2898 let cache = &self.kv_cache.layers[self.phys_layer(li)];
2899 cache.o1.is_none() || cache.o1_views().is_some()
2900 });
2901 let mtp_batch_prefill = mtp.is_some()
2906 && graph_prefill
2907 && task_mask.is_none()
2908 && !dyn_prefill
2909 && !self.o1_active()
2910 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
2911 if batch_k > 0
2912 && (graph_prefill || o1_batch_ready)
2913 && task_mask.is_none()
2914 && (!self.o1_active() || o1_batch_ready)
2915 && (mtp.is_none() || mtp_batch_prefill)
2916 && !dyn_prefill
2917 && pos + 1 < input_ids.len()
2918 {
2919 let hs = self.hidden_size;
2920 let chunk = batch_k;
2921 while pos < input_ids.len() {
2922 let end = (pos + chunk).min(input_ids.len());
2923 let bk = end - pos;
2924 let mut hiddens = vec![0f32; bk * hs];
2925 for (j, &id) in input_ids[pos..end].iter().enumerate() {
2926 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2927 }
2928 let positions: Vec<usize> = (pos..end).collect();
2929 let t_chunk = std::time::Instant::now();
2930 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2931 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
2932 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2933 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2934 eprintln!(
2935 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2936 if o1_batch_ready {
2937 "o1"
2938 } else if mtp_batch_prefill {
2939 "ordinary_mtp"
2940 } else {
2941 "ordinary"
2942 },
2943 bk as f64 / (ms / 1000.0)
2944 );
2945 }
2946 {
2947 use std::sync::atomic::{AtomicBool, Ordering};
2948 static SAID: AtomicBool = AtomicBool::new(false);
2949 if !SAID.swap(true, Ordering::Relaxed) {
2950 if ok_b {
2951 tracing::info!(
2952 "batched prefill: ACTIVE mode={} (k={bk})",
2953 if o1_batch_ready {
2954 "o1"
2955 } else if mtp_batch_prefill {
2956 "ordinary_mtp"
2957 } else {
2958 "ordinary"
2959 }
2960 );
2961 } else {
2962 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
2963 }
2964 }
2965 }
2966 if ok_b {
2967 if mtp_batch_prefill {
2968 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
2969 if n_pairs > 0 {
2970 let rows: Vec<Vec<f32>> = (0..n_pairs)
2976 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
2977 .collect();
2978 let pairs: Vec<(&[f32], u32)> = rows
2979 .iter()
2980 .enumerate()
2981 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
2982 .collect();
2983 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2984 eprintln!(
2985 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
2986 pos,
2987 n_pairs,
2988 pos + n_pairs - 1,
2989 );
2990 }
2991 let warm_error = if let Some(m) = mtp.as_mut() {
2992 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
2993 } else {
2994 None
2995 };
2996 if let Some(err) = warm_error {
2997 self.finish_generation(&mut mtp, &mut router, true);
3002 return Err(err.to_string());
3003 }
3004 }
3005 }
3006 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3007 pos = end;
3008 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3009 self.finish_generation(&mut mtp, &mut router, true);
3014 return Err(if o1_batch_ready {
3015 "sealed O(1) batch graph failed after admission".to_string()
3016 } else {
3017 "ordinary recurrent batch graph failed after admission".to_string()
3018 });
3019 } else {
3020 break; }
3022 }
3023 }
3024 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3025 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3026 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3027 if let Some(m) = &mut mtp {
3028 if pos + 1 < input_ids.len() {
3029 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3035 .ok()
3036 .and_then(|v| v.parse().ok())
3037 .unwrap_or(0);
3038 if probe >= 1 && pos + 2 < input_ids.len() {
3039 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3040 let mut ok = d1 == input_ids[pos + 2];
3041 Self::chain_probe_note(0, ok);
3042 let mut d_prev = d1;
3043 let mut extra = 0usize;
3044 for j in 1..probe {
3045 if pos + 2 + j >= input_ids.len() {
3046 break;
3047 }
3048 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3049 extra += 1;
3050 ok = ok && dj == input_ids[pos + 2 + j];
3051 Self::chain_probe_note(j, ok);
3052 d_prev = dj;
3053 hx = hj;
3054 }
3055 m.kv.truncate_last(extra);
3058 } else {
3059 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3060 }
3061 }
3062 }
3063 pos += 1;
3064 }
3065 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3066 eprintln!(
3067 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3068 input_ids.len(),
3069 _tpf.elapsed().as_secs_f64() * 1000.0
3070 );
3071 }
3072 if self
3073 .graph_failed
3074 .swap(false, std::sync::atomic::Ordering::Relaxed)
3075 {
3076 self.finish_generation(&mut mtp, &mut router, true);
3081 return Err("GPU token graph failed during prefill".to_string());
3082 }
3083 if self
3086 .cancel
3087 .swap(false, std::sync::atomic::Ordering::Relaxed)
3088 {
3089 self.finish_generation(&mut mtp, &mut router, true);
3093 return Ok(GenerateResult {
3094 text: String::new(),
3095 token_ids: Vec::new(),
3096 prompt_tokens: input_ids.len(),
3097 tokens_generated: 0,
3098 finish_reason: "cancelled".to_string(),
3099 mtp_drafted: 0,
3100 mtp_accepted: 0,
3101 token_confidence: Vec::new(),
3102 traces: Vec::new(),
3103 });
3104 }
3105
3106 if !o1_sealed {
3109 match self.o1_seal_checked() {
3110 Ok(_) => {}
3111 Err(err) => {
3112 self.finish_generation(&mut mtp, &mut router, true);
3113 return Err(err);
3114 }
3115 }
3116 }
3117
3118 macro_rules! commit {
3120 ($id:expr) => {{
3121 all_ids.push($id);
3122 generated += 1;
3123 if self.tokenizer.is_eos($id) {
3124 finish_reason = "stop".to_string();
3125 false
3126 } else {
3127 let token_text = self.tokenizer.decode_token($id);
3128 let mut go = true;
3129 if let Some(ref mut cb) = on_token {
3130 if !cb(&token_text) {
3131 finish_reason = "cancelled".to_string();
3132 go = false;
3133 }
3134 }
3135 go
3136 }
3137 }};
3138 }
3139
3140 let mut spec_trial = SpecTrial::Spec {
3151 t0: std::time::Instant::now(),
3152 gen0: generated,
3153 rounds: 0,
3154 };
3155 let mut spec_mon = SpecMon::default();
3156 let mut spec_watchdog_off = false;
3157 let mut next_pos = input_ids.len();
3159 'decode: while generated < max_tokens {
3160 if self
3161 .graph_failed
3162 .swap(false, std::sync::atomic::Ordering::Relaxed)
3163 {
3164 self.finish_generation(&mut mtp, &mut router, true);
3169 return Err("GPU token graph failed during decode".to_string());
3170 }
3171 if self
3172 .cancel
3173 .swap(false, std::sync::atomic::Ordering::Relaxed)
3174 {
3175 finish_reason = "cancelled".to_string();
3176 break 'decode;
3177 }
3178 let forced = self.spec_forced.take();
3183 let mut logits = match (forced, self.graph_logits.take()) {
3184 (Some(_), _) => Vec::new(),
3185 (None, Some(lg)) => lg,
3186 (None, None) => {
3187 inference::rms_norm_into(
3188 &hidden,
3189 &self.weights.final_norm,
3190 self.rms_eps,
3191 self.norm_style,
3192 &mut self.ws.n1,
3193 );
3194 self.lm_head_forward(&self.ws.n1)
3195 }
3196 };
3197 if generated
3200 == std::env::var("CMF_LOGIT_DUMP_STEP")
3201 .ok()
3202 .and_then(|v| v.parse().ok())
3203 .unwrap_or(0)
3204 {
3205 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3206 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3207 for v in hidden.iter().chain(logits.iter()) {
3208 bytes.extend_from_slice(&v.to_le_bytes());
3209 }
3210 if let Err(e) = std::fs::write(&path, &bytes) {
3211 eprintln!("logit dump: failed to write {path}: {e}");
3212 self.finish_generation(&mut mtp, &mut router, true);
3213 return Err(format!("logit dump write failed: {e}"));
3214 }
3215 }
3216 }
3217 let t_next = match forced {
3218 Some(c) => c,
3219 None => sampler::sample_with_scratch_pool(
3220 &logits,
3221 &self.sampler_config,
3222 &all_ids,
3223 &mut self.rng,
3224 &mut self.sampler_scratch,
3225 self.pool.as_deref(),
3226 ),
3227 };
3228 if self.confidence_on {
3229 confidence.push(if logits.is_empty() {
3230 0.0
3231 } else {
3232 sampler::top1_prob_pool(
3233 self.pool.as_deref(),
3234 &mut self.sampler_scratch,
3235 &logits,
3236 t_next,
3237 calib_temp,
3238 )
3239 });
3240 }
3241 if !logits.is_empty() {
3242 attention::recycle_buf(&mut logits);
3243 }
3244 if trace_on {
3245 let skill = router.as_ref().and_then(|r| r.active_id());
3249 traces.push(TokenTrace {
3250 t: generated,
3251 token_id: t_next,
3252 confidence: confidence.last().copied().unwrap_or(0.0),
3253 active_skill: skill,
3254 recon: None,
3255 switched: false,
3256 });
3257 }
3258 if !commit!(t_next) {
3259 break 'decode;
3260 }
3261 if generated >= max_tokens {
3262 break 'decode;
3263 }
3264
3265 if self.kv_cache.needs_eviction() {
3266 static SAID: std::sync::Once = std::sync::Once::new();
3272 SAID.call_once(|| {
3273 tracing::warn!(
3274 "KV cache full at {} positions — evicting half; quality \
3275 will degrade. Raise CMF_MAX_SEQ.",
3276 self.kv_cache.max_seq_len,
3277 );
3278 });
3279 let keep = (self.kv_cache.max_seq_len / 2).max(1);
3280 self.kv_cache.evict(keep);
3281 }
3282
3283 if graph_spec {
3286 match spec_trial {
3287 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
3288 spec_mon.plain_ms =
3289 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3290 let keep = spec_mon.pays();
3291 tracing::info!(
3292 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3293 spec_mon.tokens,
3294 spec_mon.round_ms,
3295 spec_mon.plain_ms,
3296 if keep { "speculating" } else { "plain" }
3297 );
3298 spec_mon.fails = 0;
3299 spec_trial = SpecTrial::Decided {
3300 spec: keep,
3301 recheck_at: if keep { usize::MAX } else { generated + 128 },
3302 };
3303 }
3304 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3305 spec_mon.n = 0;
3306 spec_trial = SpecTrial::Spec {
3307 t0: std::time::Instant::now(),
3308 gen0: generated,
3309 rounds: 0,
3310 };
3311 }
3312 _ => {}
3313 }
3314 spec_watchdog_off = matches!(
3315 spec_trial,
3316 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3317 );
3318 }
3319 match &mut mtp {
3320 #[cfg(feature = "gpu")]
3322 Some(m)
3323 if graph_spec
3324 && !spec_watchdog_off
3325 && generated + 1 < max_tokens
3326 && next_pos > 0 =>
3327 {
3328 let t_round = std::time::Instant::now();
3329 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3330 m,
3331 &hidden,
3332 t_next,
3333 next_pos,
3334 &mut drafted,
3335 &mut accepted,
3336 &mut all_ids,
3337 ) {
3338 next_pos = n_pos;
3339 hidden = new_h;
3340 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3341 eprintln!(
3342 "spec-round wall {:.1} ms → {} tokens",
3343 t_round.elapsed().as_secs_f64() * 1e3,
3344 extra.len() + 1
3345 );
3346 }
3347 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3351 spec_trial = Self::spec_trial_round(
3354 spec_trial,
3355 &mut spec_mon,
3356 generated + extra.len() + 1,
3357 );
3358 let mut stopped = false;
3359 for &id in &extra {
3360 if self.confidence_on {
3361 confidence.push(0.0);
3362 }
3363 if !commit!(id) {
3364 stopped = true;
3365 break;
3366 }
3367 }
3368 if stopped {
3369 break 'decode;
3370 }
3371 continue 'decode;
3372 }
3373 if self
3374 .graph_failed
3375 .swap(false, std::sync::atomic::Ordering::Relaxed)
3376 {
3377 self.finish_generation(&mut mtp, &mut router, true);
3383 return Err("GPU MTP graph failed during speculative decode".to_string());
3384 }
3385 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
3396 spec_mon.tokens = 0.0;
3397 spec_mon.fails = 3;
3398 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
3399 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
3400 next_pos += 1;
3401 continue 'decode;
3402 }
3403 Some(m) if !graph_spec && generated + 1 < max_tokens => {
3405 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
3406 drafted += 1;
3407 let emb1 = self.embed_single(t_next);
3408 let emb2 = self.embed_single(draft);
3409 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
3410
3411 inference::rms_norm_into(
3412 &h1,
3413 &self.weights.final_norm,
3414 self.rms_eps,
3415 self.norm_style,
3416 &mut self.ws.n1,
3417 );
3418 let mut logits1 = self.lm_head_forward(&self.ws.n1);
3419 let t_after = sampler::sample_with_scratch_pool(
3420 &logits1,
3421 &self.sampler_config,
3422 &all_ids,
3423 &mut self.rng,
3424 &mut self.sampler_scratch,
3425 self.pool.as_deref(),
3426 );
3427 if self.confidence_on {
3428 confidence.push(sampler::top1_prob_pool(
3429 self.pool.as_deref(),
3430 &mut self.sampler_scratch,
3431 &logits1,
3432 t_after,
3433 calib_temp,
3434 ));
3435 }
3436 attention::recycle_buf(&mut logits1);
3437 if trace_on {
3438 traces.push(TokenTrace {
3441 t: generated,
3442 token_id: t_after,
3443 confidence: confidence.last().copied().unwrap_or(0.0),
3444 active_skill: None,
3445 recon: None,
3446 switched: false,
3447 });
3448 }
3449 let stop = !commit!(t_after);
3450
3451 if t_after == draft {
3452 accepted += 1;
3453 self.commit_linear_scratch();
3454 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3455 hidden = h2;
3456 next_pos += 2;
3457 } else {
3458 for layer in &mut self.kv_cache.layers {
3460 layer.truncate_last(1);
3461 }
3462 if !stop {
3463 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3464 hidden = self.forward_layers(
3465 &self.embed_single(t_after),
3466 next_pos + 1,
3467 None,
3468 );
3469 }
3470 next_pos += 2;
3471 }
3472 if stop {
3473 break 'decode;
3474 }
3475 }
3476 _ => {
3478 #[cfg(feature = "gpu")]
3483 if Self::dsv4_spec_on() && self.dsv4.is_some() {
3484 static SAID: std::sync::Once = std::sync::Once::new();
3485 SAID.call_once(|| {
3486 eprintln!(
3487 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
3488 !self.dsv4_mtp.is_empty(),
3489 task_mask.is_none(),
3490 router.is_none(),
3491 !trace_on,
3492 self.sampler_config.temperature < 1e-6,
3493 self.sampler_config.repetition_penalty == 1.0,
3494 );
3495 });
3496 }
3497 #[cfg(feature = "gpu")]
3498 if Self::dsv4_spec_on()
3499 && self.dsv4.is_some()
3500 && !self.dsv4_mtp.is_empty()
3501 && task_mask.is_none()
3502 && router.is_none()
3503 && !trace_on
3504 && self.sampler_config.temperature < 1e-6
3505 && self.sampler_config.repetition_penalty == 1.0
3506 && generated + 1 < max_tokens
3507 && all_ids.len() >= 2
3508 && generated >= dsv4_spec_retry_at
3509 {
3510 let tip_token = all_ids[all_ids.len() - 2];
3511 let drafted0 = drafted;
3512 let round = self.dsv4_spec_step(
3513 tip_token,
3514 t_next,
3515 next_pos,
3516 max_tokens.saturating_sub(generated),
3517 &mut drafted,
3518 &mut accepted,
3519 );
3520 if drafted > drafted0 {
3521 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
3522 if useful {
3523 dsv4_spec_bad = 0;
3524 } else {
3525 dsv4_spec_bad += 1;
3526 if dsv4_spec_bad >= 2 {
3527 dsv4_spec_bad = 0;
3528 dsv4_spec_retry_at = generated.saturating_add(32);
3529 tracing::info!(
3530 "dsv4: draft не окупился дважды — точный walk на 32 токена"
3531 );
3532 }
3533 }
3534 }
3535 if let Some((extra, n_pos)) = round {
3536 next_pos = n_pos;
3537 let mut stopped = false;
3538 for &id in &extra {
3539 if self.confidence_on {
3540 confidence.push(0.0);
3541 }
3542 if !commit!(id) {
3543 stopped = true;
3544 break;
3545 }
3546 }
3547 if stopped {
3548 break 'decode;
3549 }
3550 continue 'decode;
3551 }
3552 }
3553 self.graph_want_logits = fuse_lm;
3554 let mut t_fwd = t_next;
3560 let pure_greedy = self.sampler_config.temperature < 1e-6
3561 && self.sampler_config.repetition_penalty == 1.0
3562 && self.sampler_config.suppress_tokens.is_empty();
3563 let burst_k = std::env::var("CMF_MULTISTEP")
3568 .ok()
3569 .and_then(|v| v.parse::<usize>().ok())
3570 .unwrap_or(0);
3571 if pure_greedy
3572 && burst_k >= 1
3573 && fuse_lm
3574 && task_mask.is_none()
3575 && router.is_none()
3576 && !trace_on
3577 && !self.confidence_on
3578 {
3579 let mut stopped = false;
3580 loop {
3581 let room = max_tokens.saturating_sub(generated);
3582 if room <= 2 {
3583 break;
3584 }
3585 let k = burst_k.min(room - 1);
3586 if k < 1 {
3587 break;
3588 }
3589 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
3590 if self
3591 .graph_failed
3592 .swap(false, std::sync::atomic::Ordering::Relaxed)
3593 {
3594 self.finish_generation(&mut mtp, &mut router, true);
3595 return Err(
3596 "GPU token graph failed during greedy burst".to_string()
3597 );
3598 }
3599 break;
3600 };
3601 next_pos += k;
3602 for &id in &ids {
3603 if !commit!(id) {
3604 stopped = true;
3605 break;
3606 }
3607 }
3608 if stopped {
3609 break;
3610 }
3611 t_fwd = *ids.last().unwrap();
3612 }
3613 if stopped {
3614 break 'decode;
3615 }
3616 }
3617 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
3618 next_pos += 1;
3619 if let Some(r) = &mut router {
3622 let phi = self.dyn_phi_ema.clone();
3623 let decision = r.step(&phi, generated);
3624 if let Some(new_active) = decision {
3625 let _ = self.set_active_skill(new_active);
3626 }
3627 if trace_on {
3630 if let Some(last) = traces.last_mut() {
3631 let e = r.last_best_e();
3632 last.recon = e.is_finite().then_some(e);
3633 last.switched = decision.is_some();
3634 }
3635 }
3636 }
3637 }
3638 }
3639 }
3640
3641 let cancelled = finish_reason == "cancelled";
3642 self.finish_generation(&mut mtp, &mut router, cancelled);
3643
3644 let output_ids = &all_ids[input_ids.len()..];
3645 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3649 if cancelled {
3650 self.kv_history.clear();
3651 } else {
3652 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3653 }
3654 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
3656 Ok(GenerateResult {
3657 text: self.tokenizer.decode(output_ids),
3658 token_ids: output_ids.to_vec(),
3659 prompt_tokens: input_ids.len(),
3660 tokens_generated: generated,
3661 finish_reason,
3662 mtp_drafted: drafted,
3663 mtp_accepted: accepted,
3664 token_confidence: confidence,
3665 traces,
3666 })
3667 }
3668
3669 fn mtp_step(
3673 &mut self,
3674 m: &mut MtpModule,
3675 hidden: &[f32],
3676 next_token: u32,
3677 position: usize,
3678 ) -> u32 {
3679 self.mtp_step_h(m, hidden, next_token, position).0
3680 }
3681
3682 fn chain_probe_note(depth: usize, prefix_ok: bool) {
3686 use std::sync::Mutex;
3687 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3688 let mut t = T.lock().unwrap();
3689 if t.len() <= depth {
3690 t.resize(depth + 1, (0, 0));
3691 }
3692 t[depth].0 += 1;
3693 t[depth].1 += prefix_ok as u64;
3694 if depth == 0 && t[0].0 % 128 == 0 {
3695 let line: Vec<String> = t
3696 .iter()
3697 .enumerate()
3698 .map(|(d, (n, k))| {
3699 format!(
3700 "d{}={:.0}%({n})",
3701 d + 1,
3702 100.0 * *k as f64 / (*n).max(1) as f64
3703 )
3704 })
3705 .collect();
3706 eprintln!("mtp-chain: {}", line.join(" "));
3707 }
3708 }
3709
3710 fn mtp_step_hl(
3718 &mut self,
3719 m: &mut MtpModule,
3720 hidden: &[f32],
3721 next_token: u32,
3722 position: usize,
3723 ) -> (Vec<f32>, Vec<f32>) {
3724 #[cfg(target_os = "macos")]
3729 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
3730 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
3731 self.mtp_graph_mode = Some(true);
3732 return r;
3733 }
3734 if self.mtp_graph_mode == Some(true) {
3735 tracing::error!("mtp Metal graph failed after admission");
3736 self.clear_sequence_state();
3737 self.graph_failed
3738 .store(true, std::sync::atomic::Ordering::Relaxed);
3739 self.cancel
3740 .store(true, std::sync::atomic::Ordering::Relaxed);
3741 return (Vec::new(), Vec::new());
3742 }
3743 self.mtp_graph_mode = Some(false);
3744 }
3745 #[cfg(feature = "gpu")]
3746 if self.mtp_graph_mode != Some(false) {
3747 if !self.mtp_graph_ok(m) {
3748 if self.mtp_graph_mode == Some(true) {
3749 tracing::error!("mtp graph became unavailable after admission");
3754 self.clear_sequence_state();
3755 self.graph_failed
3756 .store(true, std::sync::atomic::Ordering::Relaxed);
3757 self.cancel
3758 .store(true, std::sync::atomic::Ordering::Relaxed);
3759 return (Vec::new(), Vec::new());
3760 }
3761 self.mtp_graph_mode = Some(false);
3762 } else {
3763 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
3764 self.mtp_graph_mode = Some(true);
3765 return r;
3766 }
3767 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
3768 return (Vec::new(), Vec::new());
3775 }
3776 tracing::error!("mtp graph failed or declined after admission");
3780 self.clear_sequence_state();
3781 self.graph_failed
3782 .store(true, std::sync::atomic::Ordering::Relaxed);
3783 self.cancel
3784 .store(true, std::sync::atomic::Ordering::Relaxed);
3785 return (Vec::new(), Vec::new());
3786 }
3787 }
3788 let e = self.embed_single(next_token);
3792 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3793 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3794 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3795 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3796 let mut x = vec![0.0f32; self.hidden_size];
3797 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3798
3799 let lw = &m.layer;
3801 inference::rms_norm_into(
3802 &x,
3803 &lw.input_norm,
3804 self.rms_eps,
3805 self.norm_style,
3806 &mut self.ws.n1,
3807 );
3808 let attn = match &lw.attn {
3809 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
3811 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
3812 AttnKind::Full {
3813 wq,
3814 wk,
3815 wv,
3816 wo,
3817 q_norm,
3818 k_norm,
3819 output_gate,
3820 softplus_gate,
3821 bias,
3822 } => {
3823 let mut cfg = self.attn_cfg(position);
3824 cfg.q_norm = q_norm.as_deref();
3825 cfg.k_norm = k_norm.as_deref();
3826 cfg.output_gate = *output_gate;
3827 cfg.softplus_gate = softplus_gate
3828 .as_ref()
3829 .map(|(gate, per_head)| (gate, *per_head));
3830 cfg.bias = bias
3831 .as_ref()
3832 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3833 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3834 }
3835 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
3836 unreachable!("MTP block is full attention")
3837 }
3838 };
3839 for (i, &a) in attn.iter().enumerate() {
3840 x[i] += a;
3841 }
3842 inference::rms_norm_into(
3843 &x,
3844 &lw.post_norm,
3845 self.rms_eps,
3846 self.norm_style,
3847 &mut self.ws.p1,
3848 );
3849 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
3850 for (i, &f) in ffn.iter().enumerate() {
3851 x[i] += f;
3852 }
3853
3854 inference::rms_norm_into(
3855 &x,
3856 &m.final_norm,
3857 self.rms_eps,
3858 self.norm_style,
3859 &mut self.ws.n1,
3860 );
3861 let lg = self.lm_head_forward(&self.ws.n1);
3862 (lg, x)
3863 }
3864
3865 fn mtp_step_h(
3867 &mut self,
3868 m: &mut MtpModule,
3869 hidden: &[f32],
3870 next_token: u32,
3871 position: usize,
3872 ) -> (u32, Vec<f32>) {
3873 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
3874 let draft = sampler::argmax(&lg);
3875 attention::recycle_buf(&mut lg);
3876 (draft, x)
3877 }
3878
3879 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
3885 match trial {
3886 SpecTrial::Spec { t0, gen0, rounds } => {
3887 let rounds = rounds + 1;
3888 if rounds >= 5 {
3889 if mon.plain_ms > 0.0 {
3890 let keep = mon.pays();
3891 mon.fails = 0;
3892 tracing::info!(
3893 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3894 mon.tokens,
3895 mon.round_ms,
3896 mon.plain_ms,
3897 if keep { "speculating" } else { "plain" }
3898 );
3899 SpecTrial::Decided {
3900 spec: keep,
3901 recheck_at: if keep { usize::MAX } else { generated + 128 },
3902 }
3903 } else {
3904 SpecTrial::Plain {
3905 t0: std::time::Instant::now(),
3906 gen0: generated,
3907 }
3908 }
3909 } else {
3910 SpecTrial::Spec { t0, gen0, rounds }
3911 }
3912 }
3913 SpecTrial::Decided { spec: true, .. } => {
3914 if mon.pays() {
3915 mon.fails = 0;
3916 trial
3917 } else {
3918 mon.fails += 1;
3919 if mon.fails >= 4 {
3920 tracing::info!(
3921 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
3922 mon.tokens,
3923 mon.round_ms,
3924 mon.plain_ms
3925 );
3926 SpecTrial::Decided {
3927 spec: false,
3928 recheck_at: generated + 128,
3929 }
3930 } else {
3931 trial
3932 }
3933 }
3934 }
3935 other => other,
3936 }
3937 }
3938
3939 fn mtp_kv_id(&self) -> u64 {
3942 self.graph_kv_id | (1u64 << 40)
3943 }
3944
3945 const MTP_LAYER_BASE: usize = 0;
3950
3951 #[cfg(feature = "gpu")]
3958 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
3959 self.mtp_graph_mode != Some(true)
3960 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
3961 }
3962
3963 #[cfg(feature = "gpu")]
3969 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
3970 let mut ok = true;
3971 let mut expected = false;
3972 for li in 0..self.num_layers {
3973 if matches!(
3974 self.weights.layers[self.phys_layer(li)].attn,
3975 AttnKind::Full { .. }
3976 ) {
3977 expected = true;
3978 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
3979 }
3980 }
3981 !expected || ok
3982 }
3983
3984 fn graph_gdn_layer_count(&self) -> usize {
3988 (0..self.num_layers)
3989 .filter(|&li| {
3990 matches!(
3991 &self.weights.layers[self.phys_layer(li)].attn,
3992 AttnKind::LinearGdn(_)
3993 )
3994 })
3995 .count()
3996 }
3997
3998 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
4001 let e = self.embed_single(next_token);
4002 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4003 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4004 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4005 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4006 let mut x = vec![0.0f32; self.hidden_size];
4007 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4008 x
4009 }
4010
4011 #[cfg(feature = "gpu")]
4014 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
4015 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
4016 return false;
4017 }
4018 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
4019 || !crate::gpu::enabled_here()
4020 || self.attn_softcap > 0.0
4021 || self.attention_heads_per_layer.is_some()
4022 {
4023 return false;
4024 }
4025 matches!(
4026 &m.layer.attn,
4027 AttnKind::Full {
4028 softplus_gate: None,
4029 ..
4030 }
4031 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
4032 }
4033
4034 #[cfg(feature = "gpu")]
4038 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
4039 if !self.mtp_block_graph_ok(m) {
4040 return false;
4041 }
4042 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
4043 return false;
4044 };
4045 let FfnKind::Dense(d) = &m.layer.ffn else {
4046 return false;
4047 };
4048 d.segs.is_empty()
4049 && wq.graph_weight().is_some()
4050 && wk.graph_weight().is_some()
4051 && wv.graph_weight().is_some()
4052 && wo.graph_weight().is_some()
4053 && d.gate_proj.graph_weight().is_some()
4054 && d.up_proj.graph_weight().is_some()
4055 && d.down_proj.graph_weight().is_some()
4056 && self.weights.lm_head.graph_weight().is_some()
4057 }
4058
4059 #[cfg(feature = "gpu")]
4065 fn mtp_step_graph(
4066 &mut self,
4067 m: &mut MtpModule,
4068 hidden: &[f32],
4069 next_token: u32,
4070 position: usize,
4071 ) -> Option<(Vec<f32>, Vec<f32>)> {
4072 if !self.mtp_graph_ok(m) {
4073 return None;
4074 }
4075 let lw = &m.layer;
4076 let AttnKind::Full {
4077 wq,
4078 wk,
4079 wv,
4080 wo,
4081 q_norm,
4082 k_norm,
4083 output_gate,
4084 softplus_gate,
4085 bias,
4086 } = &lw.attn
4087 else {
4088 return None;
4089 };
4090 if softplus_gate.is_some() {
4091 return None;
4092 }
4093 let FfnKind::Dense(d) = &lw.ffn else {
4094 return None;
4095 };
4096 if !d.segs.is_empty() {
4097 return None; }
4099 let mut x = self.mtp_block_input(m, hidden, next_token);
4102 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4103 let (_, i, kind, rs) = t.graph_weight()?;
4104 Some(crate::gpu::GraphW {
4105 idx: i,
4106 kind,
4107 row_scale: rs,
4108 data: &[],
4109 })
4110 }
4111 let (model, _, _, _) = wq.graph_weight()?;
4112 let model = model.clone();
4113 let (lm_gw, lm_rows) = {
4114 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4115 (
4116 crate::gpu::GraphW {
4117 idx: i,
4118 kind,
4119 row_scale: rs,
4120 data: &[],
4121 },
4122 self.weights.lm_head.rows(),
4123 )
4124 };
4125 let layer = crate::gpu::GraphLayer {
4126 input_norm: &lw.input_norm,
4127 attn: crate::gpu::GraphAttn::Full {
4128 wq: gw(wq)?,
4129 wk: gw(wk)?,
4130 wv: gw(wv)?,
4131 wo: gw(wo)?,
4132 q_norm: q_norm.as_deref(),
4133 k_norm: k_norm.as_deref(),
4134 bias: bias
4135 .as_ref()
4136 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4137 output_gate: *output_gate,
4138 cpu_k: m.kv.k_heads(),
4139 cpu_v: m.kv.v_heads(),
4140 },
4141 post_norm: &lw.post_norm,
4142 ffn: crate::gpu::GraphFfn::Dense {
4143 gate: gw(&d.gate_proj)?,
4144 up: gw(&d.up_proj)?,
4145 down: gw(&d.down_proj)?,
4146 },
4147 };
4148 let nh = self.num_heads;
4149 let (nkv, hd, rd) = self.layer_geom(0);
4150 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4151 let mut logits = Vec::new();
4152 let ok = crate::gpu::forward_token_graph(
4153 &model,
4154 self.mtp_kv_id(),
4155 std::slice::from_ref(&layer),
4156 &[None],
4157 self.o1_epoch,
4158 &self.inv_freq,
4159 &mut x,
4160 nh,
4161 nkv,
4162 hd,
4163 self.attn_scale,
4164 rd,
4165 self.hidden_size,
4166 self.intermediate_size,
4167 position,
4168 self.kv_cache.max_seq_len,
4169 gemma,
4170 self.rms_eps as f32,
4171 Some((&lm_gw, lm_rows)),
4172 &m.final_norm,
4173 &mut logits,
4174 &[],
4175 1,
4176 None,
4177 None,
4178 None,
4179 Self::MTP_LAYER_BASE,
4180 true,
4181 );
4182 match ok {
4183 crate::gpu::TokenGraphOutcome::Completed => {}
4184 crate::gpu::TokenGraphOutcome::Declined => return None,
4185 crate::gpu::TokenGraphOutcome::Failed => {
4186 self.clear_sequence_state();
4190 self.graph_failed
4191 .store(true, std::sync::atomic::Ordering::Relaxed);
4192 self.cancel
4193 .store(true, std::sync::atomic::Ordering::Relaxed);
4194 return None;
4195 }
4196 }
4197 logits.resize(self.vocab_size, 0.0);
4198 Some((logits, x))
4199 }
4200
4201 #[cfg(feature = "gpu")]
4209 fn mtp_warm_graph(
4210 &mut self,
4211 m: &mut MtpModule,
4212 pairs: &[(&[f32], u32)],
4213 first_pos: usize,
4214 ) -> crate::gpu::BatchGraphOutcome {
4215 if pairs.is_empty() {
4216 return crate::gpu::BatchGraphOutcome::Completed;
4217 }
4218 if !self.mtp_block_graph_ok(m) {
4219 return crate::gpu::BatchGraphOutcome::Declined;
4220 }
4221 let hs = self.hidden_size;
4222 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4225 for (h, t) in pairs {
4226 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4227 }
4228 let lw = &m.layer;
4229 let AttnKind::Full {
4230 wq,
4231 wk,
4232 wv,
4233 wo,
4234 q_norm,
4235 k_norm,
4236 output_gate,
4237 bias,
4238 ..
4239 } = &lw.attn
4240 else {
4241 return crate::gpu::BatchGraphOutcome::Declined;
4242 };
4243 let FfnKind::Dense(d) = &lw.ffn else {
4244 return crate::gpu::BatchGraphOutcome::Declined;
4245 };
4246 if !d.segs.is_empty() {
4247 return crate::gpu::BatchGraphOutcome::Declined; }
4249 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4250 let (_, i, kind, rs) = t.graph_weight()?;
4251 Some(crate::gpu::GraphW {
4252 idx: i,
4253 kind,
4254 row_scale: rs,
4255 data: &[],
4256 })
4257 }
4258 let Some((model, _, _, _)) = wq.graph_weight() else {
4259 return crate::gpu::BatchGraphOutcome::Declined;
4260 };
4261 let model = model.clone();
4262 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4263 gw(wq),
4264 gw(wk),
4265 gw(wv),
4266 gw(wo),
4267 gw(&d.gate_proj),
4268 gw(&d.up_proj),
4269 gw(&d.down_proj),
4270 ) else {
4271 return crate::gpu::BatchGraphOutcome::Declined;
4272 };
4273 let layer = crate::gpu::GraphLayer {
4274 input_norm: &lw.input_norm,
4275 attn: crate::gpu::GraphAttn::Full {
4276 wq: gwq,
4277 wk: gwk,
4278 wv: gwv,
4279 wo: gwo,
4280 q_norm: q_norm.as_deref(),
4281 k_norm: k_norm.as_deref(),
4282 bias: bias
4283 .as_ref()
4284 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4285 output_gate: *output_gate,
4286 cpu_k: m.kv.k_heads(),
4287 cpu_v: m.kv.v_heads(),
4288 },
4289 post_norm: &lw.post_norm,
4290 ffn: crate::gpu::GraphFfn::Dense {
4291 gate: gg,
4292 up: gu,
4293 down: gd,
4294 },
4295 };
4296 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4297 let nh = self.num_heads;
4298 let (nkv, hd, rd) = self.layer_geom(0);
4299 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4300 crate::gpu::forward_batch_graph(
4301 &model,
4302 self.mtp_kv_id(),
4303 std::slice::from_ref(&layer),
4304 &self.inv_freq,
4305 &mut hiddens,
4306 nh,
4307 nkv,
4308 hd,
4309 rd,
4310 hs,
4311 self.intermediate_size,
4312 &positions,
4313 self.kv_cache.max_seq_len,
4314 gemma,
4315 self.rms_eps as f32,
4316 self.attn_scale,
4317 pairs.len(),
4318 &[],
4319 0,
4320 None,
4321 )
4322 }
4323
4324 #[cfg(feature = "gpu")]
4331 fn mtp_warm_graph_fallback(
4332 &mut self,
4333 m: &mut MtpModule,
4334 pairs: &[(&[f32], u32)],
4335 first_pos: usize,
4336 ) -> bool {
4337 if pairs.is_empty() {
4338 return true;
4339 }
4340 let graphable = self.mtp_block_graph_ok(m);
4341 if !graphable {
4342 if self.mtp_graph_mode == Some(true) {
4346 return false;
4347 }
4348 self.mtp_graph_mode = Some(false);
4349 for (j, (h, t)) in pairs.iter().enumerate() {
4350 self.mtp_warm(m, h, *t, first_pos + j);
4351 }
4352 return true;
4353 }
4354
4355 for (j, (h, t)) in pairs.iter().enumerate() {
4360 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
4361 return false;
4362 }
4363 }
4364 self.mtp_graph_mode = Some(true);
4365 true
4366 }
4367
4368 #[cfg(feature = "gpu")]
4373 fn mtp_warm_prefill_pairs(
4374 &mut self,
4375 m: &mut MtpModule,
4376 pairs: &[(&[f32], u32)],
4377 first_pos: usize,
4378 ) -> Result<(), &'static str> {
4379 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
4384 if self.mtp_graph_mode == Some(true) {
4385 return Err("MTP token graph became unavailable after admission");
4386 }
4387 self.mtp_graph_mode = Some(false);
4388 for (j, (h, t)) in pairs.iter().enumerate() {
4389 self.mtp_warm(m, h, *t, first_pos + j);
4390 }
4391 return Ok(());
4392 }
4393 match self.mtp_warm_graph(m, pairs, first_pos) {
4394 crate::gpu::BatchGraphOutcome::Completed => {
4395 if !pairs.is_empty() {
4396 self.mtp_graph_mode = Some(true);
4397 }
4398 Ok(())
4399 }
4400 crate::gpu::BatchGraphOutcome::Declined => {
4401 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
4402 Ok(())
4403 } else {
4404 Err("MTP warm-up fallback failed after device admission")
4405 }
4406 }
4407 crate::gpu::BatchGraphOutcome::Failed => {
4408 Err("MTP warm batch graph failed after admission")
4409 }
4410 }
4411 }
4412
4413 #[cfg(not(feature = "gpu"))]
4414 fn mtp_warm_prefill_pairs(
4415 &mut self,
4416 m: &mut MtpModule,
4417 pairs: &[(&[f32], u32)],
4418 first_pos: usize,
4419 ) -> Result<(), &'static str> {
4420 for (j, (h, t)) in pairs.iter().enumerate() {
4421 self.mtp_warm(m, h, *t, first_pos + j);
4422 }
4423 Ok(())
4424 }
4425
4426 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
4430 let e = self.embed_single(next_token);
4431 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4432 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4433 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4434 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4435 let mut x = vec![0.0f32; self.hidden_size];
4436 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4437 inference::rms_norm_into(
4438 &x,
4439 &m.layer.input_norm,
4440 self.rms_eps,
4441 self.norm_style,
4442 &mut self.ws.n1,
4443 );
4444 let attn = match &m.layer.attn {
4445 AttnKind::Full {
4446 wq,
4447 wk,
4448 wv,
4449 wo,
4450 q_norm,
4451 k_norm,
4452 output_gate,
4453 softplus_gate,
4454 bias,
4455 } => {
4456 let mut cfg = self.attn_cfg(position);
4457 cfg.q_norm = q_norm.as_deref();
4458 cfg.k_norm = k_norm.as_deref();
4459 cfg.output_gate = *output_gate;
4460 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
4461 cfg.bias = bias
4462 .as_ref()
4463 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4464 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4465 }
4466 _ => return,
4467 };
4468 let _ = attn;
4469 }
4470
4471 #[cfg(feature = "gpu")]
4478 #[allow(clippy::too_many_arguments)]
4479 fn graph_spec_step(
4480 &mut self,
4481 m: &mut MtpModule,
4482 hidden: &[f32],
4483 t_next: u32,
4484 next_pos: usize,
4485 drafted: &mut usize,
4486 accepted: &mut usize,
4487 all_ids: &mut Vec<u32>,
4491 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
4492 #[cfg(target_os = "macos")]
4503 let metal_native = crate::gpu::q1_force();
4504 #[cfg(not(target_os = "macos"))]
4505 let metal_native = false;
4506 #[cfg(feature = "gpu")]
4507 let k_default = if metal_native {
4508 7
4511 } else if crate::gpu_wgpu::verify_i8_on() {
4512 5
4513 } else {
4514 4
4515 };
4516 #[cfg(not(feature = "gpu"))]
4517 let k_default = 4;
4518 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
4519 .ok()
4520 .and_then(|v| v.parse().ok())
4521 .filter(|&v| (1..=8).contains(&v))
4522 .unwrap_or(k_default);
4523 if next_pos == 0 {
4524 return None;
4525 }
4526 let t_round = std::time::Instant::now();
4527 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
4543 let sub0 = subs();
4544 let cfg = self.sampler_config.clone();
4549 let penalized = !(cfg.repetition_penalty == 1.0
4550 && cfg.presence_penalty == 0.0
4551 && cfg.suppress_tokens.is_empty());
4552 let greedy_pen = cfg.temperature < 1e-6 && penalized;
4557 let sampling = cfg.temperature >= 1e-6;
4558 let sparse = sampling && sampler::sparse_ok(&cfg);
4564 let base_len = all_ids.len();
4565 if sampling && !sparse && self.spec_q.len() < k_spec {
4566 self.spec_q.resize_with(k_spec, Vec::new);
4567 }
4568 if sparse && self.spec_qs.len() < k_spec {
4569 self.spec_qs.resize_with(k_spec, Vec::new);
4570 }
4571 let mut drafts = Vec::with_capacity(k_spec);
4576 let mut hx = hidden.to_vec();
4577 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
4580 for j in 0..k_spec {
4581 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
4582 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
4583 if spec_dbg {
4584 let saved = self.mtp_graph_mode;
4585 self.mtp_graph_mode = Some(false);
4586 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4587 self.mtp_graph_mode = saved;
4588 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4589 return None;
4590 }
4591 m.kv.truncate_last(1);
4592 dbg_ref = Some(r);
4593 }
4594 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4595 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4596 return None;
4597 }
4598 if let Some((lg_cpu, h_cpu)) = dbg_ref {
4599 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
4600 let dl = lg
4601 .iter()
4602 .zip(&lg_cpu)
4603 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4604 let dh = hj
4605 .iter()
4606 .zip(&h_cpu)
4607 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4608 eprintln!(
4609 "spec-dbg j={j} pos {} tok_in {tok_in}: per-op draft {} graph draft {} | max|dlogit| {dl:.3} | |h_cpu| {:.2} |h_graph| {:.2} max|dh| {dh:.3} | kv rows {}",
4610 next_pos - 1 + j,
4611 sampler::argmax(&lg_cpu),
4612 sampler::argmax(&lg),
4613 n(&h_cpu),
4614 n(&hj),
4615 m.kv.seq_len
4616 );
4617 }
4618 let dj = if sparse {
4619 let mut q = std::mem::take(&mut self.spec_qs[j]);
4620 let ok = sampler::sparse_distribution_into(
4621 &lg,
4622 &cfg,
4623 all_ids,
4624 &mut self.sampler_scratch,
4625 self.pool.as_deref(),
4626 &mut q,
4627 );
4628 let d = if ok {
4629 sampler::draw_sparse(&q, &mut self.rng)
4630 } else {
4631 let t = sampler::argmax(&lg);
4633 q.clear();
4634 q.push((t, 1.0));
4635 t
4636 };
4637 self.spec_qs[j] = q;
4638 all_ids.push(d);
4639 d
4640 } else if sampling {
4641 let mut q = std::mem::take(&mut self.spec_q[j]);
4642 sampler::distribution_into(
4643 &lg,
4644 &cfg,
4645 all_ids,
4646 &mut self.sampler_scratch,
4647 self.pool.as_deref(),
4648 &mut q,
4649 );
4650 let d = sampler::draw(&q, &mut self.rng);
4651 self.spec_q[j] = q;
4652 all_ids.push(d); d
4654 } else if greedy_pen {
4655 let d = sampler::argmax_penalized(
4656 &lg,
4657 &cfg,
4658 all_ids,
4659 &mut self.sampler_scratch,
4660 self.pool.as_deref(),
4661 );
4662 all_ids.push(d);
4663 d
4664 } else {
4665 sampler::argmax(&lg)
4666 };
4667 attention::recycle_buf(&mut lg);
4668 drafts.push(dj);
4669 hx = hj;
4670 }
4671 all_ids.truncate(base_len);
4672 *drafted += k_spec;
4673 let t_draft = t_round.elapsed();
4674 let sub_draft = subs();
4675 let b = k_spec + 1;
4678 let mut hiddens = vec![0.0f32; b * self.hidden_size];
4679 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
4680 let e = self.embed_single(t);
4681 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
4682 }
4683 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
4684 let (lm_gw, lm_rows) = {
4685 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4686 (
4687 crate::gpu::GraphW {
4688 idx: i,
4689 kind,
4690 row_scale: rs,
4691 data: &[],
4692 },
4693 self.weights.lm_head.rows(),
4694 )
4695 };
4696 let mut logits = Vec::new();
4697 let final_norm = self.weights.final_norm.clone();
4698 #[cfg(target_os = "macos")]
4699 let verify_outcome = if metal_native {
4700 let lm = self.weights.lm_head.q1_parts()?;
4701 self.try_batch_graph_metal(
4702 &mut hiddens,
4703 &positions,
4704 b,
4705 Some((lm, &final_norm, &mut logits)),
4706 )
4707 } else {
4708 self.try_batch_graph_wgpu(
4709 &mut hiddens,
4710 &positions,
4711 b,
4712 Some(crate::gpu::SpecTail {
4713 lm: lm_gw,
4714 lm_rows,
4715 final_norm: &final_norm,
4716 logits_out: &mut logits,
4717 }),
4718 )
4719 };
4720 #[cfg(not(target_os = "macos"))]
4721 let verify_outcome = self.try_batch_graph_wgpu(
4722 &mut hiddens,
4723 &positions,
4724 b,
4725 Some(crate::gpu::SpecTail {
4726 lm: lm_gw,
4727 lm_rows,
4728 final_norm: &final_norm,
4729 logits_out: &mut logits,
4730 }),
4731 );
4732 match verify_outcome {
4733 crate::gpu::BatchGraphOutcome::Completed => {}
4734 crate::gpu::BatchGraphOutcome::Declined => {
4735 m.kv.truncate_last(k_spec);
4739 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
4740 self.clear_sequence_state();
4741 self.graph_failed
4742 .store(true, std::sync::atomic::Ordering::Relaxed);
4743 self.cancel
4744 .store(true, std::sync::atomic::Ordering::Relaxed);
4745 tracing::error!("MTP graph mirror rewind failed after verify decline");
4746 }
4747 return None;
4748 }
4749 crate::gpu::BatchGraphOutcome::Failed => {
4750 self.clear_sequence_state();
4754 self.graph_failed
4755 .store(true, std::sync::atomic::Ordering::Relaxed);
4756 self.cancel
4757 .store(true, std::sync::atomic::Ordering::Relaxed);
4758 tracing::error!("MTP verify batch graph failed after admission");
4759 return None;
4760 }
4761 }
4762 #[cfg(target_os = "macos")]
4768 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
4769 let snap: Vec<Vec<f32>> = self
4770 .kv_cache
4771 .layers
4772 .iter()
4773 .map(|l| l.linear_state.clone())
4774 .collect();
4775 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
4776 let toks: Vec<u32> = std::iter::once(t_next)
4777 .chain(drafts.iter().copied())
4778 .collect();
4779 let want_save = self.graph_want_logits;
4780 self.graph_want_logits = false;
4781 for (i, &t) in toks.iter().enumerate() {
4782 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
4783 let _ = self.graph_logits.take();
4784 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
4788 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
4789 }
4790 let ref_lg = self.logits_from_hidden(&hi);
4791 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
4792 let ra = sampler::argmax(&ref_lg);
4793 let va = sampler::argmax(row);
4794 let mut md = 0f32;
4795 let mut rms = 0f64;
4796 for j in 0..lm_rows.min(ref_lg.len()) {
4797 let d = (ref_lg[j] - row[j]).abs();
4798 md = md.max(d);
4799 rms += (d as f64) * (d as f64);
4800 }
4801 let mut hd = 0f32;
4802 for j in 0..self.hidden_size {
4803 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
4804 }
4805 eprintln!(
4806 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
4807 next_pos + i,
4808 if ra == va { "OK" } else { "MISMATCH" },
4809 (rms / lm_rows as f64).sqrt()
4810 );
4811 }
4812 self.graph_want_logits = want_save;
4813 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
4816 if l.linear_state.len() == st.len() {
4817 l.linear_state.copy_from_slice(&st);
4818 } else {
4819 l.linear_state = st;
4820 }
4821 }
4822 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
4823 let extra = l.seq_len.saturating_sub(n0);
4824 if extra > 0 {
4825 l.truncate_last(extra);
4826 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
4827 }
4828 }
4829 }
4830 let t_verify = t_round.elapsed();
4831 let sub_verify = subs();
4832 let mut a = 0usize;
4837 let mut forced: Option<u32> = None;
4838 let ids: Vec<u32> = if sparse {
4839 let mut p = std::mem::take(&mut self.spec_ps);
4840 let mut res = std::mem::take(&mut self.spec_ress);
4841 while a < k_spec {
4842 let ok = sampler::sparse_distribution_into(
4843 &logits[a * lm_rows..(a + 1) * lm_rows],
4844 &cfg,
4845 all_ids,
4846 &mut self.sampler_scratch,
4847 self.pool.as_deref(),
4848 &mut p,
4849 );
4850 if !ok {
4851 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
4852 p.clear();
4853 p.push((t, 1.0));
4854 }
4855 match sampler::spec_accept_or_correct_sparse(
4856 &p,
4857 &self.spec_qs[a],
4858 drafts[a],
4859 &mut self.rng,
4860 &mut res,
4861 ) {
4862 None => {
4863 all_ids.push(drafts[a]);
4864 a += 1;
4865 }
4866 Some(c) => {
4867 forced = Some(c);
4868 break;
4869 }
4870 }
4871 }
4872 all_ids.truncate(base_len);
4873 self.spec_ps = p;
4874 self.spec_ress = res;
4875 drafts.clone()
4876 } else if sampling {
4877 let mut p = std::mem::take(&mut self.spec_p);
4878 let mut res = std::mem::take(&mut self.spec_res);
4879 while a < k_spec {
4880 sampler::distribution_into(
4881 &logits[a * lm_rows..(a + 1) * lm_rows],
4882 &cfg,
4883 all_ids,
4884 &mut self.sampler_scratch,
4885 self.pool.as_deref(),
4886 &mut p,
4887 );
4888 match sampler::spec_accept_or_correct(
4889 &p,
4890 &self.spec_q[a],
4891 drafts[a],
4892 &mut self.rng,
4893 &mut res,
4894 self.pool.as_deref(),
4895 ) {
4896 None => {
4897 all_ids.push(drafts[a]);
4898 a += 1;
4899 }
4900 Some(c) => {
4901 forced = Some(c);
4902 break;
4903 }
4904 }
4905 }
4906 all_ids.truncate(base_len);
4907 self.spec_p = p;
4908 self.spec_res = res;
4909 drafts.clone()
4911 } else if greedy_pen {
4912 let mut ids: Vec<u32> = Vec::with_capacity(b);
4916 for i in 0..b {
4917 let t = sampler::argmax_penalized(
4918 &logits[i * lm_rows..(i + 1) * lm_rows],
4919 &cfg,
4920 all_ids,
4921 &mut self.sampler_scratch,
4922 self.pool.as_deref(),
4923 );
4924 ids.push(t);
4925 if i < k_spec && t == drafts[i] {
4926 all_ids.push(t);
4927 } else {
4928 break;
4929 }
4930 }
4931 all_ids.truncate(base_len);
4932 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
4933 a += 1;
4934 }
4935 ids
4938 } else {
4939 let ids: Vec<u32> = (0..b)
4940 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
4941 .collect();
4942 while a < k_spec && ids[a] == drafts[a] {
4943 a += 1;
4944 }
4945 ids
4946 };
4947 if spec_dbg {
4948 eprintln!(
4949 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
4950 drafts, ids
4951 );
4952 }
4953 #[cfg(target_os = "macos")]
4957 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
4958 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
4959 {
4960 let snap: Vec<Vec<f32>> = self
4961 .kv_cache
4962 .layers
4963 .iter()
4964 .map(|l| l.linear_state.clone())
4965 .collect();
4966 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
4967 let toks: Vec<u32> = std::iter::once(t_next)
4968 .chain(drafts.iter().copied())
4969 .collect();
4970 let want_save = self.graph_want_logits;
4971 self.graph_want_logits = false;
4972 for (i, &t) in toks.iter().take(a + 1).enumerate() {
4973 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
4974 let _ = self.graph_logits.take();
4975 }
4976 self.graph_want_logits = want_save;
4977 let plain_states: Vec<Vec<f32>> = self
4978 .kv_cache
4979 .layers
4980 .iter()
4981 .map(|l| l.linear_state.clone())
4982 .collect();
4983 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
4984 let mut rows = Vec::new();
4985 for (li, (l, n0)) in self
4986 .kv_cache
4987 .layers
4988 .iter_mut()
4989 .zip(attn_lens.iter())
4990 .enumerate()
4991 {
4992 let extra = l.seq_len.saturating_sub(*n0);
4993 if extra > 0 {
4994 let mut kk = Vec::new();
4995 let mut vv = Vec::new();
4996 for g in 0..nkv {
4997 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
4998 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
4999 }
5000 rows.push((li, kk, vv));
5001 l.truncate_last(extra);
5002 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
5003 }
5004 }
5005 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5006 if l.linear_state.len() == st.len() {
5007 l.linear_state.copy_from_slice(&st);
5008 } else {
5009 l.linear_state = st;
5010 }
5011 }
5012 Some((plain_states, rows))
5013 } else {
5014 None
5015 };
5016 #[cfg(target_os = "macos")]
5018 if metal_native {
5019 self.metal_verify_commit(a);
5022 if let Some((plain_states, rows)) = commit_ref {
5023 crate::gpu_metal::queue_fence();
5024 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5025 let mut worst_s = 0f32;
5026 let mut worst_li = 0usize;
5027 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
5028 if l.linear_state.len() != ps.len() || ps.is_empty() {
5029 continue;
5030 }
5031 let d = l
5032 .linear_state
5033 .iter()
5034 .zip(ps)
5035 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5036 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
5037 let rel = d / n.max(1e-6);
5038 if rel > worst_s {
5039 worst_s = rel;
5040 worst_li = li;
5041 }
5042 }
5043 let mut worst_k = 0f32;
5044 for (li, kk, vv) in &rows {
5045 let l = &self.kv_cache.layers[*li];
5046 let n0 = l.seq_len - (kk.len() / (nkv * hd));
5047 let mut ck = Vec::new();
5048 let mut cv = Vec::new();
5049 for g in 0..nkv {
5050 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5051 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5052 }
5053 if ck.len() == kk.len() {
5054 let dk = ck
5055 .iter()
5056 .zip(kk)
5057 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5058 let dv = cv
5059 .iter()
5060 .zip(vv)
5061 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5062 worst_k = worst_k.max(dk).max(dv);
5063 } else {
5064 eprintln!(
5065 "commit-check L{li}: kv row count mismatch {} vs {}",
5066 ck.len(),
5067 kk.len()
5068 );
5069 }
5070 }
5071 eprintln!(
5072 "commit-check a={a}: worst GDN state rel-max diff {worst_s:.2e} (L{worst_li}) | worst K/V row abs diff {worst_k:.4}"
5073 );
5074 }
5075 }
5076 if !metal_native && a + 1 < b {
5077 let expected_gdn_layers = self.graph_gdn_layer_count();
5078 if expected_gdn_layers > 0
5079 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
5080 {
5081 self.clear_sequence_state();
5082 self.graph_failed
5083 .store(true, std::sync::atomic::Ordering::Relaxed);
5084 self.cancel
5085 .store(true, std::sync::atomic::Ordering::Relaxed);
5086 tracing::error!("GDN speculative restore failed after verify");
5087 return None;
5088 }
5089 }
5090 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
5091 self.clear_sequence_state();
5096 self.graph_failed
5097 .store(true, std::sync::atomic::Ordering::Relaxed);
5098 self.cancel
5099 .store(true, std::sync::atomic::Ordering::Relaxed);
5100 tracing::error!("trunk graph KV rewind failed after speculative verify");
5101 return None;
5102 }
5103 *accepted += a;
5104 m.kv.truncate_last(k_spec.saturating_sub(1));
5115 #[cfg(target_os = "macos")]
5116 if metal_native && self.mtp_graph_mode == Some(true) {
5117 crate::gpu_metal::kv_mirror_set_stored(
5120 self.mtp_kv_id(),
5121 Self::MTP_LAYER_BASE,
5122 m.kv.seq_len,
5123 );
5124 }
5125 if !metal_native
5126 && self.mtp_graph_mode == Some(true)
5127 && !self.rewind_mtp_graph_mirror(next_pos)
5128 {
5129 self.clear_sequence_state();
5133 self.graph_failed
5134 .store(true, std::sync::atomic::Ordering::Relaxed);
5135 self.cancel
5136 .store(true, std::sync::atomic::Ordering::Relaxed);
5137 tracing::error!("MTP graph mirror rewind failed after verify commit");
5138 return None;
5139 }
5140 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
5141 if !warm_off && a > 0 {
5142 let mut warmed = false;
5145 #[cfg(target_os = "macos")]
5146 if metal_native && self.mtp_graph_mode == Some(true) {
5147 let pairs: Vec<(&[f32], u32)> = (0..a)
5151 .map(|j| {
5152 (
5153 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
5154 ids[j],
5155 )
5156 })
5157 .collect();
5158 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
5159 if !warmed {
5160 warmed = true;
5161 for j in 0..a {
5162 let row =
5163 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
5164 if self
5165 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
5166 .is_none()
5167 {
5168 warmed = false;
5169 break;
5170 }
5171 }
5172 }
5173 }
5174 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
5175 let rows: Vec<Vec<f32>> = (0..a)
5176 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
5177 .collect();
5178 let pairs: Vec<(&[f32], u32)> = rows
5179 .iter()
5180 .zip(ids.iter())
5181 .map(|(r, &t)| (r.as_slice(), t))
5182 .collect();
5183 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
5184 Ok(()) => warmed = true,
5185 Err(err) => {
5186 tracing::error!("{err}");
5192 self.clear_sequence_state();
5193 self.graph_failed
5194 .store(true, std::sync::atomic::Ordering::Relaxed);
5195 self.cancel
5196 .store(true, std::sync::atomic::Ordering::Relaxed);
5197 return None;
5198 }
5199 }
5200 }
5201 if !warmed {
5202 for j in 0..a {
5203 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
5204 let row = row.to_vec();
5205 self.mtp_warm(m, &row, ids[j], next_pos + j);
5206 }
5207 }
5208 }
5209 if let Some(c) = forced {
5213 self.spec_forced = Some(c);
5214 self.graph_logits = None;
5215 } else {
5216 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
5217 row.resize(self.vocab_size, 0.0);
5218 if let Some(c) = self.final_softcap {
5219 for l in row.iter_mut() {
5220 *l = c * (*l / c).tanh();
5221 }
5222 }
5223 self.graph_logits = Some(row);
5224 }
5225 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
5226 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5232 let end = subs();
5233 eprintln!(
5234 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
5235 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
5236 t_draft.as_secs_f64() * 1e3,
5237 sub_draft - sub0,
5238 (t_verify - t_draft).as_secs_f64() * 1e3,
5239 sub_verify - sub_draft,
5240 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
5241 end - sub_verify,
5242 );
5243 }
5244 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
5245 }
5246
5247 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
5256 if !self.pair_supported() {
5257 return (0.0, 0.0);
5258 }
5259 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
5266 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5267 let emb1 = self.embed_single(1);
5268 let emb2 = self.embed_single(2);
5269 let pos = self.kv_cache.seq_len();
5270
5271 let t0 = std::time::Instant::now();
5272 for _ in 0..iters {
5273 let _ = self.forward_layers(&emb1, pos, None);
5274 let _ = self.forward_layers(&emb2, pos + 1, None);
5275 for l in &mut self.kv_cache.layers {
5276 l.truncate_last(2);
5277 }
5278 }
5279 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5280
5281 let t1 = std::time::Instant::now();
5282 for _ in 0..iters {
5283 let _ = self.forward_pair(&emb1, &emb2, pos);
5284 for l in &mut self.kv_cache.layers {
5285 l.truncate_last(2);
5286 }
5287 }
5288 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5289 match graph_env {
5290 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
5291 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
5292 }
5293 (singles_ms, pair_ms)
5294 }
5295
5296 fn pair_supported(&self) -> bool {
5304 !self.weights.layers.is_empty()
5311 && self.g3n.is_none()
5312 && !self
5313 .weights
5314 .layers
5315 .iter()
5316 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
5317 }
5318
5319 fn forward_pair(
5320 &mut self,
5321 emb1: &[f32],
5322 emb2: &[f32],
5323 position: usize,
5324 ) -> (Vec<f32>, Vec<f32>) {
5325 let mut h1 = emb1.to_vec();
5326 let mut h2 = emb2.to_vec();
5327 let (_nkv, _hd, hs, _rd, eps) = (
5328 self.num_kv_heads,
5329 self.head_dim,
5330 self.hidden_size,
5331 self.rotary_dim,
5332 self.rms_eps,
5333 );
5334 let pool = self.pool.clone();
5335
5336 for li in 0..self.num_layers {
5337 let lw = &self.weights.layers[self.phys_layer(li)];
5338 inference::rms_norm_into(
5341 &h1,
5342 &lw.input_norm,
5343 self.rms_eps,
5344 self.norm_style,
5345 &mut self.ws.n1,
5346 );
5347 inference::rms_norm_into(
5348 &h2,
5349 &lw.input_norm,
5350 self.rms_eps,
5351 self.norm_style,
5352 &mut self.ws.n2,
5353 );
5354
5355 let (a1, a2) = match &lw.attn {
5356 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5357 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5358 AttnKind::Linear(w) => {
5359 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5360 let layer = &mut self.kv_cache.layers[li];
5361 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5362 vmf_phase_pair(
5363 &self.ws.n1,
5364 &self.ws.n2,
5365 w,
5366 &cfg,
5367 state,
5368 scratch,
5369 self.pool.as_deref(),
5370 )
5371 }
5372 AttnKind::LinearGdn(w) => {
5373 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5374 let layer = &mut self.kv_cache.layers[li];
5375 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5376 gdn_pair(
5377 &self.ws.n1,
5378 &self.ws.n2,
5379 w,
5380 &cfg,
5381 state,
5382 scratch,
5383 self.pool.as_deref(),
5384 )
5385 }
5386 AttnKind::ShortConv(w) => {
5387 let cfg = self
5388 .short_conv_cfg
5389 .expect("short-conv layer without short_conv_cfg");
5390 let layer = &mut self.kv_cache.layers[li];
5391 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5392 short_conv_pair(
5393 &self.ws.n1,
5394 &self.ws.n2,
5395 w,
5396 &cfg,
5397 state,
5398 scratch,
5399 self.pool.as_deref(),
5400 )
5401 }
5402 AttnKind::Full {
5403 wq,
5404 wk,
5405 wv,
5406 wo,
5407 q_norm,
5408 k_norm,
5409 output_gate,
5410 softplus_gate,
5411 bias,
5412 } => {
5413 let inv_freq_l = self.layer_inv_freq(li);
5414 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5415 let cfg = QwenAttnCfg {
5416 num_heads: self.layer_num_heads(li),
5417 num_kv_heads: nkv_l,
5418 head_dim: hd_l,
5419 hidden_size: hs,
5420 position,
5421 inv_freq: &inv_freq_l,
5422 rotary_dim: rd_l,
5423 scale: self.attn_scale,
5424 softcap: self.attn_softcap,
5425 window: self.layer_window(li),
5426 v_norm: self.attn_v_norm,
5427 q_norm: q_norm.as_deref(),
5428 k_norm: k_norm.as_deref(),
5429 output_gate: *output_gate,
5430 softplus_gate: softplus_gate
5431 .as_ref()
5432 .map(|(gate, per_head)| (gate, *per_head)),
5433 rope_scale: self.layer_rope_scale(li),
5434 bias: bias
5435 .as_ref()
5436 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5437 rms_eps: eps,
5438 norm_style: self.norm_style,
5439 pool: pool.as_deref(),
5440 };
5441 attention::qwen_attention_pair(
5442 &self.ws.n1,
5443 &self.ws.n2,
5444 wq,
5445 wk,
5446 wv,
5447 wo,
5448 &mut self.kv_cache.layers[li],
5449 &cfg,
5450 )
5451 }
5452 };
5453 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5454 Some(w) => (
5455 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
5456 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
5457 ),
5458 None => (a1, a2),
5459 };
5460 for i in 0..self.hidden_size {
5461 h1[i] += a1[i];
5462 h2[i] += a2[i];
5463 }
5464 let (mut a1, mut a2) = (a1, a2);
5465 attention::recycle_buf(&mut a1);
5466 attention::recycle_buf(&mut a2);
5467
5468 let lw = &self.weights.layers[self.phys_layer(li)];
5469 inference::rms_norm_into(
5470 &h1,
5471 &lw.post_norm,
5472 self.rms_eps,
5473 self.norm_style,
5474 &mut self.ws.p1,
5475 );
5476 inference::rms_norm_into(
5477 &h2,
5478 &lw.post_norm,
5479 self.rms_eps,
5480 self.norm_style,
5481 &mut self.ws.p2,
5482 );
5483 let (f1, f2) = match &lw.ffn {
5484 FfnKind::DenseMoe(dm) => (
5487 dense_moe_ffn(
5488 dm,
5489 &self.ws.p1,
5490 &h1,
5491 self.rms_eps,
5492 self.norm_style,
5493 self.pool.as_deref(),
5494 ),
5495 dense_moe_ffn(
5496 dm,
5497 &self.ws.p2,
5498 &h2,
5499 self.rms_eps,
5500 self.norm_style,
5501 self.pool.as_deref(),
5502 ),
5503 ),
5504 _ => ffn_forward_pair(
5505 &lw.ffn,
5506 &self.ws.p1,
5507 &self.ws.p2,
5508 self.pool.as_deref(),
5509 None,
5510 ),
5511 };
5512 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5513 Some(w) => (
5514 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
5515 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
5516 ),
5517 None => (f1, f2),
5518 };
5519 for i in 0..self.hidden_size {
5520 h1[i] += f1[i];
5521 h2[i] += f2[i];
5522 }
5523 let (mut f1, mut f2) = (f1, f2);
5524 attention::recycle_buf(&mut f1);
5525 attention::recycle_buf(&mut f2);
5526 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5527 for i in 0..self.hidden_size {
5528 h1[i] *= sc;
5529 h2[i] *= sc;
5530 }
5531 }
5532 if self.is_loop_end(li) && li + 1 < self.num_layers {
5534 h1 = inference::rms_norm(
5535 &h1,
5536 &self.weights.final_norm,
5537 self.rms_eps,
5538 self.norm_style,
5539 );
5540 h2 = inference::rms_norm(
5541 &h2,
5542 &self.weights.final_norm,
5543 self.rms_eps,
5544 self.norm_style,
5545 );
5546 }
5547 }
5548 if self.o1_active() {
5554 self.commit_linear_scratch();
5555 }
5556 self.o1_progress();
5557 (h1, h2)
5558 }
5559
5560 fn commit_linear_scratch(&mut self) {
5562 for layer in &mut self.kv_cache.layers {
5563 if !layer.linear_scratch.is_empty() {
5564 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
5565 layer.linear_scratch.clear();
5566 }
5567 }
5568 }
5569
5570 pub fn forward_ids(
5573 &mut self,
5574 ids: &[u32],
5575 task_mask: Option<&TaskMask>,
5576 ) -> Result<Vec<f32>, String> {
5577 if ids.is_empty() {
5578 return Err("empty id sequence".to_string());
5579 }
5580 self.clear_sequence_state();
5581 self.check_forward_graph("forward_ids setup", 0)?;
5582 if task_mask.is_none() {
5583 self.o1_begin();
5584 }
5585 let mut hidden = vec![0.0f32; self.hidden_size];
5586 let mut pos = 0usize;
5587 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
5595 let chunk = prefill_chunk();
5599 let hs = self.hidden_size;
5600 while pos < ids.len() {
5601 let end = (pos + chunk).min(ids.len());
5602 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5603 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
5604 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5605 pos = end;
5606 }
5607 }
5608 if task_mask.is_none()
5617 && !self.graph_prefill_preferred()
5618 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
5619 && self.pair_supported()
5620 {
5621 while pos + 1 < ids.len() {
5622 let e1 = self.embed_single(ids[pos]);
5623 let e2 = self.embed_single(ids[pos + 1]);
5624 let (_, h2) = self.forward_pair(&e1, &e2, pos);
5625 self.check_forward_graph("forward_ids pair", pos + 1)?;
5626 self.commit_linear_scratch();
5627 hidden = h2;
5628 pos += 2;
5629 }
5630 }
5631 while pos < ids.len() {
5632 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5633 self.check_forward_graph("forward_ids", pos)?;
5634 pos += 1;
5635 }
5636 if let Err(err) = self.o1_seal_checked() {
5640 self.clear_sequence_state();
5641 return Err(err);
5642 }
5643 let normed = inference::rms_norm(
5644 &hidden,
5645 &self.weights.final_norm,
5646 self.rms_eps,
5647 self.norm_style,
5648 );
5649 Ok(self.lm_head_forward(&normed))
5650 }
5651
5652 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
5659 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
5660 Ok((nll / cnt.max(1) as f64).exp())
5661 }
5662
5663 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
5668 self.clear_sequence_state();
5669 FFN_PROBE.with(|p| {
5670 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
5671 });
5672 crate::gpu::cpu_scope(|| {
5673 for (pos, &id) in ids.iter().enumerate() {
5674 let emb = self.embed_single(id);
5675 let _ = self.forward_layers(&emb, pos, None);
5676 }
5677 });
5678 self.clear_sequence_state();
5679 FFN_PROBE
5680 .with(|p| p.borrow_mut().take())
5681 .unwrap_or_default()
5682 }
5683
5684 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
5688 if let Err(err) = self.nll_begin() {
5689 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
5693 self.nll_end();
5694 return Err(err);
5695 }
5696 FFN_PROBE.with(|p| {
5697 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
5698 });
5699 let result: Result<(), String> = (|| {
5700 for chunk in ids.chunks(256) {
5701 if chunk.len() < 2 {
5702 continue;
5703 }
5704 self.nll_ids_masked(chunk, 0, None)?;
5705 }
5706 Ok(())
5707 })();
5708 self.nll_end();
5709 let probe = FFN_PROBE
5710 .with(|p| p.borrow_mut().take())
5711 .unwrap_or_default();
5712 match result {
5713 Ok(()) => Ok(probe),
5714 Err(err) => {
5715 drop(probe);
5716 Err(err)
5717 }
5718 }
5719 }
5720
5721 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
5725 self.nll_begin()?;
5726 let result: Result<f64, String> = (|| {
5727 let mut nll = 0f64;
5728 let mut cnt = 0usize;
5729 let mut hidden = vec![0f32; self.hidden_size];
5730 for (pos, &id) in ids.iter().enumerate() {
5731 if pos > 0 {
5732 inference::rms_norm_into(
5733 &hidden,
5734 &self.weights.final_norm,
5735 self.rms_eps,
5736 self.norm_style,
5737 &mut self.ws.n1,
5738 );
5739 let mut logits = self.lm_head_forward(&self.ws.n1);
5740 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
5741 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
5742 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
5743 nll -= p.max(1e-300).ln();
5744 cnt += 1;
5745 attention::recycle_buf(&mut logits);
5746 }
5747 let emb = self.embed_single(id);
5748 hidden = self.forward_layers(&emb, pos, Some(mask));
5749 self.nll_check_graph("masked serial forward", pos)?;
5750 let _ = self.graph_logits.take();
5754 }
5755 Ok((nll / cnt.max(1) as f64).exp())
5756 })();
5757 self.nll_end();
5758 result
5759 }
5760
5761 pub fn nll_ids_masked(
5780 &mut self,
5781 ids: &[u32],
5782 start: usize,
5783 task_mask: Option<&TaskMask>,
5784 ) -> Result<(f64, usize), String> {
5785 let task_mask = self.drop_open_mask(task_mask);
5786 self.nll_ids_inner(ids, start, task_mask)
5787 }
5788
5789 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
5790 self.nll_ids_inner(ids, start, None)
5791 }
5792
5793 fn nll_ids_inner(
5794 &mut self,
5795 ids: &[u32],
5796 start: usize,
5797 task_mask: Option<&TaskMask>,
5798 ) -> Result<(f64, usize), String> {
5799 self.nll_begin()?;
5800 let result: Result<(f64, usize), String> = (|| {
5801 let mut nll = 0f64;
5802 let mut cnt = 0usize;
5803 if self.can_prefill_batched() {
5804 const CHUNK: usize = 128;
5810 const LM_SUB: usize = 32;
5811 let n = ids.len().saturating_sub(1);
5812 let hs = self.hidden_size;
5813 let rows = self.weights.lm_head.rows();
5814 let mut pos = 0usize;
5815 while pos < n {
5816 let end = (pos + CHUNK).min(n);
5817 let bsz = end - pos;
5818 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5819 self.nll_check_graph("batched prefill", pos)?;
5820 let mut k0 = 0usize;
5821 while k0 < bsz {
5822 let k1 = (k0 + LM_SUB).min(bsz);
5823 let sb = k1 - k0;
5824 if pos + k1 <= start {
5827 k0 = k1;
5828 continue;
5829 }
5830 let mut normed = vec![0.0f32; sb * hs];
5831 for k in 0..sb {
5832 let r = inference::rms_norm(
5833 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
5834 &self.weights.final_norm,
5835 self.rms_eps,
5836 self.norm_style,
5837 );
5838 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
5839 }
5840 let mut logits = vec![0.0f32; sb * rows];
5841 self.weights
5842 .lm_head
5843 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
5844 for k in 0..sb {
5845 if pos + k0 + k < start {
5846 continue;
5847 }
5848 self.nll_check_graph("batched score row", pos + k0 + k)?;
5849 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
5850 if let Some(mu) = self.logit_multiplier {
5851 for v in lg.iter_mut() {
5852 *v *= mu;
5853 }
5854 }
5855 if let Some(c) = self.final_softcap {
5859 for v in lg.iter_mut() {
5860 *v = c * (*v / c).tanh();
5861 }
5862 }
5863 if let Some(cm) = self.head_clusters.clone() {
5866 self.hierarchical_head_logprobs(
5867 &normed[k * hs..(k + 1) * hs],
5868 &cm,
5869 lg,
5870 );
5871 }
5872 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
5873 let target = ids[pos + k0 + k + 1] as usize;
5874 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
5875 let lse: f64 = lg
5876 .iter()
5877 .map(|&v| ((v - max) as f64).exp())
5878 .sum::<f64>()
5879 .ln()
5880 + max as f64;
5881 nll += lse - lg[target] as f64;
5882 cnt += 1;
5883 if std::env::var("CMF_PPL_TRACE").is_ok() {
5884 let top = lg
5885 .iter()
5886 .enumerate()
5887 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5888 .map(|(i, _)| i)
5889 .unwrap_or(0);
5890 eprintln!(
5891 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
5892 pos + k0 + k,
5893 target,
5894 lse - lg[target] as f64,
5895 top,
5896 lg[target],
5897 lg[top]
5898 );
5899 }
5900 }
5901 k0 = k1;
5902 }
5903 pos = end;
5904 }
5905 return Ok((nll, cnt));
5906 }
5907 for pos in 0..ids.len().saturating_sub(1) {
5908 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5909 self.nll_check_graph("serial forward", pos)?;
5910 let out_of_band = self.graph_logits.take();
5918 if pos < start {
5919 continue;
5920 }
5921 let logits = match out_of_band {
5922 Some(lg) => lg,
5923 None => {
5924 let normed = inference::rms_norm(
5925 &hidden,
5926 &self.weights.final_norm,
5927 self.rms_eps,
5928 self.norm_style,
5929 );
5930 self.lm_head_forward(&normed)
5934 }
5935 };
5936 let target = ids[pos + 1] as usize;
5937 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
5938 let lse: f64 = logits
5939 .iter()
5940 .map(|&v| ((v - max) as f64).exp())
5941 .sum::<f64>()
5942 .ln()
5943 + max as f64;
5944 let tok_nll = lse - logits[target] as f64;
5945 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
5946 let top = logits
5947 .iter()
5948 .enumerate()
5949 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5950 .map(|(i, _)| i)
5951 .unwrap_or(0);
5952 eprintln!(
5953 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
5954 logits[target], logits[top]
5955 );
5956 }
5957 nll += tok_nll;
5958 cnt += 1;
5959 }
5960 Ok((nll, cnt))
5961 })();
5962 self.nll_end();
5963 result
5964 }
5965
5966 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
5971 let normed = inference::rms_norm(
5972 hidden,
5973 &self.weights.final_norm,
5974 self.rms_eps,
5975 self.norm_style,
5976 );
5977 let mut logits = self.lm_head_forward(&normed);
5980 let target = target as usize;
5981 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
5982 let lse: f64 = logits
5983 .iter()
5984 .map(|&v| ((v - max) as f64).exp())
5985 .sum::<f64>()
5986 .ln()
5987 + max as f64;
5988 let tok_nll = lse - logits[target] as f64;
5989 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
5990 let top = logits
5991 .iter()
5992 .enumerate()
5993 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5994 .map(|(i, _)| i)
5995 .unwrap_or(0);
5996 eprintln!(
5997 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
5998 logits[target], logits[top]
5999 );
6000 }
6001 attention::recycle_buf(&mut logits);
6002 tok_nll
6003 }
6004
6005 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
6023 self.nll_begin()?;
6028 let requested_prefix = (prefill > 0).then_some(prefill);
6029 self.o1_begin_with_prefix(requested_prefix);
6030 let n = ids.len().saturating_sub(1);
6031 let requested_start = prefill.min(n);
6032 let exact_end = if self.o1_active() {
6037 match requested_prefix {
6038 Some(requested) => self.o1_effective_boundary(requested),
6039 None => self
6040 .o1_cfg
6041 .as_ref()
6042 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
6043 }
6044 .unwrap_or(requested_start)
6045 .min(n)
6046 } else {
6047 requested_start
6048 };
6049 let mut nll = 0f64;
6050 let mut cnt = 0usize;
6051
6052 let mut pos = 0usize;
6056 if self.can_prefill_batched() {
6057 const CHUNK: usize = 128;
6058 while pos < exact_end {
6059 let end = (pos + CHUNK).min(exact_end);
6060 let hiddens = self.prefill_batch(&ids[pos..end], pos);
6061 if self
6062 .graph_failed
6063 .swap(false, std::sync::atomic::Ordering::Relaxed)
6064 {
6065 self.cancel
6066 .store(false, std::sync::atomic::Ordering::Relaxed);
6067 self.nll_end();
6068 return Err("GPU graph failed during O(1) NLL prefix".into());
6069 }
6070 for row in 0..end - pos {
6071 let score_pos = pos + row;
6072 if score_pos >= requested_start && score_pos < n {
6073 nll += self.nll_from_hidden(
6074 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
6075 ids[score_pos + 1],
6076 score_pos,
6077 );
6078 cnt += 1;
6079 }
6080 }
6081 pos = end;
6082 }
6083 } else {
6084 while pos < exact_end {
6085 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6086 if self
6087 .graph_failed
6088 .swap(false, std::sync::atomic::Ordering::Relaxed)
6089 {
6090 self.cancel
6091 .store(false, std::sync::atomic::Ordering::Relaxed);
6092 self.nll_end();
6093 return Err("GPU graph failed during O(1) NLL prefix".into());
6094 }
6095 if pos >= requested_start && pos < n {
6096 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6097 cnt += 1;
6098 }
6099 pos += 1;
6100 }
6101 }
6102 self.o1_seal_checked().map_err(|err| {
6103 self.nll_end();
6104 err
6105 })?;
6106
6107 let batch_k = std::env::var("CMF_BATCH_K")
6116 .ok()
6117 .and_then(|v| v.parse::<usize>().ok())
6118 .unwrap_or(0);
6119 let batch_admitted = batch_k > 0
6120 && self.can_prefill_batched()
6121 && self.o1_active()
6122 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6123 && (0..self.num_layers).all(|li| {
6124 let cache = &self.kv_cache.layers[self.phys_layer(li)];
6125 cache.o1.is_none() || cache.o1_views().is_some()
6126 });
6127 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6128 eprintln!(
6129 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
6130 batch_admitted,
6131 batch_k,
6132 n.saturating_sub(exact_end),
6133 );
6134 }
6135 let mut batch_completed = false;
6136 if batch_admitted && exact_end < n {
6137 let hs = self.hidden_size;
6138 let mut batch_pos = exact_end;
6139 while batch_pos < n {
6140 let end = (batch_pos + batch_k).min(n);
6141 let bk = end - batch_pos;
6142 let mut hiddens = vec![0.0f32; bk * hs];
6143 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
6144 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
6145 }
6146 let positions: Vec<usize> = (batch_pos..end).collect();
6147 let t_batch = std::time::Instant::now();
6148 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6149 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6150 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
6151 eprintln!(
6152 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6153 batch_pos,
6154 end.saturating_sub(1),
6155 bk as f64 / (ms / 1000.0),
6156 );
6157 }
6158 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
6159 self.nll_end();
6160 return Err(err);
6161 }
6162 match outcome {
6163 crate::gpu::BatchGraphOutcome::Completed => {
6164 batch_completed = true;
6165 for row in 0..bk {
6166 nll += self.nll_from_hidden(
6167 &hiddens[row * hs..(row + 1) * hs],
6168 ids[batch_pos + row + 1],
6169 batch_pos + row,
6170 );
6171 cnt += 1;
6172 }
6173 batch_pos = end;
6174 }
6175 crate::gpu::BatchGraphOutcome::Declined => {
6176 if batch_completed {
6177 self.nll_end();
6178 return Err(format!(
6179 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
6180 ));
6181 }
6182 break;
6183 }
6184 crate::gpu::BatchGraphOutcome::Failed => {
6185 self.nll_end();
6186 return Err(format!(
6187 "O(1) NLL batch graph failed after admission at position {batch_pos}"
6188 ));
6189 }
6190 }
6191 }
6192 if batch_completed && cnt == n.saturating_sub(requested_start) {
6193 self.nll_end();
6194 return Ok((nll, cnt));
6195 }
6196 }
6197
6198 for pos in exact_end..n {
6203 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6204 if self
6205 .graph_failed
6206 .swap(false, std::sync::atomic::Ordering::Relaxed)
6207 {
6208 self.cancel
6209 .store(false, std::sync::atomic::Ordering::Relaxed);
6210 self.nll_end();
6211 return Err(format!(
6212 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
6213 ));
6214 }
6215 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6216 cnt += 1;
6217 }
6218 self.nll_end();
6219 Ok((nll, cnt))
6220 }
6221
6222 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
6230 self.clear_sequence_state();
6231 let n = ids.len().saturating_sub(1);
6232 let mut correct = Vec::with_capacity(n);
6233 let mut pmax = Vec::with_capacity(n);
6234 for pos in 0..n {
6235 let emb = self.embed_single(ids[pos]);
6236 let hidden = self.forward_layers(&emb, pos, None);
6237 let normed = inference::rms_norm(
6238 &hidden,
6239 &self.weights.final_norm,
6240 self.rms_eps,
6241 self.norm_style,
6242 );
6243 let logits = self.lm_head_forward(&normed);
6247 let target = ids[pos + 1] as usize;
6248 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
6249 for (i, &v) in logits.iter().enumerate() {
6250 if v > mval {
6251 mval = v;
6252 amax = i;
6253 }
6254 }
6255 correct.push(amax == target);
6256 let row: Vec<f32> = temps
6257 .iter()
6258 .map(|&t| {
6259 let tt = t.max(1e-3);
6260 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
6261 1.0 / s.max(1e-12) })
6263 .collect();
6264 pmax.push(row);
6265 }
6266 self.clear_sequence_state();
6267 (correct, pmax)
6268 }
6269
6270 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
6277 if self.dyn_router.is_none() {
6278 return Ok((self.ppl_ids(ids)?, 0));
6279 }
6280 self.nll_begin()?;
6281 let saved_active = self.dyn_active;
6282 let mut router = self
6283 .dyn_router
6284 .take()
6285 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
6286 router.reset();
6287 self.dyn_phi_seen = 0;
6288 let _ = self.set_active_skill(None);
6289
6290 let result: Result<(f64, usize), String> = (|| {
6291 let mut nll = 0f64;
6292 let mut cnt = 0usize;
6293 for pos in 0..ids.len().saturating_sub(1) {
6294 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6295 self.nll_check_graph("dynamic serial forward", pos)?;
6296 let out_of_band = self.graph_logits.take();
6297 let mut logits = match out_of_band {
6298 Some(lg) => lg,
6299 None => {
6300 let normed = inference::rms_norm(
6301 &hidden,
6302 &self.weights.final_norm,
6303 self.rms_eps,
6304 self.norm_style,
6305 );
6306 self.lm_head_forward(&normed)
6310 }
6311 };
6312 let target = ids[pos + 1] as usize;
6313 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6314 let lse: f64 = logits
6315 .iter()
6316 .map(|&v| ((v - max) as f64).exp())
6317 .sum::<f64>()
6318 .ln()
6319 + max as f64;
6320 let tok_nll = lse - logits[target] as f64;
6321 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6322 let top = logits
6323 .iter()
6324 .enumerate()
6325 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6326 .map(|(i, _)| i)
6327 .unwrap_or(0);
6328 eprintln!(
6329 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6330 logits[target], logits[top]
6331 );
6332 }
6333 nll += tok_nll;
6334 cnt += 1;
6335 attention::recycle_buf(&mut logits);
6336 let phi = self.dyn_phi_ema.clone();
6338 if let Some(new_active) = router.step(&phi, pos) {
6339 let _ = self.set_active_skill(new_active);
6340 }
6341 }
6342 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
6343 })();
6344
6345 let _ = self.set_active_skill(saved_active);
6348 self.dyn_router = Some(router);
6349 self.nll_end();
6350 result
6351 }
6352
6353 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
6355 self.clear_sequence_state();
6356 let mut acc = vec![0f32; self.hidden_size];
6357 for (pos, &id) in ids.iter().enumerate() {
6358 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
6359 for (a, v) in acc.iter_mut().zip(&h) {
6360 *a += v;
6361 }
6362 }
6363 let n = ids.len().max(1) as f32;
6364 for a in acc.iter_mut() {
6365 *a /= n;
6366 }
6367 self.clear_sequence_state();
6368 acc
6369 }
6370
6371 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
6377 self.prefill_batch_masked(ids, start_pos, None)
6378 }
6379
6380 fn prefill_batch_masked(
6386 &mut self,
6387 ids: &[u32],
6388 start_pos: usize,
6389 task_mask: Option<&TaskMask>,
6390 ) -> Vec<f32> {
6391 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
6392 }
6393
6394 fn prefill_batch_span(
6400 &mut self,
6401 input: PrefillIn<'_>,
6402 start_pos: usize,
6403 task_mask: Option<&TaskMask>,
6404 from: usize,
6405 upto_excl: usize,
6406 ) -> Vec<f32> {
6407 let hs = self.hidden_size;
6408 let b = match input {
6409 PrefillIn::Ids(ids) => ids.len(),
6410 PrefillIn::Hidden(hb) => hb.len() / hs,
6411 };
6412 let upto_excl = upto_excl.min(self.num_layers);
6413 let mut h: Vec<f32>;
6417 let mut h_ready;
6418 match input {
6419 PrefillIn::Ids(_) => {
6420 h = vec![0.0; b * hs];
6421 h_ready = false;
6422 }
6423 PrefillIn::Hidden(hb) => {
6424 h = hb.to_vec();
6425 h_ready = true;
6426 }
6427 }
6428 let fill_h = |h: &mut Vec<f32>, me: &Self| {
6429 if let PrefillIn::Ids(ids) = input {
6430 for (bi, &id) in ids.iter().enumerate() {
6431 let e = me.embed_single(id);
6432 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
6433 }
6434 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6435 if let Ok(t) = tp.parse::<usize>() {
6436 if t >= start_pos && t < start_pos + ids.len() {
6437 let bi = t - start_pos;
6438 let row = &h[bi * hs..(bi + 1) * hs];
6439 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6440 eprintln!(
6441 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
6442 ids[bi],
6443 row[0],
6444 row[1],
6445 ids.len(),
6446 &ids[..ids.len().min(8)]
6447 );
6448 }
6449 }
6450 }
6451 }
6452 };
6453 let (_nkv, _hd, _rd, eps) = (
6454 self.num_kv_heads,
6455 self.head_dim,
6456 self.rotary_dim,
6457 self.rms_eps,
6458 );
6459 let pool = self.pool.clone();
6460 let norm_style = self.norm_style;
6461 let automatic_gpu_prefix = self.automatic_gpu_prefix();
6462
6463 #[cfg(target_os = "macos")]
6464 let mut chunk_skip_until = 0usize;
6465 for li in from..upto_excl {
6466 let _capacity_tail = automatic_gpu_prefix
6467 .filter(|&prefix| li >= prefix)
6468 .map(|_| crate::gpu::enter_cpu_scope());
6469 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
6476 if task_mask.is_none() {
6477 if li < chunk_skip_until {
6478 continue;
6479 }
6480 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
6486 fill_h(&mut h, self);
6487 h_ready = true;
6488 }
6489 let ids_for_embed = match input {
6490 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
6491 PrefillIn::Hidden(_) => None,
6492 };
6493 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
6494 if end > li {
6495 h_ready = true;
6496 chunk_skip_until = end;
6497 if self.is_loop_end(end - 1) && end < self.num_layers {
6500 for bi in 0..b {
6501 let normed = inference::rms_norm(
6502 &h[bi * hs..(bi + 1) * hs],
6503 &self.weights.final_norm,
6504 eps,
6505 norm_style,
6506 );
6507 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6508 }
6509 }
6510 continue;
6511 }
6512 }
6513 if !h_ready {
6514 fill_h(&mut h, self);
6515 h_ready = true;
6516 }
6517 let lw = &self.weights.layers[self.phys_layer(li)];
6518 match &lw.attn {
6520 AttnKind::Kda(w) => {
6521 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
6523 let mut normed = vec![0.0f32; b * hs];
6524 for bi in 0..b {
6525 inference::rms_norm_into(
6526 &h[bi * hs..(bi + 1) * hs],
6527 &lw.input_norm,
6528 eps,
6529 norm_style,
6530 &mut normed[bi * hs..(bi + 1) * hs],
6531 );
6532 }
6533 let attn = crate::linear_core::kda_forward_batch(
6534 &normed,
6535 b,
6536 w,
6537 &cfg,
6538 &mut self.kv_cache.layers[li].linear_state,
6539 pool.as_deref(),
6540 );
6541 for (dst, &a) in h.iter_mut().zip(&attn) {
6542 *dst += a;
6543 }
6544 }
6545 AttnKind::LinearGdn(w) => {
6546 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6548 let mut normed = vec![0.0f32; b * hs];
6549 for bi in 0..b {
6550 let r = inference::rms_norm(
6551 &h[bi * hs..(bi + 1) * hs],
6552 &lw.input_norm,
6553 eps,
6554 norm_style,
6555 );
6556 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6557 }
6558 let attn = crate::linear_core::gdn_forward_batch(
6559 &normed,
6560 b,
6561 w,
6562 &cfg,
6563 &mut self.kv_cache.layers[li].linear_state,
6564 pool.as_deref(),
6565 );
6566 for (dst, &a) in h.iter_mut().zip(&attn) {
6567 *dst += a;
6568 }
6569 }
6570 AttnKind::ShortConv(w) => {
6571 let cfg = self
6574 .short_conv_cfg
6575 .expect("short-conv layer without short_conv_cfg");
6576 let mut normed = vec![0.0f32; b * hs];
6577 for bi in 0..b {
6578 inference::rms_norm_into(
6579 &h[bi * hs..(bi + 1) * hs],
6580 &lw.input_norm,
6581 eps,
6582 norm_style,
6583 &mut normed[bi * hs..(bi + 1) * hs],
6584 );
6585 }
6586 let attn = short_conv_forward_batch(
6587 &normed,
6588 b,
6589 w,
6590 &cfg,
6591 &mut self.kv_cache.layers[li].linear_state,
6592 pool.as_deref(),
6593 );
6594 for (dst, &a) in h.iter_mut().zip(&attn) {
6595 *dst += a;
6596 }
6597 }
6598 AttnKind::Mla(w) => {
6599 let inv_freq_l = self.layer_inv_freq(li);
6602 let rs = self.layer_rope_scale(li);
6603 let mut normed = vec![0.0f32; hs];
6604 for bi in 0..b {
6605 inference::rms_norm_into(
6606 &h[bi * hs..(bi + 1) * hs],
6607 &lw.input_norm,
6608 eps,
6609 norm_style,
6610 &mut normed,
6611 );
6612 let ao = mla_attention(
6613 w,
6614 &normed,
6615 &mut self.kv_cache.layers[li],
6616 start_pos + bi,
6617 &inv_freq_l,
6618 rs,
6619 eps,
6620 pool.as_deref(),
6621 );
6622 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
6623 *dst += a;
6624 }
6625 }
6626 }
6627 AttnKind::Full {
6628 wq,
6629 wk,
6630 wv,
6631 wo,
6632 q_norm,
6633 k_norm,
6634 output_gate,
6635 softplus_gate,
6636 bias,
6637 } => {
6638 let mut normed = vec![0.0f32; b * hs];
6642 for bi in 0..b {
6643 inference::rms_norm_into(
6644 &h[bi * hs..(bi + 1) * hs],
6645 &lw.input_norm,
6646 eps,
6647 norm_style,
6648 &mut normed[bi * hs..(bi + 1) * hs],
6649 );
6650 }
6651 let inv_freq_l = self.layer_inv_freq(li);
6652 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
6653 let cfg = QwenAttnCfg {
6654 num_heads: self.layer_num_heads(li),
6655 num_kv_heads: nkv_l,
6656 head_dim: hd_l,
6657 hidden_size: hs,
6658 position: start_pos,
6659 inv_freq: &inv_freq_l,
6660 rotary_dim: rd_l,
6661 scale: self.attn_scale,
6662 softcap: self.attn_softcap,
6663 window: self.layer_window(li),
6664 v_norm: self.attn_v_norm,
6665 q_norm: q_norm.as_deref(),
6666 k_norm: k_norm.as_deref(),
6667 output_gate: *output_gate,
6668 softplus_gate: softplus_gate
6669 .as_ref()
6670 .map(|(gate, per_head)| (gate, *per_head)),
6671 rope_scale: self.layer_rope_scale(li),
6672 bias: bias
6673 .as_ref()
6674 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6675 rms_eps: eps,
6676 norm_style,
6677 pool: pool.as_deref(),
6678 };
6679 let mut attn = attention::qwen_attention_batch(
6680 &normed,
6681 b,
6682 wq,
6683 wk,
6684 wv,
6685 wo,
6686 &mut self.kv_cache.layers[li],
6687 &cfg,
6688 );
6689 if let Some(w) = &lw.attn_out_norm {
6690 for bi in 0..b {
6691 inference::rms_norm_into(
6692 &attn[bi * hs..(bi + 1) * hs],
6693 w,
6694 eps,
6695 norm_style,
6696 &mut normed[bi * hs..(bi + 1) * hs],
6697 );
6698 }
6699 attn.copy_from_slice(&normed);
6700 }
6701 for (dst, &a) in h.iter_mut().zip(&attn) {
6702 *dst += a;
6703 }
6704 }
6705 AttnKind::Linear(w) => {
6706 for bi in 0..b {
6707 let normed = inference::rms_norm(
6708 &h[bi * hs..(bi + 1) * hs],
6709 &lw.input_norm,
6710 eps,
6711 norm_style,
6712 );
6713 vmf_phase_forward(
6714 &normed,
6715 w,
6716 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
6717 &mut self.kv_cache.layers[li].linear_state,
6718 pool.as_deref(),
6719 )
6720 .iter()
6721 .enumerate()
6722 .for_each(|(i, &a)| h[bi * hs + i] += a);
6723 }
6724 }
6725 }
6726
6727 let lw = &self.weights.layers[self.phys_layer(li)];
6729 let mut post = vec![0.0f32; b * hs];
6730 for bi in 0..b {
6731 let r =
6732 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
6733 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6734 }
6735 let mask_row = task_mask
6738 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
6739 .and_then(|m| m.ffn_masks.get(li))
6740 .map(|v| v.as_slice());
6741 let mut ffn = match &lw.ffn {
6742 FfnKind::Dense(d) if !d.segs.is_empty() => {
6743 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
6744 }
6745 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
6746 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
6747 FfnKind::DenseMoe(dm) => {
6750 let mut out = vec![0.0f32; b * hs];
6751 for bi in 0..b {
6752 let r = dense_moe_ffn(
6753 dm,
6754 &post[bi * hs..(bi + 1) * hs],
6755 &h[bi * hs..(bi + 1) * hs],
6756 eps,
6757 norm_style,
6758 pool.as_deref(),
6759 );
6760 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6761 }
6762 out
6763 }
6764 };
6765 if let Some(w) = &lw.ffn_out_norm {
6766 for bi in 0..b {
6767 inference::rms_norm_into(
6768 &ffn[bi * hs..(bi + 1) * hs],
6769 w,
6770 eps,
6771 norm_style,
6772 &mut post[bi * hs..(bi + 1) * hs],
6773 );
6774 }
6775 ffn.copy_from_slice(&post);
6776 }
6777 for (dst, &f) in h.iter_mut().zip(&ffn) {
6778 *dst += f;
6779 }
6780 if let Some(sc) = lw.layer_scale {
6781 for v in h.iter_mut() {
6782 *v *= sc;
6783 }
6784 }
6785 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6786 if let Ok(t) = tp.parse::<usize>() {
6787 if t >= start_pos && t < start_pos + b {
6788 let bi = t - start_pos;
6789 let row = &h[bi * hs..(bi + 1) * hs];
6790 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6791 eprintln!(
6792 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
6793 row[0], row[1]
6794 );
6795 }
6796 }
6797 }
6798 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
6802 let row = &h[(b - 1) * hs..b * hs];
6803 let rms =
6804 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
6805 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
6806 eprintln!(
6807 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
6808 match &self.weights.layers[self.phys_layer(li)].attn {
6809 AttnKind::LinearGdn(_) => "gdn",
6810 AttnKind::Linear(_) => "vmf",
6811 AttnKind::ShortConv(_) => "conv",
6812 _ => "attn",
6813 },
6814 match &lw.ffn {
6815 FfnKind::Moe(_) => "moe",
6816 FfnKind::Dense(_) => "dense",
6817 FfnKind::DenseMoe(_) => "dense+moe",
6818 },
6819 );
6820 }
6821 if self.is_loop_end(li) && li + 1 < self.num_layers {
6823 for bi in 0..b {
6824 let normed = inference::rms_norm(
6825 &h[bi * hs..(bi + 1) * hs],
6826 &self.weights.final_norm,
6827 eps,
6828 norm_style,
6829 );
6830 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6831 }
6832 }
6833 if std::env::var("CMF_TRACE_H").is_ok() {
6834 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
6835 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
6836 eprintln!(
6837 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
6838 lw.layer_scale
6839 );
6840 }
6841 }
6842 crate::gpu::set_layer(-1); self.o1_progress();
6848 h
6849 }
6850
6851 fn embed_single(&self, id: u32) -> Vec<f32> {
6853 let mut out = vec![0.0f32; self.hidden_size];
6854 if (id as usize) < self.weights.embed_tokens.rows() {
6855 self.weights.embed_tokens.row_f32(id as usize, &mut out);
6856 }
6857 if self.embed_multiplier != 1.0 {
6858 for v in out.iter_mut() {
6859 *v *= self.embed_multiplier;
6860 }
6861 }
6862 if self.dsv4.is_some() || self.qwen4_exp.is_some() {
6866 let mut v = vec![0.0f32; self.hidden_size.max(1)];
6867 v[0] = id as f32;
6868 return v;
6869 }
6870 if let Some(b) = &self.g3n {
6873 return b.0.extend_embedding(id, &out, self.pool.as_deref());
6874 }
6875 out
6876 }
6877
6878 #[cfg(target_os = "macos")]
6884 fn chunk_run_gpu(
6885 &mut self,
6886 li0: usize,
6887 h: &mut [f32],
6888 b: usize,
6889 pos0: usize,
6890 embed_ids: Option<&[u32]>,
6891 cap: usize,
6892 ) -> usize {
6893 if !crate::gpu::enabled_here()
6897 || std::env::var("CMF_GPU_CHUNK")
6898 .map(|v| v == "0")
6899 .unwrap_or(false)
6900 || b < 32
6901 || self.swa.is_some()
6902 || self.global_attn.is_some()
6903 || self.o1_active()
6906 || self.attn_v_norm
6907 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
6908 {
6909 return li0;
6910 }
6911 let Some(model) = self.model.clone() else {
6912 return li0;
6913 };
6914 let inv_freq = self.inv_freq.clone();
6915 let (nh, nkv, hd, hs) = (
6916 self.num_heads,
6917 self.num_kv_heads,
6918 self.head_dim,
6919 self.hidden_size,
6920 );
6921 let loop_end = if self.loop_final_norm {
6925 ((li0 / self.physical_layers) + 1) * self.physical_layers
6926 } else {
6927 self.num_layers
6928 };
6929 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
6930 let mut stored_at: Vec<usize> = Vec::new();
6931 for li in li0..self.num_layers.min(loop_end).min(cap) {
6932 let lw = &self.weights.layers[self.phys_layer(li)];
6933 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
6934 break;
6935 }
6936 let AttnKind::Full {
6937 wq,
6938 wk,
6939 wv,
6940 wo,
6941 q_norm,
6942 k_norm,
6943 output_gate: false,
6944 softplus_gate: None,
6945 bias,
6946 } = &lw.attn
6947 else {
6948 break;
6949 };
6950 let FfnKind::Dense(d) = &lw.ffn else { break };
6951 if d.act != Act::Silu || !d.segs.is_empty() {
6952 break;
6953 }
6954 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
6959 t.q8_row_parts()
6960 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
6961 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
6962 }
6963 let parts = (
6964 cw(wq),
6965 cw(wk),
6966 cw(wv),
6967 cw(wo),
6968 cw(&d.gate_proj),
6969 cw(&d.up_proj),
6970 cw(&d.down_proj),
6971 );
6972 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
6973 else {
6974 break;
6975 };
6976 let layer = &self.kv_cache.layers[li];
6977 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
6978 break;
6979 }
6980 stored_at.push(layer.head_len(0));
6981 layers.push(crate::gpu_metal::ChunkLayer {
6982 model: &model,
6983 kv_id: self.graph_kv_id,
6984 layer: li,
6985 wq: pq,
6986 wk: pk,
6987 wv: pv,
6988 wo: po,
6989 gate: pg,
6990 up: pu,
6991 down: pd,
6992 input_norm: &lw.input_norm,
6993 post_norm: &lw.post_norm,
6994 bias: bias
6995 .as_ref()
6996 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
6997 q_norm: q_norm.as_deref(),
6998 k_norm: k_norm.as_deref(),
6999 inv_freq: &inv_freq,
7000 rd: self.rotary_dim,
7001 nh,
7002 nkv,
7003 hd,
7004 hs,
7005 inter: d.gate_proj.rows(),
7006 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
7007 eps: self.rms_eps as f32,
7008 });
7009 }
7010 if layers.is_empty() {
7011 return li0;
7012 }
7013 let row = nkv * hd;
7014 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
7015 .iter()
7016 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
7017 .collect();
7018 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
7019 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
7020 let li = layers[i].layer;
7021 let layer = &self.kv_cache.layers[li];
7022 io.push(crate::gpu_metal::ChunkIo {
7023 cpu_stored: stored_at[i],
7024 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
7025 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
7026 out_k: ok,
7027 out_v: ov,
7028 imp: oi,
7029 });
7030 }
7031 let n_run = layers.len();
7032 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
7033 let ep = embed_ids.and_then(|ids| {
7036 self.weights
7037 .embed_tokens
7038 .q8_row_parts()
7039 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
7040 idx,
7041 rows,
7042 row_scale: rs,
7043 ids,
7044 mult: self.embed_multiplier,
7045 })
7046 });
7047 if embed_ids.is_some() && ep.is_none() {
7048 return li0;
7049 }
7050 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
7051 return li0;
7052 }
7053 drop(io);
7054 drop(layers);
7055 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
7058 let li = li0 + i;
7059 let layer = &mut self.kv_cache.layers[li];
7060 for bi in 0..b {
7061 layer.append(
7062 &ok[bi * row..(bi + 1) * row],
7063 &ov[bi * row..(bi + 1) * row],
7064 &[],
7065 );
7066 }
7067 layer.accumulate_imp(oi);
7068 }
7069 last
7070 }
7071
7072 fn layer_is_local(&self, li: usize) -> bool {
7075 if let Some(layers) = &self.sliding_layers {
7076 return layers.get(li).copied().unwrap_or(false);
7077 }
7078 match self.swa {
7079 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
7080 None => false,
7081 }
7082 }
7083
7084 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
7087 if self.layer_is_local(li) {
7088 if let Some(f) = &self.inv_freq_local {
7089 return f.clone();
7090 }
7091 } else if let Some(f) = &self.inv_freq_global {
7092 return f.clone();
7093 }
7094 self.inv_freq.clone()
7095 }
7096
7097 fn layer_window(&self, li: usize) -> Option<usize> {
7099 self.swa
7100 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
7101 }
7102
7103 fn layer_num_heads(&self, li: usize) -> usize {
7104 self.attention_heads_per_layer
7105 .as_ref()
7106 .and_then(|v| v.get(li).copied())
7107 .unwrap_or(self.num_heads)
7108 }
7109
7110 fn layer_rope_scale(&self, li: usize) -> f32 {
7111 if self.layer_is_local(li) {
7112 self.rope_scale_local
7113 } else {
7114 self.rope_scale
7115 }
7116 }
7117
7118 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
7121 if !self.layer_is_local(li) {
7122 if let Some((ghd, gkv)) = self.global_attn {
7123 return (gkv, ghd, ghd);
7124 }
7125 }
7126 (
7127 self.num_kv_heads,
7128 self.head_dim,
7129 if self.layer_is_local(li) {
7130 self.rotary_dim_local.unwrap_or(self.rotary_dim)
7131 } else {
7132 self.rotary_dim
7133 },
7134 )
7135 }
7136
7137 fn forward_layers(
7139 &mut self,
7140 hidden: &[f32],
7141 position: usize,
7142 task_mask: Option<&TaskMask>,
7143 ) -> Vec<f32> {
7144 let out = self.forward_layers_upto(hidden, position, task_mask, None);
7145 self.o1_progress();
7146 out
7147 }
7148
7149 pub fn embed_id(&self, id: u32) -> Vec<f32> {
7157 self.embed_single(id)
7158 }
7159
7160 pub fn split_supported(&self) -> Result<(), String> {
7164 if self.dsv4.is_some() {
7165 return Err(
7166 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
7167 );
7168 }
7169 if self.qwen4_exp.is_some() {
7170 return Err(
7171 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
7172 );
7173 }
7174 if self.g3n.is_some() {
7175 return Err(
7176 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
7177 );
7178 }
7179 Ok(())
7180 }
7181
7182 pub fn forward_span(
7187 &mut self,
7188 hidden: &[f32],
7189 position: usize,
7190 from: usize,
7191 upto: usize,
7192 task_mask: Option<&TaskMask>,
7193 ) -> Result<Vec<f32>, String> {
7194 self.split_supported()?;
7195 if from > upto || upto >= self.num_layers {
7196 return Err(format!(
7197 "forward_span: layer range {from}..={upto} outside 0..{}",
7198 self.num_layers
7199 ));
7200 }
7201 if hidden.len() != self.hidden_size {
7202 return Err(format!(
7203 "forward_span: hidden len {} ≠ hidden_size {}",
7204 hidden.len(),
7205 self.hidden_size
7206 ));
7207 }
7208 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
7209 self.o1_progress();
7210 if self
7211 .graph_failed
7212 .swap(false, std::sync::atomic::Ordering::Relaxed)
7213 {
7214 self.cancel
7215 .store(false, std::sync::atomic::Ordering::Relaxed);
7216 self.clear_sequence_state();
7217 return Err("forward_span: deferred O(1) transition failed".into());
7218 }
7219 Ok(out)
7220 }
7221
7222 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
7225 let normed = inference::rms_norm(
7226 hidden,
7227 &self.weights.final_norm,
7228 self.rms_eps,
7229 self.norm_style,
7230 );
7231 self.lm_head_forward(&normed)
7232 }
7233
7234 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
7236 sampler::sample_with_scratch(
7237 logits,
7238 &self.sampler_config,
7239 past_tokens,
7240 &mut self.rng,
7241 &mut self.sampler_scratch,
7242 )
7243 }
7244
7245 pub fn reset_session(&mut self) {
7247 self.clear_sequence_state();
7248 }
7249
7250 pub fn prefill_span_ids(
7256 &mut self,
7257 ids: &[u32],
7258 start_pos: usize,
7259 upto: usize,
7260 task_mask: Option<&TaskMask>,
7261 ) -> Result<Vec<f32>, String> {
7262 self.split_supported()?;
7263 if upto >= self.num_layers {
7264 return Err(format!(
7265 "prefill_span_ids: upto {upto} outside 0..{}",
7266 self.num_layers
7267 ));
7268 }
7269 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7273 let out =
7274 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
7275 self.check_o1_progress_failure("prefill_span_ids")?;
7276 Ok(out)
7277 } else {
7278 let hs = self.hidden_size;
7279 let mut out = Vec::with_capacity(ids.len() * hs);
7280 for (i, &id) in ids.iter().enumerate() {
7281 let emb = self.embed_id(id);
7282 out.extend_from_slice(&self.forward_span(
7283 &emb,
7284 start_pos + i,
7285 0,
7286 upto,
7287 task_mask,
7288 )?);
7289 }
7290 Ok(out)
7291 }
7292 }
7293
7294 pub fn prefill_span_hidden(
7297 &mut self,
7298 hidden: &[f32],
7299 start_pos: usize,
7300 from: usize,
7301 upto: usize,
7302 task_mask: Option<&TaskMask>,
7303 ) -> Result<Vec<f32>, String> {
7304 self.split_supported()?;
7305 let hs = self.hidden_size;
7306 if hidden.is_empty() || hidden.len() % hs != 0 {
7307 return Err(format!(
7308 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
7309 hidden.len()
7310 ));
7311 }
7312 if from > upto || upto >= self.num_layers {
7313 return Err(format!(
7314 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
7315 self.num_layers
7316 ));
7317 }
7318 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7319 let out = self.prefill_batch_span(
7320 PrefillIn::Hidden(hidden),
7321 start_pos,
7322 task_mask,
7323 from,
7324 upto + 1,
7325 );
7326 self.check_o1_progress_failure("prefill_span_hidden")?;
7327 Ok(out)
7328 } else {
7329 let b = hidden.len() / hs;
7330 let mut out = Vec::with_capacity(hidden.len());
7331 for i in 0..b {
7332 let h = self.forward_span(
7333 &hidden[i * hs..(i + 1) * hs],
7334 start_pos + i,
7335 from,
7336 upto,
7337 task_mask,
7338 )?;
7339 out.extend_from_slice(&h);
7340 }
7341 Ok(out)
7342 }
7343 }
7344
7345 fn try_token_graph_wgpu(
7349 &self,
7350 hidden: &[f32],
7351 position: usize,
7352 logits_out: &mut Vec<f32>,
7353 layers_run: &mut usize,
7354 ) -> Option<Result<Vec<f32>, ()>> {
7355 self.try_token_graph_wgpu_steps(
7356 hidden,
7357 position,
7358 logits_out,
7359 1,
7360 None,
7361 Some(layers_run),
7362 0,
7363 self.num_layers,
7364 )
7365 }
7366
7367 fn try_token_graph_wgpu_span(
7371 &self,
7372 hidden: &[f32],
7373 position: usize,
7374 logits_out: &mut Vec<f32>,
7375 from: usize,
7376 upto_excl: usize,
7377 layers_run: &mut usize,
7378 ) -> Option<Result<Vec<f32>, ()>> {
7379 self.try_token_graph_wgpu_steps(
7380 hidden,
7381 position,
7382 logits_out,
7383 1,
7384 None,
7385 Some(layers_run),
7386 from,
7387 upto_excl,
7388 )
7389 }
7390
7391 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
7395 if self.o1_active() || self.attn_softcap > 0.0 {
7396 return None;
7397 }
7398 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
7399 if !graph_on || crate::gpu::graph_unsupported() {
7400 return None;
7407 }
7408 let emb = self.embed_single(t_next);
7409 let mut lg = Vec::new();
7410 let mut ids = Vec::new();
7411 match self.try_token_graph_wgpu_steps(
7412 &emb,
7413 position,
7414 &mut lg,
7415 k,
7416 Some(&mut ids),
7417 None,
7418 0,
7419 self.num_layers,
7420 ) {
7421 Some(Ok(_)) => {}
7422 Some(Err(())) => {
7423 self.graph_failed
7428 .store(true, std::sync::atomic::Ordering::Relaxed);
7429 return None;
7430 }
7431 None => return None,
7432 }
7433 (ids.len() == k).then_some(ids)
7434 }
7435
7436 fn try_token_graph_wgpu_steps(
7440 &self,
7441 hidden: &[f32],
7442 position: usize,
7443 logits_out: &mut Vec<f32>,
7444 steps: usize,
7445 ids_out: Option<&mut Vec<u32>>,
7446 layers_run: Option<&mut usize>,
7447 from: usize,
7448 upto_excl: usize,
7449 ) -> Option<Result<Vec<f32>, ()>> {
7450 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
7453 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
7454 return None;
7458 }
7459 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
7464 .map(|li| {
7465 if !o1_gpu {
7466 return None;
7467 }
7468 self.kv_cache.layers[self.phys_layer(li)].o1_views()
7469 })
7470 .collect();
7471 if self.o1_active() && o1_gpu {
7472 let want: usize = (from..upto_excl)
7475 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
7476 .count();
7477 let have = o1_views.iter().filter(|v| v.is_some()).count();
7478 if want == 0 || have != want {
7479 use std::sync::atomic::{AtomicUsize, Ordering};
7489 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
7490 let code = have * 1000 + want;
7491 if LAST.swap(code, Ordering::Relaxed) != code {
7492 tracing::warn!(
7493 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
7494 );
7495 }
7496 return None;
7497 }
7498 }
7499 let nh = self.num_heads;
7500 let (nkv, hd, rd) = self.layer_geom(0);
7501 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7502 let mut layers = Vec::with_capacity(upto_excl - from);
7503 let mut model = None;
7504 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
7505 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7506 if let Some((_, i, kind, rs)) = t.graph_weight() {
7507 return Some(crate::gpu::GraphW {
7508 idx: i,
7509 kind,
7510 row_scale: rs,
7511 data: &[],
7512 });
7513 }
7514 t.as_f32().map(|d| crate::gpu::GraphW {
7516 idx: 0,
7517 kind: 4,
7518 row_scale: &[],
7519 data: d,
7520 })
7521 }
7522 for li in from..upto_excl {
7523 let lw = &self.weights.layers[self.phys_layer(li)];
7524 if dbg {
7525 let ak = match &lw.attn {
7526 AttnKind::Mla(_) => "Mla".into(),
7527 AttnKind::Full {
7528 output_gate, bias, ..
7529 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
7530 AttnKind::LinearGdn(_) => "LinearGdn".into(),
7531 AttnKind::Kda(_) => "Kda".into(),
7532 AttnKind::Linear(_) => "Linear".into(),
7533 AttnKind::ShortConv(_) => "ShortConv".into(),
7534 };
7535 let fk = match &lw.ffn {
7536 FfnKind::Dense(_) => "Dense",
7537 FfnKind::Moe(_) => "Moe",
7538 FfnKind::DenseMoe(_) => "DenseMoe",
7539 };
7540 eprintln!("graph L{li}: attn={ak} ffn={fk}");
7541 }
7542 let gffn = match &lw.ffn {
7543 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7547 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7548 gate: gw(&d.gate_proj)?,
7549 up: gw(&d.up_proj)?,
7550 down: gw(&d.down_proj)?,
7551 },
7552 FfnKind::Moe(m) => {
7553 if m.route_tau.is_some()
7560 || m.mask.is_some()
7561 || (m.routed_scaling - 1.0).abs() > 1e-9
7562 {
7563 return None;
7564 }
7565 let shared = m.shared.as_ref();
7566 let has_shared = shared.is_some();
7567 let sgate = match shared {
7568 Some((_, sg)) => gw(sg.as_ref()?)?,
7569 None => gw(&m.router)?,
7572 };
7573 let router = gw(&m.router)?;
7574 let inter = m.experts.first()?.gate_proj.rows();
7575 let mut experts = Vec::with_capacity(m.experts.len() + 1);
7576 let mut q4tp: Option<bool> = None;
7579 let mut gu_q2: Option<bool> = None;
7582 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
7583 if !matches!(e.act, Act::Silu)
7584 || e.gate_proj.rows() != inter
7585 || e.up_proj.rows() != inter
7586 {
7587 return None;
7588 }
7589 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
7590 Some((mm, gi)) => (
7591 mm,
7592 gi,
7593 e.up_proj.mapped_q4t()?.1,
7594 e.down_proj.mapped_q4t()?.1,
7595 false,
7596 false,
7597 ),
7598 None => match e.gate_proj.mapped_q2tp() {
7599 Some((mm, gi)) => (
7600 mm,
7601 gi,
7602 e.up_proj.mapped_q2tp()?.1,
7603 e.down_proj.mapped_q4tp()?.1,
7604 true,
7605 true,
7606 ),
7607 None => {
7608 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
7609 (
7610 mm,
7611 gi,
7612 e.up_proj.mapped_q4tp()?.1,
7613 e.down_proj.mapped_q4tp()?.1,
7614 true,
7615 false,
7616 )
7617 }
7618 },
7619 };
7620 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
7621 {
7622 tracing::warn!(
7628 "MoE layer mixes expert layouts (q4tp={is_p}, q2tp gate/up={is_q2}) — every expert of a layer, INCLUDING the shared one, must share a layout. The whole-token graph declines this layer."
7629 );
7630 return None;
7631 }
7632 model.get_or_insert_with(|| mm.clone());
7633 experts.push((gi, ui, di));
7634 }
7635 crate::gpu::GraphFfn::Moe {
7636 router,
7637 shared_gate: sgate,
7638 experts,
7639 n_exp: m.experts.len(),
7640 top_k: std::env::var("CMF_TOPK_PROBE")
7646 .ok()
7647 .and_then(|v| v.parse::<usize>().ok())
7648 .filter(|k| *k > 0 && *k <= m.top_k)
7649 .unwrap_or(m.top_k),
7650 inter,
7651 norm_topk: m.norm_topk_prob,
7652 q4tp: q4tp?,
7653 gu_q2: gu_q2.unwrap_or(false),
7654 sigmoid: m.router_sigmoid,
7655 bias: m.expert_bias.as_deref(),
7656 has_shared,
7657 }
7658 }
7659 };
7660 let attn = match &lw.attn {
7661 AttnKind::Full {
7662 wq,
7663 wk,
7664 wv,
7665 wo,
7666 q_norm,
7667 k_norm,
7668 output_gate,
7669 softplus_gate,
7670 bias,
7671 } => {
7672 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
7673 return None;
7674 }
7675 let (m, _, _, _) = wq.graph_weight()?;
7676 model = Some(m.clone());
7677 crate::gpu::GraphAttn::Full {
7678 wq: gw(wq)?,
7679 wk: gw(wk)?,
7680 wv: gw(wv)?,
7681 wo: gw(wo)?,
7682 q_norm: q_norm.as_deref(),
7683 k_norm: k_norm.as_deref(),
7684 bias: bias
7685 .as_ref()
7686 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7687 output_gate: *output_gate,
7688 cpu_k: self.kv_cache.layers[li].k_heads(),
7689 cpu_v: self.kv_cache.layers[li].v_heads(),
7690 }
7691 }
7692 AttnKind::LinearGdn(w) => {
7693 let cfg = self.gdn_cfg?;
7694 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
7695 model = Some(m.clone());
7696 crate::gpu::GraphAttn::Gdn {
7697 qkv: gw(&w.in_proj_qkv)?,
7698 z: gw(&w.in_proj_z)?,
7699 a: gw(&w.in_proj_a)?,
7700 b: gw(&w.in_proj_b)?,
7701 out: gw(&w.out_proj)?,
7702 conv1d: &w.conv1d,
7703 a_log: &w.a_log,
7704 dt_bias: &w.dt_bias,
7705 norm: &w.norm,
7706 nv: cfg.num_v_heads,
7707 nk: cfg.num_k_heads,
7708 dk: cfg.key_head_dim,
7709 dv: cfg.value_head_dim,
7710 kk: cfg.conv_kernel,
7711 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7712 }
7713 }
7714 AttnKind::ShortConv(w) => {
7715 let cfg = self.short_conv_cfg?;
7716 let (m, _, _, _) = w.in_proj.graph_weight()?;
7717 model = Some(m.clone());
7718 crate::gpu::GraphAttn::ShortConv {
7719 inp: gw(&w.in_proj)?,
7720 out: gw(&w.out_proj)?,
7721 taps: &w.conv,
7722 kernel: cfg.kernel,
7723 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7724 }
7725 }
7726 _ => return None,
7727 };
7728 layers.push(crate::gpu::GraphLayer {
7729 input_norm: &lw.input_norm,
7730 attn,
7731 post_norm: &lw.post_norm,
7732 ffn: gffn,
7733 });
7734 }
7735 let model = model?;
7736 let lm_gw = if upto_excl == self.num_layers
7742 && self.graph_want_logits
7743 && std::env::var("CMF_GPU_LMHEAD")
7744 .map(|v| v != "0")
7745 .unwrap_or(true)
7746 {
7747 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
7748 (
7749 crate::gpu::GraphW {
7750 idx: i,
7751 kind,
7752 row_scale: rs,
7753 data: &[],
7754 },
7755 self.weights.lm_head.rows(),
7756 )
7757 })
7758 } else {
7759 None
7760 };
7761 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
7762 let emb_gw = if steps > 1 {
7764 self.weights
7765 .embed_tokens
7766 .graph_weight()
7767 .map(|(_, i, kind, rs)| {
7768 (
7769 crate::gpu::GraphW {
7770 idx: i,
7771 kind,
7772 row_scale: rs,
7773 data: &[],
7774 },
7775 self.weights.embed_tokens.rows(),
7776 self.embed_multiplier,
7777 )
7778 })
7779 } else {
7780 None
7781 };
7782
7783 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
7789 (from..upto_excl.min(self.num_layers - 1))
7790 .filter(|&li| (li + 1) % self.physical_layers == 0)
7791 .map(|li| li - from)
7792 .collect()
7793 } else {
7794 Vec::new()
7795 };
7796 let mut h = hidden.to_vec();
7797 let outcome = crate::gpu::forward_token_graph(
7798 &model,
7799 self.graph_kv_id,
7800 &layers,
7801 &o1_views,
7802 self.o1_epoch,
7803 &self.inv_freq,
7804 &mut h,
7805 nh,
7806 nkv,
7807 hd,
7808 self.attn_scale,
7809 rd,
7810 self.hidden_size,
7811 self.intermediate_size,
7812 position,
7813 self.kv_cache.max_seq_len,
7814 gemma,
7815 self.rms_eps as f32,
7816 lm,
7817 &self.weights.final_norm,
7818 logits_out,
7819 &loop_norm_at,
7820 steps,
7821 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
7822 ids_out,
7823 layers_run,
7824 from,
7825 false,
7826 );
7827 match outcome {
7828 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
7829 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
7830 crate::gpu::TokenGraphOutcome::Declined => None,
7831 }
7832 }
7833
7834 #[cfg(target_os = "macos")]
7843 #[allow(clippy::type_complexity)]
7844 fn metal_rows_plan(
7845 &self,
7846 ) -> Option<(
7847 Vec<MetalRowsItem<'_>>,
7848 std::sync::Arc<cortiq_core::CmfModel>,
7849 Option<crate::gpu_metal::GdnGpuCfg>,
7850 )> {
7851 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
7852 if !crate::gpu::q1_force()
7853 || !crate::gpu::enabled_here()
7854 || std::env::var("CMF_GPU_BLOCK")
7855 .map(|v| v == "0")
7856 .unwrap_or(false)
7857 || self.attn_softcap > 0.0
7858 || self.o1_active()
7859 || self.swa.is_some()
7860 || self.global_attn.is_some()
7861 || self.attention_heads_per_layer.is_some()
7862 || self.attn_v_norm
7863 || self.loop_final_norm
7864 {
7865 return None;
7866 }
7867 let attend_contract = self.head_dim % 4 == 0
7868 && self.head_dim <= 256
7869 && self.rotary_dim >= 2
7870 && self.rotary_dim <= self.head_dim
7871 && (self.rotary_dim / 2) % 32 == 0
7872 && self.num_kv_heads > 0
7873 && self.num_heads % self.num_kv_heads == 0;
7874 if !attend_contract {
7875 return None;
7876 }
7877 let mut plan: Vec<MetalRowsItem> = Vec::new();
7878 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
7879 for li in 0..self.num_layers {
7880 let lw = &self.weights.layers[self.phys_layer(li)];
7881 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7882 return None;
7883 }
7884 let ffn = match &lw.ffn {
7885 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
7886 let (Some(g), Some(u), Some(dn)) = (
7887 d.gate_proj.q1_parts(),
7888 d.up_proj.q1_parts(),
7889 d.down_proj.q1_parts(),
7890 ) else {
7891 return None;
7892 };
7893 MetalFfn::Dense {
7894 gate: g,
7895 up: u,
7896 down: dn,
7897 }
7898 }
7899 _ => return None,
7900 };
7901 match &lw.attn {
7902 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
7903 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
7904 w.in_proj_qkv.q1_parts(),
7905 w.in_proj_z.q1_parts(),
7906 w.in_proj_a.f32_parts(),
7907 w.in_proj_b.f32_parts(),
7908 w.out_proj.q1_parts(),
7909 ) else {
7910 return None;
7911 };
7912 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
7913 model_ref.get_or_insert_with(|| model.clone());
7914 }
7915 let gl = GdnGpuLayer {
7916 attn_norm: &lw.input_norm,
7917 post_norm: &lw.post_norm,
7918 qkv,
7919 z,
7920 a,
7921 b: bb,
7922 out,
7923 ffn,
7924 conv1d: &w.conv1d,
7925 a_log: &w.a_log,
7926 dt_bias: &w.dt_bias,
7927 gnorm: &w.norm,
7928 };
7929 match plan.last_mut() {
7930 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
7931 _ => plan.push(MetalRowsItem::Gdn {
7932 run: vec![gl],
7933 first: li,
7934 }),
7935 }
7936 }
7937 AttnKind::Full {
7938 wq,
7939 wk,
7940 wv,
7941 wo,
7942 q_norm,
7943 k_norm,
7944 output_gate,
7945 softplus_gate: None,
7946 bias: None,
7947 } => {
7948 let (Some(pq), Some(pk), Some(pv), Some(po)) =
7949 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
7950 else {
7951 return None;
7952 };
7953 if let QTensor::Mapped { model, .. } = wq {
7954 model_ref.get_or_insert_with(|| model.clone());
7955 }
7956 let cache = &self.kv_cache.layers[li];
7957 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
7958 return None;
7959 }
7960 plan.push(MetalRowsItem::Attn {
7961 l: AttnGpuLayer {
7962 attn_norm: &lw.input_norm,
7963 post_norm: &lw.post_norm,
7964 wq: pq,
7965 wk: pk,
7966 wv: pv,
7967 wo: po,
7968 ffn,
7969 },
7970 li,
7971 q_norm: q_norm.as_deref(),
7972 k_norm: k_norm.as_deref(),
7973 output_gate: *output_gate,
7974 });
7975 }
7976 _ => return None,
7977 }
7978 }
7979 let model = model_ref?;
7980 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
7981 nv: cfg.num_v_heads,
7982 nk: cfg.num_k_heads,
7983 dk: cfg.key_head_dim,
7984 dv: cfg.value_head_dim,
7985 kk: cfg.conv_kernel,
7986 hidden: self.hidden_size,
7987 inter: self.intermediate_size,
7988 c_dim: cfg.conv_dim(),
7989 eps: cfg.rms_eps as f32,
7990 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7991 });
7992 Some((plan, model, gcfg))
7993 }
7994
7995 #[cfg(target_os = "macos")]
7997 #[allow(clippy::too_many_arguments)]
7998 fn metal_attn_params<'a>(
7999 li: usize,
8000 cache: &'a crate::kv_cache::LayerKvCache,
8001 q_norm: Option<&'a [f32]>,
8002 k_norm: Option<&'a [f32]>,
8003 output_gate: bool,
8004 inv_freq: &'a [f32],
8005 geom: (usize, usize, usize, usize),
8006 pos0: usize,
8007 kv_id: u64,
8008 scale: f32,
8009 eps: f32,
8010 gemma: bool,
8011 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
8012 let (nh, nkv, hd, rd) = geom;
8013 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8014 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8015 let cpu_stored = cpu_k[0].len() / hd;
8016 (
8017 crate::gpu_metal::AttnDeviceParams {
8018 kv_id,
8019 layer: li,
8020 nh,
8021 nkv,
8022 hd,
8023 rd,
8024 position: pos0,
8025 scale,
8026 eps,
8027 gemma,
8028 output_gate,
8029 q_norm,
8030 k_norm,
8031 inv_freq,
8032 cpu_k,
8033 cpu_v,
8034 cpu_stored,
8035 o1: None,
8036 },
8037 cpu_stored,
8038 )
8039 }
8040
8041 #[cfg(target_os = "macos")]
8046 #[allow(clippy::type_complexity)]
8047 fn metal_rows_run(
8048 &mut self,
8049 hiddens: &mut [f32],
8050 pos0: usize,
8051 b: usize,
8052 prefill: bool,
8053 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8054 ) -> Option<MetalVerifyPending> {
8055 use crate::gpu_metal::{GraphDims, VerifyGraph};
8056 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
8057 for l in &mut self.kv_cache.layers {
8058 if l.linear_state.len() != want && want > 0 {
8059 l.linear_state = vec![0f32; want];
8060 }
8061 }
8062 let (plan, model, gcfg) = self.metal_rows_plan()?;
8063 let dims = GraphDims {
8064 hidden: self.hidden_size,
8065 eps: self.rms_eps as f32,
8066 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8067 };
8068 let mut graph = if prefill {
8069 VerifyGraph::new_prefill(&model, dims, hiddens, b)?
8070 } else {
8071 VerifyGraph::new(&model, dims, hiddens, b)?
8072 };
8073 let geom = (
8074 self.num_heads,
8075 self.num_kv_heads,
8076 self.head_dim,
8077 self.rotary_dim,
8078 );
8079 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8080 let eps = self.rms_eps as f32;
8081 let kv_id = self.graph_kv_id;
8082 let inv_freq = self.inv_freq.clone();
8083 for item in &plan {
8084 let ok = match item {
8085 MetalRowsItem::Gdn { run, .. } => gcfg
8086 .as_ref()
8087 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
8088 .unwrap_or(false),
8089 MetalRowsItem::Attn {
8090 l,
8091 li,
8092 q_norm,
8093 k_norm,
8094 output_gate,
8095 } => {
8096 let (p, _) = Self::metal_attn_params(
8097 *li,
8098 &self.kv_cache.layers[*li],
8099 *q_norm,
8100 *k_norm,
8101 *output_gate,
8102 &inv_freq,
8103 geom,
8104 pos0,
8105 kv_id,
8106 self.attn_scale,
8107 eps,
8108 gemma,
8109 );
8110 graph.attn_ok(l, &p)
8111 }
8112 };
8113 if !ok {
8114 use std::sync::atomic::{AtomicBool, Ordering};
8115 static SAID: AtomicBool = AtomicBool::new(false);
8116 if !SAID.swap(true, Ordering::Relaxed) {
8117 tracing::warn!("metal rows graph: a layer failed preflight — declining");
8118 }
8119 return None;
8120 }
8121 }
8122 let lm = match &spec {
8123 Some((lm, _, _)) => {
8124 if !graph.lm_head_ok(*lm) {
8125 return None;
8126 }
8127 Some(*lm)
8128 }
8129 None => None,
8130 };
8131 let mut gdn_layers = Vec::new();
8132 let mut attn_layers = Vec::new();
8133 for item in &plan {
8134 match item {
8135 MetalRowsItem::Gdn { run, first } => {
8136 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
8137 .iter()
8138 .map(|l| l.linear_state.as_slice())
8139 .collect();
8140 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
8141 return None;
8142 }
8143 gdn_layers.extend(*first..*first + run.len());
8144 }
8145 MetalRowsItem::Attn {
8146 l,
8147 li,
8148 q_norm,
8149 k_norm,
8150 output_gate,
8151 } => {
8152 let (p, cpu_stored) = Self::metal_attn_params(
8153 *li,
8154 &self.kv_cache.layers[*li],
8155 *q_norm,
8156 *k_norm,
8157 *output_gate,
8158 &inv_freq,
8159 geom,
8160 pos0,
8161 kv_id,
8162 self.attn_scale,
8163 eps,
8164 gemma,
8165 );
8166 if !graph.encode_attn_b(l, &p) {
8167 return None;
8168 }
8169 attn_layers.push((*li, cpu_stored));
8170 }
8171 }
8172 }
8173 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
8174 if !graph.encode_lm_head_b(final_norm, lm) {
8175 return None;
8176 }
8177 }
8178 graph.sync();
8179 if let Some((lm, _, logits)) = spec {
8180 logits.resize(b * lm.1, 0.0);
8181 graph.read_logits(logits);
8182 }
8183 graph.read_hidden(hiddens);
8184 Some(MetalVerifyPending {
8185 graph,
8186 gdn_layers,
8187 attn_layers,
8188 })
8189 }
8190
8191 #[cfg(target_os = "macos")]
8197 fn try_batch_graph_metal(
8198 &mut self,
8199 hiddens: &mut [f32],
8200 positions: &[usize],
8201 b: usize,
8202 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8203 ) -> crate::gpu::BatchGraphOutcome {
8204 let _t0 = std::time::Instant::now();
8205 if positions.len() != b
8206 || positions.windows(2).any(|w| w[1] != w[0] + 1)
8207 || hiddens.len() != b * self.hidden_size
8208 {
8209 return crate::gpu::BatchGraphOutcome::Declined;
8210 }
8211 let Some(pending) = self.metal_rows_run(hiddens, positions[0], b, false, spec) else {
8212 return crate::gpu::BatchGraphOutcome::Declined;
8213 };
8214 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8215 eprintln!(
8216 "metal-verify: {:.1} ms | b={b}",
8217 _t0.elapsed().as_secs_f64() * 1e3
8218 );
8219 }
8220 self.metal_verify = Some(pending);
8221 crate::gpu::BatchGraphOutcome::Completed
8222 }
8223
8224 #[cfg(target_os = "macos")]
8229 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> Option<Vec<f32>> {
8230 let b = ids.len();
8231 if b == 0 || b > 512 {
8232 return None;
8233 }
8234 let hs = self.hidden_size;
8235 let mut hiddens = vec![0f32; b * hs];
8236 for (j, &id) in ids.iter().enumerate() {
8237 let e = self.embed_single(id);
8238 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
8239 }
8240 let mut pending = self.metal_rows_run(&mut hiddens, start_pos, b, true, None)?;
8241 let idxs = pending.gdn_layers.clone();
8243 let mut outs: Vec<&mut [f32]> = self
8244 .kv_cache
8245 .layers
8246 .iter_mut()
8247 .enumerate()
8248 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8249 .map(|(_, l)| l.linear_state.as_mut_slice())
8250 .collect();
8251 pending.graph.finish_states(&mut outs);
8252 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8253 let mut kbuf = vec![0f32; b * nkv * hd];
8254 let mut vbuf = vec![0f32; b * nkv * hd];
8255 for (li, cpu_stored) in &pending.attn_layers {
8256 if crate::gpu_metal::kv_mirror_read_rows(
8257 self.graph_kv_id,
8258 *li,
8259 nkv,
8260 hd,
8261 *cpu_stored,
8262 b,
8263 &mut kbuf,
8264 &mut vbuf,
8265 ) {
8266 let cache = &mut self.kv_cache.layers[*li];
8267 for r in 0..b {
8268 cache.append(
8269 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8270 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8271 &[],
8272 );
8273 }
8274 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + b);
8275 }
8276 }
8277 Some(hiddens)
8278 }
8279
8280 #[cfg(target_os = "macos")]
8284 fn metal_verify_commit(&mut self, a: usize) -> bool {
8285 let Some(mut pending) = self.metal_verify.take() else {
8286 return false;
8287 };
8288 let n = a + 1;
8289 let idxs = pending.gdn_layers.clone();
8291 let mut outs: Vec<&mut [f32]> = self
8292 .kv_cache
8293 .layers
8294 .iter_mut()
8295 .enumerate()
8296 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8297 .map(|(_, l)| l.linear_state.as_mut_slice())
8298 .collect();
8299 if !pending.graph.commit(n, &mut outs) {
8300 return false;
8301 }
8302 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8303 let mut kbuf = vec![0f32; n * nkv * hd];
8304 let mut vbuf = vec![0f32; n * nkv * hd];
8305 for (li, cpu_stored) in &pending.attn_layers {
8306 if crate::gpu_metal::kv_mirror_read_rows(
8307 self.graph_kv_id,
8308 *li,
8309 nkv,
8310 hd,
8311 *cpu_stored,
8312 n,
8313 &mut kbuf,
8314 &mut vbuf,
8315 ) {
8316 let cache = &mut self.kv_cache.layers[*li];
8317 for r in 0..n {
8318 cache.append(
8319 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8320 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8321 &[],
8322 );
8323 }
8324 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + n);
8325 }
8326 }
8327 true
8328 }
8329
8330 #[cfg(target_os = "macos")]
8336 fn mtp_warm_batch_metal(
8337 &mut self,
8338 m: &mut MtpModule,
8339 pairs: &[(&[f32], u32)],
8340 first_pos: usize,
8341 ) -> bool {
8342 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
8343 let b = pairs.len();
8344 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
8345 return false;
8346 }
8347 let AttnKind::Full {
8348 wq,
8349 wk,
8350 wv,
8351 wo,
8352 q_norm,
8353 k_norm,
8354 output_gate,
8355 softplus_gate: None,
8356 bias: None,
8357 } = &m.layer.attn
8358 else {
8359 return false;
8360 };
8361 let FfnKind::Dense(d) = &m.layer.ffn else {
8362 return false;
8363 };
8364 if !d.segs.is_empty() {
8365 return false;
8366 }
8367 let (Some(pq), Some(pk), Some(pv), Some(po)) =
8368 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
8369 else {
8370 return false;
8371 };
8372 let (Some(g), Some(u), Some(dn)) = (
8373 d.gate_proj.q1_parts(),
8374 d.up_proj.q1_parts(),
8375 d.down_proj.q1_parts(),
8376 ) else {
8377 return false;
8378 };
8379 let Some(eh) = m.eh_proj.q1_parts() else {
8380 return false;
8381 };
8382 let QTensor::Mapped { model, .. } = wq else {
8383 return false;
8384 };
8385 let model = model.clone();
8386 let hs = self.hidden_size;
8387 let mut cat = vec![0f32; b * 2 * hs];
8389 for (j, (h, tok)) in pairs.iter().enumerate() {
8390 let e = self.embed_single(*tok);
8391 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
8392 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
8393 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
8394 }
8395 let dims = GraphDims {
8396 hidden: hs,
8397 eps: self.rms_eps as f32,
8398 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8399 };
8400 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
8401 return false;
8402 };
8403 let l = AttnGpuLayer {
8404 attn_norm: &m.layer.input_norm,
8405 post_norm: &m.layer.post_norm,
8406 wq: pq,
8407 wk: pk,
8408 wv: pv,
8409 wo: po,
8410 ffn: MetalFfn::Dense {
8411 gate: g,
8412 up: u,
8413 down: dn,
8414 },
8415 };
8416 let (nh, nkv, hd, rd) = (
8417 self.num_heads,
8418 self.num_kv_heads,
8419 self.head_dim,
8420 self.rotary_dim,
8421 );
8422 let inv_freq = self.inv_freq.clone();
8423 let cpu_stored;
8424 {
8425 let cache = &m.kv;
8426 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8427 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8428 cpu_stored = cpu_k[0].len() / hd;
8429 if cpu_stored != first_pos {
8430 return false;
8431 }
8432 let p = AttnDeviceParams {
8433 kv_id: self.mtp_kv_id(),
8434 layer: Self::MTP_LAYER_BASE,
8435 nh,
8436 nkv,
8437 hd,
8438 rd,
8439 position: first_pos,
8440 scale: self.attn_scale,
8441 eps: self.rms_eps as f32,
8442 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8443 output_gate: *output_gate,
8444 q_norm: q_norm.as_deref(),
8445 k_norm: k_norm.as_deref(),
8446 inv_freq: &inv_freq,
8447 cpu_k,
8448 cpu_v,
8449 cpu_stored,
8450 o1: None,
8451 };
8452 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
8453 return false;
8454 }
8455 }
8456 graph.sync();
8457 let mut kbuf = vec![0f32; b * nkv * hd];
8458 let mut vbuf = vec![0f32; b * nkv * hd];
8459 if !crate::gpu_metal::kv_mirror_read_rows(
8460 self.mtp_kv_id(),
8461 Self::MTP_LAYER_BASE,
8462 nkv,
8463 hd,
8464 cpu_stored,
8465 b,
8466 &mut kbuf,
8467 &mut vbuf,
8468 ) {
8469 return false;
8470 }
8471 for r in 0..b {
8472 m.kv.append(
8473 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8474 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8475 &[],
8476 );
8477 }
8478 crate::gpu_metal::kv_mirror_set_stored(
8479 self.mtp_kv_id(),
8480 Self::MTP_LAYER_BASE,
8481 cpu_stored + b,
8482 );
8483 true
8484 }
8485
8486 fn draft_vocab_rows(head_rows: usize) -> usize {
8489 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
8490 let n = *N.get_or_init(|| {
8491 std::env::var("CMF_DRAFT_VOCAB")
8492 .ok()
8493 .and_then(|v| v.parse().ok())
8494 .unwrap_or(65536)
8495 });
8496 if n == 0 { head_rows } else { n.min(head_rows) }
8497 }
8498
8499 #[cfg(target_os = "macos")]
8504 fn mtp_step_metal(
8505 &mut self,
8506 m: &mut MtpModule,
8507 hidden: &[f32],
8508 next_token: u32,
8509 position: usize,
8510 want_logits: bool,
8511 ) -> Option<(Vec<f32>, Vec<f32>)> {
8512 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
8513 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
8514 || !crate::gpu::q1_force()
8515 || !crate::gpu::enabled_here()
8516 || self.attn_softcap > 0.0
8517 || self.attention_heads_per_layer.is_some()
8518 || m.kv.mode != crate::kv_cache::KvMode::F32
8519 || m.kv.o1.is_some()
8520 {
8521 return None;
8522 }
8523 let AttnKind::Full {
8524 wq,
8525 wk,
8526 wv,
8527 wo,
8528 q_norm,
8529 k_norm,
8530 output_gate,
8531 softplus_gate: None,
8532 bias: None,
8533 } = &m.layer.attn
8534 else {
8535 return None;
8536 };
8537 let FfnKind::Dense(d) = &m.layer.ffn else {
8538 return None;
8539 };
8540 if d.act != Act::Silu || !d.segs.is_empty() {
8541 return None;
8542 }
8543 let (pq, pk, pv, po) = (
8544 wq.q1_parts()?,
8545 wk.q1_parts()?,
8546 wv.q1_parts()?,
8547 wo.q1_parts()?,
8548 );
8549 let (g, u, dn) = (
8550 d.gate_proj.q1_parts()?,
8551 d.up_proj.q1_parts()?,
8552 d.down_proj.q1_parts()?,
8553 );
8554 let QTensor::Mapped { model, .. } = wq else {
8555 return None;
8556 };
8557 let model = model.clone();
8558 let lm = if want_logits {
8559 Some(self.weights.lm_head.q1_parts()?)
8560 } else {
8561 None
8562 };
8563 let dims = GraphDims {
8564 hidden: self.hidden_size,
8565 eps: self.rms_eps as f32,
8566 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8567 };
8568 let hs = self.hidden_size;
8571 let mut x = vec![0f32; hs];
8572 let mut graph = TokenGraph::new(&model, dims, &x)?;
8573 let mut folded = false;
8574 if let Some(eh) = m.eh_proj.q1_parts() {
8575 let e = self.embed_single(next_token);
8576 let mut cat = vec![0.0f32; 2 * hs];
8577 let (cat_e, cat_h) = cat.split_at_mut(hs);
8578 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
8579 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
8580 folded = graph.encode_input_proj(eh, &cat);
8581 }
8582 if !folded {
8583 x = self.mtp_block_input(m, hidden, next_token);
8584 graph = TokenGraph::new(&model, dims, &x)?;
8585 }
8586 let l = AttnGpuLayer {
8587 attn_norm: &m.layer.input_norm,
8588 post_norm: &m.layer.post_norm,
8589 wq: pq,
8590 wk: pk,
8591 wv: pv,
8592 wo: po,
8593 ffn: MetalFfn::Dense {
8594 gate: g,
8595 up: u,
8596 down: dn,
8597 },
8598 };
8599 let (nh, nkv, hd, rd) = (
8600 self.num_heads,
8601 self.num_kv_heads,
8602 self.head_dim,
8603 self.rotary_dim,
8604 );
8605 let inv_freq = self.inv_freq.clone();
8606 {
8607 let cache = &m.kv;
8608 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8609 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8610 let cpu_stored = cpu_k[0].len() / hd;
8611 let p = AttnDeviceParams {
8612 kv_id: self.mtp_kv_id(),
8613 layer: Self::MTP_LAYER_BASE,
8614 nh,
8615 nkv,
8616 hd,
8617 rd,
8618 position,
8619 scale: self.attn_scale,
8620 eps: self.rms_eps as f32,
8621 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8622 output_gate: *output_gate,
8623 q_norm: q_norm.as_deref(),
8624 k_norm: k_norm.as_deref(),
8625 inv_freq: &inv_freq,
8626 cpu_k,
8627 cpu_v,
8628 cpu_stored,
8629 o1: None,
8630 };
8631 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
8632 return None;
8633 }
8634 }
8635 let draft_rows = if let Some(lm) = lm {
8641 Self::draft_vocab_rows(lm.1)
8642 } else {
8643 0
8644 };
8645 if let Some(lm) = lm {
8646 if !graph.lm_head_ok(lm) {
8647 return None;
8648 }
8649 if draft_rows < lm.1 {
8650 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
8651 return None;
8652 }
8653 } else {
8654 graph.encode_lm_head(&m.final_norm, lm);
8655 }
8656 }
8657 graph.sync();
8658 let mut logits = Vec::new();
8659 if let Some(lm) = lm {
8660 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
8661 logits = attention::take_buf(n_read);
8662 graph.read_logits(&mut logits);
8663 logits.resize(self.vocab_size, f32::NEG_INFINITY);
8665 }
8666 graph.finish(&mut x);
8667 let mut krow = attention::take_buf(nkv * hd);
8668 let mut vrow = attention::take_buf(nkv * hd);
8669 if crate::gpu_metal::kv_mirror_read_last(
8670 self.mtp_kv_id(),
8671 Self::MTP_LAYER_BASE,
8672 nkv,
8673 hd,
8674 &mut krow,
8675 &mut vrow,
8676 ) {
8677 m.kv.append(&krow, &vrow, &[]);
8678 }
8679 attention::recycle_buf(&mut krow);
8680 attention::recycle_buf(&mut vrow);
8681 Some((logits, x))
8682 }
8683
8684 fn try_batch_graph_wgpu(
8685 &self,
8686 hiddens: &mut [f32],
8687 positions: &[usize],
8688 k: usize,
8689 spec: Option<crate::gpu::SpecTail<'_>>,
8690 ) -> crate::gpu::BatchGraphOutcome {
8691 let _tb = std::time::Instant::now();
8692 if self.attn_softcap > 0.0 {
8693 return crate::gpu::BatchGraphOutcome::Declined; }
8695 let nh = self.num_heads;
8696 let (nkv, hd, rd) = self.layer_geom(0);
8697 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8698 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
8699 if let Some((_, i, kind, rs)) = t.graph_weight() {
8700 return Some(crate::gpu::GraphW {
8701 idx: i,
8702 kind,
8703 row_scale: rs,
8704 data: &[],
8705 });
8706 }
8707 t.as_f32().map(|d| crate::gpu::GraphW {
8708 idx: 0,
8709 kind: 4,
8710 row_scale: &[],
8711 data: d,
8712 })
8713 }
8714 let built: Option<(
8715 Vec<crate::gpu::GraphLayer<'_>>,
8716 std::sync::Arc<cortiq_core::CmfModel>,
8717 )> = (|| {
8718 let mut layers = Vec::with_capacity(self.num_layers);
8719 let mut model = None;
8720 for li in 0..self.num_layers {
8721 let lw = &self.weights.layers[self.phys_layer(li)];
8722 let gffn = match &lw.ffn {
8729 FfnKind::Dense(d) if !d.segs.is_empty() => return None,
8730 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
8731 gate: gw(&d.gate_proj)?,
8732 up: gw(&d.up_proj)?,
8733 down: gw(&d.down_proj)?,
8734 },
8735 FfnKind::Moe(m) => {
8736 if m.router_sigmoid
8737 || m.expert_bias.is_some()
8738 || m.route_tau.is_some()
8739 || m.mask.is_some()
8740 {
8741 return None;
8742 }
8743 let (se, sg) = m.shared.as_ref()?;
8744 let sgate = gw(sg.as_ref()?)?;
8745 let router = gw(&m.router)?;
8746 let inter = m.experts.first()?.gate_proj.rows();
8747 let mut experts = Vec::with_capacity(m.experts.len() + 1);
8748 let mut q4tp: Option<bool> = None;
8749 let mut gu_q2: Option<bool> = None;
8750 for e in m.experts.iter().chain(std::iter::once(se)) {
8751 if !matches!(e.act, Act::Silu)
8752 || e.gate_proj.rows() != inter
8753 || e.up_proj.rows() != inter
8754 {
8755 return None;
8756 }
8757 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8761 Some((mm, gi)) => (
8762 mm,
8763 gi,
8764 e.up_proj.mapped_q4t()?.1,
8765 e.down_proj.mapped_q4t()?.1,
8766 false,
8767 false,
8768 ),
8769 None => match e.gate_proj.mapped_q2tp() {
8770 Some((mm, gi)) => (
8771 mm,
8772 gi,
8773 e.up_proj.mapped_q2tp()?.1,
8774 e.down_proj.mapped_q4tp()?.1,
8775 true,
8776 true,
8777 ),
8778 None => {
8779 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8780 (
8781 mm,
8782 gi,
8783 e.up_proj.mapped_q4tp()?.1,
8784 e.down_proj.mapped_q4tp()?.1,
8785 true,
8786 false,
8787 )
8788 }
8789 },
8790 };
8791 if *q4tp.get_or_insert(is_p) != is_p
8792 || *gu_q2.get_or_insert(is_q2) != is_q2
8793 {
8794 return None;
8795 }
8796 model.get_or_insert_with(|| mm.clone());
8797 experts.push((gi, ui, di));
8798 }
8799 crate::gpu::GraphFfn::Moe {
8800 router,
8801 shared_gate: sgate,
8802 experts,
8803 n_exp: m.experts.len(),
8804 top_k: m.top_k,
8805 inter,
8806 norm_topk: m.norm_topk_prob,
8807 q4tp: q4tp?,
8808 gu_q2: gu_q2.unwrap_or(false),
8809 sigmoid: false,
8810 bias: None,
8811 has_shared: true,
8812 }
8813 }
8814 _ => return None,
8815 };
8816 let attn = match &lw.attn {
8817 AttnKind::Full {
8818 wq,
8819 wk,
8820 wv,
8821 wo,
8822 q_norm,
8823 k_norm,
8824 output_gate,
8825 softplus_gate,
8826 bias,
8827 } => {
8828 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
8829 return None;
8830 }
8831 let (m, _, _, _) = wq.graph_weight()?;
8832 model = Some(m.clone());
8833 crate::gpu::GraphAttn::Full {
8834 wq: gw(wq)?,
8835 wk: gw(wk)?,
8836 wv: gw(wv)?,
8837 wo: gw(wo)?,
8838 q_norm: q_norm.as_deref(),
8839 k_norm: k_norm.as_deref(),
8840 bias: bias
8841 .as_ref()
8842 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8843 output_gate: *output_gate,
8844 cpu_k: self.kv_cache.layers[li].k_heads(),
8845 cpu_v: self.kv_cache.layers[li].v_heads(),
8846 }
8847 }
8848 AttnKind::LinearGdn(w) => {
8849 let cfg = self.gdn_cfg?;
8850 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
8851 model = Some(m.clone());
8852 crate::gpu::GraphAttn::Gdn {
8853 qkv: gw(&w.in_proj_qkv)?,
8854 z: gw(&w.in_proj_z)?,
8855 a: gw(&w.in_proj_a)?,
8856 b: gw(&w.in_proj_b)?,
8857 out: gw(&w.out_proj)?,
8858 conv1d: &w.conv1d,
8859 a_log: &w.a_log,
8860 dt_bias: &w.dt_bias,
8861 norm: &w.norm,
8862 nv: cfg.num_v_heads,
8863 nk: cfg.num_k_heads,
8864 dk: cfg.key_head_dim,
8865 dv: cfg.value_head_dim,
8866 kk: cfg.conv_kernel,
8867 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8868 }
8869 }
8870 _ => return None,
8871 };
8872 layers.push(crate::gpu::GraphLayer {
8873 input_norm: &lw.input_norm,
8874 attn,
8875 post_norm: &lw.post_norm,
8876 ffn: gffn,
8877 });
8878 }
8879 Some((layers, model?))
8880 })();
8881 let Some((layers, model)) = built else {
8882 {
8883 use std::sync::atomic::{AtomicBool, Ordering};
8884 static SAID: AtomicBool = AtomicBool::new(false);
8885 if !SAID.swap(true, Ordering::Relaxed) {
8886 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
8887 }
8888 }
8889 return crate::gpu::BatchGraphOutcome::Declined;
8890 };
8891 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8892 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
8893 }
8894 crate::gpu::forward_batch_graph(
8895 &model,
8896 self.graph_kv_id,
8897 &layers,
8898 &self.inv_freq,
8899 hiddens,
8900 nh,
8901 nkv,
8902 hd,
8903 rd,
8904 self.hidden_size,
8905 self.intermediate_size,
8906 positions,
8907 self.kv_cache.max_seq_len,
8908 gemma,
8909 self.rms_eps as f32,
8910 self.attn_scale,
8911 k,
8912 &(0..self.num_layers)
8913 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
8914 .collect::<Vec<_>>(),
8915 self.o1_epoch,
8916 spec,
8917 )
8918 }
8919
8920 fn draft_probe() -> bool {
8924 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8925 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
8926 }
8927
8928 #[cfg(feature = "gpu")]
8940 fn dsv4_spec_on() -> bool {
8941 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8942 *ON.get_or_init(|| {
8943 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
8947 return v != "0";
8948 }
8949 std::env::var("CMF_DSV4_SPEC")
8956 .map(|v| v != "0")
8957 .unwrap_or_else(|_| {
8958 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
8959 })
8960 })
8961 }
8962
8963 #[cfg(feature = "gpu")]
8970 fn dsv4_spec_step(
8971 &mut self,
8972 tip_token: u32,
8973 t_next: u32,
8974 next_pos: usize,
8975 max_extra: usize,
8976 drafted: &mut usize,
8977 accepted_ctr: &mut usize,
8978 ) -> Option<(Vec<u32>, usize)> {
8979 let t_all = std::time::Instant::now();
8980 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
8981 thread_local! {
8982 static LAST: std::cell::Cell<Option<std::time::Instant>> =
8983 const { std::cell::Cell::new(None) };
8984 }
8985 LAST.with(|l| {
8986 if let Some(prev) = l.get() {
8987 eprintln!(
8988 "между раундами {:.1} мс",
8989 prev.elapsed().as_secs_f64() * 1e3
8990 );
8991 }
8992 l.set(Some(std::time::Instant::now()));
8993 });
8994 }
8995 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
8996 eprintln!("spec_step: вход pos={next_pos}");
8997 }
8998 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
8999 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
9000 if self.dspark.is_none() {
9002 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9003 if t.is_empty() {
9004 return None;
9005 }
9006 crate::dsv4::dspark_arm(&t, cfg.dim);
9007 self.dspark = Some(crate::dsv4::DsparkState::new(
9008 self.dsv4_mtp.len(),
9009 &cfg,
9010 t.len(),
9011 ));
9012 }
9013 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9014 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
9015 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9016 eprintln!("spec_step: пак не построился (targets {targets:?})");
9017 }
9018 let pack = pack?;
9019 let block = crate::dsv4::dspark_block();
9020 let b_box = self.dsv4.as_mut()?;
9021 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
9022 let ds = self.dspark.as_mut()?;
9023 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
9026 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
9027 if dbg {
9028 eprintln!("spec_step: нет захвата");
9029 }
9030 return None;
9031 }
9032 ds.have_hidden = true;
9033 let tip_pos = next_pos.checked_sub(1)?;
9034 let draft_started = std::time::Instant::now();
9035 let mut conf = Vec::new();
9036 let props = crate::dsv4::dspark_draft_gpu(
9037 g,
9038 &self.dsv4_mtp,
9039 &cfg,
9040 ds,
9041 pack,
9042 st.kv_id,
9043 tip_token,
9044 tip_pos,
9045 self.pool.as_deref(),
9046 &mut conf,
9047 );
9048 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9049 *drafted += block;
9050 if props.is_empty() || props[0] != t_next {
9051 if dbg {
9052 eprintln!(
9053 "spec_step: черновик {} (props0={:?} t_next={t_next})",
9054 if props.is_empty() {
9055 "пуст"
9056 } else {
9057 "мимо"
9058 },
9059 props.first()
9060 );
9061 }
9062 return None;
9063 }
9064 let mut k_verify = crate::dsv4::dspark_verify_k()
9071 .min(props.len())
9072 .min(max_extra.saturating_add(1));
9073 let conf_min = {
9079 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
9080 *M.get_or_init(|| {
9081 std::env::var("CMF_DSPARK_CONF_MIN")
9082 .ok()
9083 .and_then(|v| v.parse().ok())
9084 .unwrap_or(0.0)
9085 })
9086 };
9087 if conf_min > 0.0 && conf.len() >= props.len() {
9088 let mut keep = 1usize;
9089 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
9090 keep += 1;
9091 }
9092 k_verify = k_verify.min(keep.max(2));
9093 }
9094 if k_verify < 2 {
9095 return None;
9096 }
9097 let mut fed = Vec::with_capacity(k_verify);
9098 fed.push(t_next);
9099 fed.extend_from_slice(&props[1..k_verify]);
9100 let mut argmax = Vec::new();
9101 let mut logits_all = Vec::new();
9102 let mut walked = Vec::new();
9103 let txn = crate::dsv4::dsv4_verify_chunk(
9104 g,
9105 layers,
9106 &cfg,
9107 st,
9108 &fed,
9109 next_pos,
9110 &self.inv_freq,
9111 self.pool.as_deref(),
9112 &targets,
9113 &mut argmax,
9114 &mut logits_all,
9115 &mut walked,
9116 );
9117 if txn.is_none() && dbg {
9118 eprintln!("spec_step: verify отказал");
9119 }
9120 let txn = txn?;
9121 let spec_gpu_end = txn.gpu_end;
9122 let b = fed.len();
9123 let mut accepted = 1usize;
9124 while accepted < b && fed[accepted] == argmax[accepted - 1] {
9125 accepted += 1;
9126 }
9127 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
9132 accepted = 1;
9133 }
9134 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
9135 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
9136 }
9137 let t_fin = std::time::Instant::now();
9138 if !crate::dsv4::dsv4_spec_finish(
9139 g,
9140 layers,
9141 &cfg,
9142 st,
9143 txn,
9144 accepted,
9145 &fed,
9146 &self.inv_freq,
9147 self.pool.as_deref(),
9148 ) {
9149 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
9150 return None;
9151 }
9152 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9153 eprintln!(
9154 "finish(k={accepted}): {:.1} мс",
9155 t_fin.elapsed().as_secs_f64() * 1e3
9156 );
9157 }
9158 *accepted_ctr += accepted - 1;
9159 let (hc, dim) = (cfg.hc_mult, cfg.dim);
9164 let dev_caps: Vec<usize> = targets
9169 .iter()
9170 .copied()
9171 .filter(|&t| t < spec_gpu_end)
9172 .collect();
9173 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
9174 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
9175 return None;
9176 }
9177 for t in 0..accepted {
9178 let tip = t + 1 == accepted;
9179 for (slot, &tl) in targets.iter().enumerate() {
9180 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
9181 let lo = (di * b + t) * hc * dim;
9182 crate::dsv4::dspark_capture(
9183 &caps_all[lo..lo + hc * dim],
9184 &cfg,
9185 slot,
9186 &mut ds.main_hidden,
9187 );
9188 } else if tip
9189 && crate::dsv4::dspark_peek_slot(slot, dim, {
9190 let lo = slot * dim;
9191 &mut ds.main_hidden[lo..lo + dim]
9192 })
9193 {
9194 } else {
9199 crate::dsv4::dspark_capture(
9203 &walked[t * hc * dim..(t + 1) * hc * dim],
9204 &cfg,
9205 slot,
9206 &mut ds.main_hidden,
9207 );
9208 }
9209 }
9210 crate::dsv4::dspark_ring_append(
9211 g,
9212 &self.dsv4_mtp,
9213 &cfg,
9214 ds,
9215 next_pos + t,
9216 self.pool.as_deref(),
9217 );
9218 }
9219 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
9220 self.graph_logits = Some(row);
9221 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
9226 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
9227 crate::dsv4::pick_tally_arm();
9228 }
9229 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9230 eprintln!(
9231 "spec_step total {:.1} мс (k={accepted})",
9232 t_all.elapsed().as_secs_f64() * 1e3
9233 );
9234 }
9235 Some((fed[1..accepted].to_vec(), next_pos + accepted))
9236 }
9237
9238 fn dspark_probe(&mut self, position: usize, token_id: u32) {
9239 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
9240 return;
9241 }
9242 let trunk_now = crate::dsv4::pick_tally_take();
9244 crate::dsv4::trunk_freq_note(&trunk_now);
9245 if !trunk_now.is_empty() {
9246 self.dspark_trunk_picks.push(trunk_now);
9247 let keep = crate::dsv4::dspark_block();
9248 if self.dspark_trunk_picks.len() > keep {
9249 self.dspark_trunk_picks.remove(0);
9250 }
9251 }
9252 for p in std::mem::take(&mut self.dspark_pending) {
9255 let Some(i) = position.checked_sub(p.0 + 1) else {
9256 continue;
9257 };
9258 let mut p = p;
9259 if i < p.1.len() {
9260 if p.2 && p.1[i] == token_id {
9261 p.3 = i + 1;
9262 } else {
9263 p.2 = false;
9264 }
9265 if i + 1 < p.1.len() {
9266 self.dspark_pending.push(p);
9267 continue;
9268 }
9269 }
9270 self.dspark_hist.push(p.3);
9271 self.dspark_real.push(token_id);
9272 }
9273 let Some(b) = &mut self.dsv4 else { return };
9274 let (g, layers, cfg) = (&b.0, &b.1, b.2);
9275 let n_layers = layers.len();
9276 if self.dspark.is_none() {
9277 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9278 if t.is_empty() {
9279 return;
9280 }
9281 eprintln!(
9282 "DSpark: захват со слоёв {t:?}, блок {}",
9283 crate::dsv4::dspark_block()
9284 );
9285 crate::dsv4::dspark_arm(&t, cfg.dim);
9286 self.dspark = Some(crate::dsv4::DsparkState::new(
9287 self.dsv4_mtp.len(),
9288 &cfg,
9289 t.len(),
9290 ));
9291 }
9292 let ds = self.dspark.as_mut().unwrap();
9293 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
9294 return; }
9296 let mut conf = Vec::new();
9297 crate::dsv4::pick_tally_arm();
9298 let draft_started = std::time::Instant::now();
9303 #[cfg(feature = "gpu")]
9304 let gpu_draft = crate::dsv4::dspark_gpu_on();
9305 #[cfg(not(feature = "gpu"))]
9306 let gpu_draft = false;
9307 let props = if gpu_draft {
9308 #[cfg(feature = "gpu")]
9309 {
9310 let kv_id = b.3.kv_id;
9311 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
9312 Some(pk) => crate::dsv4::dspark_draft_gpu(
9313 g,
9314 &self.dsv4_mtp,
9315 &cfg,
9316 ds,
9317 pk,
9318 kv_id,
9319 token_id,
9320 position,
9321 self.pool.as_deref(),
9322 &mut conf,
9323 ),
9324 None => Vec::new(),
9325 }
9326 }
9327 #[cfg(not(feature = "gpu"))]
9328 Vec::new()
9329 } else {
9330 crate::gpu::cpu_scope(|| {
9331 crate::dsv4::dspark_draft(
9332 g,
9333 &self.dsv4_mtp,
9334 &cfg,
9335 ds,
9336 token_id,
9337 position,
9338 self.pool.as_deref(),
9339 &mut conf,
9340 )
9341 })
9342 };
9343 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9344 let draft_picks = crate::dsv4::pick_tally_take();
9345 crate::dsv4::dspark_freq_note(&draft_picks);
9346 crate::dsv4::pick_tally_arm();
9349 if !props.is_empty() {
9350 let (tu, tt) = {
9354 let flat: Vec<(usize, Vec<usize>)> = self
9355 .dspark_trunk_picks
9356 .iter()
9357 .flat_map(|v| v.iter().cloned())
9358 .collect();
9359 let mut per: std::collections::HashMap<usize, Vec<usize>> =
9361 std::collections::HashMap::new();
9362 for (li, picks) in flat {
9363 per.entry(li).or_default().extend(picks);
9364 }
9365 let n = per.len().max(1);
9366 let mut u = 0usize;
9367 let mut t = 0usize;
9368 for (_, v) in per {
9369 t += v.len();
9370 u += v.iter().collect::<std::collections::HashSet<_>>().len();
9371 }
9372 (u / n, t / n)
9373 };
9374 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
9375 self.dspark_exp.push((tu, tt, du, dt));
9376 self.dspark_pending.push((position, props, true, 0));
9377 }
9378 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
9379 let n = self.dspark_hist.len() as f32;
9380 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
9381 let block = crate::dsv4::dspark_block();
9382 let mut at = vec![0usize; block + 1];
9383 for &k in &self.dspark_hist {
9384 at[k] += 1;
9385 }
9386 let mut surv = Vec::with_capacity(block);
9388 for i in 1..=block {
9389 let k = at[i..].iter().sum::<usize>() as f32 / n;
9390 surv.push(format!("{k:.2}"));
9391 }
9392 let distinct = self
9393 .dspark_real
9394 .iter()
9395 .collect::<std::collections::HashSet<_>>()
9396 .len();
9397 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
9398 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
9399 });
9400 let m = self.dspark_exp.len().max(1);
9401 eprintln!(
9402 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
9403 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
9404 self.dspark_hist.len(),
9405 mean + 1.0,
9406 surv.join(" ")
9407 );
9408 eprintln!(
9409 "DSpark: разных токенов {distinct} из {} (вырожденность), \
9410 эксперты ствол {}/{} на слой за {block} токенов, \
9411 черновик {}/{} за блок, draft {:.2} мс/блок",
9412 self.dspark_real.len(),
9413 tu / m,
9414 tt / m,
9415 du / m,
9416 dt / m,
9417 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
9418 );
9419 }
9420 }
9421
9422 fn forward_layers_upto(
9423 &mut self,
9424 hidden: &[f32],
9425 position: usize,
9426 task_mask: Option<&TaskMask>,
9427 upto: Option<usize>,
9428 ) -> Vec<f32> {
9429 if let Some(plan) = self.gpu_plan.clone() {
9435 if upto.is_none() && plan.len() > 1 {
9436 let mut h = hidden.to_vec();
9437 for &(dev, from, upto_incl) in plan.iter() {
9438 h = crate::gpu::with_device(dev, || {
9439 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
9440 });
9441 }
9442 return h;
9443 }
9444 }
9445 self.forward_layers_span(hidden, position, task_mask, 0, upto)
9446 }
9447
9448 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
9453 self.set_gpu_plan_at(devices, None)
9454 }
9455
9456 pub fn set_gpu_plan_at(
9460 &mut self,
9461 devices: Option<&[usize]>,
9462 at: Option<usize>,
9463 ) -> Result<(), String> {
9464 let Some(devs) = devices.filter(|d| d.len() > 1) else {
9465 self.gpu_plan = None;
9466 return Ok(());
9467 };
9468 self.split_supported()?;
9469 let n = self.num_layers;
9470 if devs.len() > n {
9471 return Err(format!("{} devices for {n} layers", devs.len()));
9472 }
9473 if let Some(k) = at {
9474 if k == 0 || k >= n {
9475 return Err(format!("split at {k}: the model has {n} layers"));
9476 }
9477 if devs.len() == 2 {
9478 self.gpu_plan = Some(std::sync::Arc::new(vec![
9479 (devs[0], 0, k - 1),
9480 (devs[1], k, n - 1),
9481 ]));
9482 return Ok(());
9483 }
9484 return Err(format!(
9485 "an explicit split point takes exactly 2 devices, got {}",
9486 devs.len()
9487 ));
9488 }
9489 let per = n.div_ceil(devs.len());
9490 let mut plan = Vec::with_capacity(devs.len());
9491 let mut from = 0usize;
9492 for &d in devs {
9493 if from >= n {
9494 break;
9495 }
9496 let upto = (from + per - 1).min(n - 1);
9497 plan.push((d, from, upto));
9498 from = upto + 1;
9499 }
9500 self.gpu_plan = Some(std::sync::Arc::new(plan));
9501 Ok(())
9502 }
9503
9504 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
9506 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
9507 }
9508
9509 fn forward_layers_span(
9515 &mut self,
9516 hidden: &[f32],
9517 position: usize,
9518 task_mask: Option<&TaskMask>,
9519 from: usize,
9520 upto: Option<usize>,
9521 ) -> Vec<f32> {
9522 debug_assert!(
9523 from == 0 || (self.dsv4.is_none() && self.qwen4_exp.is_none() && self.g3n.is_none())
9524 );
9525 if let Some(b) = &mut self.qwen4_exp {
9526 let _ = (task_mask, upto);
9527 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
9528 let mut logits = Vec::new();
9529 crate::qwen4_exp::forward_token(
9530 &b.0,
9531 &b.1,
9532 &b.2,
9533 &mut b.3,
9534 token_id,
9535 position,
9536 &self.inv_freq,
9537 self.pool.as_deref(),
9538 &mut logits,
9539 true,
9540 );
9541 self.graph_logits = Some(logits);
9542 return vec![0.0; self.hidden_size];
9543 }
9544 if let Some(b) = &mut self.dsv4 {
9550 let _ = (task_mask, upto);
9551 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
9552 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
9553 st.pos = position;
9554 let mut logits = Vec::new();
9555 crate::dsv4::forward_token(
9556 g,
9557 layers,
9558 &cfg,
9559 st,
9560 token_id,
9561 &self.inv_freq,
9562 self.pool.as_deref(),
9563 &mut logits,
9564 );
9565 self.graph_logits = Some(logits);
9566 self.dspark_probe(position, token_id);
9567 return vec![0.0; self.hidden_size];
9570 }
9571 if let Some(b) = &self.g3n {
9574 let _ = (task_mask, upto);
9575 return crate::g3n::g3n_forward(
9576 &b.0,
9577 &b.1,
9578 hidden,
9579 position,
9580 &mut self.kv_cache.layers,
9581 self.num_heads,
9582 self.num_kv_heads,
9583 self.head_dim,
9584 self.pool.as_deref(),
9585 );
9586 }
9587 let mut h = hidden.to_vec();
9588 let (nh, _nkv, _hd, hs, _rd, eps) = (
9591 self.num_heads,
9592 self.num_kv_heads,
9593 self.head_dim,
9594 self.hidden_size,
9595 self.rotary_dim,
9596 self.rms_eps,
9597 );
9598 let pool = self.pool.clone();
9599 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
9611 let graph_on = match graph_env.as_deref() {
9612 Some("0") => false,
9613 Some("prefill") => false, Some(_) => true,
9615 None => crate::gpu::wgpu_graph_default(),
9621 };
9622 let graph_trusted =
9623 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
9624 let race_eligible = graph_on
9625 && upto.is_none()
9626 && task_mask.is_none()
9627 && from == 0
9628 && !crate::gpu::graph_unsupported();
9629 let mut tail_start = 0usize;
9630 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
9631 let t_graph = std::time::Instant::now();
9632 let mut lg = Vec::new();
9633 let mut gl = 0usize;
9634 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
9635 let declined = built.is_none();
9636 let built = match built {
9637 Some(Ok(hh)) => Some(hh),
9638 Some(Err(())) => {
9639 self.clear_sequence_state();
9643 self.graph_failed
9644 .store(true, std::sync::atomic::Ordering::Relaxed);
9645 self.cancel
9646 .store(true, std::sync::atomic::Ordering::Relaxed);
9647 tracing::error!("token graph failed after admission; sequence state cleared");
9648 return vec![0.0; self.hidden_size];
9649 }
9650 None => None,
9651 };
9652 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
9657 crate::gpu::graph_mark_unsupported();
9658 }
9659 graph_note(built.is_some(), gl, self.num_layers);
9660 if let Some(hh) = built {
9661 let dur = t_graph.elapsed();
9662 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9663 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
9664 }
9665 if gl > 0 && gl < self.num_layers {
9666 h = hh;
9672 tail_start = gl;
9673 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
9674 if !graph_trusted {
9675 crate::gpu::graph_race_record(true, dur);
9676 }
9677 if !lg.is_empty() {
9678 lg.resize(self.vocab_size, 0.0);
9681 if let Some(c) = self.final_softcap {
9682 for l in lg.iter_mut() {
9683 *l = c * (*l / c).tanh();
9684 }
9685 }
9686 self.graph_logits = Some(lg);
9687 }
9688 return hh;
9689 }
9690 }
9696 }
9697 let span = from > 0 || upto.is_some();
9721 if span && graph_on && task_mask.is_none() && graph_trusted {
9722 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
9723 let mut lg = Vec::new();
9724 let mut gl = 0usize;
9725 let span_res =
9726 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
9727 let span_res = match span_res {
9728 Some(Ok(hh)) => Some(hh),
9729 Some(Err(())) => {
9730 self.clear_sequence_state();
9731 self.graph_failed
9732 .store(true, std::sync::atomic::Ordering::Relaxed);
9733 self.cancel
9734 .store(true, std::sync::atomic::Ordering::Relaxed);
9735 tracing::error!(
9736 "span token graph failed after admission; sequence state cleared"
9737 );
9738 return vec![0.0; self.hidden_size];
9739 }
9740 None => None,
9741 };
9742 graph_note(span_res.is_some(), gl, upto_excl - from);
9743 if std::env::var("CMF_GPU_DEBUG").is_ok() {
9744 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
9748 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
9749 eprintln!(
9750 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
9751 upto_excl - from,
9752 span_res.is_some()
9753 );
9754 }
9755 }
9756 if let Some(hh) = span_res {
9757 if gl == upto_excl - from {
9758 if !lg.is_empty() {
9759 lg.resize(self.vocab_size, 0.0);
9760 if let Some(c) = self.final_softcap {
9761 for l in lg.iter_mut() {
9762 *l = c * (*l / c).tanh();
9763 }
9764 }
9765 self.graph_logits = Some(lg);
9766 }
9767 crate::gpu::set_layer(-1);
9768 return hh;
9769 }
9770 h = hh;
9772 tail_start = from + gl;
9773 }
9774 }
9775 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
9776
9777 let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
9783 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9784
9785 #[cfg(target_os = "macos")]
9786 let mut gpu_skip_until = 0usize;
9787 for li in tail_start.max(from)..self.num_layers {
9788 let _capacity_tail = automatic_gpu_prefix
9789 .filter(|&prefix| li >= prefix)
9790 .map(|_| crate::gpu::enter_cpu_scope());
9791 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
9793 if li > u {
9794 break;
9795 }
9796 }
9797 if let Some(mask) = task_mask {
9798 if !mask.layer_alive(li) {
9799 continue; }
9801 }
9802 #[cfg(target_os = "macos")]
9806 {
9807 if li < gpu_skip_until {
9808 continue;
9809 }
9810 if task_mask.is_none() {
9811 let end = self.q1_graph_gpu(li, upto, position, &mut h);
9812 if end > li {
9813 gpu_skip_until = end;
9814 if self.is_loop_end(end - 1) && end < self.num_layers {
9817 h = inference::rms_norm(
9818 &h,
9819 &self.weights.final_norm,
9820 self.rms_eps,
9821 self.norm_style,
9822 );
9823 }
9824 continue;
9825 }
9826 }
9827 }
9828
9829 let lw = &self.weights.layers[self.phys_layer(li)];
9830 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9831 if tp.parse::<usize>().ok() == Some(position) {
9832 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
9833 eprintln!(
9834 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
9835 h[0], h[1]
9836 );
9837 }
9838 }
9839 inference::rms_norm_into(
9842 &h,
9843 &lw.input_norm,
9844 self.rms_eps,
9845 self.norm_style,
9846 &mut self.ws.n1,
9847 );
9848
9849 let attn_out = match &lw.attn {
9850 AttnKind::Mla(w) => {
9851 let inv_freq_l = self.layer_inv_freq(li);
9852 let rs = self.layer_rope_scale(li);
9853 let eps = self.rms_eps;
9854 let pool = self.pool.clone();
9855 mla_attention(
9856 w,
9857 &self.ws.n1,
9858 &mut self.kv_cache.layers[li],
9859 position,
9860 &inv_freq_l,
9861 rs,
9862 eps,
9863 pool.as_deref(),
9864 )
9865 }
9866 AttnKind::Linear(w) => {
9867 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
9868 vmf_phase_forward(
9869 &self.ws.n1,
9870 w,
9871 &cfg,
9872 &mut self.kv_cache.layers[li].linear_state,
9873 self.pool.as_deref(),
9874 )
9875 }
9876 AttnKind::Kda(w) => {
9877 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
9878 crate::linear_core::kda_forward(
9879 &self.ws.n1,
9880 w,
9881 &cfg,
9882 &mut self.kv_cache.layers[li].linear_state,
9883 self.pool.as_deref(),
9884 )
9885 }
9886 AttnKind::LinearGdn(w) => {
9887 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9888 gdn_forward(
9889 &self.ws.n1,
9890 w,
9891 &cfg,
9892 &mut self.kv_cache.layers[li].linear_state,
9893 self.pool.as_deref(),
9894 )
9895 }
9896 AttnKind::ShortConv(w) => {
9897 let cfg = self
9898 .short_conv_cfg
9899 .expect("short-conv layer without short_conv_cfg");
9900 short_conv_forward(
9901 &self.ws.n1,
9902 w,
9903 &cfg,
9904 &mut self.kv_cache.layers[li].linear_state,
9905 self.pool.as_deref(),
9906 )
9907 }
9908 AttnKind::Full {
9909 wq,
9910 wk,
9911 wv,
9912 wo,
9913 q_norm,
9914 k_norm,
9915 output_gate,
9916 softplus_gate,
9917 bias,
9918 } if self.kv_cache.layers[li].o1_sealed() => {
9919 let inv_freq_l = self.layer_inv_freq(li);
9922 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9923 let cfg = QwenAttnCfg {
9924 num_heads: self.layer_num_heads(li),
9925 num_kv_heads: nkv_l,
9926 head_dim: hd_l,
9927 hidden_size: hs,
9928 position,
9929 inv_freq: &inv_freq_l,
9930 rotary_dim: rd_l,
9931 scale: self.attn_scale,
9932 softcap: self.attn_softcap,
9933 window: None,
9934 v_norm: self.attn_v_norm,
9935 q_norm: q_norm.as_deref(),
9936 k_norm: k_norm.as_deref(),
9937 output_gate: *output_gate,
9938 softplus_gate: softplus_gate
9939 .as_ref()
9940 .map(|(gate, per_head)| (gate, *per_head)),
9941 rope_scale: self.layer_rope_scale(li),
9942 bias: bias
9943 .as_ref()
9944 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9945 rms_eps: eps,
9946 norm_style: self.norm_style,
9947 pool: pool.as_deref(),
9948 };
9949 attention::qwen_attention_nystrom(
9950 &self.ws.n1,
9951 wq,
9952 wk,
9953 wv,
9954 wo,
9955 &mut self.kv_cache.layers[li],
9956 &cfg,
9957 )
9958 }
9959 AttnKind::Full {
9960 wq,
9961 wk,
9962 wv,
9963 wo,
9964 q_norm,
9965 k_norm,
9966 output_gate,
9967 softplus_gate,
9968 bias,
9969 } => 'attn: {
9970 if graph_on
9973 && !*output_gate
9974 && softplus_gate.is_none()
9975 && self.attention_heads_per_layer.is_none()
9976 && bias.is_none()
9977 && task_mask.is_none()
9978 {
9979 let inv_freq_l = self.layer_inv_freq(li);
9980 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9981 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9982 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
9983 wq.mapped_q1(),
9984 wk.mapped_q1(),
9985 wv.mapped_q1(),
9986 wo.mapped_q1(),
9987 ) {
9988 let gm = gm.clone();
9989 let mut out = vec![0f32; hs];
9990 let cache = &self.kv_cache.layers[li];
9991 if crate::gpu::attn_dropin(
9992 &gm,
9993 self.graph_kv_id,
9994 li,
9995 &self.ws.n1,
9996 qi,
9997 ki,
9998 vi,
9999 oi,
10000 q_norm.as_deref(),
10001 k_norm.as_deref(),
10002 &inv_freq_l,
10003 nh,
10004 nkv_l,
10005 hd_l,
10006 rd_l,
10007 hs,
10008 position,
10009 self.kv_cache.max_seq_len,
10010 gemma,
10011 eps as f32,
10012 cache.k_heads(),
10013 cache.v_heads(),
10014 &mut out,
10015 ) {
10016 break 'attn out;
10017 }
10018 }
10019 }
10020 let masked = task_mask
10021 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
10022 .unwrap_or(false);
10023 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
10024 match (masked, f32_view) {
10025 (true, (Some(q), Some(k), Some(v), Some(o))) => {
10028 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
10029 attention::multi_head_attention(
10030 &self.ws.n1,
10031 q,
10032 k,
10033 v,
10034 o,
10035 &mut self.kv_cache.layers[li],
10036 self.num_heads,
10037 self.num_kv_heads,
10038 self.head_dim,
10039 self.hidden_size,
10040 position,
10041 &active_heads,
10042 &self.inv_freq,
10043 )
10044 }
10045 (masked, _) => {
10046 if masked {
10047 tracing::warn!(
10048 "layer {li}: head mask on quantized weights not \
10049 supported yet — executing dense"
10050 );
10051 }
10052 let inv_freq_l = self.layer_inv_freq(li);
10053 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10054 let cfg = QwenAttnCfg {
10055 num_heads: self.layer_num_heads(li),
10056 num_kv_heads: nkv_l,
10057 head_dim: hd_l,
10058 hidden_size: hs,
10059 position,
10060 inv_freq: &inv_freq_l,
10061 rotary_dim: rd_l,
10062 scale: self.attn_scale,
10063 softcap: self.attn_softcap,
10064 window: self.layer_window(li),
10065 v_norm: self.attn_v_norm,
10066 q_norm: q_norm.as_deref(),
10067 k_norm: k_norm.as_deref(),
10068 output_gate: *output_gate,
10069 softplus_gate: softplus_gate
10070 .as_ref()
10071 .map(|(gate, per_head)| (gate, *per_head)),
10072 rope_scale: self.layer_rope_scale(li),
10073 bias: bias
10074 .as_ref()
10075 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10076 rms_eps: eps,
10077 norm_style: self.norm_style,
10078 pool: pool.as_deref(),
10079 };
10080 attention::qwen_attention(
10081 &self.ws.n1,
10082 wq,
10083 wk,
10084 wv,
10085 wo,
10086 &mut self.kv_cache.layers[li],
10087 &cfg,
10088 )
10089 }
10090 }
10091 }
10092 };
10093 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
10096 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
10097 None => attn_out,
10098 };
10099 let lw = &self.weights.layers[self.phys_layer(li)];
10100 inference::add_rmsnorm_fused_into(
10101 &mut h,
10102 &attn_out,
10103 &lw.post_norm,
10104 self.rms_eps,
10105 self.norm_style,
10106 &mut self.ws.p1,
10107 );
10108 let mut attn_out = attn_out;
10109 attention::recycle_buf(&mut attn_out);
10110 let post_normed = &self.ws.p1;
10111
10112 let ffn_masked = task_mask
10113 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
10114 .unwrap_or(false);
10115 let ffn_out = match (ffn_masked, &lw.ffn) {
10127 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
10131 let row = task_mask
10132 .and_then(|tm| tm.ffn_masks.get(li))
10133 .map(|v| v.as_slice());
10134 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
10135 }
10136 (true, FfnKind::Dense(d)) => {
10137 let tm = task_mask.unwrap();
10138 let alive = tm.ffn_active_count(li);
10139 let deep = alive * 2 <= self.intermediate_size;
10140 if deep && d.down_proj.sparse_col_ok() {
10141 let active = tm.ffn_active_indices(li);
10142 sparse_ffn_quant(
10143 d,
10144 post_normed,
10145 &active,
10146 self.hidden_size,
10147 self.pool.as_deref(),
10148 )
10149 } else if deep
10150 && let (Some(g), Some(u), Some(dn)) = (
10151 d.gate_proj.as_f32(),
10152 d.up_proj.as_f32(),
10153 d.down_proj.as_f32(),
10154 )
10155 {
10156 let active = tm.ffn_active_indices(li);
10157 inference::sparse_ffn_forward(
10158 post_normed,
10159 g,
10160 u,
10161 dn,
10162 self.hidden_size,
10163 self.intermediate_size,
10164 &active,
10165 self.pool.as_deref(),
10166 )
10167 } else {
10168 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
10169 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
10170 }
10171 }
10172 (true, FfnKind::Moe(m)) => {
10173 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
10177 ffn_forward(
10178 &lw.ffn,
10179 post_normed,
10180 self.pool.as_deref(),
10181 allowed.as_deref(),
10182 )
10183 }
10184 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
10185 dm,
10186 post_normed,
10187 &h,
10188 self.rms_eps,
10189 self.norm_style,
10190 self.pool.as_deref(),
10191 ),
10192 (false, _) => match &lw.ffn {
10193 FfnKind::DenseMoe(dm) => dense_moe_ffn(
10194 dm,
10195 post_normed,
10196 &h,
10197 self.rms_eps,
10198 self.norm_style,
10199 self.pool.as_deref(),
10200 ),
10201 _ => {
10202 let allowed = match (&lw.ffn, task_mask) {
10203 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
10204 _ => None,
10205 };
10206 ffn_forward(
10207 &lw.ffn,
10208 post_normed,
10209 self.pool.as_deref(),
10210 allowed.as_deref(),
10211 )
10212 }
10213 },
10214 };
10215 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
10216 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
10217 None => ffn_out,
10218 };
10219 for (i, &f) in ffn_out.iter().enumerate() {
10220 h[i] += f;
10221 }
10222 let mut ffn_out = ffn_out;
10223 attention::recycle_buf(&mut ffn_out);
10224
10225 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
10227 for v in h.iter_mut() {
10228 *v *= sc;
10229 }
10230 }
10231
10232 if self.is_loop_end(li) && li + 1 < self.num_layers {
10235 h = inference::rms_norm(
10236 &h,
10237 &self.weights.final_norm,
10238 self.rms_eps,
10239 self.norm_style,
10240 );
10241 }
10242
10243 if self.dyn_phi_layer == Some(li) {
10247 self.update_dyn_phi(&h);
10248 }
10249 }
10250 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
10252 crate::gpu::graph_race_record(false, t.elapsed());
10253 }
10254
10255 h
10256 }
10257
10258 fn update_dyn_phi(&mut self, h: &[f32]) {
10261 const A: f32 = 0.2;
10262 if self.dyn_phi_ema.len() != h.len() {
10263 self.dyn_phi_ema = vec![0.0; h.len()];
10264 self.dyn_phi_seen = 0;
10265 }
10266 if self.dyn_phi_seen == 0 {
10267 self.dyn_phi_ema.copy_from_slice(h);
10268 } else {
10269 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
10270 *e = (1.0 - A) * *e + A * v;
10271 }
10272 }
10273 self.dyn_phi_seen += 1;
10274 }
10275
10276 pub fn dyn_phi(&self) -> &[f32] {
10278 &self.dyn_phi_ema
10279 }
10280
10281 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
10283 self.dyn_phi_layer = layer;
10284 self.dyn_phi_ema.clear();
10285 self.dyn_phi_seen = 0;
10286 }
10287
10288 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
10290 let Some(model) = &self.model else {
10291 return Vec::new();
10292 };
10293 model
10294 .header
10295 .skills
10296 .iter()
10297 .enumerate()
10298 .filter_map(|(i, sk)| {
10299 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
10300 let sel = sk.selection.as_ref()?;
10301 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
10302 })
10303 .collect()
10304 }
10305
10306 pub fn active_skill(&self) -> Option<usize> {
10308 self.dyn_active
10309 }
10310
10311 pub fn enable_dynamic_routing(&mut self) -> usize {
10316 use crate::swarm::{DynRouter, RoutableSkill};
10317 let Some(model) = self.model.clone() else {
10318 return 0;
10319 };
10320 if self.dyn_blend_loaded {
10323 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
10324 return 0;
10325 }
10326 if let Some(a) = self.dyn_active {
10330 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
10331 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
10332 return 0;
10333 }
10334 }
10335 let hidden = self.hidden_size;
10336 let mut skills = Vec::new();
10337 for (idx, id, _phi) in self.dynamic_skills() {
10338 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
10339 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
10340 skills.push(rs);
10341 }
10342 }
10343 }
10344 if skills.is_empty() {
10345 return 0;
10346 }
10347 let phi = skills[0].phi_layer;
10349 if skills.iter().any(|s| s.phi_layer != phi) {
10350 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
10351 }
10352 let n = skills.len();
10353 self.set_dyn_phi_layer(Some(phi));
10354 self.dyn_router = Some(DynRouter::new(skills));
10355 n
10356 }
10357
10358 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
10360 self.dyn_router
10361 .as_ref()
10362 .map(|r| r.switches.clone())
10363 .unwrap_or_default()
10364 }
10365
10366 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
10369 let rows = self.weights.lm_head.rows();
10370 let mut logits = attention::take_buf(rows.min(self.vocab_size));
10371 self.weights
10372 .lm_head
10373 .matvec(hidden, &mut logits, self.pool.as_deref());
10374 logits.resize(self.vocab_size, 0.0);
10375 if let Some(m) = self.logit_multiplier {
10376 for l in logits.iter_mut() {
10377 *l *= m;
10378 }
10379 }
10380 if let Some(c) = self.final_softcap {
10381 for l in logits.iter_mut() {
10382 *l = c * (*l / c).tanh();
10383 }
10384 }
10385 if let Some(cm) = self.head_clusters.as_ref() {
10386 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
10387 }
10388 logits
10389 }
10390
10391 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
10394 let h = hidden.len();
10395 let ncl = cm.len() / h.max(1);
10396 if ncl == 0 || logits.len() % ncl != 0 {
10397 return;
10398 }
10399 let cs = logits.len() / ncl;
10400 let mut lc = vec![0.0f32; ncl];
10402 for c in 0..ncl {
10403 let row = &cm[c * h..(c + 1) * h];
10404 let mut s = 0.0f32;
10405 for j in 0..h {
10406 s += row[j] * hidden[j];
10407 }
10408 lc[c] = s;
10409 }
10410 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10411 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
10412 for c in 0..ncl {
10413 let blk = &mut logits[c * cs..(c + 1) * cs];
10414 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10415 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
10416 let add = lc[c] - lse - bl;
10417 for v in blk.iter_mut() {
10418 *v += add;
10419 }
10420 }
10421 }
10422
10423 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
10428 self.clear_sequence_state();
10429 crate::gpu::graph_race_begin_generation();
10433 if task_mask.is_none() {
10434 self.o1_begin();
10435 }
10436 let mut hidden = vec![0.0f32; self.hidden_size];
10437 for (pos, &id) in ids.iter().enumerate() {
10438 let emb = self.embed_single(id);
10439 hidden = self.forward_layers(&emb, pos, task_mask);
10440 }
10441 if let Err(err) = self.o1_seal_checked() {
10442 self.o1_fail(err);
10443 }
10444 inference::rms_norm_into(
10445 &hidden,
10446 &self.weights.final_norm,
10447 self.rms_eps,
10448 self.norm_style,
10449 &mut self.ws.n1,
10450 );
10451 self.lm_head_forward(&self.ws.n1)
10452 }
10453}
10454
10455pub fn create_test_pipeline(
10457 hidden_size: usize,
10458 intermediate_size: usize,
10459 num_heads: usize,
10460 num_kv_heads: usize,
10461 head_dim: usize,
10462 num_layers: usize,
10463 vocab_size: usize,
10464) -> Pipeline {
10465 let synth = |n: usize, salt: usize| -> Vec<f32> {
10468 (0..n)
10469 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
10470 .collect()
10471 };
10472 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
10473 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
10474 };
10475 let layer_weights: Vec<LayerWeights> = (0..num_layers)
10476 .map(|li| LayerWeights {
10477 input_norm: vec![1.0; hidden_size],
10478 post_norm: vec![1.0; hidden_size],
10479 attn_out_norm: None,
10480 ffn_out_norm: None,
10481 layer_scale: None,
10482 ffn: FfnKind::Dense(DenseFfn {
10483 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
10484 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
10485 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
10486 act: Act::Silu,
10487 down_t: None,
10488 segs: Vec::new(),
10489 }),
10490 attn: AttnKind::Full {
10491 bias: None,
10492 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
10493 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
10494 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
10495 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
10496 q_norm: None,
10497 k_norm: None,
10498 output_gate: false,
10499 softplus_gate: None,
10500 },
10501 })
10502 .collect();
10503
10504 Pipeline::new(
10505 Tokenizer::byte_level(),
10506 PipelineWeights {
10507 embed_tokens: qt(vocab_size, hidden_size, 100),
10508 layers: layer_weights,
10509 lm_head: qt(vocab_size, hidden_size, 200),
10510 final_norm: vec![1.0; hidden_size],
10511 },
10512 hidden_size,
10513 intermediate_size,
10514 num_heads,
10515 num_kv_heads,
10516 head_dim,
10517 num_layers,
10518 num_layers, false, vocab_size,
10521 1e-6,
10522 10_000.0,
10523 NormStyle::Qwen,
10524 4096,
10525 SamplerConfig {
10526 seed: Some(42),
10527 ..Default::default()
10528 },
10529 )
10530}
10531
10532#[inline]
10537fn mask_bit(row: &[u8], j: usize) -> bool {
10538 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
10539}
10540
10541fn mask_gain() -> f32 {
10552 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
10553 *G.get_or_init(|| {
10554 std::env::var("CMF_FFN_MASK_GAIN")
10555 .ok()
10556 .and_then(|v| v.parse().ok())
10557 .unwrap_or(1.0)
10558 })
10559}
10560
10561fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
10562 let fill = meanfill().and_then(|(i, v)| {
10565 let li = crate::gpu::cur_layer();
10566 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
10567 });
10568 for r in 0..rows {
10569 let base = r * inter;
10570 for (bi, &byte) in row.iter().enumerate() {
10571 if byte == 0xFF {
10572 continue;
10573 }
10574 let j0 = bi * 8;
10575 for bit in 0..8 {
10576 let j = j0 + bit;
10577 if j < inter && byte & (1 << bit) == 0 {
10578 g[base + j] = fill.map_or(0.0, |f| f[j]);
10579 }
10580 }
10581 }
10582 }
10583 let gain = mask_gain();
10584 if gain != 1.0 {
10585 for v in g[..rows * inter].iter_mut() {
10586 *v *= gain;
10587 }
10588 }
10589}
10590
10591#[inline]
10593fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
10594 row.is_none_or(|r| mask_bit(r, i))
10595}
10596
10597fn all_bits_on(row: &[u8], n: usize) -> bool {
10600 (0..n).all(|i| mask_bit(row, i))
10601}
10602
10603fn tube_topk() -> usize {
10611 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10612 *K.get_or_init(|| {
10613 std::env::var("CMF_TUBE_TOPK")
10614 .ok()
10615 .and_then(|v| v.parse().ok())
10616 .unwrap_or(0)
10617 })
10618}
10619
10620fn tube_score_oracle() -> bool {
10621 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10622 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
10623}
10624
10625fn tube_ffn_routed(
10632 d: &DenseFfn,
10633 xs: &[f32],
10634 b: usize,
10635 pool: Option<&Pool>,
10636 mask_row: Option<&[u8]>,
10637 k: usize,
10638) -> Vec<f32> {
10639 let hidden = d.down_proj.rows();
10640 let core = d.gate_proj.rows();
10641 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
10642 let mut out = match (b, core_full, mask_row) {
10643 (1, true, _) => dense_ffn(d, xs, pool),
10644 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
10645 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
10646 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
10647 };
10648 let cand: Vec<usize> = (0..d.segs.len())
10649 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
10650 .collect();
10651 if cand.is_empty() {
10652 return out;
10653 }
10654 let oracle = tube_score_oracle();
10658 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
10659 let mut scores = vec![0f32; b * cand.len()];
10660 for (ci, &i) in cand.iter().enumerate() {
10661 let seg = &d.segs[i];
10662 let w = seg.width;
10663 let mut g = vec![0.0f32; b * w];
10664 if b == 1 {
10665 seg.gate.matvec(xs, &mut g, pool);
10666 } else {
10667 seg.gate.matmat(xs, b, &mut g, pool);
10668 }
10669 for v in g.iter_mut() {
10670 *v = Act::Silu.combine(*v, 1.0);
10671 }
10672 if !oracle {
10673 for t in 0..b {
10674 scores[t * cand.len() + ci] =
10675 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
10676 }
10677 }
10678 if oracle || b > 1 {
10679 let mut u = vec![0.0f32; b * w];
10680 if b == 1 {
10681 seg.up.matvec(xs, &mut u, pool);
10682 } else {
10683 seg.up.matmat(xs, b, &mut u, pool);
10684 }
10685 for (a, &v) in g.iter_mut().zip(u.iter()) {
10686 *a *= v;
10687 }
10688 if oracle {
10689 for t in 0..b {
10690 scores[t * cand.len() + ci] =
10691 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
10692 }
10693 }
10694 }
10695 acts.push(g);
10696 }
10697 let keep = k.min(cand.len());
10699 let mut scratch: Vec<f32> = Vec::new();
10700 for t in 0..b {
10701 let mut sc: Vec<(f32, usize)> = (0..cand.len())
10702 .map(|ci| (scores[t * cand.len() + ci], ci))
10703 .collect();
10704 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
10705 let mut alive = vec![false; cand.len()];
10706 for &(_, ci) in sc.iter().take(keep) {
10707 alive[ci] = true;
10708 }
10709 if b > 1 {
10710 for (ci, a) in acts.iter_mut().enumerate() {
10711 if !alive[ci] {
10712 let w = d.segs[cand[ci]].width;
10713 a[t * w..(t + 1) * w].fill(0.0);
10714 }
10715 }
10716 } else {
10717 for (ci, &i) in cand.iter().enumerate() {
10721 if !alive[ci] {
10722 continue;
10723 }
10724 let seg = &d.segs[i];
10725 let w = seg.width;
10726 let g = &mut acts[ci];
10727 if !tube_score_oracle() {
10728 scratch.clear();
10729 scratch.resize(w, 0.0);
10730 seg.up.matvec(xs, &mut scratch, pool);
10731 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
10732 *a *= v;
10733 }
10734 }
10735 let mut acc = vec![0.0f32; hidden];
10736 seg.down.matvec(g, &mut acc, pool);
10737 for (o, a) in out.iter_mut().zip(&acc) {
10738 *o += *a;
10739 }
10740 }
10741 }
10742 }
10743 if b > 1 {
10744 for (ci, &i) in cand.iter().enumerate() {
10745 let seg = &d.segs[i];
10746 let mut acc = vec![0.0f32; b * hidden];
10747 seg.down.matmat(&acts[ci], b, &mut acc, pool);
10748 for (o, a) in out.iter_mut().zip(&acc) {
10749 *o += *a;
10750 }
10751 }
10752 }
10753 out
10754}
10755
10756fn tube_ffn(
10762 d: &DenseFfn,
10763 xs: &[f32],
10764 b: usize,
10765 pool: Option<&Pool>,
10766 mask_row: Option<&[u8]>,
10767) -> Vec<f32> {
10768 if tube_topk() > 0 {
10769 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
10770 }
10771 let hidden = d.down_proj.rows();
10772 let core = d.gate_proj.rows();
10773 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
10774 let mut out = match (b, core_full, mask_row) {
10775 (1, true, _) => dense_ffn(d, xs, pool),
10776 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
10777 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
10778 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
10779 };
10780 TUBE_SCRATCH.with(|sc| {
10781 let mut sc = sc.borrow_mut();
10782 let [g, u, acc] = &mut *sc;
10783 for seg in &d.segs {
10784 if !tube_bit(mask_row, seg.start) {
10785 continue;
10786 }
10787 let w = seg.width;
10788 g.resize(b * w, 0.0);
10789 if b == 1
10790 && d.act == Act::Silu
10791 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
10792 {
10793 } else {
10795 u.resize(b * w, 0.0);
10796 if b == 1 {
10797 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
10798 } else {
10799 seg.gate.matmat(xs, b, g, pool);
10800 seg.up.matmat(xs, b, u, pool);
10801 }
10802 for i in 0..b * w {
10803 g[i] = d.act.combine(g[i], u[i]);
10804 }
10805 }
10806 acc.resize(b * hidden, 0.0);
10807 acc.fill(0.0);
10808 if b == 1 {
10809 seg.down.matvec(g, acc, pool);
10810 } else {
10811 seg.down.matmat(g, b, acc, pool);
10812 }
10813 for (o, a) in out.iter_mut().zip(acc.iter()) {
10814 *o += *a;
10815 }
10816 }
10817 out
10818 })
10819}
10820
10821thread_local! {
10822 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
10826 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
10827}
10828
10829fn dense_ffn_batch(
10830 d: &DenseFfn,
10831 xs: &[f32],
10832 b: usize,
10833 pool: Option<&Pool>,
10834 mask_row: Option<&[u8]>,
10835) -> Vec<f32> {
10836 let inter = d.gate_proj.rows();
10837 let hidden = d.down_proj.rows();
10838 if mask_row.is_none()
10846 && d.act == Act::Silu
10847 && b >= 32
10848 && crate::gpu::enabled_here()
10849 && !crate::gpu::mm_killed()
10850 && refit_dir().is_none()
10855 && !ffn_probe_active()
10860 {
10861 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
10862 d.gate_proj.mapped_q4t(),
10863 d.up_proj.mapped_q4t(),
10864 d.down_proj.mapped_q4t(),
10865 ) {
10866 let mut out = vec![0.0f32; b * hidden];
10867 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
10868 return out;
10869 }
10870 }
10871 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
10876 d.gate_proj.mapped_q4tp(),
10877 d.up_proj.mapped_q4tp(),
10878 d.down_proj.mapped_q4tp(),
10879 ) {
10880 let mut out = vec![0.0f32; b * hidden];
10881 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
10882 return out;
10883 }
10884 }
10885 }
10886 let mut g = vec![0.0f32; b * inter];
10887 d.gate_proj.matmat(xs, b, &mut g, pool);
10888 let mut u = vec![0.0f32; b * inter];
10889 d.up_proj.matmat(xs, b, &mut u, pool);
10890 if gate_topk() > 0 && d.act == Act::Silu {
10891 for t in 0..b {
10892 let row = &mut g[t * inter..(t + 1) * inter];
10893 for v in row.iter_mut() {
10894 *v = Act::Silu.combine(*v, 1.0);
10895 }
10896 keep_top_k(row, gate_topk());
10897 }
10898 for i in 0..b * inter {
10899 g[i] *= u[i];
10900 }
10901 } else {
10902 for i in 0..b * inter {
10903 g[i] = d.act.combine(g[i], u[i]);
10904 }
10905 }
10906 if let Some(row) = mask_row {
10907 zero_masked_cols(&mut g, b, inter, row);
10908 }
10909 if oracle_topk() > 0 {
10910 for t in 0..b {
10911 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
10912 }
10913 }
10914 let mut out = vec![0.0f32; b * hidden];
10915 d.down_proj.matmat(&g, b, &mut out, pool);
10916 if refit_dir().is_some() {
10917 let li = crate::gpu::cur_layer();
10918 if li >= 0 {
10919 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
10920 }
10921 }
10922 FFN_PROBE.with(|pr| {
10926 if let Some(acc) = pr.borrow_mut().as_mut() {
10927 let li = crate::gpu::cur_layer();
10928 if li < 0 {
10929 return;
10930 }
10931 let Some(row) = acc.get_mut(li as usize) else {
10932 return;
10933 };
10934 let sq = probe_sq();
10935 for t in 0..b {
10936 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
10937 *a += if sq {
10938 (v as f64) * (v as f64)
10939 } else {
10940 (v as f64).abs()
10941 };
10942 }
10943 }
10944 }
10945 });
10946 out
10947}
10948
10949fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
10954 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10955 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10956 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
10957 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
10958 if (!on && !dump) || b == 0 {
10959 return;
10960 }
10961 let hidden = xs.len() / b;
10962 if on {
10963 let mut acc = m.act_sq.borrow_mut();
10964 if acc.len() < hidden {
10965 acc.resize(hidden, 0.0);
10966 }
10967 for t in 0..b {
10968 let row = &xs[t * hidden..(t + 1) * hidden];
10969 for (a, &v) in acc.iter_mut().zip(row) {
10970 *a += (v as f64) * (v as f64);
10971 }
10972 }
10973 }
10974 if dump {
10975 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
10978 .ok()
10979 .and_then(|v| v.parse().ok())
10980 .unwrap_or(4096);
10981 let mut rows = m.act_rows.borrow_mut();
10982 if rows.len() < cap * hidden {
10983 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
10984 rows.extend_from_slice(&xs[..take * hidden]);
10985 }
10986 }
10987}
10988
10989#[derive(Clone, Copy)]
10992struct SendVecs(*mut Vec<f32>);
10993unsafe impl Send for SendVecs {}
10994unsafe impl Sync for SendVecs {}
10995impl SendVecs {
10996 #[inline]
10997 fn at(self, i: usize) -> *mut Vec<f32> {
10998 unsafe { self.0.add(i) }
10999 }
11000}
11001
11002fn moe_ffn_batch(
11003 m: &MoeFfn,
11004 xs: &[f32],
11005 b: usize,
11006 hidden: usize,
11007 pool: Option<&Pool>,
11008 allowed: Option<&[bool]>,
11009) -> Vec<f32> {
11010 accumulate_act(m, xs, b);
11011 let ne = m.experts.len();
11012 let mut logits = vec![0.0f32; b * ne];
11013 match &m.resonance {
11014 Some(r) => {
11015 let hdim = xs.len() / b.max(1);
11016 for bi in 0..b {
11017 r.scores(
11018 &xs[bi * hdim..(bi + 1) * hdim],
11019 &mut logits[bi * ne..(bi + 1) * ne],
11020 );
11021 }
11022 }
11023 None => m.router.matmat(xs, b, &mut logits, pool),
11024 }
11025
11026 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
11029 {
11030 let mut st = m.stats.borrow_mut();
11031 if st.len() < ne {
11032 st.resize(ne, 0);
11033 }
11034 for bi in 0..b {
11035 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
11036 for &e in &idx {
11037 st[e] += 1;
11038 assign[e].push((bi, p[e] / wsum));
11039 }
11040 }
11041 }
11042
11043 let mut out = vec![0.0f32; b * hidden];
11044 let cols = m.experts[0].gate_proj.cols();
11045 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
11046 let sb = list.len();
11047 let mut sub = vec![0.0f32; sb * cols];
11048 for (k, &(bi, _)) in list.iter().enumerate() {
11049 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11050 }
11051 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
11052 for (k, &(bi, w)) in list.iter().enumerate() {
11053 for i in 0..hidden {
11054 out[bi * hidden + i] += w * eo[k * hidden + i];
11055 }
11056 }
11057 };
11058 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
11064 if pool.is_some() && active.len() >= 8 {
11065 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
11066 {
11067 let panel_ptr = SendVecs(panels.as_mut_ptr());
11068 let experts = &m.experts;
11071 let (active_r, assign_r) = (&active, &assign);
11072 let run = |start: usize, end: usize| {
11073 for ai in start..end {
11074 let e = active_r[ai];
11075 let list = &assign_r[e];
11076 let sb = list.len();
11077 let mut sub = vec![0.0f32; sb * cols];
11078 for (k, &(bi, _)) in list.iter().enumerate() {
11079 sub[k * cols..(k + 1) * cols]
11080 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11081 }
11082 unsafe {
11084 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
11085 }
11086 }
11087 };
11088 match pool {
11089 Some(p) => p.run_rows(active.len(), &run),
11090 None => run(0, active.len()),
11091 }
11092 }
11093 for (ai, &e) in active.iter().enumerate() {
11094 for (k, &(bi, w)) in assign[e].iter().enumerate() {
11095 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
11096 for i in 0..hidden {
11097 out[bi * hidden + i] += w * eo[i];
11098 }
11099 }
11100 }
11101 } else {
11102 for &e in &active {
11103 run_expert(&m.experts[e], &assign[e], &mut out);
11104 }
11105 }
11106 if let Some((se, gate)) = &m.shared {
11107 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
11108 let mut gl = vec![0.0f32; b];
11109 gate.matmat(xs, b, &mut gl, pool);
11110 (0..b)
11111 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
11112 .collect()
11113 } else {
11114 (0..b).map(|bi| (bi, 1.0)).collect()
11115 };
11116 run_expert(se, &all, &mut out);
11117 }
11118 out
11119}
11120
11121thread_local! {
11122 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
11126 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
11127}
11128
11129fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11131 if gate_topk() > 0
11134 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
11135 {
11136 return out;
11137 }
11138 if crate::gpu::enabled_here()
11149 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
11150 {
11151 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
11152 crate::gpu::ProbeArm::Gpu
11153 } else {
11154 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
11155 };
11156 match arm {
11157 crate::gpu::ProbeArm::Gpu => {
11158 let t0 = std::time::Instant::now();
11159 if let Some(out) = dense_ffn_gpu(d, x, pool) {
11160 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
11161 return out;
11162 }
11163 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
11167 }
11168 crate::gpu::ProbeArm::CpuTimed => {
11169 let t0 = std::time::Instant::now();
11170 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11171 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
11172 return out;
11173 }
11174 crate::gpu::ProbeArm::Cpu => {
11175 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11176 }
11177 }
11178 }
11179 dense_ffn_cpu(d, x, pool)
11180}
11181
11182fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11184 let inter = d.gate_proj.rows();
11185 FFN_SCRATCH.with(|s| {
11186 let mut s = s.borrow_mut();
11187 let [g, u, ..] = &mut *s;
11188 g.resize(inter, 0.0);
11189 if gate_topk() > 0 {
11192 u.resize(inter, 0.0);
11196 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11197 for i in 0..inter {
11198 g[i] = Act::Silu.combine(g[i], 1.0);
11199 }
11200 keep_top_k(g, gate_topk());
11201 for i in 0..inter {
11202 g[i] *= u[i];
11203 }
11204 } else if d.act == Act::Silu
11205 && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
11206 {
11207 } else {
11209 u.resize(inter, 0.0);
11210 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11212 for i in 0..inter {
11213 g[i] = d.act.combine(g[i], u[i]);
11214 }
11215 }
11216 FFN_PROBE.with(|pr| {
11224 if let Some(acc) = pr.borrow_mut().as_mut() {
11225 let li = crate::gpu::cur_layer();
11226 if li >= 0 {
11227 if let Some(row) = acc.get_mut(li as usize) {
11228 match probe_topk() {
11229 0 if probe_sq() => {
11230 for (a, &v) in row.iter_mut().zip(g.iter()) {
11231 *a += (v as f64) * (v as f64);
11232 }
11233 }
11234 0 if probe_signed() => {
11235 for (a, &v) in row.iter_mut().zip(g.iter()) {
11236 *a += v as f64;
11237 }
11238 }
11239 0 => {
11240 for (a, &v) in row.iter_mut().zip(g.iter()) {
11241 *a += (v as f64).abs();
11242 }
11243 }
11244 k => {
11245 let n = g.len();
11246 let k = k.min(n);
11247 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
11248 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
11249 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11250 });
11251 let thr = *kth;
11252 for (a, &v) in row.iter_mut().zip(g.iter()) {
11253 if v.abs() >= thr {
11254 *a += 1.0;
11255 }
11256 }
11257 }
11258 }
11259 }
11260 }
11261 }
11262 });
11263 if oracle_topk() > 0 {
11264 keep_top_k(g, oracle_topk());
11265 }
11266 {
11267 let li = crate::gpu::cur_layer();
11268 if li >= 0 {
11269 adump_row(li as usize, g);
11270 }
11271 }
11272 let mut out = attention::take_buf(d.down_proj.rows());
11273 d.down_proj.matvec(g, &mut out, pool);
11274 out
11275 })
11276}
11277
11278pub struct RefitAcc {
11291 pub support: Vec<u32>,
11292 pub gss: Vec<f32>,
11293 pub ya: Vec<f32>,
11294 pub hidden: usize,
11295 pub tokens: u64,
11296 pub buf_g: Vec<f32>,
11302 pub buf_o: Vec<f32>,
11303 pub buf_t: usize,
11304}
11305
11306type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
11310
11311static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
11312 std::sync::OnceLock::new();
11313
11314fn ffn_probe_active() -> bool {
11317 FFN_PROBE.with(|p| p.borrow().is_some())
11318}
11319
11320fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
11321 REFIT
11322 .get_or_init(|| {
11323 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
11324 (
11325 d,
11326 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
11327 )
11328 })
11329 })
11330 .as_ref()
11331}
11332
11333fn refit_accumulate(
11335 li: usize,
11336 g: &[f32],
11337 b: usize,
11338 inter: usize,
11339 out: &[f32],
11340 hidden: usize,
11341 pool: Option<&Pool>,
11342) {
11343 let Some((dir, map)) = refit_dir() else {
11344 return;
11345 };
11346 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
11347 let (from, to) = *SPAN.get_or_init(|| {
11348 let g = |k: &str, d: usize| {
11349 std::env::var(k)
11350 .ok()
11351 .and_then(|v| v.parse().ok())
11352 .unwrap_or(d)
11353 };
11354 (
11355 g("CMF_FFN_REFIT_FROM", 0),
11356 g("CMF_FFN_REFIT_TO", usize::MAX),
11357 )
11358 });
11359 if li < from || li > to {
11360 return;
11361 }
11362 let mut guard = map.lock().unwrap();
11363 let (map, shared) = &mut *guard;
11364 let acc = match map.entry(li) {
11365 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
11366 std::collections::hash_map::Entry::Vacant(e) => {
11367 let path = format!("{dir}/support.{li}.u32");
11368 let Ok(bytes) = std::fs::read(&path) else {
11369 eprintln!("refit: no {path} — layer {li} skipped");
11370 return;
11371 };
11372 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
11373 let support: Vec<u32> = bytes[4..4 + n * 4]
11374 .chunks_exact(4)
11375 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11376 .collect();
11377 eprintln!(
11378 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
11379 (n * n + hidden * n) as f64 * 4.0 / 1e6
11380 );
11381 e.insert(RefitAcc {
11382 gss: vec![0.0; n * n],
11383 ya: vec![0.0; hidden * n],
11384 buf_g: Vec::new(),
11385 buf_o: Vec::new(),
11386 buf_t: 0,
11387 support,
11388 hidden,
11389 tokens: 0,
11390 })
11391 }
11392 };
11393 let ns = acc.support.len();
11394 let cap = refit_batch();
11396 if acc.buf_g.is_empty() {
11397 acc.buf_g = vec![0.0; ns * cap];
11398 acc.buf_o = vec![0.0; hidden * cap];
11399 }
11400 let take = b.min(cap - acc.buf_t);
11401 for t in 0..take {
11402 let col = acc.buf_t + t;
11403 for (j, &n) in acc.support.iter().enumerate() {
11404 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
11405 }
11406 for h in 0..hidden {
11407 acc.buf_o[h * cap + col] = out[t * hidden + h];
11408 }
11409 }
11410 acc.buf_t += take;
11411 acc.tokens += take as u64;
11412 if acc.buf_t < cap {
11413 return;
11414 }
11415 let bt = acc.buf_t;
11416 acc.buf_t = 0;
11417 let RefitAcc {
11427 gss,
11428 ya,
11429 buf_g,
11430 buf_o,
11431 ..
11432 } = acc;
11433 let need = (ns * ns).max(hidden * ns);
11434 if shared.len() < need {
11435 shared.resize(need, 0.0);
11436 }
11437 let scratch = &mut shared[..];
11438 let _ = bt;
11439 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
11440 add_into(gss, &scratch[..ns * ns], pool);
11441 if crate::gpu::gemm_nt_f32_transient(
11442 buf_o,
11443 buf_g,
11444 &mut scratch[..hidden * ns],
11445 hidden,
11446 cap,
11447 ns,
11448 ) {
11449 add_into(ya, &scratch[..hidden * ns], pool);
11450 } else {
11451 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
11452 }
11453 } else {
11454 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
11455 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
11456 }
11457 }
11461
11462fn refit_batch() -> usize {
11464 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11465 *B.get_or_init(|| {
11466 std::env::var("CMF_FFN_REFIT_BATCH")
11467 .ok()
11468 .and_then(|v| v.parse().ok())
11469 .unwrap_or(4096)
11470 })
11471}
11472
11473fn accum_outer_t(
11476 c: &mut [f32],
11477 m: usize,
11478 n: usize,
11479 b: usize,
11480 left: &[f32],
11481 right: &[f32],
11482 pool: Option<&Pool>,
11483) {
11484 let ptr = SendMut(c.as_mut_ptr());
11485 let body = |i: usize| {
11486 let ptr = &ptr;
11487 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
11488 for t in 0..b {
11489 let a = left[i * b + t];
11490 if a == 0.0 {
11491 continue;
11492 }
11493 for (j, o) in row.iter_mut().enumerate() {
11494 *o += a * right[j * b + t];
11495 }
11496 }
11497 };
11498 match pool {
11499 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
11500 for i in s..e {
11501 body(i);
11502 }
11503 }),
11504 _ => {
11505 for i in 0..m {
11506 body(i);
11507 }
11508 }
11509 }
11510}
11511
11512fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
11515 let n = dst.len().min(src.len());
11516 match pool {
11517 Some(p) if n >= 1 << 16 => {
11518 let ptr = SendMut(dst.as_mut_ptr());
11519 let f = |s: usize, e: usize| {
11520 let ptr = &ptr;
11521 for blk in s..e {
11522 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
11523 for i in a..b {
11524 unsafe { *ptr.0.add(i) += src[i] };
11525 }
11526 }
11527 };
11528 p.run_rows(n.div_ceil(4096), &f);
11529 }
11530 _ => {
11531 for (d, v) in dst.iter_mut().zip(&src[..n]) {
11532 *d += *v;
11533 }
11534 }
11535 }
11536}
11537
11538fn accum_outer(
11543 c: &mut [f32],
11544 m: usize,
11545 n: usize,
11546 b: usize,
11547 left: &[f32],
11548 right: &[f32],
11549 pool: Option<&Pool>,
11550) {
11551 const TILE: usize = 32;
11552 let tiles = m.div_ceil(TILE);
11553 let cp = SendMut(c.as_mut_ptr());
11554 let body = |ti: usize| {
11555 let cp = &cp;
11556 let i0 = ti * TILE;
11557 let i1 = (i0 + TILE).min(m);
11558 for t in 0..b {
11559 let r = &right[t * n..t * n + n];
11560 for i in i0..i1 {
11561 let a = left[i * b + t];
11562 if a == 0.0 {
11563 continue;
11564 }
11565 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
11567 for (o, v) in row.iter_mut().zip(r) {
11568 *o += a * *v;
11569 }
11570 }
11571 }
11572 };
11573 match pool {
11574 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
11575 for ti in s..e {
11576 body(ti);
11577 }
11578 }),
11579 _ => {
11580 for ti in 0..tiles {
11581 body(ti);
11582 }
11583 }
11584 }
11585}
11586
11587pub fn refit_flush() -> usize {
11589 let Some((dir, map)) = refit_dir() else {
11590 return 0;
11591 };
11592 let guard = map.lock().unwrap();
11593 let mut n = 0;
11594 for (li, acc) in guard.0.iter() {
11595 let w = |name: &str, v: &[f32]| {
11598 let path = format!("{dir}/{name}.{li}.f32");
11599 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
11600 match std::fs::write(&path, &bytes) {
11601 Ok(()) => {}
11602 Err(e) => eprintln!(
11603 "refit: FAILED to write {path} ({} MB): {e}",
11604 bytes.len() / 1_000_000
11605 ),
11606 }
11607 };
11608 w("gss", &acc.gss);
11609 w("ya", &acc.ya);
11610 println!(
11611 "refit L{li}: {} support, {} tokens, hidden {}",
11612 acc.support.len(),
11613 acc.tokens,
11614 acc.hidden
11615 );
11616 n += 1;
11617 }
11618 n
11619}
11620
11621fn adump_row(li: usize, g: &[f32]) {
11626 use std::io::Write as _;
11627 static FILES: std::sync::OnceLock<
11628 Option<(
11629 String,
11630 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
11631 )>,
11632 > = std::sync::OnceLock::new();
11633 let Some((prefix, map)) = FILES
11634 .get_or_init(|| {
11635 std::env::var("CMF_FFN_ADUMP")
11636 .ok()
11637 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
11638 })
11639 .as_ref()
11640 else {
11641 return;
11642 };
11643 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
11646 let (from, to) = *SPAN.get_or_init(|| {
11647 let g = |k: &str, d: usize| {
11648 std::env::var(k)
11649 .ok()
11650 .and_then(|v| v.parse().ok())
11651 .unwrap_or(d)
11652 };
11653 (
11654 g("CMF_FFN_ADUMP_FROM", 0),
11655 g("CMF_FFN_ADUMP_TO", usize::MAX),
11656 )
11657 });
11658 if li < from || li > to {
11659 return;
11660 }
11661 let mut map = map.lock().unwrap();
11662 let f = map.entry(li).or_insert_with(|| {
11663 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
11664 });
11665 let mut bytes = Vec::with_capacity(g.len() * 2);
11666 for v in g {
11667 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
11668 }
11669 let _ = f.write_all(&bytes);
11670}
11671
11672fn oracle_topk() -> usize {
11678 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11679 *K.get_or_init(|| {
11680 std::env::var("CMF_FFN_ORACLE_TOPK")
11681 .ok()
11682 .and_then(|v| v.parse().ok())
11683 .unwrap_or(0)
11684 })
11685}
11686
11687fn gate_topk() -> usize {
11693 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11694 *K.get_or_init(|| {
11695 std::env::var("CMF_FFN_GATE_TOPK")
11696 .ok()
11697 .and_then(|v| v.parse().ok())
11698 .unwrap_or(0)
11699 })
11700}
11701
11702fn gate_block() -> usize {
11709 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11710 *B.get_or_init(|| {
11711 std::env::var("CMF_FFN_GATE_BLOCK")
11712 .ok()
11713 .and_then(|v| v.parse().ok())
11714 .unwrap_or(1)
11715 })
11716}
11717
11718fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
11720 let n = g.len();
11721 let nb = n.div_ceil(block);
11722 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
11723 if kb >= nb {
11724 return;
11725 }
11726 let mut score: Vec<f32> = (0..nb)
11727 .map(|b| {
11728 g[b * block..((b + 1) * block).min(n)]
11729 .iter()
11730 .map(|v| v * v)
11731 .sum::<f32>()
11732 })
11733 .collect();
11734 let mut ord = score.clone();
11735 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
11736 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11737 });
11738 let thr = *kth;
11739 for b in 0..nb {
11740 if score[b] < thr {
11741 g[b * block..((b + 1) * block).min(n)].fill(0.0);
11742 }
11743 }
11744 score.clear();
11745}
11746
11747fn keep_top_k(g: &mut [f32], k: usize) {
11749 if gate_block() > 1 {
11750 return keep_top_blocks(g, k, gate_block());
11751 }
11752 let n = g.len();
11753 if k == 0 || k >= n {
11754 return;
11755 }
11756 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
11757 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
11758 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11759 });
11760 let thr = *kth;
11761 for v in g.iter_mut() {
11762 if v.abs() < thr {
11763 *v = 0.0;
11764 }
11765 }
11766}
11767
11768fn probe_sq() -> bool {
11772 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11773 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
11774}
11775
11776fn probe_signed() -> bool {
11780 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11781 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
11782}
11783
11784fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
11792 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
11793 M.get_or_init(|| {
11794 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
11795 let b = std::fs::read(&p).ok()?;
11796 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
11797 let vals: Vec<f32> = b[8..]
11798 .chunks_exact(4)
11799 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11800 .collect();
11801 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
11802 Some((inter, vals))
11803 })
11804 .as_ref()
11805}
11806
11807fn probe_topk() -> usize {
11810 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11811 *K.get_or_init(|| {
11812 std::env::var("CMF_FFN_PROBE_TOPK")
11813 .ok()
11814 .and_then(|v| v.parse().ok())
11815 .unwrap_or(0)
11816 })
11817}
11818
11819thread_local! {
11820 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
11823 const { std::cell::RefCell::new(None) };
11824}
11825
11826fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
11839 let dt = d.down_t.as_ref()?;
11840 let inter = d.gate_proj.rows();
11841 let hidden = dt.cols();
11842 if k == 0 || k >= inter || d.act != Act::Silu {
11843 return None;
11844 }
11845 DYN_SCRATCH.with(|sc| {
11846 let mut sc = sc.borrow_mut();
11847 let DynScratch {
11848 g,
11849 mag,
11850 live,
11851 parts,
11852 } = &mut *sc;
11853 g.resize(inter, 0.0);
11854 d.gate_proj.matvec(x, g, pool);
11855 for v in g.iter_mut() {
11856 *v = inference::silu(*v);
11857 }
11858 mag.clear();
11861 mag.extend(g.iter().map(|v| v.abs()));
11862 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
11863 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11864 });
11865 let thr = *kth;
11866 live.clear();
11867 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
11868 let mut out = vec![0.0f32; hidden];
11869 match pool {
11870 Some(p) if live.len() >= 64 => {
11871 let nw = p.n_workers() + 1;
11872 parts.clear();
11873 parts.resize(nw * hidden, 0.0);
11874 let ptr = SendMut(parts.as_mut_ptr());
11875 let n = live.len();
11876 let live_ref: &[u32] = live;
11877 let g_ref: &[f32] = g;
11878 p.run(&|w, workers| {
11879 let chunk = n.div_ceil(workers);
11880 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
11881 if s >= e {
11882 return;
11883 }
11884 WORKER_SCRATCH.with(|ws| {
11885 let mut ws = ws.borrow_mut();
11886 let [scratch, acc] = &mut *ws;
11887 scratch.resize(hidden.max(x.len()), 0.0);
11888 acc.clear();
11889 acc.resize(hidden, 0.0);
11890 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
11891 if let Some(&nx) = live_ref[s..e].get(o + 1) {
11894 d.up_proj.prefetch_row(nx as usize);
11895 dt.prefetch_row(nx as usize);
11896 }
11897 let idx = nrm as usize;
11898 let up = d.up_proj.row_dot(idx, x, scratch);
11899 let a = g_ref[idx] * up;
11900 if a != 0.0 {
11901 dt.add_row_scaled(idx, a, acc, scratch);
11902 }
11903 }
11904 for (j, v) in acc.iter().enumerate() {
11905 unsafe { *ptr.at(w * hidden + j) = *v };
11906 }
11907 });
11908 });
11909 for w in 0..nw {
11910 for (j, o) in out.iter_mut().enumerate() {
11911 *o += parts[w * hidden + j];
11912 }
11913 }
11914 }
11915 _ => {
11916 WORKER_SCRATCH.with(|ws| {
11917 let mut ws = ws.borrow_mut();
11918 let [scratch, _acc] = &mut *ws;
11919 scratch.resize(hidden.max(x.len()), 0.0);
11920 for &nrm in live.iter() {
11921 let idx = nrm as usize;
11922 let up = d.up_proj.row_dot(idx, x, scratch);
11923 let a = g[idx] * up;
11924 if a != 0.0 {
11925 dt.add_row_scaled(idx, a, &mut out, scratch);
11926 }
11927 }
11928 });
11929 }
11930 }
11931 Some(out)
11932 })
11933}
11934
11935struct DynScratch {
11938 g: Vec<f32>,
11939 mag: Vec<f32>,
11940 live: Vec<u32>,
11941 parts: Vec<f32>,
11942}
11943
11944thread_local! {
11945 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
11946 std::cell::RefCell::new(DynScratch {
11947 g: Vec::new(),
11948 mag: Vec::new(),
11949 live: Vec::new(),
11950 parts: Vec::new(),
11951 })
11952 };
11953 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
11955 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
11956}
11957
11958fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
11963 let inter = d.gate_proj.rows();
11964 FFN_SCRATCH.with(|s| {
11965 let mut s = s.borrow_mut();
11966 let [g, u, ..] = &mut *s;
11967 g.resize(inter, 0.0);
11968 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
11969 } else {
11971 u.resize(inter, 0.0);
11972 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11973 for i in 0..inter {
11974 g[i] = d.act.combine(g[i], u[i]);
11975 }
11976 }
11977 zero_masked_cols(g, 1, inter, mask_row);
11978 let mut out = attention::take_buf(d.down_proj.rows());
11979 d.down_proj.matvec(g, &mut out, pool);
11980 out
11981 })
11982}
11983
11984fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
11990 if d.act != Act::Silu {
11992 return None;
11993 }
11994 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
11997 return None;
11998 }
11999 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
12000 let mut model_ref = None;
12001 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
12002 let model = model_ref?;
12003 let hidden = jobs[0].down.1;
12004 let mut out = attention::take_buf(hidden);
12005 if crate::gpu::moe_block(&model, &jobs, &mut out) {
12006 Some(out)
12007 } else {
12008 let mut out = out;
12009 attention::recycle_buf(&mut out);
12010 None
12011 }
12012}
12013
12014#[allow(clippy::type_complexity)]
12019#[allow(clippy::type_complexity)]
12020pub(crate) fn moe_parts(
12021 t: &QTensor,
12022) -> Option<(
12023 &std::sync::Arc<cortiq_core::CmfModel>,
12024 usize,
12025 usize,
12026 usize,
12027 &[f32],
12028 &[f32],
12029 bool,
12030 bool,
12031 bool,
12032)> {
12033 match t {
12034 QTensor::Mapped {
12035 model,
12036 idx,
12037 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
12038 rows,
12039 cols,
12040 row_scale,
12041 col_field,
12042 ..
12043 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
12044 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
12045 )),
12046 QTensor::Mapped {
12048 model,
12049 idx,
12050 dtype: cortiq_core::TensorDtype::Q1,
12051 rows,
12052 cols,
12053 ..
12054 } => Some((
12055 model,
12056 *idx,
12057 *rows,
12058 *cols,
12059 &[][..],
12060 &[][..],
12061 true,
12062 false,
12063 false,
12064 )),
12065 QTensor::Mapped {
12067 model,
12068 idx,
12069 dtype: cortiq_core::TensorDtype::Q4Tiled,
12070 rows,
12071 cols,
12072 ..
12073 } => Some((
12074 model,
12075 *idx,
12076 *rows,
12077 *cols,
12078 &[][..],
12079 &[][..],
12080 false,
12081 true,
12082 false,
12083 )),
12084 QTensor::Mapped {
12086 model,
12087 idx,
12088 dtype: cortiq_core::TensorDtype::Q4TiledP,
12089 rows,
12090 cols,
12091 ..
12092 } => Some((
12093 model,
12094 *idx,
12095 *rows,
12096 *cols,
12097 &[][..],
12098 &[][..],
12099 false,
12100 true,
12101 false,
12102 )),
12103 QTensor::Mapped {
12107 model,
12108 idx,
12109 dtype: cortiq_core::TensorDtype::Q2TiledP,
12110 rows,
12111 cols,
12112 ..
12113 } => Some((
12114 model,
12115 *idx,
12116 *rows,
12117 *cols,
12118 &[][..],
12119 &[][..],
12120 false,
12121 true,
12122 true,
12123 )),
12124 _ => None,
12125 }
12126}
12127
12128#[cfg(target_os = "macos")]
12134fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
12135 if m.router_sigmoid
12136 || m.router_input_norm
12137 || m.expert_bias.is_some()
12138 || m.route_tau.is_some()
12139 || m.mask.is_some()
12140 || m.per_expert_scale.is_some()
12141 || m.experts.is_empty()
12142 || m.top_k == 0
12143 || m.resonance.is_some()
12144 {
12145 return None;
12146 }
12147 let (sh, sg) = match &m.shared {
12150 Some((sh, Some(sg))) => (sh, sg),
12151 _ => return None,
12152 };
12153 let (rf, rr, rc) = m.router.f32_parts()?;
12154 if rr != m.experts.len() || rc != hidden {
12155 return None;
12156 }
12157 let (sf, sr, sc) = sg.f32_parts()?;
12158 if sr * sc != hidden {
12159 return None;
12160 }
12161 let inter = m.experts[0].gate_proj.rows();
12162 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
12165 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
12166 if e.act != Act::Silu
12167 || e.gate_proj.rows() != inter
12168 || e.gate_proj.cols() != hidden
12169 || e.up_proj.rows() != inter
12170 || e.up_proj.cols() != hidden
12171 || e.down_proj.rows() != hidden
12172 || e.down_proj.cols() != inter
12173 {
12174 return None;
12175 }
12176 let pick = |t: &QTensor| -> Option<usize> {
12177 if gu_q2 {
12178 t.mapped_q2tp().map(|(_, i)| i)
12179 } else {
12180 t.mapped_q4tp().map(|(_, i)| i)
12181 }
12182 };
12183 Some((
12184 pick(&e.gate_proj)?,
12185 pick(&e.up_proj)?,
12186 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
12187 ))
12188 };
12189 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
12190 let shared = trio(sh)?;
12191 Some(crate::gpu::GpuMoe {
12192 router: rf,
12193 sgate: sf,
12194 experts,
12195 shared,
12196 n_exp: m.experts.len(),
12197 top_k: m.top_k,
12198 inter,
12199 norm_topk: m.norm_topk_prob,
12200 route_scale: m.routed_scaling,
12201 gu_q2,
12202 })
12203}
12204
12205pub(crate) fn moe_push_job_parts<'a>(
12209 gate: &'a QTensor,
12210 up: &'a QTensor,
12211 down: &'a QTensor,
12212 x: &[f32],
12213 w: f32,
12214 swiglu_limit: f32,
12215 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
12216 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
12217) -> Option<()> {
12218 use crate::qtensor::prescale;
12219 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
12220 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
12221 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
12222 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
12223 return None; }
12225 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
12228 return None;
12229 }
12230 if !gq2 && dq2 {
12231 return None;
12232 }
12233 model_ref.get_or_insert_with(|| gm.clone());
12234 let dt = |cf: &[f32]| {
12235 if cf.is_empty() {
12236 cortiq_core::TensorDtype::Q8Row
12237 } else {
12238 cortiq_core::TensorDtype::Q8_2f
12239 }
12240 };
12241 jobs.push(crate::gpu::MoeJob {
12242 gate: (gi, gr, gc, grs),
12243 up: (ui, ur, uc, urs),
12244 down: (di, dr, dc, drs),
12245 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
12246 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
12247 down_col: dcf,
12248 w,
12249 q1: gq1,
12250 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
12251 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
12252 gu_q2: gq2,
12253 swiglu_limit,
12254 });
12255 Some(())
12256}
12257
12258fn moe_push_job<'a>(
12260 d: &'a DenseFfn,
12261 x: &[f32],
12262 w: f32,
12263 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
12264 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
12265) -> Option<()> {
12266 use crate::qtensor::prescale;
12267 if d.act != Act::Silu {
12268 return None; }
12270 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
12271 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
12272 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
12273 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
12274 return None; }
12276 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
12277 return None;
12278 }
12279 if !gq2 && dq2 {
12280 return None;
12281 }
12282 model_ref.get_or_insert_with(|| gm.clone());
12283 let gdt = if gcf.is_empty() {
12284 cortiq_core::TensorDtype::Q8Row
12285 } else {
12286 cortiq_core::TensorDtype::Q8_2f
12287 };
12288 let udt = if ucf.is_empty() {
12289 cortiq_core::TensorDtype::Q8Row
12290 } else {
12291 cortiq_core::TensorDtype::Q8_2f
12292 };
12293 jobs.push(crate::gpu::MoeJob {
12294 gate: (gi, gr, gc, grs),
12295 up: (ui, ur, uc, urs),
12296 down: (di, dr, dc, drs),
12297 xs_gate: prescale(x, gcf, gdt).into_owned(),
12298 xs_up: prescale(x, ucf, udt).into_owned(),
12299 down_col: dcf,
12300 w,
12301 q1: gq1,
12302 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
12303 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
12304 gu_q2: gq2,
12305 swiglu_limit: 0.0,
12306 });
12307 Some(())
12308}
12309
12310fn sparse_ffn_quant(
12317 d: &DenseFfn,
12318 x: &[f32],
12319 active: &[u16],
12320 hidden: usize,
12321 pool: Option<&Pool>,
12322) -> Vec<f32> {
12323 let n = active.len();
12324 let inter = d.gate_proj.rows();
12325 let mut act = vec![0.0f32; n];
12326 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
12329 let compute = |ai: usize| -> f32 {
12330 let idx = active[ai] as usize;
12331 if idx >= inter {
12332 return 0.0; }
12334 let mut s = if need_scratch {
12335 vec![0.0f32; hidden]
12336 } else {
12337 Vec::new()
12338 };
12339 let gate = d.gate_proj.row_dot(idx, x, &mut s);
12340 let up = d.up_proj.row_dot(idx, x, &mut s);
12341 d.act.combine(gate, up)
12342 };
12343 match pool {
12344 Some(p) if n >= 256 => {
12345 let ptr = SendMut(act.as_mut_ptr());
12346 p.run(&|widx, nw| {
12347 let chunk = n.div_ceil(nw);
12348 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
12349 for ai in s..e {
12350 unsafe { *ptr.at(ai) = compute(ai) };
12351 }
12352 });
12353 }
12354 _ => {
12355 for (ai, a) in act.iter_mut().enumerate() {
12356 *a = compute(ai);
12357 }
12358 }
12359 }
12360 let mut out = vec![0.0f32; hidden];
12362 for (ai, &idx) in active.iter().enumerate() {
12363 let w = act[ai];
12364 if w.abs() >= 1e-12 && (idx as usize) < inter {
12365 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
12366 }
12367 }
12368 out
12369}
12370
12371#[doc(hidden)]
12373pub fn sparse_ffn_quant_for_test(
12374 d: &DenseFfn,
12375 x: &[f32],
12376 active: &[u16],
12377 hidden: usize,
12378) -> Vec<f32> {
12379 sparse_ffn_quant(d, x, active, hidden, None)
12380}
12381
12382fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
12386 let deq = |t: &QTensor| -> Vec<f32> {
12387 let (rows, cols) = (t.rows(), t.cols());
12388 let mut out = vec![0.0f32; rows * cols];
12389 for r in 0..rows {
12390 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
12391 }
12392 out
12393 };
12394 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
12395}
12396
12397struct SendMut(*mut f32);
12399unsafe impl Send for SendMut {}
12400unsafe impl Sync for SendMut {}
12401impl SendMut {
12402 #[inline]
12403 #[allow(clippy::mut_from_ref)]
12406 unsafe fn at(&self, i: usize) -> &mut f32 {
12407 unsafe { &mut *self.0.add(i) }
12408 }
12409}
12410
12411pub(crate) fn moe_route(
12421 logits: &[f32],
12422 m: &MoeFfn,
12423 allowed: Option<&[bool]>,
12424) -> (Vec<usize>, Vec<f32>, f32) {
12425 let ne = logits.len();
12426 let p: Vec<f32> = if m.router_sigmoid {
12427 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
12428 } else {
12429 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
12430 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
12431 let s: f32 = e.iter().sum();
12432 for v in &mut e {
12433 *v /= s;
12434 }
12435 e
12436 };
12437 let admit = |e: usize| {
12443 m.mask.as_ref().is_none_or(|mk| mk[e])
12444 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
12445 };
12446 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
12447 match &m.expert_bias {
12449 Some(b) => idx.sort_unstable_by(|&x, &y| {
12450 (p[y] + b[y])
12451 .partial_cmp(&(p[x] + b[x]))
12452 .unwrap()
12453 .then(x.cmp(&y))
12454 }),
12455 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
12456 }
12457 idx.truncate(m.top_k);
12458 if let Some(tau) = m.route_tau {
12462 let total: f32 = idx.iter().map(|&e| p[e]).sum();
12463 if total > 0.0 {
12464 let mut acc = 0.0f32;
12465 let mut keep = idx.len();
12466 for (i, &e) in idx.iter().enumerate() {
12467 acc += p[e];
12468 if acc >= tau * total {
12469 keep = i + 1;
12470 break;
12471 }
12472 }
12473 idx.truncate(keep);
12474 }
12475 }
12476 let wsum: f32 = if m.norm_topk_prob {
12477 let s: f32 = idx.iter().map(|&e| p[e]).sum();
12478 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
12481 } else {
12482 1.0 / m.routed_scaling
12483 };
12484 (idx, p, wsum)
12485}
12486
12487fn moe_trace(idx: &[usize]) {
12489 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
12490}
12491
12492pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
12495 use std::io::Write;
12496 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
12497 std::sync::OnceLock::new();
12498 let Some(f) = F.get_or_init(|| {
12499 let p = std::env::var("CMF_MOE_TRACE").ok()?;
12500 Some(std::sync::Mutex::new(
12501 std::fs::OpenOptions::new()
12502 .create(true)
12503 .append(true)
12504 .open(p)
12505 .ok()?,
12506 ))
12507 }) else {
12508 return;
12509 };
12510 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
12511 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
12512}
12513
12514pub(crate) fn moe_ffn(
12517 m: &MoeFfn,
12518 x: &[f32],
12519 pool: Option<&Pool>,
12520 allowed: Option<&[bool]>,
12521) -> Vec<f32> {
12522 accumulate_act(m, x, 1);
12523 let ne = m.experts.len();
12524 let mut logits = vec![0.0f32; ne];
12525 match &m.resonance {
12526 Some(r) => r.scores(x, &mut logits),
12527 None => m.router.matvec(x, &mut logits, pool),
12528 }
12529 let (idx, p, wsum) = moe_route(&logits, m, allowed);
12530 {
12531 let mut st = m.stats.borrow_mut();
12532 if st.len() < ne {
12533 st.resize(ne, 0);
12534 }
12535 for &e in &idx {
12536 st[e] += 1;
12537 }
12538 }
12539 moe_trace(&idx);
12545 if crate::gpu::enabled_here() {
12550 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
12551 crate::gpu::ProbeArm::Gpu => {
12552 let t0 = std::time::Instant::now();
12553 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
12554 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
12555 return out;
12556 }
12557 }
12558 crate::gpu::ProbeArm::CpuTimed => {
12559 let t0 = std::time::Instant::now();
12560 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
12561 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
12562 return out;
12563 }
12564 crate::gpu::ProbeArm::Cpu => {
12565 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
12566 }
12567 }
12568 }
12569 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
12570}
12571
12572fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
12577 use std::sync::atomic::{AtomicBool, Ordering};
12578 if built {
12579 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
12580 if total_layers > 0 && layers_run < total_layers {
12581 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
12582 } else {
12583 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
12584 }
12585 } else {
12586 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
12587 }
12588 static SAID: AtomicBool = AtomicBool::new(false);
12589 if !SAID.swap(true, Ordering::Relaxed) {
12590 if built {
12591 tracing::info!("wgpu whole-token graph: ACTIVE");
12592 } else {
12593 tracing::warn!("wgpu whole-token graph refused — per-op path");
12594 }
12595 }
12596}
12597
12598pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12602pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12603pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12607pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12609
12610fn moe_batch_enabled() -> bool {
12613 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12614 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
12615}
12616
12617fn moe_ffn_cpu_batched(
12623 m: &MoeFfn,
12624 x: &[f32],
12625 idx: &[usize],
12626 p: &[f32],
12627 wsum: f32,
12628 pool: Option<&Pool>,
12629) -> Option<Vec<f32>> {
12630 if idx.is_empty() || !moe_batch_enabled() {
12631 return None;
12632 }
12633 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
12637 return None;
12638 }
12639 let n = idx.len() + usize::from(m.shared.is_some());
12640 let mut pairs = Vec::with_capacity(n);
12641 let mut downs = Vec::with_capacity(n);
12642 let mut ws = Vec::with_capacity(n);
12643 for &e in idx {
12644 let d = &m.experts[e];
12645 if d.act != Act::Silu {
12646 return None;
12647 }
12648 pairs.push((&d.gate_proj, &d.up_proj));
12649 downs.push(&d.down_proj);
12650 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
12651 }
12652 if let Some((se, gate)) = &m.shared {
12655 if se.act != Act::Silu {
12656 return None;
12657 }
12658 let g = gate.as_ref().map_or(1.0, |gate| {
12659 let mut gl = [0.0f32; 1];
12660 gate.matvec(x, &mut gl, pool);
12661 1.0 / (1.0 + (-gl[0]).exp())
12662 });
12663 pairs.push((&se.gate_proj, &se.up_proj));
12664 downs.push(&se.down_proj);
12665 ws.push(g);
12666 }
12667 let inter = pairs[0].0.rows();
12668 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
12669 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
12670 return None;
12671 }
12672 let mut out = attention::take_buf(x.len());
12673 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
12674 attention::recycle_buf(&mut out);
12675 return None;
12676 }
12677 Some(out)
12678}
12679
12680pub(crate) fn moe_cold_experts_cpu(
12686 experts: &[(&DenseFfn, f32)],
12687 x: &[f32],
12688 pool: Option<&Pool>,
12689) -> Vec<f32> {
12690 let mut out = attention::take_buf(x.len());
12691 if experts.is_empty() {
12692 return out;
12693 }
12694 let pairs: Vec<_> = experts
12695 .iter()
12696 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
12697 .collect();
12698 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
12699 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
12700 let inter = experts[0].0.gate_proj.rows();
12701 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
12702 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
12703 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
12704 {
12705 return out;
12706 }
12707 out.fill(0.0);
12708 for &(expert, weight) in experts {
12709 let mut one = dense_ffn(expert, x, pool);
12710 for (o, v) in out.iter_mut().zip(&one) {
12711 *o += weight * v;
12712 }
12713 attention::recycle_buf(&mut one);
12714 }
12715 out
12716}
12717
12718fn moe_ffn_cpu(
12720 m: &MoeFfn,
12721 x: &[f32],
12722 idx: &[usize],
12723 p: &[f32],
12724 wsum: f32,
12725 pool: Option<&Pool>,
12726) -> Vec<f32> {
12727 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
12728 return out;
12729 }
12730 let mut out = attention::take_buf(x.len());
12731 for &e in idx {
12732 let mut eo = dense_ffn(&m.experts[e], x, pool);
12733 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
12734 for i in 0..out.len() {
12735 out[i] += w * eo[i];
12736 }
12737 attention::recycle_buf(&mut eo);
12738 }
12739 if let Some((se, gate)) = &m.shared {
12740 let mut so = dense_ffn(se, x, pool);
12741 let g = gate.as_ref().map_or(1.0, |gate| {
12742 let mut gl = [0.0f32; 1];
12743 gate.matvec(x, &mut gl, pool);
12744 1.0 / (1.0 + (-gl[0]).exp())
12745 });
12746 for i in 0..out.len() {
12747 out[i] += g * so[i];
12748 }
12749 attention::recycle_buf(&mut so);
12750 }
12751 out
12752}
12753
12754#[allow(clippy::too_many_arguments)]
12762fn mla_attention(
12763 w: &MlaWeights,
12764 normed: &[f32],
12765 cache: &mut crate::kv_cache::LayerKvCache,
12766 position: usize,
12767 inv_freq: &[f32],
12768 rope_scale: f32,
12769 eps: f64,
12770 pool: Option<&Pool>,
12771) -> Vec<f32> {
12772 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
12773 let hd = dr + dn;
12774 let mut q = vec![0.0f32; nh * hd];
12775 match (&w.q_a, &w.q_a_norm) {
12776 (Some(qa), Some(qn)) => {
12777 let mut t = vec![0.0f32; qa.rows()];
12778 qa.matvec(normed, &mut t, pool);
12779 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
12780 w.q_proj.matvec(&tn, &mut q, pool);
12781 }
12782 _ => w.q_proj.matvec(normed, &mut q, pool),
12783 }
12784 let mut ca = vec![0.0f32; lora + dr];
12785 w.kv_a.matvec(normed, &mut ca, pool);
12786 let (c_lat, k_rope) = ca.split_at_mut(lora);
12787 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
12788 let mut kvb = vec![0.0f32; nh * (dn + dv)];
12789 w.kv_b.matvec(&latn, &mut kvb, pool);
12790 if !w.nope {
12791 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
12792 }
12793 for h in 0..nh {
12794 if !w.nope {
12795 attention::rope_rotate_scaled(
12796 &mut q[h * hd..h * hd + dr],
12797 position,
12798 inv_freq,
12799 rope_scale,
12800 );
12801 }
12802 }
12803 let mut k = vec![0.0f32; nh * hd];
12804 let mut v = vec![0.0f32; nh * hd];
12805 for h in 0..nh {
12806 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
12807 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
12808 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
12809 }
12810 cache.append(&k, &v, &vec![true; nh]);
12811 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
12812 attention::recycle_buf(&mut imp);
12813 let mut ov = vec![0.0f32; nh * dv];
12814 for h in 0..nh {
12815 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
12816 }
12817 let mut out = vec![0.0f32; w.o_proj.rows()];
12818 w.o_proj.matvec(&ov, &mut out, pool);
12819 out
12820}
12821
12822fn dense_moe_ffn(
12829 dm: &DenseMoeFfn,
12830 x_normed: &[f32],
12831 h_raw: &[f32],
12832 eps: f64,
12833 norm_style: NormStyle,
12834 pool: Option<&Pool>,
12835) -> Vec<f32> {
12836 let mut d = dense_ffn(&dm.dense, x_normed, pool);
12837 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
12838 let m = &dm.moe;
12839 let ne = m.experts.len();
12840 let mut logits = vec![0.0f32; ne];
12841 if m.router_input_norm {
12842 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
12843 let inv = 1.0 / (ss + eps as f32).sqrt();
12844 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
12845 m.router.matvec(&xr, &mut logits, pool);
12846 } else {
12847 m.router.matvec(h_raw, &mut logits, pool);
12848 }
12849 let (idx, p, wsum) = moe_route(&logits, m, None);
12850 {
12851 let mut st = m.stats.borrow_mut();
12852 if st.len() < ne {
12853 st.resize(ne, 0);
12854 }
12855 for &e in &idx {
12856 st[e] += 1;
12857 }
12858 }
12859 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
12860 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
12861 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
12862 for (di, mi) in d.iter_mut().zip(&mo) {
12863 *di += mi;
12864 }
12865 d
12866}
12867
12868fn moe_gpu_refused(why: &'static str) {
12875 use std::sync::atomic::{AtomicBool, Ordering};
12876 static SAID: AtomicBool = AtomicBool::new(false);
12877 if !SAID.swap(true, Ordering::Relaxed) {
12878 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
12879 }
12880}
12881
12882fn moe_ffn_gpu(
12883 m: &MoeFfn,
12884 x: &[f32],
12885 idx: &[usize],
12886 p: &[f32],
12887 wsum: f32,
12888 pool: Option<&Pool>,
12889) -> Option<Vec<f32>> {
12890 use crate::gpu::MoeJob;
12891
12892 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
12893 let mut model_ref = None;
12894 for &e in idx {
12895 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
12896 moe_gpu_refused("push_job(expert)");
12897 return None;
12898 }
12899 }
12900 if let Some((se, gate)) = &m.shared {
12901 let g = gate.as_ref().map_or(1.0, |gate| {
12902 let mut gl = [0.0f32; 1];
12903 gate.matvec(x, &mut gl, pool);
12904 1.0 / (1.0 + (-gl[0]).exp())
12905 });
12906 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
12907 moe_gpu_refused("push_job(shared)");
12908 return None;
12909 }
12910 }
12911 let Some(model) = model_ref else {
12912 moe_gpu_refused("no model_ref");
12913 return None;
12914 };
12915 let hidden = jobs[0].down.1;
12916 let mut out = vec![0.0f32; hidden];
12917 if crate::gpu::moe_block(&model, &jobs, &mut out) {
12918 Some(out)
12919 } else {
12920 moe_gpu_refused("gpu::moe_block");
12921 None
12922 }
12923}
12924
12925fn ffn_forward(
12927 ffn: &FfnKind,
12928 x: &[f32],
12929 pool: Option<&Pool>,
12930 experts_allowed: Option<&[bool]>,
12931) -> Vec<f32> {
12932 match ffn {
12933 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
12934 FfnKind::Dense(d) => dense_ffn(d, x, pool),
12935 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
12936 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
12940 }
12941}
12942
12943fn ffn_forward_pair(
12947 ffn: &FfnKind,
12948 x1: &[f32],
12949 x2: &[f32],
12950 pool: Option<&Pool>,
12951 experts_allowed: Option<&[bool]>,
12952) -> (Vec<f32>, Vec<f32>) {
12953 let d = match ffn {
12954 FfnKind::Dense(d) if !d.segs.is_empty() => {
12957 return (
12958 tube_ffn(d, x1, 1, pool, None),
12959 tube_ffn(d, x2, 1, pool, None),
12960 );
12961 }
12962 FfnKind::Dense(d) => d,
12963 FfnKind::Moe(m) => {
12964 return (
12965 moe_ffn(m, x1, pool, experts_allowed),
12966 moe_ffn(m, x2, pool, experts_allowed),
12967 );
12968 }
12969 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
12970 };
12971 let inter = d.gate_proj.rows();
12972 FFN_SCRATCH.with(|s| {
12973 let mut s = s.borrow_mut();
12974 let [g1, g2, u1, u2] = &mut *s;
12975 g1.resize(inter, 0.0);
12976 g2.resize(inter, 0.0);
12977 u1.resize(inter, 0.0);
12978 u2.resize(inter, 0.0);
12979 QTensor::matvec2_many(
12982 [&d.gate_proj, &d.up_proj],
12983 x1,
12984 x2,
12985 [g1.as_mut_slice(), u1.as_mut_slice()],
12986 [g2.as_mut_slice(), u2.as_mut_slice()],
12987 pool,
12988 );
12989 for i in 0..inter {
12990 g1[i] = d.act.combine(g1[i], u1[i]);
12991 g2[i] = d.act.combine(g2[i], u2[i]);
12992 }
12993 let mut o1 = attention::take_buf(d.down_proj.rows());
12994 let mut o2 = attention::take_buf(d.down_proj.rows());
12995 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
12996 (o1, o2)
12997 })
12998}
12999
13000#[cfg(test)]
13001mod tests {
13002
13003 #[test]
13004 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
13005 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
13006 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
13007 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
13008 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
13009 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
13010 }
13011
13012 #[test]
13013 fn cancel_flag_stops_generation() {
13014 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
13015 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13018 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
13019 assert_eq!(r.finish_reason, "cancelled");
13020 assert!(
13021 r.token_ids.is_empty(),
13022 "no tokens after cancel: {:?}",
13023 r.token_ids
13024 );
13025 assert_eq!(p.kv_cache.seq_len(), 0);
13026 assert!(p.kv_history.is_empty());
13027 assert!(!p.graph_want_logits);
13028 assert!(p.graph_logits.is_none());
13029 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
13031 assert_ne!(r2.finish_reason, "cancelled");
13032 }
13033 use super::*;
13034
13035 #[test]
13043 fn dynamic_ffn_equals_the_zeroing_arm() {
13044 let (hidden, inter) = (8usize, 32usize);
13045 let synth = |n: usize, salt: usize| -> Vec<f32> {
13046 (0..n)
13047 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
13048 .collect()
13049 };
13050 let down = synth(hidden * inter, 3);
13051 let mut down_t = vec![0.0f32; inter * hidden];
13052 for r in 0..hidden {
13053 for c in 0..inter {
13054 down_t[c * hidden + r] = down[r * inter + c];
13055 }
13056 }
13057 let d = DenseFfn {
13058 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13059 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13060 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
13061 act: Act::Silu,
13062 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
13063 segs: Vec::new(),
13064 };
13065 let x = synth(hidden, 11);
13066 let k = 12usize;
13067 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
13068 let mut g = vec![0.0f32; inter];
13070 d.gate_proj.matvec(&x, &mut g, None);
13071 let mut u = vec![0.0f32; inter];
13072 d.up_proj.matvec(&x, &mut u, None);
13073 for v in g.iter_mut() {
13074 *v = inference::silu(*v);
13075 }
13076 keep_top_k(&mut g, k);
13077 for i in 0..inter {
13078 g[i] *= u[i];
13079 }
13080 let mut want = vec![0.0f32; hidden];
13081 d.down_proj.matvec(&g, &mut want, None);
13082 for (a, b) in want.iter().zip(&got) {
13083 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
13084 }
13085 }
13086
13087 #[test]
13093 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
13094 let (hidden, core, tube) = (8usize, 12usize, 8usize);
13095 let inter = core + tube;
13096 let synth = |n: usize, salt: usize| -> Vec<f32> {
13097 (0..n)
13098 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
13099 .collect()
13100 };
13101 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
13102 let d_all = synth(hidden * inter, 3);
13103 let dense = DenseFfn {
13105 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
13106 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
13107 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
13108 act: Act::Silu,
13109 down_t: None,
13110 segs: Vec::new(),
13111 };
13112 let rows =
13113 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
13114 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
13115 let mut o = Vec::with_capacity(hidden * (b - a));
13116 for r in 0..hidden {
13117 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
13118 }
13119 o
13120 };
13121 let tubed = DenseFfn {
13122 down_t: None,
13123 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
13124 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
13125 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
13126 act: Act::Silu,
13127 segs: vec![FfnSeg {
13128 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
13129 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
13130 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
13131 start: core,
13132 width: tube,
13133 }],
13134 };
13135 let x = synth(hidden, 7);
13136 let want = dense_ffn(&dense, &x, None);
13137 let got = tube_ffn(&tubed, &x, 1, None, None);
13138 for (a, b) in want.iter().zip(&got) {
13139 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
13140 }
13141 let mut bits = vec![0u8; inter.div_ceil(8)];
13143 for n in 0..core {
13144 bits[n / 8] |= 1 << (n % 8);
13145 }
13146 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13147 let masked = dense_ffn_masked(&dense, &x, None, &bits);
13148 for (a, b) in masked.iter().zip(&closed) {
13149 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
13150 }
13151 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13153 for (a, b) in closed.iter().zip(&batch) {
13154 assert_eq!(a, b, "batch arm disagrees with decode arm");
13155 }
13156 }
13157
13158 #[test]
13160 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
13161 let (hidden, inter) = (16usize, 40usize);
13162 let synth = |n: usize, salt: usize| -> Vec<f32> {
13163 (0..n)
13164 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
13165 .collect()
13166 };
13167 let d = DenseFfn {
13168 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13169 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13170 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
13171 act: Act::Silu,
13172 down_t: None,
13173 segs: Vec::new(),
13174 };
13175 let x = synth(hidden, 9);
13176 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
13178
13179 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
13180
13181 let mut g = vec![0.0f32; inter];
13183 d.gate_proj.matvec(&x, &mut g, None);
13184 let mut u = vec![0.0f32; inter];
13185 d.up_proj.matvec(&x, &mut u, None);
13186 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
13187 for i in 0..inter {
13188 g[i] = if act_set.contains(&(i as u16)) {
13189 inference::silu(g[i]) * u[i]
13190 } else {
13191 0.0
13192 };
13193 }
13194 let mut reference = vec![0.0f32; hidden];
13195 d.down_proj.matvec(&g, &mut reference, None);
13196
13197 let max_d = sparse
13198 .iter()
13199 .zip(&reference)
13200 .map(|(a, b)| (a - b).abs())
13201 .fold(0.0f32, f32::max);
13202 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
13203 }
13204
13205 fn attach_test_mtp(p: &mut Pipeline) {
13207 let (h, inter, heads, kv, hd) = (
13208 p.hidden_size,
13209 p.intermediate_size,
13210 p.num_heads,
13211 p.num_kv_heads,
13212 p.head_dim,
13213 );
13214 let synth = |n: usize, salt: usize| -> Vec<f32> {
13215 (0..n)
13216 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
13217 .collect()
13218 };
13219 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
13220 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
13221 };
13222 p.mtp = Some(MtpModule {
13223 enorm: vec![1.0; h],
13224 hnorm: vec![1.0; h],
13225 eh_proj: qt(h, 2 * h, 301),
13226 layer: LayerWeights {
13227 input_norm: vec![1.0; h],
13228 post_norm: vec![1.0; h],
13229 attn_out_norm: None,
13230 ffn_out_norm: None,
13231 layer_scale: None,
13232 ffn: FfnKind::Dense(DenseFfn {
13233 gate_proj: qt(inter, h, 315),
13234 up_proj: qt(inter, h, 316),
13235 down_proj: qt(h, inter, 317),
13236 act: Act::Silu,
13237 down_t: None,
13238 segs: Vec::new(),
13239 }),
13240 attn: AttnKind::Full {
13241 bias: None,
13242 wq: qt(heads * hd, h, 311),
13243 wk: qt(kv * hd, h, 312),
13244 wv: qt(kv * hd, h, 313),
13245 wo: qt(h, heads * hd, 314),
13246 q_norm: None,
13247 k_norm: None,
13248 output_gate: false,
13249 softplus_gate: None,
13250 },
13251 },
13252 final_norm: vec![1.0; h],
13253 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
13254 });
13255 }
13256
13257 #[test]
13258 fn speculative_equals_vanilla_greedy() {
13259 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
13263 let run = |spec: bool| {
13264 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13265 p.sampler_config.temperature = 0.0;
13266 attach_test_mtp(&mut p);
13267 p.speculative = spec;
13268 let r = p.generate("abcdef", 12, None, None).unwrap();
13269 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
13270 };
13271 let (vanilla, d0, _) = run(false);
13272 let (spec, d1, a1) = run(true);
13273 assert_eq!(d0, 0, "vanilla path must not draft");
13274 assert!(d1 > 0, "speculative path must draft");
13275 assert_eq!(
13276 vanilla, spec,
13277 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
13278 );
13279 }
13280
13281 #[test]
13282 fn speculative_accepts_constant_oracle() {
13283 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
13285 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13286 p.sampler_config.temperature = 0.0;
13287 p.sampler_config.repetition_penalty = 1.0;
13288 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
13291 attach_test_mtp(&mut p);
13292 p.speculative = true;
13293 let r = p.generate("abcd", 10, None, None).unwrap();
13294 assert!(r.mtp_drafted > 0);
13295 assert_eq!(
13296 r.mtp_accepted, r.mtp_drafted,
13297 "constant logits → every draft accepted"
13298 );
13299 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
13302 }
13303
13304 #[test]
13305 fn empty_prompt_is_an_error_not_a_panic() {
13306 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
13307 let r = p.generate("", 4, None, None);
13308 assert!(r.is_err(), "empty prompt must be a clean error");
13309 }
13310
13311 #[test]
13312 fn every_token_enters_kv_exactly_once() {
13313 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13314 p.sampler_config.temperature = 0.0;
13316 let r = p.generate("abc", 2, None, None).unwrap();
13317 assert_eq!(r.prompt_tokens, 3);
13318 assert_eq!(
13322 p.kv_cache.seq_len(),
13323 3 + r.tokens_generated - 1,
13324 "each token must be cached exactly once (v1 cached the last prompt token twice)"
13325 );
13326 }
13327
13328 #[test]
13329 fn generation_is_reproducible_with_seed() {
13330 let run = || {
13331 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13332 p.generate("hello", 8, None, None).unwrap().token_ids
13333 };
13334 assert_eq!(run(), run());
13335 }
13336
13337 #[test]
13338 fn resetting_sampler_restarts_the_seeded_stream() {
13339 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13340 let config = SamplerConfig {
13341 seed: Some(1234),
13342 ..SamplerConfig::default()
13343 };
13344 p.set_sampler_config(config.clone());
13345 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
13346 p.set_sampler_config(config);
13347 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
13348 assert_eq!(first, second);
13349 }
13350
13351 #[test]
13352 fn eviction_bounds_the_cache() {
13353 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
13354 p.kv_cache.max_seq_len = 6;
13355 p.sampler_config.temperature = 0.0;
13356 let _ = p.generate("abcd", 12, None, None).unwrap();
13357 assert!(
13358 p.kv_cache.seq_len() <= 6 + 1,
13359 "cache must stay bounded by max_seq_len (got {})",
13360 p.kv_cache.seq_len()
13361 );
13362 }
13363
13364 #[test]
13365 fn confidence_matches_tokens_and_is_a_probability() {
13366 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13367 p.sampler_config.temperature = 0.0;
13368 p.sampler_config.repetition_penalty = 1.0;
13369 let r = p.generate("abcd", 10, None, None).unwrap();
13370 assert_eq!(
13371 r.token_confidence.len(),
13372 r.token_ids.len(),
13373 "one confidence per emitted token"
13374 );
13375 for &c in &r.token_confidence {
13376 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
13377 }
13378 let logits = [1.0f32, 3.0, 0.5, 3.0];
13380 let p0 = top1_prob_t(&logits, 1, 1.0);
13381 let p1 = top1_prob_t(&logits, 3, 1.0);
13382 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
13383 assert!(p0 > 0.0 && p0 < 1.0);
13384 let sharp = top1_prob_t(&logits, 1, 1.0);
13386 let soft = top1_prob_t(&logits, 1, 2.0);
13387 assert!(soft < sharp, "higher temperature lowers peak confidence");
13388 }
13389
13390 #[test]
13391 fn trace_is_opt_in_and_parallels_the_output() {
13392 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13394 p.sampler_config.temperature = 0.0;
13395 p.sampler_config.repetition_penalty = 1.0;
13396 let r = p.generate("abcd", 10, None, None).unwrap();
13397 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
13398
13399 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13401 p.sampler_config.temperature = 0.0;
13402 p.sampler_config.repetition_penalty = 1.0;
13403 p.set_trace(true);
13404 let r = p.generate("abcd", 10, None, None).unwrap();
13405 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
13406 for (i, tr) in r.traces.iter().enumerate() {
13407 assert_eq!(tr.t, i, "trace index is sequential");
13408 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
13409 assert_eq!(
13410 tr.confidence, r.token_confidence[i],
13411 "trace confidence matches the confidence channel"
13412 );
13413 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
13415 }
13416 }
13417
13418 #[test]
13419 fn explain_prefill_logits_match_greedy_first_token() {
13420 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13424 p.sampler_config.temperature = 0.0;
13425 p.sampler_config.repetition_penalty = 1.0;
13426 let ids = p.tokenizer.encode("abcd");
13427 let logits = p.prefill_next_logits(&ids, None);
13428 let argmax = logits
13429 .iter()
13430 .enumerate()
13431 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
13432 .unwrap()
13433 .0 as u32;
13434 let r = p.generate("abcd", 1, None, None).unwrap();
13435 assert_eq!(
13436 argmax, r.token_ids[0],
13437 "explain preview must match greedy emit"
13438 );
13439 }
13440
13441 #[test]
13442 fn laguna_shared_expert_is_unconditionally_added() {
13443 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
13444 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
13445 let zero_dense = || DenseFfn {
13446 gate_proj: matrix(vec![0.0; 4]),
13447 up_proj: matrix(vec![0.0; 4]),
13448 down_proj: matrix(vec![0.0; 4]),
13449 act: Act::Silu,
13450 down_t: None,
13451 segs: Vec::new(),
13452 };
13453 let shared = DenseFfn {
13454 gate_proj: identity(),
13455 up_proj: identity(),
13456 down_proj: identity(),
13457 act: Act::Silu,
13458 down_t: None,
13459 segs: Vec::new(),
13460 };
13461 let x = [1.0, 2.0];
13462 let expected = dense_ffn(&shared, &x, None);
13463 let moe = MoeFfn {
13464 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
13465 experts: vec![zero_dense()],
13466 top_k: 1,
13467 norm_topk_prob: true,
13468 router_sigmoid: true,
13469 expert_bias: None,
13470 routed_scaling: 1.0,
13471 route_tau: None,
13472 shared: Some((shared, None)),
13473 stats: std::cell::RefCell::new(Vec::new()),
13474 act_sq: std::cell::RefCell::new(Vec::new()),
13475 act_rows: std::cell::RefCell::new(Vec::new()),
13476 mask: None,
13477 per_expert_scale: None,
13478 router_input_norm: false,
13479 resonance: None,
13480 };
13481 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
13482 for (actual, expected) in actual.iter().zip(expected) {
13483 assert!((actual - expected).abs() < 1e-6);
13484 }
13485 }
13486
13487 #[test]
13488 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
13489 const B: usize = 19;
13490 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13491 p.set_o1(Some(crate::nystrom::O1Cfg {
13492 layers: crate::nystrom::O1Layers::All,
13493 m: 4,
13494 w: 8,
13495 sink: 2,
13496 rect: crate::nystrom::O1Rect::Aggregate,
13497 }));
13498 p.o1_begin_with_prefix(Some(B));
13499 let ids: Vec<u32> = (0..B as u32).collect();
13500 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
13501
13502 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
13503 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
13504 let next = p.embed_single(B as u32);
13505 let _ = p.forward_layers(&next, B, None);
13506 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
13507 }
13508
13509 #[test]
13510 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
13511 const B: usize = 19;
13512 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13513 let gdn_cfg = crate::linear_core::GdnCfg {
13517 num_v_heads: 2,
13518 num_k_heads: 1,
13519 key_head_dim: 2,
13520 value_head_dim: 4,
13521 conv_kernel: 3,
13522 hidden_size: 8,
13523 rms_eps: 1e-6,
13524 output_gate_sigmoid: false,
13525 };
13526 let synth = |n: usize, salt: usize| -> Vec<f32> {
13527 (0..n)
13528 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
13529 .collect()
13530 };
13531 let qt = |rows: usize, cols: usize, salt: usize| {
13532 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
13533 };
13534 let c_dim = gdn_cfg.conv_dim();
13535 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
13536 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
13537 in_proj_qkv: qt(c_dim, 8, 1),
13538 in_proj_z: qt(vd, 8, 2),
13539 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
13540 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
13541 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
13542 a_log: vec![0.2, 0.5],
13543 dt_bias: synth(gdn_cfg.num_v_heads, 6),
13544 norm: vec![1.0; gdn_cfg.value_head_dim],
13545 out_proj: qt(8, vd, 7),
13546 });
13547 p.gdn_cfg = Some(gdn_cfg);
13548 p.set_o1(Some(crate::nystrom::O1Cfg {
13549 layers: crate::nystrom::O1Layers::All,
13550 m: 4,
13551 w: 8,
13552 sink: 2,
13553 rect: crate::nystrom::O1Rect::Aggregate,
13554 }));
13555 p.o1_begin_with_prefix(Some(B));
13556 for pos in 0..B - 2 {
13557 let emb = p.embed_single(pos as u32);
13558 let _ = p.forward_layers(&emb, pos, None);
13559 }
13560 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
13561
13562 let e1 = p.embed_single((B - 2) as u32);
13563 let e2 = p.embed_single((B - 1) as u32);
13564 let _ = p.forward_pair(&e1, &e2, B - 2);
13565
13566 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
13567 assert!(
13568 p.kv_cache
13569 .layers
13570 .iter()
13571 .enumerate()
13572 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
13573 );
13574 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
13575 assert_ne!(
13576 p.kv_cache.layers[0].linear_state, lane1_state,
13577 "real pair must commit GDN lane 2 before returning"
13578 );
13579 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
13580 let next = p.embed_single(B as u32);
13581 let _ = p.forward_layers(&next, B, None);
13582 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
13583 }
13584
13585 #[test]
13586 fn o1_error_observation_stays_terminal_until_reset() {
13587 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13588 p.set_o1(Some(crate::nystrom::O1Cfg {
13589 layers: crate::nystrom::O1Layers::All,
13590 m: 4,
13591 w: 8,
13592 sink: 2,
13593 rect: crate::nystrom::O1Rect::Aggregate,
13594 }));
13595 p.o1_begin();
13596 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
13597
13598 assert!(p.o1_seal_checked().is_err());
13599 assert!(
13600 p.o1_seal_checked().is_err(),
13601 "retry must see the sticky error"
13602 );
13603 let k = vec![0.2f32; 4];
13604 let v = vec![0.3f32; 4];
13605 p.kv_cache.layers[0].append(&k, &v, &[]);
13606 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
13607
13608 p.reset_session();
13609 p.o1_begin();
13610 p.kv_cache.layers[0].append(&k, &v, &[]);
13611 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
13612 }
13613
13614 #[test]
13615 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
13616 let ids = vec![1u32, 2, 3, 4, 5, 6];
13617 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13618 p.graph_logits = Some(vec![123.0]);
13619 p.graph_want_logits = true;
13620 p.graph_failed
13621 .store(true, std::sync::atomic::Ordering::Relaxed);
13622 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13623 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
13624 assert!(err.contains("before NLL"));
13625 assert!(p.graph_logits.is_none());
13626 assert!(!p.graph_want_logits);
13627 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13628 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
13629
13630 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13631 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
13632 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
13633 assert_eq!(actual.1, expected.1);
13634 assert!((actual.0 - expected.0).abs() < 1e-9);
13635 }
13636
13637 #[test]
13638 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
13639 let ids = vec![1u32, 2, 3, 4, 5, 6];
13640 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13641 p.nll_test_fail_at = Some(1);
13642 let err = p
13643 .nll_ids_from(&ids, 0)
13644 .expect_err("one-shot forward failure");
13645 assert!(err.contains("forward") || err.contains("score row"));
13646 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13647 assert!(!p.graph_want_logits);
13648 assert!(p.graph_logits.is_none());
13649 assert!(p.kv_history.is_empty());
13650
13651 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13652 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
13653 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
13654 assert_eq!(actual.1, expected.1);
13655 assert!((actual.0 - expected.0).abs() < 1e-9);
13656 }
13657
13658 #[test]
13659 fn nll_serial_failure_before_first_row_is_reported() {
13660 let ids = vec![1u32, 2, 3, 4];
13661 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13662 p.nll_test_force_serial = true;
13663 p.nll_test_fail_at = Some(0);
13664 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
13665 assert!(err.contains("serial forward"));
13666 assert!(p.kv_history.is_empty());
13667 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13668 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
13669 }
13670
13671 #[test]
13672 fn ffn_probe_failure_discards_recorder_and_state() {
13673 let ids = vec![1u32, 2, 3, 4];
13674 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13675 p.nll_test_fail_at = Some(0);
13676 let err = p
13677 .probe_ffn_mass_batch(&ids)
13678 .expect_err("probe forward failure");
13679 assert!(err.contains("NLL"));
13680 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
13681 assert!(p.kv_history.is_empty());
13682 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13683 }
13684
13685 #[test]
13686 fn nll_test_controls_are_pipeline_scoped() {
13687 let ids = vec![1u32, 2, 3, 4];
13688 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13689 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13690 failing.nll_test_force_serial = true;
13691 failing.nll_test_fail_at = Some(0);
13692
13693 assert!(!failing.can_prefill_batched());
13694 assert!(unaffected.can_prefill_batched());
13695 let expected = unaffected
13696 .nll_ids_from(&ids, 0)
13697 .expect("unaffected pipeline remains usable");
13698 let err = failing
13699 .nll_ids_from(&ids, 0)
13700 .expect_err("failure injection belongs to failing pipeline");
13701 assert!(err.contains("serial forward"));
13702 assert!(failing.nll_test_fail_at.is_none());
13703 assert!(unaffected.can_prefill_batched());
13704 let actual = unaffected
13705 .nll_ids_from(&ids, 0)
13706 .expect("unaffected pipeline remains reusable");
13707 assert_eq!(actual.1, expected.1);
13708 assert!((actual.0 - expected.0).abs() < 1e-9);
13709 }
13710
13711 #[test]
13712 fn forward_ids_failure_channel_is_terminal_and_reusable() {
13713 let ids = vec![1u32, 2, 3, 4, 5, 6];
13714 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13715 p.graph_logits = Some(vec![123.0]);
13716 p.graph_want_logits = true;
13717 p.graph_failed
13718 .store(true, std::sync::atomic::Ordering::Relaxed);
13719 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13720
13721 let err = p
13722 .forward_ids(&ids, None)
13723 .expect_err("a failed forward must not become a valid head result");
13724 assert!(err.contains("forward_ids setup"));
13725 assert!(p.graph_logits.is_none());
13726 assert!(!p.graph_want_logits);
13727 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13728 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
13729 assert_eq!(p.kv_cache.seq_len(), 0);
13730
13731 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
13732 .forward_ids(&ids, None)
13733 .expect("fresh forward_ids");
13734 let actual = p
13735 .forward_ids(&ids, None)
13736 .expect("pipeline remains reusable after a failed forward");
13737 assert_eq!(actual.len(), expected.len());
13738 assert!(
13739 actual
13740 .iter()
13741 .zip(expected)
13742 .all(|(a, b)| (a - b).abs() < 1e-9)
13743 );
13744 assert_eq!(p.kv_cache.seq_len(), ids.len());
13745 }
13746}