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 dsv41: Option<
121 Box<(
122 crate::dsv41::Dsv41Globals,
123 Vec<crate::dsv41::Dsv41Layer>,
124 crate::dsv41::Dsv41Cfg,
125 crate::dsv41::Dsv41State,
126 )>,
127 >,
128 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
130 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
132 pub qwen4_exp: Option<
135 Box<(
136 crate::qwen4_exp::Globals,
137 Vec<crate::qwen4_exp::Layer>,
138 crate::qwen4_exp::Cfg,
139 crate::qwen4_exp::State,
140 )>,
141 >,
142 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
146 pub dspark: Option<crate::dsv4::DsparkState>,
148 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
151 pub dspark_hist: Vec<usize>,
153 pub dspark_real: Vec<u32>,
157 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
161 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
163 pub dspark_draft_ns: u128,
167 pub short_conv_cfg: Option<ShortConvCfg>,
170 pub mtp: Option<MtpModule>,
172 pub speculative: bool,
174 rng: SplitMix64,
175 sampler_scratch: SamplerScratch,
176 spec_forced: Option<u32>,
182 spec_q: Vec<Vec<f32>>,
183 spec_p: Vec<f32>,
184 spec_res: Vec<f32>,
185 spec_qs: Vec<sampler::Sparse>,
187 spec_ps: sampler::Sparse,
188 spec_ress: sampler::Sparse,
189 mtp_graph_mode: Option<bool>,
196 #[cfg(target_os = "macos")]
199 metal_verify: Option<MetalVerifyPending>,
200 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
204 ws: ForwardScratch,
208 pool: Option<std::sync::Arc<Pool>>,
210 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
214 pub(crate) dyn_force_f32: bool,
216 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
221 pub(crate) dyn_active: Option<usize>,
227 pub(crate) dyn_blend_loaded: bool,
231 pub(crate) dyn_phi_layer: Option<usize>,
234 dyn_phi_ema: Vec<f32>,
236 dyn_phi_seen: usize,
237 pub dyn_router: Option<crate::swarm::DynRouter>,
240 o1_cfg: Option<crate::nystrom::O1Cfg>,
243 o1_epoch: u64,
246 o1_flags: Vec<bool>,
248 trace: bool,
251 calib_temp: f32,
254 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
256 graph_kv_id: u64,
257 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
260 graph_want_logits: bool,
261 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
265 graph_head_required: bool,
266 graph_logits: Option<Vec<f32>>,
269 pub embed_multiplier: f32,
271 pub attn_scale: f32,
274 pub swa: Option<(usize, usize)>,
277 pub sliding_layers: Option<Vec<bool>>,
280 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
283 pub rotary_dim_local: Option<usize>,
284 pub rope_scale: f32,
285 pub rope_scale_local: f32,
286 pub global_attn: Option<(usize, usize)>,
289 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
292 pub attn_v_norm: bool,
294 pub final_softcap: Option<f32>,
296 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
300 pub attn_softcap: f32,
302 confidence_on: bool,
306 #[cfg(test)]
309 nll_test_fail_at: Option<usize>,
310 #[cfg(test)]
313 nll_test_force_serial: bool,
314}
315
316#[cfg(target_os = "macos")]
317impl Drop for Pipeline {
318 fn drop(&mut self) {
319 crate::gpu::kv_mirror_drop(self.graph_kv_id);
320 }
321}
322
323pub struct PipelineWeights {
328 pub embed_tokens: QTensor,
330 pub layers: Vec<LayerWeights>,
332 pub lm_head: QTensor,
334 pub final_norm: Vec<f32>,
336}
337
338pub struct LayerWeights {
340 pub input_norm: Vec<f32>,
341 pub post_norm: Vec<f32>,
344 pub attn_out_norm: Option<Vec<f32>>,
347 pub layer_scale: Option<f32>,
349 pub ffn_out_norm: Option<Vec<f32>>,
352 pub ffn: FfnKind,
353 pub attn: AttnKind,
354}
355
356#[derive(Clone, Copy, PartialEq, Debug, Default)]
359pub enum Act {
360 #[default]
361 Silu,
362 GeluTanh,
363 Situ {
366 beta: f32,
367 linear_beta: f32,
368 },
369}
370
371impl Act {
372 pub fn from_arch(name: &str) -> Self {
373 if name == "gelu_tanh" {
374 Self::GeluTanh
375 } else {
376 Self::Silu
377 }
378 }
379
380 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
382 match arch.hidden_act.as_str() {
383 "situ" => Self::Situ {
384 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
385 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
386 },
387 other => Self::from_arch(other),
388 }
389 }
390
391 #[inline]
392 pub fn apply(self, x: f32) -> f32 {
393 match self {
394 Self::Silu => inference::silu(x),
395 Self::GeluTanh => inference::gelu_tanh(x),
396 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
397 }
398 }
399
400 #[inline]
403 pub fn combine(self, g: f32, u: f32) -> f32 {
404 match self {
405 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
406 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
407 }
408 _ => self.apply(g) * u,
409 }
410 }
411}
412
413pub struct DenseFfn {
415 pub gate_proj: QTensor,
416 pub up_proj: QTensor,
417 pub down_proj: QTensor,
418 pub act: Act,
420 pub down_t: Option<QTensor>,
426 pub segs: Vec<FfnSeg>,
433}
434
435pub struct FfnSeg {
440 pub gate: QTensor,
441 pub up: QTensor,
442 pub down: QTensor,
443 pub start: usize,
444 pub width: usize,
445}
446
447pub enum FfnKind {
450 Dense(DenseFfn),
451 Moe(MoeFfn),
455 DenseMoe(Box<DenseMoeFfn>),
462}
463
464pub struct DenseMoeFfn {
466 pub dense: DenseFfn,
467 pub moe: MoeFfn,
468 pub post_norm_1: Vec<f32>,
470 pub pre_norm_2: Vec<f32>,
473 pub post_norm_2: Vec<f32>,
475}
476
477pub struct MoeFfn {
478 pub router: QTensor,
480 pub experts: Vec<DenseFfn>,
481 pub top_k: usize,
482 pub norm_topk_prob: bool,
483 pub router_sigmoid: bool,
486 pub expert_bias: Option<Vec<f32>>,
490 pub routed_scaling: f32,
493 pub route_tau: Option<f32>,
499 pub shared: Option<(DenseFfn, Option<QTensor>)>,
502 pub stats: std::cell::RefCell<Vec<u64>>,
506 pub act_sq: std::cell::RefCell<Vec<f64>>,
513 pub act_rows: std::cell::RefCell<Vec<f32>>,
519 pub mask: Option<Vec<bool>>,
524 pub per_expert_scale: Option<Vec<f32>>,
527 pub router_input_norm: bool,
531 pub resonance: Option<Resonance>,
535}
536
537pub struct Resonance {
539 pub mu: Vec<f32>,
541 pub u: Vec<f32>,
543 pub k: usize,
544 pub bias: Vec<f32>,
546}
547
548impl Resonance {
549 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
551 let h = x.len();
552 let ne = out.len();
553 for e in 0..ne {
554 let mu = &self.mu[e * h..(e + 1) * h];
555 let mut d2 = 0.0f32;
556 for j in 0..h {
557 let d = x[j] - mu[j];
558 d2 += d * d;
559 }
560 let mut proj = 0.0f32;
561 for i in 0..self.k {
562 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
563 let mut p = 0.0f32;
564 for j in 0..h {
565 p += (x[j] - mu[j]) * u[j];
566 }
567 proj += p * p;
568 }
569 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
570 }
571 }
572}
573
574pub enum AttnKind {
577 Full {
579 wq: QTensor,
580 wk: QTensor,
581 wv: QTensor,
582 wo: QTensor,
583 q_norm: Option<Vec<f32>>,
584 k_norm: Option<Vec<f32>>,
585 output_gate: bool,
586 softplus_gate: Option<(QTensor, bool)>,
590 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
592 },
593 Linear(VmfPhaseWeights),
595 LinearGdn(GdnWeights),
597 ShortConv(ShortConvWeights),
600 Mla(Box<MlaWeights>),
608 Kda(Box<crate::linear_core::KdaWeights>),
612}
613
614pub struct MlaWeights {
616 pub q_proj: QTensor,
620 pub q_a: Option<QTensor>,
623 pub q_a_norm: Option<Vec<f32>>,
624 pub kv_a: QTensor,
626 pub kv_a_norm: Vec<f32>,
628 pub kv_b: QTensor,
630 pub o_proj: QTensor,
632 pub nh: usize,
633 pub qk_rope: usize,
634 pub qk_nope: usize,
635 pub v_dim: usize,
636 pub lora: usize,
637 pub scale: f32,
639 pub nope: bool,
641}
642
643pub struct MtpModule {
648 pub enorm: Vec<f32>,
649 pub hnorm: Vec<f32>,
650 pub eh_proj: QTensor,
652 pub layer: LayerWeights,
653 pub final_norm: Vec<f32>,
654 pub kv: crate::kv_cache::LayerKvCache,
655}
656
657#[cfg(target_os = "macos")]
664enum MetalRowsItem<'a> {
665 Gdn {
666 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
667 first: usize,
668 },
669 Attn {
670 l: crate::gpu_metal::AttnGpuLayer<'a>,
671 li: usize,
672 q_norm: Option<&'a [f32]>,
673 k_norm: Option<&'a [f32]>,
674 output_gate: bool,
675 },
676}
677
678#[cfg(target_os = "macos")]
679struct MetalVerifyPending {
680 graph: crate::gpu_metal::VerifyGraph,
681 gdn_layers: Vec<usize>,
682 attn_layers: Vec<(usize, usize)>,
683}
684
685#[cfg(target_os = "macos")]
686enum MetalRowsRun {
687 Declined,
689 Failed,
692 Completed(MetalVerifyPending),
693}
694
695#[cfg(target_os = "macos")]
696enum MetalPrefillOutcome {
697 Declined,
698 Failed,
699 Completed(Vec<f32>),
700}
701
702#[cfg(target_os = "macos")]
703enum MetalBatchNllOutcome {
704 Declined,
705 Failed(String),
706 Completed(f64, usize),
707}
708
709#[derive(Clone, Copy)]
713enum SpecTrial {
714 Spec {
715 t0: std::time::Instant,
716 gen0: usize,
717 rounds: usize,
718 },
719 Plain {
720 t0: std::time::Instant,
721 gen0: usize,
722 },
723 Decided {
724 spec: bool,
725 recheck_at: usize,
726 },
727}
728
729#[derive(Default, Clone, Copy)]
740struct SpecMon {
741 round_ms: f64,
742 tokens: f64,
743 plain_ms: f64,
744 n: u32,
745 fails: u32,
746}
747
748impl SpecMon {
749 fn round(&mut self, dt_ms: f64, produced: usize) {
750 self.n += 1;
751 if self.n == 1 {
752 return; }
754 let a = if self.n == 2 { 1.0 } else { 0.3 };
755 self.round_ms += a * (dt_ms - self.round_ms);
756 self.tokens += a * (produced as f64 - self.tokens);
757 }
758 fn pays(&self) -> bool {
759 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
760 }
761}
762
763pub struct GenerateResult {
765 pub text: String,
766 pub token_ids: Vec<u32>,
767 pub prompt_tokens: usize,
768 pub tokens_generated: usize,
769 pub finish_reason: String,
770 pub mtp_drafted: usize,
772 pub mtp_accepted: usize,
773 pub token_confidence: Vec<f32>,
778 pub traces: Vec<TokenTrace>,
781}
782
783#[derive(Clone, Debug)]
788pub struct TokenTrace {
789 pub t: usize,
791 pub token_id: u32,
793 pub confidence: f32,
795 pub active_skill: Option<String>,
797 pub recon: Option<f32>,
801 pub switched: bool,
804}
805
806#[cfg_attr(not(test), allow(dead_code))]
811fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
812 let t = if temp > 1e-3 { temp } else { 1.0 };
813 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
814 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
815 if sum > 0.0 {
816 (((logits[id as usize] - max) / t).exp()) / sum
817 } else {
818 0.0
819 }
820}
821
822fn prefill_batched() -> bool {
825 std::env::var("CMF_PREFILL")
826 .map(|v| v != "seq")
827 .unwrap_or(true)
828}
829
830#[inline]
834fn nll_graph_policy(
835 unmasked: bool,
836 prefer_graph: bool,
837 native_metal: bool,
838) -> (bool, bool) {
839 let graph_quality = unmasked && prefer_graph;
840 let fused_head_quality = graph_quality && native_metal;
841 (graph_quality, fused_head_quality)
842}
843
844#[derive(Clone, Copy)]
848enum PrefillIn<'a> {
849 Ids(&'a [u32]),
850 Hidden(&'a [f32]),
851}
852
853impl Pipeline {
860 fn can_prefill_batched(&self) -> bool {
861 #[cfg(test)]
862 let force_serial = self.nll_test_force_serial;
863 #[cfg(not(test))]
864 let force_serial = false;
865 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
866 }
867
868 fn automatic_gpu_prefix(&self) -> Option<usize> {
871 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
872 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
873 }
874}
875
876pub fn prefill_chunk() -> usize {
883 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
884 .ok()
885 .and_then(|v| v.parse::<usize>().ok())
886 {
887 return n.max(1);
888 }
889 if cfg!(target_os = "macos") {
890 512
891 } else if cfg!(target_arch = "aarch64") {
892 256
895 } else {
896 48
897 }
898}
899
900#[inline]
906fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
907 if end <= start || start >= input_len {
908 return 0;
909 }
910 let rows = (end.min(input_len) - start).min(input_len - start);
911 if end < input_len {
912 rows
913 } else {
914 rows.saturating_sub(1)
915 }
916}
917
918pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
920
921impl Pipeline {
922 fn clear_sequence_state(&mut self) {
930 self.kv_cache.clear();
931 self.kv_history.clear();
932 if let Some(b) = &mut self.dsv41 {
933 b.3.clear();
934 }
935 crate::gpu::graph_kv_reset(self.graph_kv_id);
936 crate::gpu::graph_kv_reset(self.mtp_kv_id());
941 }
942
943 fn finish_generation(
949 &mut self,
950 mtp: &mut Option<MtpModule>,
951 router: &mut Option<crate::swarm::DynRouter>,
952 clear_sequence: bool,
953 ) {
954 if router.is_some() {
958 let _ = self.set_active_skill(None);
959 }
960 if clear_sequence {
961 self.clear_sequence_state();
962 if let Some(m) = mtp.as_mut() {
963 m.kv.clear();
969 }
970 if let Some(m) = self.mtp.as_mut() {
971 m.kv.clear();
975 }
976 }
977 self.graph_want_logits = false;
978 self.graph_head_required = false;
979 self.graph_logits = None;
980 self.graph_failed
981 .store(false, std::sync::atomic::Ordering::Relaxed);
982 self.cancel
983 .store(false, std::sync::atomic::Ordering::Relaxed);
984 self.dyn_router = router.take().or(self.dyn_router.take());
985 self.mtp = mtp.take().or(self.mtp.take());
986 self.mtp_graph_mode = None;
987 self.spec_forced = None;
988 }
989
990 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
994 if self
995 .graph_failed
996 .swap(false, std::sync::atomic::Ordering::Relaxed)
997 {
998 self.cancel
999 .store(false, std::sync::atomic::Ordering::Relaxed);
1000 self.clear_sequence_state();
1001 self.graph_logits = None;
1002 self.graph_want_logits = false;
1003 self.graph_head_required = false;
1004 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1005 }
1006 Ok(())
1007 }
1008
1009 #[cfg(target_os = "macos")]
1010 fn fail_metal_graph(&mut self, reason: &str) {
1011 crate::pipeline::METAL_GRAPH_ERRORS
1012 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1013 self.clear_sequence_state();
1014 self.graph_logits = None;
1015 self.graph_failed
1016 .store(true, std::sync::atomic::Ordering::Relaxed);
1017 self.cancel
1018 .store(true, std::sync::atomic::Ordering::Relaxed);
1019 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1020 }
1021
1022 fn nll_begin(&mut self) -> Result<(), String> {
1027 if self
1028 .graph_failed
1029 .swap(false, std::sync::atomic::Ordering::Relaxed)
1030 {
1031 self.cancel
1032 .store(false, std::sync::atomic::Ordering::Relaxed);
1033 self.clear_sequence_state();
1034 self.graph_logits = None;
1035 self.graph_want_logits = false;
1036 self.graph_head_required = false;
1037 return Err("GPU graph failed before NLL scoring".to_string());
1038 }
1039 self.clear_sequence_state();
1040 self.graph_logits = None;
1041 self.graph_want_logits = false;
1042 self.graph_head_required = false;
1043 Ok(())
1044 }
1045
1046 fn nll_end(&mut self) {
1050 self.clear_sequence_state();
1051 self.graph_logits = None;
1052 self.graph_want_logits = false;
1053 self.graph_head_required = false;
1054 self.graph_failed
1055 .store(false, std::sync::atomic::Ordering::Relaxed);
1056 }
1057
1058 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1061 #[cfg(test)]
1062 if self.nll_test_fail_at == Some(pos) {
1063 self.nll_test_fail_at = None;
1064 self.graph_failed
1065 .store(true, std::sync::atomic::Ordering::Relaxed);
1066 self.cancel
1067 .store(true, std::sync::atomic::Ordering::Relaxed);
1068 }
1069 if self
1070 .graph_failed
1071 .swap(false, std::sync::atomic::Ordering::Relaxed)
1072 {
1073 self.cancel
1074 .store(false, std::sync::atomic::Ordering::Relaxed);
1075 self.clear_sequence_state();
1076 self.graph_logits = None;
1077 self.graph_want_logits = false;
1078 return Err(format!(
1079 "GPU graph failed during NLL {phase} at position {pos}"
1080 ));
1081 }
1082 Ok(())
1083 }
1084
1085 #[inline]
1089 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1090 virtual_idx % self.physical_layers
1091 }
1092
1093 #[inline]
1096 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1097 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1098 }
1099
1100 #[allow(clippy::too_many_arguments)]
1102
1103 #[cfg(target_os = "macos")]
1122 fn graph_prefill_preferred(&self) -> bool {
1123 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1124 if !crate::gpu::enabled_here()
1125 || !graph_force
1126 || std::env::var("CMF_GPU_BLOCK")
1127 .map(|v| v == "0")
1128 .unwrap_or(false)
1129 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1132 {
1133 return false;
1134 }
1135 self.weights
1136 .layers
1137 .iter()
1138 .any(|lw| {
1139 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1140 })
1141 }
1142
1143 #[cfg(not(target_os = "macos"))]
1144 fn graph_prefill_preferred(&self) -> bool {
1145 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1153 if !graph_on || !crate::gpu::enabled_here() {
1154 return false;
1155 }
1156 if self.o1_active() {
1170 return false;
1171 }
1172 self.weights
1173 .layers
1174 .iter()
1175 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1176 }
1177
1178 #[cfg(target_os = "macos")]
1179 fn q1_graph_gpu(
1180 &mut self,
1181 start: usize,
1182 upto: Option<usize>,
1183 position: usize,
1184 h: &mut [f32],
1185 ) -> usize {
1186 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1188 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1189 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
1191 || !graph_force
1192 || std::env::var("CMF_GPU_BLOCK")
1193 .map(|v| v == "0")
1194 .unwrap_or(false)
1195 {
1196 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1197 eprintln!(
1198 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
1199 self.attn_softcap > 0.0,
1200 crate::gpu::enabled_here(),
1201 graph_force,
1202 );
1203 }
1204 if self.graph_head_required {
1205 self.fail_metal_graph("native graph front gate refused");
1206 }
1207 return start;
1208 }
1209 if self.swa.is_some()
1213 || self.global_attn.is_some()
1214 || self.attention_heads_per_layer.is_some()
1215 || self.attn_v_norm
1216 || self.weights.layers.iter().any(|lw| {
1217 lw.attn_out_norm.is_some()
1218 || lw.ffn_out_norm.is_some()
1219 || lw.layer_scale.is_some()
1220 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1221 })
1222 {
1223 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1224 eprintln!(
1225 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1226 self.swa.is_some(),
1227 self.global_attn.is_some(),
1228 self.attention_heads_per_layer.is_some(),
1229 self.attn_v_norm,
1230 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1231 );
1232 }
1233 if self.graph_head_required {
1234 self.fail_metal_graph("native graph architecture gate refused");
1235 }
1236 return start;
1237 }
1238 let limit = upto
1241 .map(|u| u + 1)
1242 .unwrap_or(self.num_layers)
1243 .min(self.num_layers);
1244
1245 enum Item<'a> {
1246 Gdn {
1247 run: Vec<GdnGpuLayer<'a>>,
1248 first: usize,
1249 },
1250 Attn {
1251 l: AttnGpuLayer<'a>,
1252 li: usize,
1253 q_norm: Option<&'a [f32]>,
1254 k_norm: Option<&'a [f32]>,
1255 output_gate: bool,
1256 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1257 full_gpu: bool,
1260 },
1261 }
1262
1263 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1270 let attend_contract = attend_mode != "0"
1271 && attend_mode != "off"
1272 && self.head_dim % 4 == 0
1273 && self.head_dim <= 256
1274 && self.rotary_dim >= 2
1275 && self.rotary_dim <= self.head_dim
1276 && (self.rotary_dim / 2) % 32 == 0
1277 && self.num_kv_heads > 0
1278 && self.num_heads % self.num_kv_heads == 0;
1279
1280 let mut plan: Vec<Item> = Vec::new();
1281 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1282 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1284 let mut scan = start;
1285 while scan < limit {
1286 let lw = &self.weights.layers[self.phys_layer(scan)];
1287 let ffn = match &lw.ffn {
1288 FfnKind::Dense(d) if d.segs.is_empty() => {
1289 let (Some(g), Some(u), Some(dn)) = (
1290 d.gate_proj.metal_graph_parts(),
1291 d.up_proj.metal_graph_parts(),
1292 d.down_proj.metal_graph_parts(),
1293 ) else {
1294 if block_diag {
1295 eprintln!(
1296 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1297 );
1298 }
1299 break;
1300 };
1301 MetalFfn::Dense {
1302 gate: g,
1303 up: u,
1304 down: dn,
1305 }
1306 }
1307 FfnKind::Moe(m) => {
1308 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1309 if block_diag {
1310 eprintln!(
1311 "block-graph: L{scan} MoE outside the graph contract — run ends"
1312 );
1313 }
1314 break;
1315 };
1316 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1317 model_ref.get_or_insert_with(|| model.clone());
1318 }
1319 MetalFfn::Moe(moe)
1320 }
1321 _ => {
1322 if block_diag {
1323 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1324 }
1325 break;
1326 }
1327 };
1328 match &lw.attn {
1329 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1330 let parts = (
1331 w.in_proj_qkv.metal_graph_parts(),
1332 w.in_proj_z.metal_graph_parts(),
1333 w.in_proj_a.f32_parts(),
1334 w.in_proj_b.f32_parts(),
1335 w.out_proj.metal_graph_parts(),
1336 );
1337 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1338 if block_diag {
1339 eprintln!(
1340 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1341 w.in_proj_qkv.metal_graph_parts().is_some(),
1342 w.in_proj_z.metal_graph_parts().is_some(),
1343 w.in_proj_a.f32_parts().is_some(),
1344 w.in_proj_b.f32_parts().is_some(),
1345 w.out_proj.metal_graph_parts().is_some(),
1346 );
1347 }
1348 break;
1349 };
1350 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1351 model_ref.get_or_insert_with(|| model.clone());
1352 }
1353 let gl = GdnGpuLayer {
1354 attn_norm: &lw.input_norm,
1355 post_norm: &lw.post_norm,
1356 qkv,
1357 z,
1358 a,
1359 b,
1360 out,
1361 ffn,
1362 conv1d: &w.conv1d,
1363 a_log: &w.a_log,
1364 dt_bias: &w.dt_bias,
1365 gnorm: &w.norm,
1366 };
1367 match plan.last_mut() {
1368 Some(Item::Gdn { run, .. }) => run.push(gl),
1369 _ => plan.push(Item::Gdn {
1370 run: vec![gl],
1371 first: scan,
1372 }),
1373 }
1374 }
1375 AttnKind::Full {
1376 wq,
1377 wk,
1378 wv,
1379 wo,
1380 q_norm,
1381 k_norm,
1382 output_gate,
1383 softplus_gate: None,
1384 bias,
1385 } if !self.kv_cache.layers[scan].o1_sealed()
1386 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1391 {
1392 let parts = (
1393 wq.metal_graph_parts(),
1394 wk.metal_graph_parts(),
1395 wv.metal_graph_parts(),
1396 wo.metal_graph_parts(),
1397 );
1398 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1399 break;
1400 };
1401 if let QTensor::Mapped { model, .. } = wq {
1402 model_ref.get_or_insert_with(|| model.clone());
1403 }
1404 let cache = &self.kv_cache.layers[scan];
1405 let o1_metal = cache.o1.is_some()
1409 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1410 && cache.o1_views().is_some();
1411 let full_gpu = attend_contract
1412 && cache.mode == crate::kv_cache::KvMode::F32
1413 && (cache.o1.is_none() || o1_metal)
1414 && bias.is_none()
1415 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1416 && pk.1 == self.num_kv_heads * self.head_dim
1417 && pv.1 == self.num_kv_heads * self.head_dim
1418 && po.2 == self.num_heads * self.head_dim;
1419 plan.push(Item::Attn {
1420 l: AttnGpuLayer {
1421 attn_norm: &lw.input_norm,
1422 post_norm: &lw.post_norm,
1423 wq: pq,
1424 wk: pk,
1425 wv: pv,
1426 wo: po,
1427 ffn,
1428 },
1429 li: scan,
1430 q_norm: q_norm.as_deref(),
1431 k_norm: k_norm.as_deref(),
1432 output_gate: *output_gate,
1433 bias: bias
1434 .as_ref()
1435 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1436 full_gpu,
1437 });
1438 }
1439 _ => break,
1440 }
1441 scan += 1;
1442 }
1443 let Some(model) = model_ref else {
1444 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1445 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1446 }
1447 if self.graph_head_required {
1448 self.fail_metal_graph("native graph has no mapped model reference");
1449 }
1450 return start;
1451 };
1452 if plan.is_empty() {
1453 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1454 eprintln!("q1-graph: empty plan at layer {start}");
1455 }
1456 if self.graph_head_required {
1457 self.fail_metal_graph("native graph plan is empty");
1458 }
1459 return start;
1460 }
1461 let has_moe = plan.iter().any(|it| match it {
1462 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1463 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1464 });
1465 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1466 let dev_attend = attend_contract
1467 && (self.head_dim <= 128
1468 || has_moe
1469 || (self.head_dim <= 256 && has_gdn)
1475 || attend_mode == "force"
1476 || attend_mode == "256");
1477 if !dev_attend {
1478 for it in &mut plan {
1479 if let Item::Attn { li, full_gpu, .. } = it {
1480 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1483 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1484 if !keep_o1 {
1485 *full_gpu = false;
1486 }
1487 }
1488 }
1489 }
1490 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1491 use std::sync::atomic::{AtomicBool, Ordering};
1492 static SAID: AtomicBool = AtomicBool::new(false);
1493 if !SAID.swap(true, Ordering::Relaxed) {
1494 let fg = plan
1495 .iter()
1496 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1497 .count();
1498 let att = plan
1499 .iter()
1500 .filter(|it| matches!(it, Item::Attn { .. }))
1501 .count();
1502 eprintln!(
1503 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1504 plan.len(),
1505 self.head_dim,
1506 self.rotary_dim,
1507 self.num_kv_heads,
1508 self.num_heads,
1509 );
1510 }
1511 }
1512 let dims = GraphDims {
1513 hidden: self.hidden_size,
1514 eps: self.rms_eps as f32,
1515 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1516 };
1517 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1518 if self.graph_head_required {
1519 self.fail_metal_graph("native TokenGraph allocation refused");
1520 }
1521 return start;
1522 };
1523 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1524 nv: cfg.num_v_heads,
1525 nk: cfg.num_k_heads,
1526 dk: cfg.key_head_dim,
1527 dv: cfg.value_head_dim,
1528 kk: cfg.conv_kernel,
1529 hidden: self.hidden_size,
1530 inter: self.intermediate_size,
1531 c_dim: cfg.conv_dim(),
1532 eps: cfg.rms_eps as f32,
1533 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1534 });
1535 let mut valid = 0usize;
1539 let mut end = start;
1540 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1542 static ONCE: std::sync::Once = std::sync::Once::new();
1543 ONCE.call_once(|| {
1544 for it in &plan {
1545 match it {
1546 Item::Gdn { first, run } => {
1547 eprintln!("plan: Gdn first={first} len={}", run.len())
1548 }
1549 Item::Attn { li, full_gpu, .. } => {
1550 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1551 }
1552 }
1553 }
1554 });
1555 }
1556 for item in &plan {
1557 let ok = match item {
1558 Item::Gdn { run, .. } => gcfg
1559 .as_ref()
1560 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1561 .unwrap_or(false),
1562 Item::Attn { l, .. } => graph.attn_ok(l),
1563 };
1564 if !ok {
1565 if block_diag {
1566 eprintln!(
1567 "block-graph: plan item {} ({}) failed graph preflight",
1568 valid,
1569 match item {
1570 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1571 Item::Attn { li, .. } => format!("Attn L{li}"),
1572 }
1573 );
1574 }
1575 break;
1576 }
1577 valid += 1;
1578 end += match item {
1579 Item::Gdn { run, .. } => run.len(),
1580 Item::Attn { .. } => 1,
1581 };
1582 }
1583 plan.truncate(valid);
1584 if plan.is_empty() {
1585 if self.graph_head_required {
1586 self.fail_metal_graph("native graph preflight produced no valid items");
1587 }
1588 return start;
1589 }
1590
1591 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
1592 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
1593 return start;
1594 }
1595
1596 let inv_freq = self.inv_freq.clone();
1597 let pool = self.pool.clone();
1598 let (nh, nkv, hd, hs, rd, eps) = (
1599 self.num_heads,
1600 self.num_kv_heads,
1601 self.head_dim,
1602 self.hidden_size,
1603 self.rotary_dim,
1604 self.rms_eps,
1605 );
1606 let norm_style = self.norm_style;
1607 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1608 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1609 let kv_id = self.graph_kv_id;
1610 let mut pending: Vec<(usize, usize)> = Vec::new();
1613 let mut dev_attn: Vec<usize> = Vec::new();
1616 for item in &plan {
1617 let _xt0 = std::time::Instant::now();
1618 let _xkind: u32 = match item {
1619 Item::Gdn { .. } => 2,
1620 Item::Attn { .. } => 3,
1621 };
1622 if self.loop_final_norm {
1624 let item_start = match item {
1625 Item::Gdn { first, .. } => *first,
1626 Item::Attn { li, .. } => *li,
1627 };
1628 if item_start > start && self.is_loop_end(item_start - 1) {
1629 graph.encode_loop_norm(&self.weights.final_norm);
1630 }
1631 }
1632 match item {
1633 Item::Gdn { run, first } => {
1634 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1635 if l.linear_state.len() != want {
1636 l.linear_state = vec![0f32; want];
1637 }
1638 }
1639 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1640 .iter()
1641 .map(|l| l.linear_state.as_slice())
1642 .collect();
1643 let _ig = std::time::Instant::now();
1644 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1645 tracing::error!("q1 graph: GDN run refused after validation");
1647 return start;
1648 }
1649 graph.commit_kind = 2;
1652 graph.commit();
1653 crate::gpu::stageprof(0, _ig.elapsed());
1654 pending.push((*first, run.len()));
1655 }
1656 Item::Attn {
1657 l,
1658 li,
1659 q_norm,
1660 k_norm,
1661 output_gate,
1662 bias,
1663 full_gpu,
1664 } => {
1665 let _ia = std::time::Instant::now();
1666 if *full_gpu {
1668 let cache = &self.kv_cache.layers[*li];
1669 let o1p = if cache.o1.is_some() {
1670 match cache.o1_views() {
1671 Some(views) => Some(crate::gpu::O1AttnParams {
1672 views,
1673 epoch: self.o1_epoch,
1674 }),
1675 None => None,
1677 }
1678 } else {
1679 None
1680 };
1681 let o1_layer = cache.o1.is_some();
1682 if o1_layer && o1p.is_none() {
1683 }
1685 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1686 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1687 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1688 let p = crate::gpu::AttnDeviceParams {
1689 kv_id,
1690 layer: *li,
1691 nh,
1692 nkv,
1693 hd,
1694 rd,
1695 position,
1696 scale: self.attn_scale,
1697 eps: eps as f32,
1698 gemma,
1699 output_gate: *output_gate,
1700 q_norm: *q_norm,
1701 k_norm: *k_norm,
1702 inv_freq: &inv_freq,
1703 cpu_k,
1704 cpu_v,
1705 cpu_stored,
1706 o1: o1p,
1707 };
1708 let o1_bad = o1_layer && p.o1.is_none();
1709 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1710 {
1711 if p.o1.is_none() {
1713 dev_attn.push(*li);
1714 }
1715 graph.commit_kind = 3;
1716 graph.commit();
1717 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1721 continue;
1722 }
1723 }
1725 graph.encode_attn_prefix(l);
1726 if let Err(err) = graph.sync_checked() {
1727 self.fail_metal_graph(&err);
1728 return start;
1729 }
1730 if !pending.is_empty() {
1731 let idxs: Vec<usize> =
1732 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1733 let mut outs: Vec<&mut [f32]> = self
1734 .kv_cache
1735 .layers
1736 .iter_mut()
1737 .enumerate()
1738 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1739 .map(|(_, s)| s.linear_state.as_mut_slice())
1740 .collect();
1741 graph.read_states(&mut outs);
1742 }
1743 let mut q_raw = attention::take_buf(l.wq.1);
1744 let mut k = attention::take_buf(l.wk.1);
1745 let mut v = attention::take_buf(l.wv.1);
1746 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1747 let cfg = QwenAttnCfg {
1748 num_heads: nh,
1749 num_kv_heads: nkv,
1750 head_dim: hd,
1751 hidden_size: hs,
1752 position,
1753 inv_freq: &inv_freq,
1754 rotary_dim: rd,
1755 scale: self.attn_scale,
1756 softcap: self.attn_softcap,
1757 window: None,
1758 v_norm: false,
1759 q_norm: *q_norm,
1760 k_norm: *k_norm,
1761 output_gate: *output_gate,
1762 softplus_gate: None,
1763 rope_scale: 1.0,
1764 bias: *bias,
1765 rms_eps: eps,
1766 norm_style,
1767 pool: pool.as_deref(),
1768 };
1769 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1772 || std::env::var("CMF_ATTN_DUMP").is_ok();
1773 let _ = full_gpu;
1774 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1775 let mut ao = attention::qwen_attention_core(
1776 q_raw,
1777 k,
1778 v,
1779 &mut self.kv_cache.layers[*li],
1780 &cfg,
1781 );
1782 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1786 if let Some((qr0, k0, v0)) = oracle_in.clone() {
1787 let (cq, _cg, _ck, _cv) =
1788 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1789 let cache = &self.kv_cache.layers[*li];
1790 let n = cache.head_keys(0).len() / hd;
1791 let mut bytes: Vec<u8> = Vec::new();
1792 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1793 bytes.extend_from_slice(&v.to_le_bytes());
1794 }
1795 for v in &cq {
1796 bytes.extend_from_slice(&v.to_le_bytes());
1797 }
1798 for g in 0..nkv {
1799 for v in cache.head_keys(g) {
1800 bytes.extend_from_slice(&v.to_le_bytes());
1801 }
1802 }
1803 for g in 0..nkv {
1804 for v in cache.head_values(g) {
1805 bytes.extend_from_slice(&v.to_le_bytes());
1806 }
1807 }
1808 let _ =
1809 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1810 }
1811 }
1812 if let Some((qr0, k0, v0)) =
1813 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1814 {
1815 let (cq, _cg, ck, cv) =
1816 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1817 let mut h_now = vec![0f32; hs];
1818 graph.read_h(&mut h_now);
1819 let cache = &self.kv_cache.layers[*li];
1820 let n_after = cache.head_keys(0).len() / hd;
1821 let stored = n_after.saturating_sub(1);
1825 let cpu_k: Vec<&[f32]> = (0..nkv)
1826 .map(|g| &cache.head_keys(g)[..stored * hd])
1827 .collect();
1828 let cpu_v: Vec<&[f32]> = (0..nkv)
1829 .map(|g| &cache.head_values(g)[..stored * hd])
1830 .collect();
1831 let p = crate::gpu::AttnDeviceParams {
1832 kv_id,
1833 layer: *li,
1834 nh,
1835 nkv,
1836 hd,
1837 rd,
1838 position,
1839 scale: self.attn_scale,
1840 eps: eps as f32,
1841 gemma,
1842 output_gate: *output_gate,
1843 q_norm: *q_norm,
1844 k_norm: *k_norm,
1845 inv_freq: &inv_freq,
1846 cpu_k,
1847 cpu_v,
1848 cpu_stored: stored,
1849 o1: None,
1850 };
1851 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1852 let md = |a: &[f32], b: &[f32]| {
1853 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
1854 };
1855 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1856 eprintln!(
1857 "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}",
1858 nn(&cq),
1859 md(&cq, &dq),
1860 nn(&ck),
1861 md(&ck, &dk),
1862 nn(&cv),
1863 md(&cv, &dv),
1864 nn(&ao),
1865 md(&ao, &dao)
1866 );
1867 } else {
1868 eprintln!("attn-oracle L{li}: device probe declined");
1869 }
1870 }
1871 graph.encode_attn_suffix(l, &ao);
1872 graph.commit();
1875 attention::recycle_buf(&mut ao);
1876 }
1877 }
1878
1879 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1880 }
1881 let mut lm_rows = None;
1886 if self.graph_want_logits
1887 && upto.is_none()
1888 && end == self.num_layers
1889 && std::env::var("CMF_GPU_LMHEAD")
1890 .map(|v| v != "0")
1891 .unwrap_or(true)
1892 {
1893 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
1894 if graph.lm_head_ok(lm) {
1895 graph.encode_lm_head(&self.weights.final_norm, lm);
1896 lm_rows = Some(lm.1);
1897 }
1898 }
1899 }
1900 if self.graph_head_required && lm_rows.is_none() {
1901 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1902 self.fail_metal_graph("fused graph head was requested but not encodable");
1903 return start;
1904 }
1905 let _sy0 = std::time::Instant::now();
1906 if let Err(err) = graph.sync_checked() {
1907 self.fail_metal_graph(&err);
1908 return start;
1909 }
1910 let _rs0 = std::time::Instant::now();
1911 if !pending.is_empty() {
1912 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1913 let mut outs: Vec<&mut [f32]> = self
1914 .kv_cache
1915 .layers
1916 .iter_mut()
1917 .enumerate()
1918 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1919 .map(|(_, s)| s.linear_state.as_mut_slice())
1920 .collect();
1921 graph.read_states(&mut outs);
1922 }
1923 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1924 use std::sync::atomic::{AtomicU64, Ordering};
1925 static SY: AtomicU64 = AtomicU64::new(0);
1926 static RS: AtomicU64 = AtomicU64::new(0);
1927 static N: AtomicU64 = AtomicU64::new(0);
1928 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1929 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1930 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1931 if n % 100 == 0 {
1932 eprintln!(
1933 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1934 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1935 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1936 );
1937 }
1938 }
1939 if let Some(rows) = lm_rows {
1940 crate::gpu::hostprof_encode_done(_mt0);
1941 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1942 graph.read_logits(&mut lg);
1943 crate::gpu::hostprof_total(_mt0);
1944 lg.resize(self.vocab_size, 0.0);
1945 if let Some(c) = self.final_softcap {
1946 for l in lg.iter_mut() {
1947 *l = c * (*l / c).tanh();
1948 }
1949 }
1950 self.graph_logits = Some(lg);
1951 }
1952 graph.read_h(h);
1953 if self.graph_head_required && self.graph_logits.is_none() {
1954 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1955 self.fail_metal_graph("fused graph head completed without logits readback");
1956 return start;
1957 }
1958 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1959 METAL_GRAPH_LAYERS.fetch_add(
1960 end.saturating_sub(start) as u64,
1961 std::sync::atomic::Ordering::Relaxed,
1962 );
1963 if self.graph_head_required {
1964 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1965 }
1966 for li in dev_attn {
1970 let mut krow = attention::take_buf(nkv * hd);
1971 let mut vrow = attention::take_buf(nkv * hd);
1972 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1973 let cache = &mut self.kv_cache.layers[li];
1974 cache.append(&krow, &vrow, &[]);
1975 let n = cache.seq_len;
1976 let mut imp = attention::take_buf(n);
1977 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1978 cache.accumulate_imp(&imp);
1979 attention::recycle_buf(&mut imp);
1980 }
1981 attention::recycle_buf(&mut krow);
1982 attention::recycle_buf(&mut vrow);
1983 }
1984 end
1985 }
1986
1987 pub fn new(
1988 tokenizer: Tokenizer,
1989 weights: PipelineWeights,
1990 hidden_size: usize,
1991 intermediate_size: usize,
1992 num_heads: usize,
1993 num_kv_heads: usize,
1994 head_dim: usize,
1995 num_layers: usize,
1996 physical_layers: usize,
1997 loop_final_norm: bool,
1998 vocab_size: usize,
1999 rms_eps: f64,
2000 rope_base: f32,
2001 norm_style: NormStyle,
2002 max_seq_len: usize,
2003 sampler_config: SamplerConfig,
2004 ) -> Self {
2005 let rng = match sampler_config.seed {
2006 Some(s) => SplitMix64::new(s),
2007 None => SplitMix64::from_entropy(),
2008 };
2009 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
2010 let pool = Pool::from_env();
2011 if let Some(p) = &pool {
2012 tracing::info!("worker pool: {} threads", p.n_workers());
2013 }
2014 Self {
2015 gpu_plan: None,
2016 tokenizer: std::sync::Arc::new(tokenizer),
2017 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
2018 sampler_config,
2019 weights,
2020 hidden_size,
2021 intermediate_size,
2022 num_heads,
2023 num_kv_heads,
2024 head_dim,
2025 num_layers,
2026 physical_layers,
2027 loop_final_norm,
2028 vocab_size,
2029 rms_eps,
2030 rope_base,
2031 norm_style,
2032 rotary_dim: head_dim,
2033 attention_heads_per_layer: None,
2034 vmf_cfg: None,
2035 gdn_cfg: None,
2036 kda_cfg: None,
2037 g3n: None,
2038 dsv4: None,
2039 dsv41: None,
2040 dsv41_vision: None,
2041 dsv41_prefill: None,
2042 qwen4_exp: None,
2043 dsv4_mtp: Vec::new(),
2044 dspark: None,
2045 dspark_pending: Vec::new(),
2046 dspark_hist: Vec::new(),
2047 dspark_real: Vec::new(),
2048 dspark_trunk_picks: Vec::new(),
2049 dspark_exp: Vec::new(),
2050 dspark_draft_ns: 0,
2051 logit_multiplier: None,
2052 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2053 graph_failed: std::sync::atomic::AtomicBool::new(false),
2054 kv_history: Vec::new(),
2055 short_conv_cfg: None,
2056 mtp: None,
2057 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
2058 rng,
2059 sampler_scratch: SamplerScratch::default(),
2060 spec_forced: None,
2061 spec_q: Vec::new(),
2062 spec_p: Vec::new(),
2063 spec_res: Vec::new(),
2064 spec_qs: Vec::new(),
2065 spec_ps: Vec::new(),
2066 spec_ress: Vec::new(),
2067 mtp_graph_mode: None,
2068 #[cfg(target_os = "macos")]
2069 metal_verify: None,
2070 inv_freq,
2071 ws: ForwardScratch::new(hidden_size),
2072 pool,
2073 model: None,
2074 dyn_force_f32: false,
2075 dyn_skill_layers: Vec::new(),
2076 dyn_active: None,
2077 dyn_blend_loaded: false,
2078 dyn_phi_layer: None,
2079 dyn_phi_ema: Vec::new(),
2080 dyn_phi_seen: 0,
2081 dyn_router: None,
2082 o1_cfg: None,
2083 o1_epoch: 0,
2084 o1_flags: Vec::new(),
2085 trace: false,
2086 calib_temp: 1.0,
2087 confidence_on: true,
2088 embed_multiplier: 1.0,
2089 attn_scale: 1.0 / (head_dim as f32).sqrt(),
2090 swa: None,
2091 sliding_layers: None,
2092 inv_freq_local: None,
2093 rotary_dim_local: None,
2094 rope_scale: 1.0,
2095 rope_scale_local: 1.0,
2096 global_attn: None,
2097 inv_freq_global: None,
2098 attn_v_norm: false,
2099 final_softcap: None,
2100 head_clusters: None,
2101 attn_softcap: 0.0,
2102 graph_want_logits: false,
2103 graph_head_required: false,
2104 graph_logits: None,
2105 graph_kv_id: {
2106 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2107 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2108 },
2109 #[cfg(test)]
2110 nll_test_fail_at: None,
2111 #[cfg(test)]
2112 nll_test_force_serial: false,
2113 }
2114 }
2115
2116 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2124 if let Some(c) = &cfg {
2125 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2126 tracing::error!(
2127 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2128 c.w,
2129 c.sink
2130 );
2131 self.o1_flags.clear();
2132 self.o1_cfg = None;
2133 return;
2134 }
2135 }
2136 self.o1_flags = match &cfg {
2137 Some(c) => {
2138 let mut flags = c.layer_flags(self.num_layers);
2139 for (li, f) in flags.iter_mut().enumerate() {
2140 if *f
2141 && !matches!(
2142 self.weights.layers[self.phys_layer(li)].attn,
2143 AttnKind::Full { .. }
2144 )
2145 {
2146 *f = false;
2147 }
2148 }
2149 flags
2150 }
2151 None => Vec::new(),
2152 };
2153 if let Some(c) = &cfg {
2154 let n = self.o1_flags.iter().filter(|&&f| f).count();
2155 tracing::info!(
2156 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2157 self.num_layers,
2158 c.m,
2159 c.w,
2160 c.sink,
2161 c.rect
2162 );
2163 }
2164 self.o1_cfg = cfg;
2165 }
2166
2167 pub fn o1_active(&self) -> bool {
2169 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2170 }
2171
2172 pub fn generation_graph_prefill(&self) -> bool {
2177 let graph = self.graph_prefill_preferred();
2178 #[cfg(not(target_os = "macos"))]
2186 if graph
2187 && self.mtp.is_some()
2188 && std::env::var("CMF_BATCH_K")
2189 .ok()
2190 .and_then(|v| v.parse::<usize>().ok())
2191 .is_some_and(|k| k > 0)
2192 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2193 {
2194 return false;
2195 }
2196 graph
2197 }
2198
2199 pub fn o1_device_stats(&self) -> (usize, u64) {
2204 crate::gpu::o1_device_stats(self.graph_kv_id)
2205 }
2206
2207 pub fn o1_begin(&mut self) {
2212 self.o1_begin_with_prefix(None);
2213 }
2214
2215 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2219 if let Some(c) = &self.o1_cfg {
2220 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2221 let boundary = requested_prefix.map(|p| {
2222 p.max(
2223 crate::nystrom::o1_deferred_boundary(w, sink)
2224 .expect("o1 config boundary validated in set_o1"),
2225 )
2226 });
2227 for (li, &f) in self.o1_flags.iter().enumerate() {
2228 if f {
2229 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2230 }
2231 }
2232 }
2233 }
2234
2235 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2237 self.o1_cfg.as_ref().and_then(|c| {
2238 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2239 .map(|floor| requested_prefix.max(floor))
2240 })
2241 }
2242
2243 fn o1_note_transition(&mut self) {
2244 let mut transitioned = false;
2248 for (li, &flagged) in self.o1_flags.iter().enumerate() {
2249 if flagged {
2250 transitioned |= self.kv_cache.layers[li].take_o1_transition();
2251 }
2252 }
2253 if transitioned {
2254 self.o1_epoch = self.o1_epoch.wrapping_add(1);
2255 }
2256 }
2257
2258 fn o1_pending(&self) -> bool {
2259 self.o1_flags.iter().enumerate().any(|(li, &f)| {
2260 f && self.kv_cache.layers[li].seq_len > 0
2261 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2262 })
2263 }
2264
2265 fn o1_fail(&mut self, err: String) {
2266 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2267 self.clear_sequence_state();
2268 self.graph_failed
2269 .store(true, std::sync::atomic::Ordering::Relaxed);
2270 self.cancel
2271 .store(true, std::sync::atomic::Ordering::Relaxed);
2272 }
2273
2274 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2279 if self.o1_cfg.is_none() {
2280 return Ok(false);
2281 }
2282 let mut participating = false;
2283 for li in 0..self.num_layers {
2284 if !self.o1_flags.get(li).copied().unwrap_or(false) {
2285 continue;
2286 }
2287 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2288 return Err(err);
2289 }
2290 if self.kv_cache.layers[li].seq_len == 0 {
2291 continue;
2292 }
2293 participating = true;
2294 let num_heads = self.layer_num_heads(li);
2295 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2296 }
2297 self.o1_note_transition();
2298 for li in 0..self.num_layers {
2299 if self.o1_flags.get(li).copied().unwrap_or(false) {
2300 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2301 return Err(err);
2302 }
2303 }
2304 }
2305 Ok(participating
2306 && (0..self.num_layers).all(|li| {
2307 !self.o1_flags.get(li).copied().unwrap_or(false)
2308 || self.kv_cache.layers[li].seq_len == 0
2309 || self.kv_cache.layers[li].o1_sealed()
2310 }))
2311 }
2312
2313 fn o1_progress(&mut self) {
2316 if !self.o1_active() {
2317 return;
2318 }
2319 for li in 0..self.num_layers {
2320 if self.o1_flags.get(li).copied().unwrap_or(false) {
2321 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2322 self.o1_fail(err);
2323 return;
2324 }
2325 }
2326 }
2327 self.o1_note_transition();
2331 if !self.o1_pending() {
2332 return;
2333 }
2334 if let Err(err) = self.o1_seal_checked() {
2335 self.o1_fail(err);
2336 }
2337 }
2338
2339 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2344 if self
2345 .graph_failed
2346 .swap(false, std::sync::atomic::Ordering::Relaxed)
2347 {
2348 self.cancel
2349 .store(false, std::sync::atomic::Ordering::Relaxed);
2350 self.clear_sequence_state();
2351 return Err(format!("{phase}: deferred O(1) transition failed"));
2352 }
2353 Ok(())
2354 }
2355
2356 pub fn o1_seal(&mut self) {
2360 if let Err(err) = self.o1_seal_checked() {
2361 self.o1_fail(err);
2362 }
2363 }
2364
2365 pub fn set_trace(&mut self, on: bool) {
2367 self.trace = on;
2368 }
2369
2370 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2373 self.rng = match config.seed {
2374 Some(seed) => SplitMix64::new(seed),
2375 None => SplitMix64::from_entropy(),
2376 };
2377 self.sampler_config = config;
2378 }
2379
2380 pub fn set_confidence(&mut self, on: bool) {
2385 self.confidence_on = on;
2386 }
2387
2388 pub fn set_calib_temp(&mut self, t: f32) {
2391 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2392 }
2393
2394 pub fn calib_temp(&self) -> f32 {
2396 self.calib_temp
2397 }
2398
2399 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2402 self.rotary_dim = rotary_dim.min(self.head_dim);
2403 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2404 }
2405
2406 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2407 QwenAttnCfg {
2408 num_heads: self.num_heads,
2409 num_kv_heads: self.num_kv_heads,
2410 head_dim: self.head_dim,
2411 hidden_size: self.hidden_size,
2412 position,
2413 inv_freq: &self.inv_freq,
2414 rotary_dim: self.rotary_dim,
2415 scale: self.attn_scale,
2416 softcap: self.attn_softcap,
2417 window: None,
2418 v_norm: false,
2419 q_norm: None,
2420 k_norm: None,
2421 output_gate: false,
2422 softplus_gate: None,
2423 rope_scale: self.rope_scale,
2424 bias: None,
2425 rms_eps: self.rms_eps,
2426 norm_style: self.norm_style,
2427 pool: self.pool.as_deref(),
2428 }
2429 }
2430
2431 pub fn generate(
2433 &mut self,
2434 prompt: &str,
2435 max_tokens: usize,
2436 task_mask: Option<&TaskMask>,
2437 on_token: Option<TokenCallback>,
2438 ) -> Result<GenerateResult, String> {
2439 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2440 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2441 }
2442
2443 pub fn generate_from_vl(
2446 &mut self,
2447 input: &crate::dsv41_vision::PreparedVlInputs,
2448 max_tokens: usize,
2449 task_mask: Option<&TaskMask>,
2450 on_token: Option<TokenCallback>,
2451 ) -> Result<GenerateResult, String> {
2452 let Some(dsv41) = &self.dsv41 else {
2453 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
2454 };
2455 if input.token_ids.is_empty() {
2456 return Err("empty V4.1 multimodal prompt".into());
2457 }
2458 if input.token_types.len() != input.token_ids.len() {
2459 return Err(format!(
2460 "V4.1 token type count {} != token count {}",
2461 input.token_types.len(),
2462 input.token_ids.len()
2463 ));
2464 }
2465 let dim = dsv41.2.dim;
2466 let mut embeddings = vec![None; input.token_ids.len()];
2467 let mut participates = vec![true; input.token_ids.len()];
2468 if !input.images.is_empty() {
2469 let vision = self
2470 .dsv41_vision
2471 .as_ref()
2472 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
2473 for image in &input.images {
2474 let end = image.start.saturating_add(image.types.len());
2475 if end > input.token_ids.len() {
2476 return Err(format!(
2477 "V4.1 image span {}..{} exceeds prompt length {}",
2478 image.start,
2479 end,
2480 input.token_ids.len()
2481 ));
2482 }
2483 let mut span = vec![0.0f32; image.types.len() * dim];
2484 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
2485 for (offset, &kind) in image.types.iter().enumerate() {
2486 let pos = image.start + offset;
2487 if input.token_types[pos] != kind {
2488 return Err(format!(
2489 "V4.1 image type mismatch at position {pos}: {} != {kind}",
2490 input.token_types[pos]
2491 ));
2492 }
2493 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
2494 participates[pos] = false;
2495 }
2496 }
2497 }
2498 for (pos, &kind) in input.token_types.iter().enumerate() {
2499 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
2500 return Err(format!("V4.1 text position {pos} has an image embedding"));
2501 }
2502 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
2503 return Err(format!("V4.1 image position {pos} has no image embedding"));
2504 }
2505 }
2506 self.dsv41_prefill = Some((embeddings, participates));
2507 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
2508 self.dsv41_prefill = None;
2509 result
2510 }
2511
2512 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2514 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2515 }
2516
2517 pub fn generate_from_ids(
2525 &mut self,
2526 input_ids: &[u32],
2527 max_tokens: usize,
2528 task_mask: Option<&TaskMask>,
2529 mut on_token: Option<TokenCallback>,
2530 ) -> Result<GenerateResult, String> {
2531 if std::env::var("CMF_TRACE_H").is_ok() {
2532 eprintln!("input_ids: {input_ids:?}");
2533 }
2534 if input_ids.is_empty() {
2535 return Err("empty prompt: nothing to generate from".to_string());
2536 }
2537 self.graph_failed
2541 .store(false, std::sync::atomic::Ordering::Relaxed);
2542 let task_mask = self.drop_open_mask(task_mask);
2547
2548 let reuse_from = {
2556 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2557 let h = &self.kv_history;
2558 if on
2559 && task_mask.is_none()
2560 && self.mtp.is_none()
2561 && self.o1_cfg.is_none()
2562 && self.dsv41.is_none()
2563 && !h.is_empty()
2564 && h.len() < input_ids.len()
2565 && input_ids[..h.len()] == h[..]
2566 {
2567 h.len()
2568 } else {
2569 0
2570 }
2571 };
2572 if reuse_from == 0 {
2573 self.clear_sequence_state();
2575 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2576 eprintln!(
2577 "kv-reuse: {} of {} prompt positions already cached",
2578 reuse_from,
2579 input_ids.len()
2580 );
2581 }
2582 crate::gpu::graph_race_begin_generation();
2583 let o1_prefill = if self.o1_active() && task_mask.is_none() {
2587 std::env::var("CMF_O1_PREFILL")
2588 .ok()
2589 .and_then(|v| v.parse::<usize>().ok())
2590 .filter(|&p| p > 0)
2591 } else {
2592 None
2593 };
2594 if task_mask.is_none() {
2595 self.o1_begin_with_prefix(o1_prefill);
2596 }
2597
2598 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2604 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2631 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2632 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2648 for lw in &self.weights.layers {
2649 if let FfnKind::Dense(d) = &lw.ffn {
2650 dense_n += 1;
2651 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2652 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2653 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2654 {
2655 dense_q4tp += 1;
2656 }
2657 }
2658 }
2659 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2660 let penalized = self.sampler_config.repetition_penalty != 1.0
2664 || self.sampler_config.presence_penalty != 0.0
2665 || !self.sampler_config.suppress_tokens.is_empty();
2666 #[cfg(feature = "gpu")]
2671 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2672 #[cfg(not(feature = "gpu"))]
2673 let metal_wgpu = false;
2674 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2675 let spec_wanted = match spec_env.as_deref() {
2676 Some("0") => false,
2677 Some(_) => {
2678 if metal_wgpu {
2679 tracing::warn!(
2680 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2681 verified on this backend (garbage measured on Qwen3.5-0.8B)"
2682 );
2683 }
2684 true
2685 }
2686 None => spec_default_ok && !penalized && !metal_wgpu,
2687 };
2688 #[cfg(target_os = "macos")]
2691 let metal_graph = crate::gpu::q1_force()
2692 && crate::gpu::enabled_here()
2693 && std::env::var("CMF_GPU_BLOCK")
2694 .map(|v| v != "0")
2695 .unwrap_or(true);
2696 #[cfg(not(target_os = "macos"))]
2697 let metal_graph = false;
2698 let graph_spec = self.speculative
2699 && (graph_on || metal_graph)
2700 && self.mtp.is_some()
2701 && task_mask.is_none()
2702 && !self.o1_active()
2703 && spec_sampling_ok
2704 && spec_wanted;
2705 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2712 let spec_active = self.speculative
2713 && self.mtp.is_some()
2714 && task_mask.is_none()
2715 && !self.o1_active()
2716 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2717 let mut mtp = if spec_active { self.mtp.take() } else { None };
2720 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2721 eprintln!(
2722 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2723 mtp.is_some(),
2724 self.speculative,
2725 self.sampler_config.temperature < 1e-6,
2726 );
2727 }
2728 if let Some(m) = &mut mtp {
2729 m.kv.clear();
2730 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2732 self.mtp_graph_mode = None;
2733 }
2734 let mut router = if mtp.is_none() {
2738 self.dyn_router.take()
2739 } else {
2740 None
2741 };
2742 if let Some(r) = &mut router {
2743 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2746 }
2747
2748 let mut all_ids = input_ids.to_vec();
2749 let mut generated = 0usize;
2750 let mut finish_reason = "max_tokens".to_string();
2751 let mut drafted = 0usize;
2752 let mut accepted = 0usize;
2753 let mut dsv4_spec_bad = 0usize;
2760 let mut dsv4_spec_retry_at = 0usize;
2761 let mut confidence: Vec<f32> = Vec::new();
2762 let trace_on = self.trace;
2763 let calib_temp = self.calib_temp;
2764 let mut traces: Vec<TokenTrace> = Vec::new();
2765
2766 let mut hidden = vec![0.0f32; self.hidden_size];
2772 let mut pos = reuse_from;
2773 let fuse_lm = mtp.is_none()
2782 && router.is_none()
2783 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2784 self.graph_logits = None;
2785 self.graph_want_logits = false;
2786 let _tpf = std::time::Instant::now();
2787 let batch_k = std::env::var("CMF_BATCH_K")
2788 .ok()
2789 .and_then(|v| v.parse::<usize>().ok())
2790 .unwrap_or(0);
2791 while self.qwen4_exp.is_some()
2802 && mtp.is_none()
2803 && pos < input_ids.len()
2804 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2805 {
2806 let token_id = input_ids[pos];
2807 let want_logits = pos + 1 == input_ids.len();
2808 let mut lg = Vec::new();
2809 if let Some(b) = &mut self.qwen4_exp {
2810 crate::qwen4_exp::forward_token(
2811 &b.0,
2812 &b.1,
2813 &b.2,
2814 &mut b.3,
2815 token_id,
2816 pos,
2817 &self.inv_freq,
2818 self.pool.as_deref(),
2819 &mut lg,
2820 want_logits,
2821 );
2822 }
2823 if want_logits {
2824 self.graph_logits = Some(lg);
2825 }
2826 pos += 1;
2827 hidden.fill(0.0);
2828 }
2829 while self.dsv4.is_some()
2830 && mtp.is_none()
2831 && pos < input_ids.len()
2832 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2833 {
2834 let end = (pos + prefill_chunk()).min(input_ids.len());
2835 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2836 let mut lg = Vec::new();
2837 if let Some(b) = &mut self.dsv4 {
2838 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2839 crate::dsv4::forward_chunk(
2840 g,
2841 layers,
2842 &cfg,
2843 st,
2844 &ids,
2845 pos,
2846 &self.inv_freq,
2847 self.pool.as_deref(),
2848 &mut lg,
2849 end == input_ids.len(),
2850 );
2851 }
2852 if end == input_ids.len() {
2853 self.graph_logits = Some(lg);
2854 }
2855 pos = end;
2856 hidden = vec![0.0; self.hidden_size];
2857 }
2858 let dsv41_prefill = self.dsv41_prefill.take();
2859 while self.dsv41.is_some()
2860 && mtp.is_none()
2861 && pos < input_ids.len()
2862 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2863 {
2864 let end = (pos + prefill_chunk()).min(input_ids.len());
2865 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2866 let mut lg = Vec::new();
2867 if let Some(b) = &mut self.dsv41 {
2868 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
2869 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
2870 crate::dsv41::forward_chunk_masked_with_embeddings(
2871 g,
2872 layers,
2873 cfg,
2874 st,
2875 &ids,
2876 pos,
2877 &embeddings[pos..end],
2878 &participates[pos..end],
2879 self.pool.as_deref(),
2880 &mut lg,
2881 );
2882 } else {
2883 crate::dsv41::forward_chunk(
2884 g,
2885 layers,
2886 cfg,
2887 st,
2888 &ids,
2889 pos,
2890 self.pool.as_deref(),
2891 &mut lg,
2892 );
2893 }
2894 }
2895 if end == input_ids.len() {
2896 self.graph_logits = Some(lg);
2897 }
2898 pos = end;
2899 hidden = vec![0.0; self.hidden_size];
2900 }
2901 let dyn_prefill = router.is_some();
2906 let o1_prefill_limit = o1_prefill
2914 .and_then(|requested| self.o1_effective_boundary(requested))
2915 .map(|boundary| boundary.min(input_ids.len()));
2916 let mut o1_sealed = false;
2917 if let Some(limit) = o1_prefill_limit {
2918 if self.can_prefill_batched() && limit > 2 {
2921 let chunk = prefill_chunk();
2922 let hs = self.hidden_size;
2923 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2924 let end = (pos + chunk).min(limit);
2925 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2926 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2927 pos = end;
2928 }
2929 } else {
2930 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2931 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
2932 pos += 1;
2933 }
2934 }
2935 if pos >= limit {
2936 o1_sealed = match self.o1_seal_checked() {
2937 Ok(sealed) => sealed,
2938 Err(err) => {
2939 self.finish_generation(&mut mtp, &mut router, true);
2940 return Err(err);
2941 }
2942 };
2943 tracing::info!(
2944 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
2945 o1_prefill.unwrap_or(0),
2946 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
2947 .unwrap_or(limit),
2948 limit,
2949 input_ids.len()
2950 );
2951 }
2952 }
2953 let graph_prefill = self.graph_prefill_preferred();
2959 #[cfg(target_os = "macos")]
2967 if task_mask.is_none()
2968 && !dyn_prefill
2969 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
2970 && crate::gpu::enabled_here()
2971 && self.gdn_cfg.is_some()
2972 && self.g3n.is_none()
2973 && input_ids.len() > 8
2974 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2975 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2976 {
2977 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2978 .ok()
2979 .and_then(|v| v.parse().ok())
2980 .filter(|&v| (16..=512).contains(&v))
2981 .unwrap_or(256);
2982 let hs = self.hidden_size;
2983 let _tp = std::time::Instant::now();
2984 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2985 let end = (pos + chunk).min(input_ids.len());
2986 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
2987 MetalPrefillOutcome::Completed(hb) => hb,
2988 MetalPrefillOutcome::Declined => break,
2989 MetalPrefillOutcome::Failed => {
2990 self.finish_generation(&mut mtp, &mut router, true);
2991 return Err("ordinary Metal prefill failed after admission".into());
2992 }
2993 };
2994 if let Some(m) = &mut mtp {
2995 let n_pairs = if end < input_ids.len() {
2996 end - pos
2997 } else {
2998 end - pos - 1
2999 };
3000 if n_pairs > 0 {
3001 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
3002 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
3003 .collect();
3004 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
3005 for (j, (h, t)) in pairs.iter().enumerate() {
3006 let h = h.to_vec();
3007 let _ = self.mtp_step(m, &h, *t, pos + j);
3008 }
3009 }
3010 }
3011 }
3012 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3013 pos = end;
3014 }
3015 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3016 eprintln!(
3017 "metal-prefill: {} of {} tokens in {:.1} ms",
3018 pos,
3019 input_ids.len(),
3020 _tp.elapsed().as_secs_f64() * 1e3
3021 );
3022 }
3023 }
3024 if task_mask.is_none()
3025 && !dyn_prefill
3026 && !graph_prefill
3027 && self.can_prefill_batched()
3028 && self.g3n.is_none()
3029 && o1_prefill.is_none()
3030 && input_ids.len() > 2
3031 {
3032 let chunk = prefill_chunk();
3038 let hs = self.hidden_size;
3039 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3040 let end = (pos + chunk).min(input_ids.len());
3041 let hb = self.prefill_batch(&input_ids[pos..end], pos);
3042 if let Some(m) = &mut mtp {
3043 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3044 .ok()
3045 .and_then(|v| v.parse().ok())
3046 .unwrap_or(0);
3047 for p in pos..end {
3048 if p + 1 < input_ids.len() {
3049 if probe >= 1 && p + 2 < input_ids.len() {
3050 let (d1, mut hx) = self.mtp_step_h(
3054 m,
3055 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3056 input_ids[p + 1],
3057 p,
3058 );
3059 let mut ok = d1 == input_ids[p + 2];
3060 Self::chain_probe_note(0, ok);
3061 let mut d_prev = d1;
3062 let mut extra = 0usize;
3063 for j in 1..probe {
3064 if p + 2 + j >= input_ids.len() {
3065 break;
3066 }
3067 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
3068 extra += 1;
3069 ok = ok && dj == input_ids[p + 2 + j];
3070 Self::chain_probe_note(j, ok);
3071 d_prev = dj;
3072 hx = hj;
3073 }
3074 m.kv.truncate_last(extra);
3075 } else {
3076 let _ = self.mtp_step(
3077 m,
3078 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3079 input_ids[p + 1],
3080 p,
3081 );
3082 }
3083 }
3084 }
3085 }
3086 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3087 pos = end;
3088 }
3089 }
3090 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
3091 if task_mask.is_none()
3092 && !dyn_prefill
3093 && !graph_prefill
3094 && !pair_off
3095 && self.pair_supported()
3096 && o1_prefill.is_none()
3097 {
3098 while pos + 1 < input_ids.len()
3099 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3100 {
3101 let e1 = self.embed_single(input_ids[pos]);
3102 let e2 = self.embed_single(input_ids[pos + 1]);
3103 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
3104 self.commit_linear_scratch();
3106 if let Some(m) = &mut mtp {
3107 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
3108 if pos + 2 < input_ids.len() {
3109 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3110 .ok()
3111 .and_then(|v| v.parse().ok())
3112 .unwrap_or(0);
3113 if probe >= 1 && pos + 3 < input_ids.len() {
3114 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
3118 let mut ok = d1 == input_ids[pos + 3];
3119 Self::chain_probe_note(0, ok);
3120 let mut d_prev = d1;
3121 let mut extra = 0usize;
3122 for j in 1..probe {
3123 if pos + 3 + j >= input_ids.len() {
3124 break;
3125 }
3126 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
3127 extra += 1;
3128 ok = ok && dj == input_ids[pos + 3 + j];
3129 Self::chain_probe_note(j, ok);
3130 d_prev = dj;
3131 hx = hj;
3132 }
3133 m.kv.truncate_last(extra);
3134 } else {
3135 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
3136 }
3137 }
3138 }
3139 hidden = h2;
3140 pos += 2;
3141 }
3142 }
3143 let o1_batch_ready = o1_sealed
3156 && o1_prefill.is_some()
3157 && mtp.is_none()
3158 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
3159 && (0..self.num_layers).all(|li| {
3160 let cache = &self.kv_cache.layers[self.phys_layer(li)];
3161 cache.o1.is_none() || cache.o1_views().is_some()
3162 });
3163 let mtp_batch_prefill = mtp.is_some()
3168 && graph_prefill
3169 && task_mask.is_none()
3170 && !dyn_prefill
3171 && !self.o1_active()
3172 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
3173 if batch_k > 0
3174 && (graph_prefill || o1_batch_ready)
3175 && task_mask.is_none()
3176 && (!self.o1_active() || o1_batch_ready)
3177 && (mtp.is_none() || mtp_batch_prefill)
3178 && !dyn_prefill
3179 && pos + 1 < input_ids.len()
3180 {
3181 let hs = self.hidden_size;
3182 let chunk = batch_k;
3183 while pos < input_ids.len() {
3184 let end = (pos + chunk).min(input_ids.len());
3185 let bk = end - pos;
3186 let mut hiddens = vec![0f32; bk * hs];
3187 for (j, &id) in input_ids[pos..end].iter().enumerate() {
3188 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3189 }
3190 let positions: Vec<usize> = (pos..end).collect();
3191 let t_chunk = std::time::Instant::now();
3192 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3193 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
3194 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3195 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3196 eprintln!(
3197 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3198 if o1_batch_ready {
3199 "o1"
3200 } else if mtp_batch_prefill {
3201 "ordinary_mtp"
3202 } else {
3203 "ordinary"
3204 },
3205 bk as f64 / (ms / 1000.0)
3206 );
3207 }
3208 {
3209 use std::sync::atomic::{AtomicBool, Ordering};
3210 static SAID: AtomicBool = AtomicBool::new(false);
3211 if !SAID.swap(true, Ordering::Relaxed) {
3212 if ok_b {
3213 tracing::info!(
3214 "batched prefill: ACTIVE mode={} (k={bk})",
3215 if o1_batch_ready {
3216 "o1"
3217 } else if mtp_batch_prefill {
3218 "ordinary_mtp"
3219 } else {
3220 "ordinary"
3221 }
3222 );
3223 } else {
3224 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
3225 }
3226 }
3227 }
3228 if ok_b {
3229 if mtp_batch_prefill {
3230 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
3231 if n_pairs > 0 {
3232 let rows: Vec<Vec<f32>> = (0..n_pairs)
3238 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
3239 .collect();
3240 let pairs: Vec<(&[f32], u32)> = rows
3241 .iter()
3242 .enumerate()
3243 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
3244 .collect();
3245 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3246 eprintln!(
3247 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
3248 pos,
3249 n_pairs,
3250 pos + n_pairs - 1,
3251 );
3252 }
3253 let warm_error = if let Some(m) = mtp.as_mut() {
3254 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
3255 } else {
3256 None
3257 };
3258 if let Some(err) = warm_error {
3259 self.finish_generation(&mut mtp, &mut router, true);
3264 return Err(err.to_string());
3265 }
3266 }
3267 }
3268 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3269 pos = end;
3270 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3271 self.finish_generation(&mut mtp, &mut router, true);
3276 return Err(if o1_batch_ready {
3277 "sealed O(1) batch graph failed after admission".to_string()
3278 } else {
3279 "ordinary recurrent batch graph failed after admission".to_string()
3280 });
3281 } else {
3282 break; }
3284 }
3285 }
3286 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3287 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3288 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3289 if let Some(m) = &mut mtp {
3290 if pos + 1 < input_ids.len() {
3291 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3297 .ok()
3298 .and_then(|v| v.parse().ok())
3299 .unwrap_or(0);
3300 if probe >= 1 && pos + 2 < input_ids.len() {
3301 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3302 let mut ok = d1 == input_ids[pos + 2];
3303 Self::chain_probe_note(0, ok);
3304 let mut d_prev = d1;
3305 let mut extra = 0usize;
3306 for j in 1..probe {
3307 if pos + 2 + j >= input_ids.len() {
3308 break;
3309 }
3310 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3311 extra += 1;
3312 ok = ok && dj == input_ids[pos + 2 + j];
3313 Self::chain_probe_note(j, ok);
3314 d_prev = dj;
3315 hx = hj;
3316 }
3317 m.kv.truncate_last(extra);
3320 } else {
3321 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3322 }
3323 }
3324 }
3325 pos += 1;
3326 }
3327 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3328 eprintln!(
3329 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3330 input_ids.len(),
3331 _tpf.elapsed().as_secs_f64() * 1000.0
3332 );
3333 }
3334 if self
3335 .graph_failed
3336 .swap(false, std::sync::atomic::Ordering::Relaxed)
3337 {
3338 self.finish_generation(&mut mtp, &mut router, true);
3343 return Err("GPU token graph failed during prefill".to_string());
3344 }
3345 if self
3348 .cancel
3349 .swap(false, std::sync::atomic::Ordering::Relaxed)
3350 {
3351 self.finish_generation(&mut mtp, &mut router, true);
3355 return Ok(GenerateResult {
3356 text: String::new(),
3357 token_ids: Vec::new(),
3358 prompt_tokens: input_ids.len(),
3359 tokens_generated: 0,
3360 finish_reason: "cancelled".to_string(),
3361 mtp_drafted: 0,
3362 mtp_accepted: 0,
3363 token_confidence: Vec::new(),
3364 traces: Vec::new(),
3365 });
3366 }
3367
3368 if !o1_sealed {
3371 match self.o1_seal_checked() {
3372 Ok(_) => {}
3373 Err(err) => {
3374 self.finish_generation(&mut mtp, &mut router, true);
3375 return Err(err);
3376 }
3377 }
3378 }
3379
3380 macro_rules! commit {
3382 ($id:expr) => {{
3383 all_ids.push($id);
3384 generated += 1;
3385 if self.tokenizer.is_eos($id) {
3386 finish_reason = "stop".to_string();
3387 false
3388 } else {
3389 let token_text = self.tokenizer.decode_token($id);
3390 let mut go = true;
3391 if let Some(ref mut cb) = on_token {
3392 if !cb(&token_text) {
3393 finish_reason = "cancelled".to_string();
3394 go = false;
3395 }
3396 }
3397 go
3398 }
3399 }};
3400 }
3401
3402 let mut spec_trial = SpecTrial::Spec {
3413 t0: std::time::Instant::now(),
3414 gen0: generated,
3415 rounds: 0,
3416 };
3417 let mut spec_mon = SpecMon::default();
3418 let mut spec_watchdog_off = false;
3419 let mut next_pos = input_ids.len();
3421 'decode: while generated < max_tokens {
3422 if self
3423 .graph_failed
3424 .swap(false, std::sync::atomic::Ordering::Relaxed)
3425 {
3426 self.finish_generation(&mut mtp, &mut router, true);
3431 return Err("GPU token graph failed during decode".to_string());
3432 }
3433 if self
3434 .cancel
3435 .swap(false, std::sync::atomic::Ordering::Relaxed)
3436 {
3437 finish_reason = "cancelled".to_string();
3438 break 'decode;
3439 }
3440 let forced = self.spec_forced.take();
3445 let mut logits = match (forced, self.graph_logits.take()) {
3446 (Some(_), _) => Vec::new(),
3447 (None, Some(lg)) => lg,
3448 (None, None) => {
3449 inference::rms_norm_into(
3450 &hidden,
3451 &self.weights.final_norm,
3452 self.rms_eps,
3453 self.norm_style,
3454 &mut self.ws.n1,
3455 );
3456 self.lm_head_forward(&self.ws.n1)
3457 }
3458 };
3459 if generated
3462 == std::env::var("CMF_LOGIT_DUMP_STEP")
3463 .ok()
3464 .and_then(|v| v.parse().ok())
3465 .unwrap_or(0)
3466 {
3467 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3468 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3469 for v in hidden.iter().chain(logits.iter()) {
3470 bytes.extend_from_slice(&v.to_le_bytes());
3471 }
3472 if let Err(e) = std::fs::write(&path, &bytes) {
3473 eprintln!("logit dump: failed to write {path}: {e}");
3474 self.finish_generation(&mut mtp, &mut router, true);
3475 return Err(format!("logit dump write failed: {e}"));
3476 }
3477 }
3478 }
3479 let t_next = match forced {
3480 Some(c) => c,
3481 None => sampler::sample_with_scratch_pool(
3482 &logits,
3483 &self.sampler_config,
3484 &all_ids,
3485 &mut self.rng,
3486 &mut self.sampler_scratch,
3487 self.pool.as_deref(),
3488 ),
3489 };
3490 if self.confidence_on {
3491 confidence.push(if logits.is_empty() {
3492 0.0
3493 } else {
3494 sampler::top1_prob_pool(
3495 self.pool.as_deref(),
3496 &mut self.sampler_scratch,
3497 &logits,
3498 t_next,
3499 calib_temp,
3500 )
3501 });
3502 }
3503 if !logits.is_empty() {
3504 attention::recycle_buf(&mut logits);
3505 }
3506 if trace_on {
3507 let skill = router.as_ref().and_then(|r| r.active_id());
3511 traces.push(TokenTrace {
3512 t: generated,
3513 token_id: t_next,
3514 confidence: confidence.last().copied().unwrap_or(0.0),
3515 active_skill: skill,
3516 recon: None,
3517 switched: false,
3518 });
3519 }
3520 if !commit!(t_next) {
3521 break 'decode;
3522 }
3523 if generated >= max_tokens {
3524 break 'decode;
3525 }
3526
3527 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
3528 static SAID: std::sync::Once = std::sync::Once::new();
3534 SAID.call_once(|| {
3535 tracing::warn!(
3536 "KV cache full at {} positions — evicting half; quality \
3537 will degrade. Raise CMF_MAX_SEQ.",
3538 self.kv_cache.max_seq_len,
3539 );
3540 });
3541 let keep = (self.kv_cache.max_seq_len / 2).max(1);
3542 self.kv_cache.evict(keep);
3543 }
3544
3545 if graph_spec {
3548 match spec_trial {
3549 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
3550 spec_mon.plain_ms =
3551 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3552 let keep = spec_mon.pays();
3553 tracing::info!(
3554 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3555 spec_mon.tokens,
3556 spec_mon.round_ms,
3557 spec_mon.plain_ms,
3558 if keep { "speculating" } else { "plain" }
3559 );
3560 spec_mon.fails = 0;
3561 spec_trial = SpecTrial::Decided {
3562 spec: keep,
3563 recheck_at: if keep { usize::MAX } else { generated + 128 },
3564 };
3565 }
3566 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3567 spec_mon.n = 0;
3568 spec_trial = SpecTrial::Spec {
3569 t0: std::time::Instant::now(),
3570 gen0: generated,
3571 rounds: 0,
3572 };
3573 }
3574 _ => {}
3575 }
3576 spec_watchdog_off = matches!(
3577 spec_trial,
3578 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3579 );
3580 }
3581 match &mut mtp {
3582 #[cfg(feature = "gpu")]
3584 Some(m)
3585 if graph_spec
3586 && !spec_watchdog_off
3587 && generated + 1 < max_tokens
3588 && next_pos > 0 =>
3589 {
3590 let t_round = std::time::Instant::now();
3591 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3592 m,
3593 &hidden,
3594 t_next,
3595 next_pos,
3596 &mut drafted,
3597 &mut accepted,
3598 &mut all_ids,
3599 ) {
3600 next_pos = n_pos;
3601 hidden = new_h;
3602 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3603 eprintln!(
3604 "spec-round wall {:.1} ms → {} tokens",
3605 t_round.elapsed().as_secs_f64() * 1e3,
3606 extra.len() + 1
3607 );
3608 }
3609 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3613 spec_trial = Self::spec_trial_round(
3616 spec_trial,
3617 &mut spec_mon,
3618 generated + extra.len() + 1,
3619 );
3620 let mut stopped = false;
3621 for &id in &extra {
3622 if self.confidence_on {
3623 confidence.push(0.0);
3624 }
3625 if !commit!(id) {
3626 stopped = true;
3627 break;
3628 }
3629 }
3630 if stopped {
3631 break 'decode;
3632 }
3633 continue 'decode;
3634 }
3635 if self
3636 .graph_failed
3637 .swap(false, std::sync::atomic::Ordering::Relaxed)
3638 {
3639 self.finish_generation(&mut mtp, &mut router, true);
3645 return Err("GPU MTP graph failed during speculative decode".to_string());
3646 }
3647 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
3658 spec_mon.tokens = 0.0;
3659 spec_mon.fails = 3;
3660 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
3661 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
3662 next_pos += 1;
3663 continue 'decode;
3664 }
3665 Some(m) if !graph_spec && generated + 1 < max_tokens => {
3667 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
3668 drafted += 1;
3669 let emb1 = self.embed_single(t_next);
3670 let emb2 = self.embed_single(draft);
3671 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
3672
3673 inference::rms_norm_into(
3674 &h1,
3675 &self.weights.final_norm,
3676 self.rms_eps,
3677 self.norm_style,
3678 &mut self.ws.n1,
3679 );
3680 let mut logits1 = self.lm_head_forward(&self.ws.n1);
3681 let t_after = sampler::sample_with_scratch_pool(
3682 &logits1,
3683 &self.sampler_config,
3684 &all_ids,
3685 &mut self.rng,
3686 &mut self.sampler_scratch,
3687 self.pool.as_deref(),
3688 );
3689 if self.confidence_on {
3690 confidence.push(sampler::top1_prob_pool(
3691 self.pool.as_deref(),
3692 &mut self.sampler_scratch,
3693 &logits1,
3694 t_after,
3695 calib_temp,
3696 ));
3697 }
3698 attention::recycle_buf(&mut logits1);
3699 if trace_on {
3700 traces.push(TokenTrace {
3703 t: generated,
3704 token_id: t_after,
3705 confidence: confidence.last().copied().unwrap_or(0.0),
3706 active_skill: None,
3707 recon: None,
3708 switched: false,
3709 });
3710 }
3711 let stop = !commit!(t_after);
3712
3713 if t_after == draft {
3714 accepted += 1;
3715 self.commit_linear_scratch();
3716 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3717 hidden = h2;
3718 next_pos += 2;
3719 } else {
3720 for layer in &mut self.kv_cache.layers {
3722 layer.truncate_last(1);
3723 }
3724 if !stop {
3725 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3726 hidden = self.forward_layers(
3727 &self.embed_single(t_after),
3728 next_pos + 1,
3729 None,
3730 );
3731 }
3732 next_pos += 2;
3733 }
3734 if stop {
3735 break 'decode;
3736 }
3737 }
3738 _ => {
3740 #[cfg(feature = "gpu")]
3745 if Self::dsv4_spec_on() && self.dsv4.is_some() {
3746 static SAID: std::sync::Once = std::sync::Once::new();
3747 SAID.call_once(|| {
3748 eprintln!(
3749 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
3750 !self.dsv4_mtp.is_empty(),
3751 task_mask.is_none(),
3752 router.is_none(),
3753 !trace_on,
3754 self.sampler_config.temperature < 1e-6,
3755 self.sampler_config.repetition_penalty == 1.0,
3756 );
3757 });
3758 }
3759 #[cfg(feature = "gpu")]
3760 if Self::dsv4_spec_on()
3761 && self.dsv4.is_some()
3762 && !self.dsv4_mtp.is_empty()
3763 && task_mask.is_none()
3764 && router.is_none()
3765 && !trace_on
3766 && self.sampler_config.temperature < 1e-6
3767 && self.sampler_config.repetition_penalty == 1.0
3768 && generated + 1 < max_tokens
3769 && all_ids.len() >= 2
3770 && generated >= dsv4_spec_retry_at
3771 {
3772 let tip_token = all_ids[all_ids.len() - 2];
3773 let drafted0 = drafted;
3774 let round = self.dsv4_spec_step(
3775 tip_token,
3776 t_next,
3777 next_pos,
3778 max_tokens.saturating_sub(generated),
3779 &mut drafted,
3780 &mut accepted,
3781 );
3782 if drafted > drafted0 {
3783 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
3784 if useful {
3785 dsv4_spec_bad = 0;
3786 } else {
3787 dsv4_spec_bad += 1;
3788 if dsv4_spec_bad >= 2 {
3789 dsv4_spec_bad = 0;
3790 dsv4_spec_retry_at = generated.saturating_add(32);
3791 tracing::info!(
3792 "dsv4: draft не окупился дважды — точный walk на 32 токена"
3793 );
3794 }
3795 }
3796 }
3797 if let Some((extra, n_pos)) = round {
3798 next_pos = n_pos;
3799 let mut stopped = false;
3800 for &id in &extra {
3801 if self.confidence_on {
3802 confidence.push(0.0);
3803 }
3804 if !commit!(id) {
3805 stopped = true;
3806 break;
3807 }
3808 }
3809 if stopped {
3810 break 'decode;
3811 }
3812 continue 'decode;
3813 }
3814 }
3815 self.graph_want_logits = fuse_lm;
3816 let mut t_fwd = t_next;
3822 let pure_greedy = self.sampler_config.temperature < 1e-6
3823 && self.sampler_config.repetition_penalty == 1.0
3824 && self.sampler_config.suppress_tokens.is_empty();
3825 let burst_k = std::env::var("CMF_MULTISTEP")
3830 .ok()
3831 .and_then(|v| v.parse::<usize>().ok())
3832 .unwrap_or(0);
3833 if pure_greedy
3834 && burst_k >= 1
3835 && fuse_lm
3836 && task_mask.is_none()
3837 && router.is_none()
3838 && !trace_on
3839 && !self.confidence_on
3840 {
3841 let mut stopped = false;
3842 loop {
3843 let room = max_tokens.saturating_sub(generated);
3844 if room <= 2 {
3845 break;
3846 }
3847 let k = burst_k.min(room - 1);
3848 if k < 1 {
3849 break;
3850 }
3851 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
3852 if self
3853 .graph_failed
3854 .swap(false, std::sync::atomic::Ordering::Relaxed)
3855 {
3856 self.finish_generation(&mut mtp, &mut router, true);
3857 return Err(
3858 "GPU token graph failed during greedy burst".to_string()
3859 );
3860 }
3861 break;
3862 };
3863 next_pos += k;
3864 for &id in &ids {
3865 if !commit!(id) {
3866 stopped = true;
3867 break;
3868 }
3869 }
3870 if stopped {
3871 break;
3872 }
3873 t_fwd = *ids.last().unwrap();
3874 }
3875 if stopped {
3876 break 'decode;
3877 }
3878 }
3879 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
3880 next_pos += 1;
3881 if let Some(r) = &mut router {
3884 let phi = self.dyn_phi_ema.clone();
3885 let decision = r.step(&phi, generated);
3886 if let Some(new_active) = decision {
3887 let _ = self.set_active_skill(new_active);
3888 }
3889 if trace_on {
3892 if let Some(last) = traces.last_mut() {
3893 let e = r.last_best_e();
3894 last.recon = e.is_finite().then_some(e);
3895 last.switched = decision.is_some();
3896 }
3897 }
3898 }
3899 }
3900 }
3901 }
3902
3903 let cancelled = finish_reason == "cancelled";
3904 self.finish_generation(&mut mtp, &mut router, cancelled);
3905
3906 let output_ids = &all_ids[input_ids.len()..];
3907 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3911 if cancelled {
3912 self.kv_history.clear();
3913 } else {
3914 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3915 }
3916 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
3918 Ok(GenerateResult {
3919 text: self.tokenizer.decode(output_ids),
3920 token_ids: output_ids.to_vec(),
3921 prompt_tokens: input_ids.len(),
3922 tokens_generated: generated,
3923 finish_reason,
3924 mtp_drafted: drafted,
3925 mtp_accepted: accepted,
3926 token_confidence: confidence,
3927 traces,
3928 })
3929 }
3930
3931 fn mtp_step(
3935 &mut self,
3936 m: &mut MtpModule,
3937 hidden: &[f32],
3938 next_token: u32,
3939 position: usize,
3940 ) -> u32 {
3941 self.mtp_step_h(m, hidden, next_token, position).0
3942 }
3943
3944 fn chain_probe_note(depth: usize, prefix_ok: bool) {
3948 use std::sync::Mutex;
3949 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3950 let mut t = T.lock().unwrap();
3951 if t.len() <= depth {
3952 t.resize(depth + 1, (0, 0));
3953 }
3954 t[depth].0 += 1;
3955 t[depth].1 += prefix_ok as u64;
3956 if depth == 0 && t[0].0 % 128 == 0 {
3957 let line: Vec<String> = t
3958 .iter()
3959 .enumerate()
3960 .map(|(d, (n, k))| {
3961 format!(
3962 "d{}={:.0}%({n})",
3963 d + 1,
3964 100.0 * *k as f64 / (*n).max(1) as f64
3965 )
3966 })
3967 .collect();
3968 eprintln!("mtp-chain: {}", line.join(" "));
3969 }
3970 }
3971
3972 fn mtp_step_hl(
3980 &mut self,
3981 m: &mut MtpModule,
3982 hidden: &[f32],
3983 next_token: u32,
3984 position: usize,
3985 ) -> (Vec<f32>, Vec<f32>) {
3986 #[cfg(target_os = "macos")]
3991 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
3992 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
3993 self.mtp_graph_mode = Some(true);
3994 return r;
3995 }
3996 if self.mtp_graph_mode == Some(true) {
3997 tracing::error!("mtp Metal graph failed after admission");
3998 self.clear_sequence_state();
3999 self.graph_failed
4000 .store(true, std::sync::atomic::Ordering::Relaxed);
4001 self.cancel
4002 .store(true, std::sync::atomic::Ordering::Relaxed);
4003 return (Vec::new(), Vec::new());
4004 }
4005 self.mtp_graph_mode = Some(false);
4006 }
4007 #[cfg(feature = "gpu")]
4008 if self.mtp_graph_mode != Some(false) {
4009 if !self.mtp_graph_ok(m) {
4010 if self.mtp_graph_mode == Some(true) {
4011 tracing::error!("mtp graph became unavailable after admission");
4016 self.clear_sequence_state();
4017 self.graph_failed
4018 .store(true, std::sync::atomic::Ordering::Relaxed);
4019 self.cancel
4020 .store(true, std::sync::atomic::Ordering::Relaxed);
4021 return (Vec::new(), Vec::new());
4022 }
4023 self.mtp_graph_mode = Some(false);
4024 } else {
4025 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
4026 self.mtp_graph_mode = Some(true);
4027 return r;
4028 }
4029 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4030 return (Vec::new(), Vec::new());
4037 }
4038 tracing::error!("mtp graph failed or declined after admission");
4042 self.clear_sequence_state();
4043 self.graph_failed
4044 .store(true, std::sync::atomic::Ordering::Relaxed);
4045 self.cancel
4046 .store(true, std::sync::atomic::Ordering::Relaxed);
4047 return (Vec::new(), Vec::new());
4048 }
4049 }
4050 let e = self.embed_single(next_token);
4054 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4055 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4056 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4057 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4058 let mut x = vec![0.0f32; self.hidden_size];
4059 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4060
4061 let lw = &m.layer;
4063 inference::rms_norm_into(
4064 &x,
4065 &lw.input_norm,
4066 self.rms_eps,
4067 self.norm_style,
4068 &mut self.ws.n1,
4069 );
4070 let attn = match &lw.attn {
4071 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4073 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4074 AttnKind::Full {
4075 wq,
4076 wk,
4077 wv,
4078 wo,
4079 q_norm,
4080 k_norm,
4081 output_gate,
4082 softplus_gate,
4083 bias,
4084 } => {
4085 let mut cfg = self.attn_cfg(position);
4086 cfg.q_norm = q_norm.as_deref();
4087 cfg.k_norm = k_norm.as_deref();
4088 cfg.output_gate = *output_gate;
4089 cfg.softplus_gate = softplus_gate
4090 .as_ref()
4091 .map(|(gate, per_head)| (gate, *per_head));
4092 cfg.bias = bias
4093 .as_ref()
4094 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4095 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4096 }
4097 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
4098 unreachable!("MTP block is full attention")
4099 }
4100 };
4101 for (i, &a) in attn.iter().enumerate() {
4102 x[i] += a;
4103 }
4104 inference::rms_norm_into(
4105 &x,
4106 &lw.post_norm,
4107 self.rms_eps,
4108 self.norm_style,
4109 &mut self.ws.p1,
4110 );
4111 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
4112 for (i, &f) in ffn.iter().enumerate() {
4113 x[i] += f;
4114 }
4115
4116 inference::rms_norm_into(
4117 &x,
4118 &m.final_norm,
4119 self.rms_eps,
4120 self.norm_style,
4121 &mut self.ws.n1,
4122 );
4123 let lg = self.lm_head_forward(&self.ws.n1);
4124 (lg, x)
4125 }
4126
4127 fn mtp_step_h(
4129 &mut self,
4130 m: &mut MtpModule,
4131 hidden: &[f32],
4132 next_token: u32,
4133 position: usize,
4134 ) -> (u32, Vec<f32>) {
4135 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
4136 let draft = sampler::argmax(&lg);
4137 attention::recycle_buf(&mut lg);
4138 (draft, x)
4139 }
4140
4141 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
4147 match trial {
4148 SpecTrial::Spec { t0, gen0, rounds } => {
4149 let rounds = rounds + 1;
4150 if rounds >= 5 {
4151 if mon.plain_ms > 0.0 {
4152 let keep = mon.pays();
4153 mon.fails = 0;
4154 tracing::info!(
4155 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4156 mon.tokens,
4157 mon.round_ms,
4158 mon.plain_ms,
4159 if keep { "speculating" } else { "plain" }
4160 );
4161 SpecTrial::Decided {
4162 spec: keep,
4163 recheck_at: if keep { usize::MAX } else { generated + 128 },
4164 }
4165 } else {
4166 SpecTrial::Plain {
4167 t0: std::time::Instant::now(),
4168 gen0: generated,
4169 }
4170 }
4171 } else {
4172 SpecTrial::Spec { t0, gen0, rounds }
4173 }
4174 }
4175 SpecTrial::Decided { spec: true, .. } => {
4176 if mon.pays() {
4177 mon.fails = 0;
4178 trial
4179 } else {
4180 mon.fails += 1;
4181 if mon.fails >= 4 {
4182 tracing::info!(
4183 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
4184 mon.tokens,
4185 mon.round_ms,
4186 mon.plain_ms
4187 );
4188 SpecTrial::Decided {
4189 spec: false,
4190 recheck_at: generated + 128,
4191 }
4192 } else {
4193 trial
4194 }
4195 }
4196 }
4197 other => other,
4198 }
4199 }
4200
4201 fn mtp_kv_id(&self) -> u64 {
4204 self.graph_kv_id | (1u64 << 40)
4205 }
4206
4207 const MTP_LAYER_BASE: usize = 0;
4212
4213 #[cfg(feature = "gpu")]
4220 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
4221 self.mtp_graph_mode != Some(true)
4222 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
4223 }
4224
4225 #[cfg(feature = "gpu")]
4231 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
4232 let mut ok = true;
4233 let mut expected = false;
4234 for li in 0..self.num_layers {
4235 if matches!(
4236 self.weights.layers[self.phys_layer(li)].attn,
4237 AttnKind::Full { .. }
4238 ) {
4239 expected = true;
4240 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
4241 }
4242 }
4243 !expected || ok
4244 }
4245
4246 fn graph_gdn_layer_count(&self) -> usize {
4250 (0..self.num_layers)
4251 .filter(|&li| {
4252 matches!(
4253 &self.weights.layers[self.phys_layer(li)].attn,
4254 AttnKind::LinearGdn(_)
4255 )
4256 })
4257 .count()
4258 }
4259
4260 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
4263 let e = self.embed_single(next_token);
4264 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4265 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4266 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4267 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4268 let mut x = vec![0.0f32; self.hidden_size];
4269 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4270 x
4271 }
4272
4273 #[cfg(feature = "gpu")]
4276 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
4277 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
4278 return false;
4279 }
4280 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
4281 || !crate::gpu::enabled_here()
4282 || self.attn_softcap > 0.0
4283 || self.attention_heads_per_layer.is_some()
4284 {
4285 return false;
4286 }
4287 matches!(
4288 &m.layer.attn,
4289 AttnKind::Full {
4290 softplus_gate: None,
4291 ..
4292 }
4293 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
4294 }
4295
4296 #[cfg(feature = "gpu")]
4300 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
4301 if !self.mtp_block_graph_ok(m) {
4302 return false;
4303 }
4304 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
4305 return false;
4306 };
4307 let FfnKind::Dense(d) = &m.layer.ffn else {
4308 return false;
4309 };
4310 d.segs.is_empty()
4311 && wq.graph_weight().is_some()
4312 && wk.graph_weight().is_some()
4313 && wv.graph_weight().is_some()
4314 && wo.graph_weight().is_some()
4315 && d.gate_proj.graph_weight().is_some()
4316 && d.up_proj.graph_weight().is_some()
4317 && d.down_proj.graph_weight().is_some()
4318 && self.weights.lm_head.graph_weight().is_some()
4319 }
4320
4321 #[cfg(feature = "gpu")]
4327 fn mtp_step_graph(
4328 &mut self,
4329 m: &mut MtpModule,
4330 hidden: &[f32],
4331 next_token: u32,
4332 position: usize,
4333 ) -> Option<(Vec<f32>, Vec<f32>)> {
4334 if !self.mtp_graph_ok(m) {
4335 return None;
4336 }
4337 let lw = &m.layer;
4338 let AttnKind::Full {
4339 wq,
4340 wk,
4341 wv,
4342 wo,
4343 q_norm,
4344 k_norm,
4345 output_gate,
4346 softplus_gate,
4347 bias,
4348 } = &lw.attn
4349 else {
4350 return None;
4351 };
4352 if softplus_gate.is_some() {
4353 return None;
4354 }
4355 let FfnKind::Dense(d) = &lw.ffn else {
4356 return None;
4357 };
4358 if !d.segs.is_empty() {
4359 return None; }
4361 let mut x = self.mtp_block_input(m, hidden, next_token);
4364 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4365 let (_, i, kind, rs) = t.graph_weight()?;
4366 Some(crate::gpu::GraphW {
4367 idx: i,
4368 kind,
4369 row_scale: rs,
4370 data: &[],
4371 prism: crate::gpu::GraphPrismOp::None,
4372 affine: false,
4373 })
4374 }
4375 let (model, _, _, _) = wq.graph_weight()?;
4376 let model = model.clone();
4377 let (lm_gw, lm_rows) = {
4378 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4379 (
4380 crate::gpu::GraphW {
4381 idx: i,
4382 kind,
4383 row_scale: rs,
4384 data: &[],
4385 prism: crate::gpu::GraphPrismOp::None,
4386 affine: false,
4387 },
4388 self.weights.lm_head.rows(),
4389 )
4390 };
4391 let layer = crate::gpu::GraphLayer {
4392 input_norm: &lw.input_norm,
4393 attn: crate::gpu::GraphAttn::Full {
4394 wq: gw(wq)?,
4395 wk: gw(wk)?,
4396 wv: gw(wv)?,
4397 wo: gw(wo)?,
4398 q_norm: q_norm.as_deref(),
4399 k_norm: k_norm.as_deref(),
4400 bias: bias
4401 .as_ref()
4402 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4403 output_gate: *output_gate,
4404 cpu_k: m.kv.k_heads(),
4405 cpu_v: m.kv.v_heads(),
4406 },
4407 post_norm: &lw.post_norm,
4408 ffn: crate::gpu::GraphFfn::Dense {
4409 gate: gw(&d.gate_proj)?,
4410 up: gw(&d.up_proj)?,
4411 down: gw(&d.down_proj)?,
4412 },
4413 };
4414 let nh = self.num_heads;
4415 let (nkv, hd, rd) = self.layer_geom(0);
4416 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4417 let mut logits = Vec::new();
4418 let ok = crate::gpu::forward_token_graph(
4419 &model,
4420 self.mtp_kv_id(),
4421 std::slice::from_ref(&layer),
4422 &[None],
4423 self.o1_epoch,
4424 &self.inv_freq,
4425 &mut x,
4426 nh,
4427 nkv,
4428 hd,
4429 self.attn_scale,
4430 rd,
4431 self.hidden_size,
4432 self.intermediate_size,
4433 position,
4434 self.kv_cache.max_seq_len,
4435 gemma,
4436 self.rms_eps as f32,
4437 Some((&lm_gw, lm_rows)),
4438 &m.final_norm,
4439 &mut logits,
4440 &[],
4441 1,
4442 None,
4443 None,
4444 None,
4445 Self::MTP_LAYER_BASE,
4446 true,
4447 );
4448 match ok {
4449 crate::gpu::TokenGraphOutcome::Completed => {}
4450 crate::gpu::TokenGraphOutcome::Declined => return None,
4451 crate::gpu::TokenGraphOutcome::Failed => {
4452 self.clear_sequence_state();
4456 self.graph_failed
4457 .store(true, std::sync::atomic::Ordering::Relaxed);
4458 self.cancel
4459 .store(true, std::sync::atomic::Ordering::Relaxed);
4460 return None;
4461 }
4462 }
4463 logits.resize(self.vocab_size, 0.0);
4464 Some((logits, x))
4465 }
4466
4467 #[cfg(feature = "gpu")]
4475 fn mtp_warm_graph(
4476 &mut self,
4477 m: &mut MtpModule,
4478 pairs: &[(&[f32], u32)],
4479 first_pos: usize,
4480 ) -> crate::gpu::BatchGraphOutcome {
4481 if pairs.is_empty() {
4482 return crate::gpu::BatchGraphOutcome::Completed;
4483 }
4484 if !self.mtp_block_graph_ok(m) {
4485 return crate::gpu::BatchGraphOutcome::Declined;
4486 }
4487 let hs = self.hidden_size;
4488 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4491 for (h, t) in pairs {
4492 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4493 }
4494 let lw = &m.layer;
4495 let AttnKind::Full {
4496 wq,
4497 wk,
4498 wv,
4499 wo,
4500 q_norm,
4501 k_norm,
4502 output_gate,
4503 bias,
4504 ..
4505 } = &lw.attn
4506 else {
4507 return crate::gpu::BatchGraphOutcome::Declined;
4508 };
4509 let FfnKind::Dense(d) = &lw.ffn else {
4510 return crate::gpu::BatchGraphOutcome::Declined;
4511 };
4512 if !d.segs.is_empty() {
4513 return crate::gpu::BatchGraphOutcome::Declined; }
4515 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4516 let (_, i, kind, rs) = t.graph_weight()?;
4517 Some(crate::gpu::GraphW {
4518 idx: i,
4519 kind,
4520 row_scale: rs,
4521 data: &[],
4522 prism: crate::gpu::GraphPrismOp::None,
4523 affine: false,
4524 })
4525 }
4526 let Some((model, _, _, _)) = wq.graph_weight() else {
4527 return crate::gpu::BatchGraphOutcome::Declined;
4528 };
4529 let model = model.clone();
4530 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4531 gw(wq),
4532 gw(wk),
4533 gw(wv),
4534 gw(wo),
4535 gw(&d.gate_proj),
4536 gw(&d.up_proj),
4537 gw(&d.down_proj),
4538 ) else {
4539 return crate::gpu::BatchGraphOutcome::Declined;
4540 };
4541 let layer = crate::gpu::GraphLayer {
4542 input_norm: &lw.input_norm,
4543 attn: crate::gpu::GraphAttn::Full {
4544 wq: gwq,
4545 wk: gwk,
4546 wv: gwv,
4547 wo: gwo,
4548 q_norm: q_norm.as_deref(),
4549 k_norm: k_norm.as_deref(),
4550 bias: bias
4551 .as_ref()
4552 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4553 output_gate: *output_gate,
4554 cpu_k: m.kv.k_heads(),
4555 cpu_v: m.kv.v_heads(),
4556 },
4557 post_norm: &lw.post_norm,
4558 ffn: crate::gpu::GraphFfn::Dense {
4559 gate: gg,
4560 up: gu,
4561 down: gd,
4562 },
4563 };
4564 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4565 let nh = self.num_heads;
4566 let (nkv, hd, rd) = self.layer_geom(0);
4567 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4568 crate::gpu::forward_batch_graph(
4569 &model,
4570 self.mtp_kv_id(),
4571 std::slice::from_ref(&layer),
4572 &self.inv_freq,
4573 &mut hiddens,
4574 nh,
4575 nkv,
4576 hd,
4577 rd,
4578 hs,
4579 self.intermediate_size,
4580 &positions,
4581 self.kv_cache.max_seq_len,
4582 gemma,
4583 self.rms_eps as f32,
4584 self.attn_scale,
4585 pairs.len(),
4586 &[],
4587 0,
4588 None,
4589 )
4590 }
4591
4592 #[cfg(feature = "gpu")]
4599 fn mtp_warm_graph_fallback(
4600 &mut self,
4601 m: &mut MtpModule,
4602 pairs: &[(&[f32], u32)],
4603 first_pos: usize,
4604 ) -> bool {
4605 if pairs.is_empty() {
4606 return true;
4607 }
4608 let graphable = self.mtp_block_graph_ok(m);
4609 if !graphable {
4610 if self.mtp_graph_mode == Some(true) {
4614 return false;
4615 }
4616 self.mtp_graph_mode = Some(false);
4617 for (j, (h, t)) in pairs.iter().enumerate() {
4618 self.mtp_warm(m, h, *t, first_pos + j);
4619 }
4620 return true;
4621 }
4622
4623 for (j, (h, t)) in pairs.iter().enumerate() {
4628 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
4629 return false;
4630 }
4631 }
4632 self.mtp_graph_mode = Some(true);
4633 true
4634 }
4635
4636 #[cfg(feature = "gpu")]
4641 fn mtp_warm_prefill_pairs(
4642 &mut self,
4643 m: &mut MtpModule,
4644 pairs: &[(&[f32], u32)],
4645 first_pos: usize,
4646 ) -> Result<(), &'static str> {
4647 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
4652 if self.mtp_graph_mode == Some(true) {
4653 return Err("MTP token graph became unavailable after admission");
4654 }
4655 self.mtp_graph_mode = Some(false);
4656 for (j, (h, t)) in pairs.iter().enumerate() {
4657 self.mtp_warm(m, h, *t, first_pos + j);
4658 }
4659 return Ok(());
4660 }
4661 match self.mtp_warm_graph(m, pairs, first_pos) {
4662 crate::gpu::BatchGraphOutcome::Completed => {
4663 if !pairs.is_empty() {
4664 self.mtp_graph_mode = Some(true);
4665 }
4666 Ok(())
4667 }
4668 crate::gpu::BatchGraphOutcome::Declined => {
4669 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
4670 Ok(())
4671 } else {
4672 Err("MTP warm-up fallback failed after device admission")
4673 }
4674 }
4675 crate::gpu::BatchGraphOutcome::Failed => {
4676 Err("MTP warm batch graph failed after admission")
4677 }
4678 }
4679 }
4680
4681 #[cfg(not(feature = "gpu"))]
4682 fn mtp_warm_prefill_pairs(
4683 &mut self,
4684 m: &mut MtpModule,
4685 pairs: &[(&[f32], u32)],
4686 first_pos: usize,
4687 ) -> Result<(), &'static str> {
4688 for (j, (h, t)) in pairs.iter().enumerate() {
4689 self.mtp_warm(m, h, *t, first_pos + j);
4690 }
4691 Ok(())
4692 }
4693
4694 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
4698 let e = self.embed_single(next_token);
4699 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4700 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4701 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4702 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4703 let mut x = vec![0.0f32; self.hidden_size];
4704 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4705 inference::rms_norm_into(
4706 &x,
4707 &m.layer.input_norm,
4708 self.rms_eps,
4709 self.norm_style,
4710 &mut self.ws.n1,
4711 );
4712 let attn = match &m.layer.attn {
4713 AttnKind::Full {
4714 wq,
4715 wk,
4716 wv,
4717 wo,
4718 q_norm,
4719 k_norm,
4720 output_gate,
4721 softplus_gate,
4722 bias,
4723 } => {
4724 let mut cfg = self.attn_cfg(position);
4725 cfg.q_norm = q_norm.as_deref();
4726 cfg.k_norm = k_norm.as_deref();
4727 cfg.output_gate = *output_gate;
4728 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
4729 cfg.bias = bias
4730 .as_ref()
4731 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4732 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4733 }
4734 _ => return,
4735 };
4736 let _ = attn;
4737 }
4738
4739 #[cfg(feature = "gpu")]
4746 #[allow(clippy::too_many_arguments)]
4747 fn graph_spec_step(
4748 &mut self,
4749 m: &mut MtpModule,
4750 hidden: &[f32],
4751 t_next: u32,
4752 next_pos: usize,
4753 drafted: &mut usize,
4754 accepted: &mut usize,
4755 all_ids: &mut Vec<u32>,
4759 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
4760 #[cfg(target_os = "macos")]
4771 let metal_native = crate::gpu::q1_force();
4772 #[cfg(not(target_os = "macos"))]
4773 let metal_native = false;
4774 #[cfg(feature = "gpu")]
4775 let k_default = if metal_native {
4776 7
4779 } else if crate::gpu_wgpu::verify_i8_on() {
4780 5
4781 } else {
4782 4
4783 };
4784 #[cfg(not(feature = "gpu"))]
4785 let k_default = 4;
4786 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
4787 .ok()
4788 .and_then(|v| v.parse().ok())
4789 .filter(|&v| (1..=8).contains(&v))
4790 .unwrap_or(k_default);
4791 if next_pos == 0 {
4792 return None;
4793 }
4794 let t_round = std::time::Instant::now();
4795 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
4811 let sub0 = subs();
4812 let cfg = self.sampler_config.clone();
4817 let penalized = !(cfg.repetition_penalty == 1.0
4818 && cfg.presence_penalty == 0.0
4819 && cfg.suppress_tokens.is_empty());
4820 let greedy_pen = cfg.temperature < 1e-6 && penalized;
4825 let sampling = cfg.temperature >= 1e-6;
4826 let sparse = sampling && sampler::sparse_ok(&cfg);
4832 let base_len = all_ids.len();
4833 if sampling && !sparse && self.spec_q.len() < k_spec {
4834 self.spec_q.resize_with(k_spec, Vec::new);
4835 }
4836 if sparse && self.spec_qs.len() < k_spec {
4837 self.spec_qs.resize_with(k_spec, Vec::new);
4838 }
4839 let mut drafts = Vec::with_capacity(k_spec);
4844 let mut hx = hidden.to_vec();
4845 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
4848 for j in 0..k_spec {
4849 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
4850 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
4851 if spec_dbg {
4852 let saved = self.mtp_graph_mode;
4853 self.mtp_graph_mode = Some(false);
4854 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4855 self.mtp_graph_mode = saved;
4856 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4857 return None;
4858 }
4859 m.kv.truncate_last(1);
4860 dbg_ref = Some(r);
4861 }
4862 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4863 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4864 return None;
4865 }
4866 if let Some((lg_cpu, h_cpu)) = dbg_ref {
4867 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
4868 let dl = lg
4869 .iter()
4870 .zip(&lg_cpu)
4871 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4872 let dh = hj
4873 .iter()
4874 .zip(&h_cpu)
4875 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4876 eprintln!(
4877 "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 {}",
4878 next_pos - 1 + j,
4879 sampler::argmax(&lg_cpu),
4880 sampler::argmax(&lg),
4881 n(&h_cpu),
4882 n(&hj),
4883 m.kv.seq_len
4884 );
4885 }
4886 let dj = if sparse {
4887 let mut q = std::mem::take(&mut self.spec_qs[j]);
4888 let ok = sampler::sparse_distribution_into(
4889 &lg,
4890 &cfg,
4891 all_ids,
4892 &mut self.sampler_scratch,
4893 self.pool.as_deref(),
4894 &mut q,
4895 );
4896 let d = if ok {
4897 sampler::draw_sparse(&q, &mut self.rng)
4898 } else {
4899 let t = sampler::argmax(&lg);
4901 q.clear();
4902 q.push((t, 1.0));
4903 t
4904 };
4905 self.spec_qs[j] = q;
4906 all_ids.push(d);
4907 d
4908 } else if sampling {
4909 let mut q = std::mem::take(&mut self.spec_q[j]);
4910 sampler::distribution_into(
4911 &lg,
4912 &cfg,
4913 all_ids,
4914 &mut self.sampler_scratch,
4915 self.pool.as_deref(),
4916 &mut q,
4917 );
4918 let d = sampler::draw(&q, &mut self.rng);
4919 self.spec_q[j] = q;
4920 all_ids.push(d); d
4922 } else if greedy_pen {
4923 let d = sampler::argmax_penalized(
4924 &lg,
4925 &cfg,
4926 all_ids,
4927 &mut self.sampler_scratch,
4928 self.pool.as_deref(),
4929 );
4930 all_ids.push(d);
4931 d
4932 } else {
4933 sampler::argmax(&lg)
4934 };
4935 attention::recycle_buf(&mut lg);
4936 drafts.push(dj);
4937 hx = hj;
4938 }
4939 all_ids.truncate(base_len);
4940 *drafted += k_spec;
4941 let t_draft = t_round.elapsed();
4942 let sub_draft = subs();
4943 let b = k_spec + 1;
4946 let mut hiddens = vec![0.0f32; b * self.hidden_size];
4947 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
4948 let e = self.embed_single(t);
4949 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
4950 }
4951 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
4952 let (lm_gw, lm_rows) = {
4953 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4954 (
4955 crate::gpu::GraphW {
4956 idx: i,
4957 kind,
4958 row_scale: rs,
4959 data: &[],
4960 prism: crate::gpu::GraphPrismOp::None,
4961 affine: false,
4962 },
4963 self.weights.lm_head.rows(),
4964 )
4965 };
4966 let mut logits = Vec::new();
4967 let final_norm = self.weights.final_norm.clone();
4968 #[cfg(target_os = "macos")]
4969 let verify_outcome = if metal_native {
4970 let lm = self.weights.lm_head.q1_parts()?;
4971 self.try_batch_graph_metal(
4972 &mut hiddens,
4973 &positions,
4974 b,
4975 Some((lm, &final_norm, &mut logits)),
4976 )
4977 } else {
4978 self.try_batch_graph_wgpu(
4979 &mut hiddens,
4980 &positions,
4981 b,
4982 Some(crate::gpu::SpecTail {
4983 lm: lm_gw,
4984 lm_rows,
4985 final_norm: &final_norm,
4986 logits_out: &mut logits,
4987 }),
4988 )
4989 };
4990 #[cfg(not(target_os = "macos"))]
4991 let verify_outcome = self.try_batch_graph_wgpu(
4992 &mut hiddens,
4993 &positions,
4994 b,
4995 Some(crate::gpu::SpecTail {
4996 lm: lm_gw,
4997 lm_rows,
4998 final_norm: &final_norm,
4999 logits_out: &mut logits,
5000 }),
5001 );
5002 match verify_outcome {
5003 crate::gpu::BatchGraphOutcome::Completed => {}
5004 crate::gpu::BatchGraphOutcome::Declined => {
5005 m.kv.truncate_last(k_spec);
5009 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
5010 self.clear_sequence_state();
5011 self.graph_failed
5012 .store(true, std::sync::atomic::Ordering::Relaxed);
5013 self.cancel
5014 .store(true, std::sync::atomic::Ordering::Relaxed);
5015 tracing::error!("MTP graph mirror rewind failed after verify decline");
5016 }
5017 return None;
5018 }
5019 crate::gpu::BatchGraphOutcome::Failed => {
5020 self.clear_sequence_state();
5024 self.graph_failed
5025 .store(true, std::sync::atomic::Ordering::Relaxed);
5026 self.cancel
5027 .store(true, std::sync::atomic::Ordering::Relaxed);
5028 tracing::error!("MTP verify batch graph failed after admission");
5029 return None;
5030 }
5031 }
5032 #[cfg(target_os = "macos")]
5038 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
5039 let snap: Vec<Vec<f32>> = self
5040 .kv_cache
5041 .layers
5042 .iter()
5043 .map(|l| l.linear_state.clone())
5044 .collect();
5045 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5046 let toks: Vec<u32> = std::iter::once(t_next)
5047 .chain(drafts.iter().copied())
5048 .collect();
5049 let want_save = self.graph_want_logits;
5050 self.graph_want_logits = false;
5051 for (i, &t) in toks.iter().enumerate() {
5052 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5053 let _ = self.graph_logits.take();
5054 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
5058 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
5059 }
5060 let ref_lg = self.logits_from_hidden(&hi);
5061 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
5062 let ra = sampler::argmax(&ref_lg);
5063 let va = sampler::argmax(row);
5064 let mut md = 0f32;
5065 let mut rms = 0f64;
5066 for j in 0..lm_rows.min(ref_lg.len()) {
5067 let d = (ref_lg[j] - row[j]).abs();
5068 md = md.max(d);
5069 rms += (d as f64) * (d as f64);
5070 }
5071 let mut hd = 0f32;
5072 for j in 0..self.hidden_size {
5073 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
5074 }
5075 eprintln!(
5076 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
5077 next_pos + i,
5078 if ra == va { "OK" } else { "MISMATCH" },
5079 (rms / lm_rows as f64).sqrt()
5080 );
5081 }
5082 self.graph_want_logits = want_save;
5083 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5086 if l.linear_state.len() == st.len() {
5087 l.linear_state.copy_from_slice(&st);
5088 } else {
5089 l.linear_state = st;
5090 }
5091 }
5092 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
5093 let extra = l.seq_len.saturating_sub(n0);
5094 if extra > 0 {
5095 l.truncate_last(extra);
5096 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
5097 }
5098 }
5099 }
5100 let t_verify = t_round.elapsed();
5101 let sub_verify = subs();
5102 let mut a = 0usize;
5107 let mut forced: Option<u32> = None;
5108 let ids: Vec<u32> = if sparse {
5109 let mut p = std::mem::take(&mut self.spec_ps);
5110 let mut res = std::mem::take(&mut self.spec_ress);
5111 while a < k_spec {
5112 let ok = sampler::sparse_distribution_into(
5113 &logits[a * lm_rows..(a + 1) * lm_rows],
5114 &cfg,
5115 all_ids,
5116 &mut self.sampler_scratch,
5117 self.pool.as_deref(),
5118 &mut p,
5119 );
5120 if !ok {
5121 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
5122 p.clear();
5123 p.push((t, 1.0));
5124 }
5125 match sampler::spec_accept_or_correct_sparse(
5126 &p,
5127 &self.spec_qs[a],
5128 drafts[a],
5129 &mut self.rng,
5130 &mut res,
5131 ) {
5132 None => {
5133 all_ids.push(drafts[a]);
5134 a += 1;
5135 }
5136 Some(c) => {
5137 forced = Some(c);
5138 break;
5139 }
5140 }
5141 }
5142 all_ids.truncate(base_len);
5143 self.spec_ps = p;
5144 self.spec_ress = res;
5145 drafts.clone()
5146 } else if sampling {
5147 let mut p = std::mem::take(&mut self.spec_p);
5148 let mut res = std::mem::take(&mut self.spec_res);
5149 while a < k_spec {
5150 sampler::distribution_into(
5151 &logits[a * lm_rows..(a + 1) * lm_rows],
5152 &cfg,
5153 all_ids,
5154 &mut self.sampler_scratch,
5155 self.pool.as_deref(),
5156 &mut p,
5157 );
5158 match sampler::spec_accept_or_correct(
5159 &p,
5160 &self.spec_q[a],
5161 drafts[a],
5162 &mut self.rng,
5163 &mut res,
5164 self.pool.as_deref(),
5165 ) {
5166 None => {
5167 all_ids.push(drafts[a]);
5168 a += 1;
5169 }
5170 Some(c) => {
5171 forced = Some(c);
5172 break;
5173 }
5174 }
5175 }
5176 all_ids.truncate(base_len);
5177 self.spec_p = p;
5178 self.spec_res = res;
5179 drafts.clone()
5181 } else if greedy_pen {
5182 let mut ids: Vec<u32> = Vec::with_capacity(b);
5186 for i in 0..b {
5187 let t = sampler::argmax_penalized(
5188 &logits[i * lm_rows..(i + 1) * lm_rows],
5189 &cfg,
5190 all_ids,
5191 &mut self.sampler_scratch,
5192 self.pool.as_deref(),
5193 );
5194 ids.push(t);
5195 if i < k_spec && t == drafts[i] {
5196 all_ids.push(t);
5197 } else {
5198 break;
5199 }
5200 }
5201 all_ids.truncate(base_len);
5202 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
5203 a += 1;
5204 }
5205 ids
5208 } else {
5209 let ids: Vec<u32> = (0..b)
5210 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
5211 .collect();
5212 while a < k_spec && ids[a] == drafts[a] {
5213 a += 1;
5214 }
5215 ids
5216 };
5217 if spec_dbg {
5218 eprintln!(
5219 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
5220 drafts, ids
5221 );
5222 }
5223 #[cfg(target_os = "macos")]
5227 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
5228 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
5229 {
5230 let snap: Vec<Vec<f32>> = self
5231 .kv_cache
5232 .layers
5233 .iter()
5234 .map(|l| l.linear_state.clone())
5235 .collect();
5236 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5237 let toks: Vec<u32> = std::iter::once(t_next)
5238 .chain(drafts.iter().copied())
5239 .collect();
5240 let want_save = self.graph_want_logits;
5241 self.graph_want_logits = false;
5242 for (i, &t) in toks.iter().take(a + 1).enumerate() {
5243 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5244 let _ = self.graph_logits.take();
5245 }
5246 self.graph_want_logits = want_save;
5247 let plain_states: Vec<Vec<f32>> = self
5248 .kv_cache
5249 .layers
5250 .iter()
5251 .map(|l| l.linear_state.clone())
5252 .collect();
5253 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5254 let mut rows = Vec::new();
5255 for (li, (l, n0)) in self
5256 .kv_cache
5257 .layers
5258 .iter_mut()
5259 .zip(attn_lens.iter())
5260 .enumerate()
5261 {
5262 let extra = l.seq_len.saturating_sub(*n0);
5263 if extra > 0 {
5264 let mut kk = Vec::new();
5265 let mut vv = Vec::new();
5266 for g in 0..nkv {
5267 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5268 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5269 }
5270 rows.push((li, kk, vv));
5271 l.truncate_last(extra);
5272 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
5273 }
5274 }
5275 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5276 if l.linear_state.len() == st.len() {
5277 l.linear_state.copy_from_slice(&st);
5278 } else {
5279 l.linear_state = st;
5280 }
5281 }
5282 Some((plain_states, rows))
5283 } else {
5284 None
5285 };
5286 #[cfg(target_os = "macos")]
5288 if metal_native {
5289 if !self.metal_verify_commit(a) {
5292 self.clear_sequence_state();
5293 self.graph_failed
5294 .store(true, std::sync::atomic::Ordering::Relaxed);
5295 self.cancel
5296 .store(true, std::sync::atomic::Ordering::Relaxed);
5297 tracing::error!("Metal verify state/KV handoff failed after admission");
5298 return None;
5299 }
5300 if let Some((plain_states, rows)) = commit_ref {
5301 crate::gpu_metal::queue_fence();
5302 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5303 let mut worst_s = 0f32;
5304 let mut worst_li = 0usize;
5305 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
5306 if l.linear_state.len() != ps.len() || ps.is_empty() {
5307 continue;
5308 }
5309 let d = l
5310 .linear_state
5311 .iter()
5312 .zip(ps)
5313 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5314 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
5315 let rel = d / n.max(1e-6);
5316 if rel > worst_s {
5317 worst_s = rel;
5318 worst_li = li;
5319 }
5320 }
5321 let mut worst_k = 0f32;
5322 for (li, kk, vv) in &rows {
5323 let l = &self.kv_cache.layers[*li];
5324 let n0 = l.seq_len - (kk.len() / (nkv * hd));
5325 let mut ck = Vec::new();
5326 let mut cv = Vec::new();
5327 for g in 0..nkv {
5328 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5329 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5330 }
5331 if ck.len() == kk.len() {
5332 let dk = ck
5333 .iter()
5334 .zip(kk)
5335 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5336 let dv = cv
5337 .iter()
5338 .zip(vv)
5339 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5340 worst_k = worst_k.max(dk).max(dv);
5341 } else {
5342 eprintln!(
5343 "commit-check L{li}: kv row count mismatch {} vs {}",
5344 ck.len(),
5345 kk.len()
5346 );
5347 }
5348 }
5349 eprintln!(
5350 "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}"
5351 );
5352 }
5353 }
5354 if !metal_native && a + 1 < b {
5355 let expected_gdn_layers = self.graph_gdn_layer_count();
5356 if expected_gdn_layers > 0
5357 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
5358 {
5359 self.clear_sequence_state();
5360 self.graph_failed
5361 .store(true, std::sync::atomic::Ordering::Relaxed);
5362 self.cancel
5363 .store(true, std::sync::atomic::Ordering::Relaxed);
5364 tracing::error!("GDN speculative restore failed after verify");
5365 return None;
5366 }
5367 }
5368 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
5369 self.clear_sequence_state();
5374 self.graph_failed
5375 .store(true, std::sync::atomic::Ordering::Relaxed);
5376 self.cancel
5377 .store(true, std::sync::atomic::Ordering::Relaxed);
5378 tracing::error!("trunk graph KV rewind failed after speculative verify");
5379 return None;
5380 }
5381 *accepted += a;
5382 m.kv.truncate_last(k_spec.saturating_sub(1));
5393 #[cfg(target_os = "macos")]
5394 if metal_native && self.mtp_graph_mode == Some(true) {
5395 crate::gpu_metal::kv_mirror_set_stored(
5398 self.mtp_kv_id(),
5399 Self::MTP_LAYER_BASE,
5400 m.kv.seq_len,
5401 );
5402 }
5403 if !metal_native
5404 && self.mtp_graph_mode == Some(true)
5405 && !self.rewind_mtp_graph_mirror(next_pos)
5406 {
5407 self.clear_sequence_state();
5411 self.graph_failed
5412 .store(true, std::sync::atomic::Ordering::Relaxed);
5413 self.cancel
5414 .store(true, std::sync::atomic::Ordering::Relaxed);
5415 tracing::error!("MTP graph mirror rewind failed after verify commit");
5416 return None;
5417 }
5418 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
5419 if !warm_off && a > 0 {
5420 let mut warmed = false;
5423 #[cfg(target_os = "macos")]
5424 if metal_native && self.mtp_graph_mode == Some(true) {
5425 let pairs: Vec<(&[f32], u32)> = (0..a)
5429 .map(|j| {
5430 (
5431 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
5432 ids[j],
5433 )
5434 })
5435 .collect();
5436 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
5437 if !warmed {
5438 warmed = true;
5439 for j in 0..a {
5440 let row =
5441 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
5442 if self
5443 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
5444 .is_none()
5445 {
5446 warmed = false;
5447 break;
5448 }
5449 }
5450 }
5451 }
5452 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
5453 let rows: Vec<Vec<f32>> = (0..a)
5454 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
5455 .collect();
5456 let pairs: Vec<(&[f32], u32)> = rows
5457 .iter()
5458 .zip(ids.iter())
5459 .map(|(r, &t)| (r.as_slice(), t))
5460 .collect();
5461 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
5462 Ok(()) => warmed = true,
5463 Err(err) => {
5464 tracing::error!("{err}");
5470 self.clear_sequence_state();
5471 self.graph_failed
5472 .store(true, std::sync::atomic::Ordering::Relaxed);
5473 self.cancel
5474 .store(true, std::sync::atomic::Ordering::Relaxed);
5475 return None;
5476 }
5477 }
5478 }
5479 if !warmed {
5480 for j in 0..a {
5481 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
5482 let row = row.to_vec();
5483 self.mtp_warm(m, &row, ids[j], next_pos + j);
5484 }
5485 }
5486 }
5487 if let Some(c) = forced {
5491 self.spec_forced = Some(c);
5492 self.graph_logits = None;
5493 } else {
5494 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
5495 row.resize(self.vocab_size, 0.0);
5496 if let Some(c) = self.final_softcap {
5497 for l in row.iter_mut() {
5498 *l = c * (*l / c).tanh();
5499 }
5500 }
5501 self.graph_logits = Some(row);
5502 }
5503 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
5504 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5510 let end = subs();
5511 eprintln!(
5512 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
5513 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
5514 t_draft.as_secs_f64() * 1e3,
5515 sub_draft - sub0,
5516 (t_verify - t_draft).as_secs_f64() * 1e3,
5517 sub_verify - sub_draft,
5518 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
5519 end - sub_verify,
5520 );
5521 }
5522 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
5523 }
5524
5525 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
5534 if !self.pair_supported() {
5535 return (0.0, 0.0);
5536 }
5537 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
5544 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5545 let emb1 = self.embed_single(1);
5546 let emb2 = self.embed_single(2);
5547 let pos = self.kv_cache.seq_len();
5548
5549 let t0 = std::time::Instant::now();
5550 for _ in 0..iters {
5551 let _ = self.forward_layers(&emb1, pos, None);
5552 let _ = self.forward_layers(&emb2, pos + 1, None);
5553 for l in &mut self.kv_cache.layers {
5554 l.truncate_last(2);
5555 }
5556 }
5557 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5558
5559 let t1 = std::time::Instant::now();
5560 for _ in 0..iters {
5561 let _ = self.forward_pair(&emb1, &emb2, pos);
5562 for l in &mut self.kv_cache.layers {
5563 l.truncate_last(2);
5564 }
5565 }
5566 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5567 match graph_env {
5568 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
5569 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
5570 }
5571 (singles_ms, pair_ms)
5572 }
5573
5574 fn pair_supported(&self) -> bool {
5582 !self.weights.layers.is_empty()
5589 && self.g3n.is_none()
5590 && !self
5591 .weights
5592 .layers
5593 .iter()
5594 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
5595 }
5596
5597 fn forward_pair(
5598 &mut self,
5599 emb1: &[f32],
5600 emb2: &[f32],
5601 position: usize,
5602 ) -> (Vec<f32>, Vec<f32>) {
5603 let mut h1 = emb1.to_vec();
5604 let mut h2 = emb2.to_vec();
5605 let (_nkv, _hd, hs, _rd, eps) = (
5606 self.num_kv_heads,
5607 self.head_dim,
5608 self.hidden_size,
5609 self.rotary_dim,
5610 self.rms_eps,
5611 );
5612 let pool = self.pool.clone();
5613
5614 for li in 0..self.num_layers {
5615 let lw = &self.weights.layers[self.phys_layer(li)];
5616 inference::rms_norm_into(
5619 &h1,
5620 &lw.input_norm,
5621 self.rms_eps,
5622 self.norm_style,
5623 &mut self.ws.n1,
5624 );
5625 inference::rms_norm_into(
5626 &h2,
5627 &lw.input_norm,
5628 self.rms_eps,
5629 self.norm_style,
5630 &mut self.ws.n2,
5631 );
5632
5633 let (a1, a2) = match &lw.attn {
5634 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5635 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5636 AttnKind::Linear(w) => {
5637 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5638 let layer = &mut self.kv_cache.layers[li];
5639 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5640 vmf_phase_pair(
5641 &self.ws.n1,
5642 &self.ws.n2,
5643 w,
5644 &cfg,
5645 state,
5646 scratch,
5647 self.pool.as_deref(),
5648 )
5649 }
5650 AttnKind::LinearGdn(w) => {
5651 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5652 let layer = &mut self.kv_cache.layers[li];
5653 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5654 gdn_pair(
5655 &self.ws.n1,
5656 &self.ws.n2,
5657 w,
5658 &cfg,
5659 state,
5660 scratch,
5661 self.pool.as_deref(),
5662 )
5663 }
5664 AttnKind::ShortConv(w) => {
5665 let cfg = self
5666 .short_conv_cfg
5667 .expect("short-conv layer without short_conv_cfg");
5668 let layer = &mut self.kv_cache.layers[li];
5669 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5670 short_conv_pair(
5671 &self.ws.n1,
5672 &self.ws.n2,
5673 w,
5674 &cfg,
5675 state,
5676 scratch,
5677 self.pool.as_deref(),
5678 )
5679 }
5680 AttnKind::Full {
5681 wq,
5682 wk,
5683 wv,
5684 wo,
5685 q_norm,
5686 k_norm,
5687 output_gate,
5688 softplus_gate,
5689 bias,
5690 } => {
5691 let inv_freq_l = self.layer_inv_freq(li);
5692 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5693 let cfg = QwenAttnCfg {
5694 num_heads: self.layer_num_heads(li),
5695 num_kv_heads: nkv_l,
5696 head_dim: hd_l,
5697 hidden_size: hs,
5698 position,
5699 inv_freq: &inv_freq_l,
5700 rotary_dim: rd_l,
5701 scale: self.attn_scale,
5702 softcap: self.attn_softcap,
5703 window: self.layer_window(li),
5704 v_norm: self.attn_v_norm,
5705 q_norm: q_norm.as_deref(),
5706 k_norm: k_norm.as_deref(),
5707 output_gate: *output_gate,
5708 softplus_gate: softplus_gate
5709 .as_ref()
5710 .map(|(gate, per_head)| (gate, *per_head)),
5711 rope_scale: self.layer_rope_scale(li),
5712 bias: bias
5713 .as_ref()
5714 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5715 rms_eps: eps,
5716 norm_style: self.norm_style,
5717 pool: pool.as_deref(),
5718 };
5719 attention::qwen_attention_pair(
5720 &self.ws.n1,
5721 &self.ws.n2,
5722 wq,
5723 wk,
5724 wv,
5725 wo,
5726 &mut self.kv_cache.layers[li],
5727 &cfg,
5728 )
5729 }
5730 };
5731 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5732 Some(w) => (
5733 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
5734 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
5735 ),
5736 None => (a1, a2),
5737 };
5738 for i in 0..self.hidden_size {
5739 h1[i] += a1[i];
5740 h2[i] += a2[i];
5741 }
5742 let (mut a1, mut a2) = (a1, a2);
5743 attention::recycle_buf(&mut a1);
5744 attention::recycle_buf(&mut a2);
5745
5746 let lw = &self.weights.layers[self.phys_layer(li)];
5747 inference::rms_norm_into(
5748 &h1,
5749 &lw.post_norm,
5750 self.rms_eps,
5751 self.norm_style,
5752 &mut self.ws.p1,
5753 );
5754 inference::rms_norm_into(
5755 &h2,
5756 &lw.post_norm,
5757 self.rms_eps,
5758 self.norm_style,
5759 &mut self.ws.p2,
5760 );
5761 let (f1, f2) = match &lw.ffn {
5762 FfnKind::DenseMoe(dm) => (
5765 dense_moe_ffn(
5766 dm,
5767 &self.ws.p1,
5768 &h1,
5769 self.rms_eps,
5770 self.norm_style,
5771 self.pool.as_deref(),
5772 ),
5773 dense_moe_ffn(
5774 dm,
5775 &self.ws.p2,
5776 &h2,
5777 self.rms_eps,
5778 self.norm_style,
5779 self.pool.as_deref(),
5780 ),
5781 ),
5782 _ => ffn_forward_pair(
5783 &lw.ffn,
5784 &self.ws.p1,
5785 &self.ws.p2,
5786 self.pool.as_deref(),
5787 None,
5788 ),
5789 };
5790 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5791 Some(w) => (
5792 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
5793 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
5794 ),
5795 None => (f1, f2),
5796 };
5797 for i in 0..self.hidden_size {
5798 h1[i] += f1[i];
5799 h2[i] += f2[i];
5800 }
5801 let (mut f1, mut f2) = (f1, f2);
5802 attention::recycle_buf(&mut f1);
5803 attention::recycle_buf(&mut f2);
5804 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5805 for i in 0..self.hidden_size {
5806 h1[i] *= sc;
5807 h2[i] *= sc;
5808 }
5809 }
5810 if self.is_loop_end(li) && li + 1 < self.num_layers {
5812 h1 = inference::rms_norm(
5813 &h1,
5814 &self.weights.final_norm,
5815 self.rms_eps,
5816 self.norm_style,
5817 );
5818 h2 = inference::rms_norm(
5819 &h2,
5820 &self.weights.final_norm,
5821 self.rms_eps,
5822 self.norm_style,
5823 );
5824 }
5825 }
5826 if self.o1_active() {
5832 self.commit_linear_scratch();
5833 }
5834 self.o1_progress();
5835 (h1, h2)
5836 }
5837
5838 fn commit_linear_scratch(&mut self) {
5840 for layer in &mut self.kv_cache.layers {
5841 if !layer.linear_scratch.is_empty() {
5842 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
5843 layer.linear_scratch.clear();
5844 }
5845 }
5846 }
5847
5848 pub fn forward_ids(
5851 &mut self,
5852 ids: &[u32],
5853 task_mask: Option<&TaskMask>,
5854 ) -> Result<Vec<f32>, String> {
5855 if ids.is_empty() {
5856 return Err("empty id sequence".to_string());
5857 }
5858 self.clear_sequence_state();
5859 self.check_forward_graph("forward_ids setup", 0)?;
5860 if task_mask.is_none() {
5861 self.o1_begin();
5862 }
5863 let mut hidden = vec![0.0f32; self.hidden_size];
5864 let mut pos = 0usize;
5865 if let Some(b) = &mut self.dsv41 {
5866 let pool = self.pool.clone();
5867 let mut logits = Vec::new();
5868 crate::dsv41::forward_chunk(
5869 &b.0,
5870 &b.1,
5871 &b.2,
5872 &mut b.3,
5873 ids,
5874 0,
5875 pool.as_deref(),
5876 &mut logits,
5877 );
5878 if let Err(err) = self.o1_seal_checked() {
5879 self.clear_sequence_state();
5880 return Err(err);
5881 }
5882 return Ok(logits);
5883 }
5884 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
5892 let chunk = prefill_chunk();
5896 let hs = self.hidden_size;
5897 while pos < ids.len() {
5898 let end = (pos + chunk).min(ids.len());
5899 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5900 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
5901 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5902 pos = end;
5903 }
5904 }
5905 if task_mask.is_none()
5914 && !self.graph_prefill_preferred()
5915 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
5916 && self.pair_supported()
5917 {
5918 while pos + 1 < ids.len() {
5919 let e1 = self.embed_single(ids[pos]);
5920 let e2 = self.embed_single(ids[pos + 1]);
5921 let (_, h2) = self.forward_pair(&e1, &e2, pos);
5922 self.check_forward_graph("forward_ids pair", pos + 1)?;
5923 self.commit_linear_scratch();
5924 hidden = h2;
5925 pos += 2;
5926 }
5927 }
5928 while pos < ids.len() {
5929 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5930 self.check_forward_graph("forward_ids", pos)?;
5931 pos += 1;
5932 }
5933 if let Err(err) = self.o1_seal_checked() {
5937 self.clear_sequence_state();
5938 return Err(err);
5939 }
5940 let normed = inference::rms_norm(
5941 &hidden,
5942 &self.weights.final_norm,
5943 self.rms_eps,
5944 self.norm_style,
5945 );
5946 Ok(self.lm_head_forward(&normed))
5947 }
5948
5949 #[doc(hidden)]
5953 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
5954 #[cfg(target_os = "macos")]
5955 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5956 if ids.is_empty() {
5957 return Err("empty id sequence".to_string());
5958 }
5959 self.clear_sequence_state();
5960 self.dsv41
5961 .as_ref()
5962 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
5963 self.o1_begin();
5964 let rows = {
5965 let pool = self.pool.clone();
5966 let b = self
5967 .dsv41
5968 .as_mut()
5969 .expect("dsv41 checked above; state cannot change during forward");
5970 let mut rows = Vec::with_capacity(ids.len());
5971 for (position, &id) in ids.iter().enumerate() {
5972 let mut logits = Vec::new();
5973 crate::dsv41::forward_token(
5974 &b.0,
5975 &b.1,
5976 &b.2,
5977 &mut b.3,
5978 id,
5979 position,
5980 pool.as_deref(),
5981 &mut logits,
5982 );
5983 rows.push(logits);
5984 }
5985 rows
5986 };
5987 self.o1_seal();
5988 Ok(rows)
5989 }
5990
5991 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
5998 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
5999 Ok((nll / cnt.max(1) as f64).exp())
6000 }
6001
6002 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
6007 self.clear_sequence_state();
6008 FFN_PROBE.with(|p| {
6009 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6010 });
6011 crate::gpu::cpu_scope(|| {
6012 for (pos, &id) in ids.iter().enumerate() {
6013 let emb = self.embed_single(id);
6014 let _ = self.forward_layers(&emb, pos, None);
6015 }
6016 });
6017 self.clear_sequence_state();
6018 FFN_PROBE
6019 .with(|p| p.borrow_mut().take())
6020 .unwrap_or_default()
6021 }
6022
6023 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
6027 if let Err(err) = self.nll_begin() {
6028 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
6032 self.nll_end();
6033 return Err(err);
6034 }
6035 FFN_PROBE.with(|p| {
6036 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6037 });
6038 let result: Result<(), String> = (|| {
6039 for chunk in ids.chunks(256) {
6040 if chunk.len() < 2 {
6041 continue;
6042 }
6043 self.nll_ids_masked(chunk, 0, None)?;
6044 }
6045 Ok(())
6046 })();
6047 self.nll_end();
6048 let probe = FFN_PROBE
6049 .with(|p| p.borrow_mut().take())
6050 .unwrap_or_default();
6051 match result {
6052 Ok(()) => Ok(probe),
6053 Err(err) => {
6054 drop(probe);
6055 Err(err)
6056 }
6057 }
6058 }
6059
6060 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
6064 self.nll_begin()?;
6065 let result: Result<f64, String> = (|| {
6066 let mut nll = 0f64;
6067 let mut cnt = 0usize;
6068 let mut hidden = vec![0f32; self.hidden_size];
6069 for (pos, &id) in ids.iter().enumerate() {
6070 if pos > 0 {
6071 inference::rms_norm_into(
6072 &hidden,
6073 &self.weights.final_norm,
6074 self.rms_eps,
6075 self.norm_style,
6076 &mut self.ws.n1,
6077 );
6078 let mut logits = self.lm_head_forward(&self.ws.n1);
6079 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6080 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
6081 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
6082 nll -= p.max(1e-300).ln();
6083 cnt += 1;
6084 attention::recycle_buf(&mut logits);
6085 }
6086 let emb = self.embed_single(id);
6087 hidden = self.forward_layers(&emb, pos, Some(mask));
6088 self.nll_check_graph("masked serial forward", pos)?;
6089 let _ = self.graph_logits.take();
6093 }
6094 Ok((nll / cnt.max(1) as f64).exp())
6095 })();
6096 self.nll_end();
6097 result
6098 }
6099
6100 pub fn nll_ids_masked(
6119 &mut self,
6120 ids: &[u32],
6121 start: usize,
6122 task_mask: Option<&TaskMask>,
6123 ) -> Result<(f64, usize), String> {
6124 let task_mask = self.drop_open_mask(task_mask);
6125 self.nll_ids_inner(ids, start, task_mask)
6126 }
6127
6128 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
6129 self.nll_ids_inner(ids, start, None)
6130 }
6131
6132 fn nll_ids_inner(
6133 &mut self,
6134 ids: &[u32],
6135 start: usize,
6136 task_mask: Option<&TaskMask>,
6137 ) -> Result<(f64, usize), String> {
6138 self.nll_begin()?;
6139 let result: Result<(f64, usize), String> = (|| {
6140 let mut nll = 0f64;
6141 let mut cnt = 0usize;
6142 let (graph_quality, fused_head_quality) = nll_graph_policy(
6155 task_mask.is_none(),
6156 self.graph_prefill_preferred(),
6157 crate::gpu::q1_force(),
6158 );
6159 self.graph_head_required = fused_head_quality;
6160 self.graph_want_logits = fused_head_quality;
6161 #[cfg(target_os = "macos")]
6162 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
6163 match self.nll_batch_metal(ids, start) {
6164 MetalBatchNllOutcome::Completed(nll, count) => {
6165 return Ok((nll, count));
6166 }
6167 MetalBatchNllOutcome::Declined => {}
6168 MetalBatchNllOutcome::Failed(err) => return Err(err),
6169 }
6170 }
6171 if self.can_prefill_batched() && !graph_quality {
6172 const CHUNK: usize = 128;
6178 const LM_SUB: usize = 32;
6179 let n = ids.len().saturating_sub(1);
6180 let hs = self.hidden_size;
6181 let rows = self.weights.lm_head.rows();
6182 let mut pos = 0usize;
6183 while pos < n {
6184 let end = (pos + CHUNK).min(n);
6185 let bsz = end - pos;
6186 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6187 self.nll_check_graph("batched prefill", pos)?;
6188 let mut k0 = 0usize;
6189 while k0 < bsz {
6190 let k1 = (k0 + LM_SUB).min(bsz);
6191 let sb = k1 - k0;
6192 if pos + k1 <= start {
6195 k0 = k1;
6196 continue;
6197 }
6198 let mut normed = vec![0.0f32; sb * hs];
6199 for k in 0..sb {
6200 let r = inference::rms_norm(
6201 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
6202 &self.weights.final_norm,
6203 self.rms_eps,
6204 self.norm_style,
6205 );
6206 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
6207 }
6208 let mut logits = vec![0.0f32; sb * rows];
6209 self.weights
6210 .lm_head
6211 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
6212 for k in 0..sb {
6213 if pos + k0 + k < start {
6214 continue;
6215 }
6216 self.nll_check_graph("batched score row", pos + k0 + k)?;
6217 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
6218 if let Some(mu) = self.logit_multiplier {
6219 for v in lg.iter_mut() {
6220 *v *= mu;
6221 }
6222 }
6223 if let Some(c) = self.final_softcap {
6227 for v in lg.iter_mut() {
6228 *v = c * (*v / c).tanh();
6229 }
6230 }
6231 if let Some(cm) = self.head_clusters.clone() {
6234 self.hierarchical_head_logprobs(
6235 &normed[k * hs..(k + 1) * hs],
6236 &cm,
6237 lg,
6238 );
6239 }
6240 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
6241 let target = ids[pos + k0 + k + 1] as usize;
6242 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6243 let lse: f64 = lg
6244 .iter()
6245 .map(|&v| ((v - max) as f64).exp())
6246 .sum::<f64>()
6247 .ln()
6248 + max as f64;
6249 nll += lse - lg[target] as f64;
6250 cnt += 1;
6251 if std::env::var("CMF_PPL_TRACE").is_ok() {
6252 let top = lg
6253 .iter()
6254 .enumerate()
6255 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6256 .map(|(i, _)| i)
6257 .unwrap_or(0);
6258 eprintln!(
6259 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
6260 pos + k0 + k,
6261 target,
6262 lse - lg[target] as f64,
6263 top,
6264 lg[target],
6265 lg[top]
6266 );
6267 }
6268 }
6269 k0 = k1;
6270 }
6271 pos = end;
6272 }
6273 return Ok((nll, cnt));
6274 }
6275 for pos in 0..ids.len().saturating_sub(1) {
6276 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6277 self.nll_check_graph("serial forward", pos)?;
6278 let out_of_band = self.graph_logits.take();
6286 if self.graph_head_required && out_of_band.is_none() {
6287 METAL_GRAPH_HEAD_MISS.fetch_add(
6288 1,
6289 std::sync::atomic::Ordering::Relaxed,
6290 );
6291 return Err(format!(
6292 "fused Metal graph head did not complete at NLL position {pos}"
6293 ));
6294 }
6295 if pos < start {
6296 continue;
6297 }
6298 let logits = match out_of_band {
6299 Some(lg) => lg,
6300 None => {
6301 let normed = inference::rms_norm(
6302 &hidden,
6303 &self.weights.final_norm,
6304 self.rms_eps,
6305 self.norm_style,
6306 );
6307 self.lm_head_forward(&normed)
6311 }
6312 };
6313 let target = ids[pos + 1] as usize;
6314 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6315 let lse: f64 = logits
6316 .iter()
6317 .map(|&v| ((v - max) as f64).exp())
6318 .sum::<f64>()
6319 .ln()
6320 + max as f64;
6321 let tok_nll = lse - logits[target] as f64;
6322 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6323 let top = logits
6324 .iter()
6325 .enumerate()
6326 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6327 .map(|(i, _)| i)
6328 .unwrap_or(0);
6329 eprintln!(
6330 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6331 logits[target], logits[top]
6332 );
6333 }
6334 nll += tok_nll;
6335 cnt += 1;
6336 }
6337 Ok((nll, cnt))
6338 })();
6339 self.nll_end();
6340 result
6341 }
6342
6343 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
6348 let normed = inference::rms_norm(
6349 hidden,
6350 &self.weights.final_norm,
6351 self.rms_eps,
6352 self.norm_style,
6353 );
6354 let mut logits = self.lm_head_forward(&normed);
6357 let target = target as usize;
6358 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6359 let lse: f64 = logits
6360 .iter()
6361 .map(|&v| ((v - max) as f64).exp())
6362 .sum::<f64>()
6363 .ln()
6364 + max as f64;
6365 let tok_nll = lse - logits[target] as f64;
6366 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6367 let top = logits
6368 .iter()
6369 .enumerate()
6370 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6371 .map(|(i, _)| i)
6372 .unwrap_or(0);
6373 eprintln!(
6374 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6375 logits[target], logits[top]
6376 );
6377 }
6378 attention::recycle_buf(&mut logits);
6379 tok_nll
6380 }
6381
6382 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
6400 self.nll_begin()?;
6405 let requested_prefix = (prefill > 0).then_some(prefill);
6406 self.o1_begin_with_prefix(requested_prefix);
6407 let n = ids.len().saturating_sub(1);
6408 let requested_start = prefill.min(n);
6409 let exact_end = if self.o1_active() {
6414 match requested_prefix {
6415 Some(requested) => self.o1_effective_boundary(requested),
6416 None => self
6417 .o1_cfg
6418 .as_ref()
6419 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
6420 }
6421 .unwrap_or(requested_start)
6422 .min(n)
6423 } else {
6424 requested_start
6425 };
6426 let mut nll = 0f64;
6427 let mut cnt = 0usize;
6428
6429 let mut pos = 0usize;
6433 if self.can_prefill_batched() {
6434 const CHUNK: usize = 128;
6435 while pos < exact_end {
6436 let end = (pos + CHUNK).min(exact_end);
6437 let hiddens = self.prefill_batch(&ids[pos..end], pos);
6438 if self
6439 .graph_failed
6440 .swap(false, std::sync::atomic::Ordering::Relaxed)
6441 {
6442 self.cancel
6443 .store(false, std::sync::atomic::Ordering::Relaxed);
6444 self.nll_end();
6445 return Err("GPU graph failed during O(1) NLL prefix".into());
6446 }
6447 for row in 0..end - pos {
6448 let score_pos = pos + row;
6449 if score_pos >= requested_start && score_pos < n {
6450 nll += self.nll_from_hidden(
6451 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
6452 ids[score_pos + 1],
6453 score_pos,
6454 );
6455 cnt += 1;
6456 }
6457 }
6458 pos = end;
6459 }
6460 } else {
6461 while pos < exact_end {
6462 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6463 if self
6464 .graph_failed
6465 .swap(false, std::sync::atomic::Ordering::Relaxed)
6466 {
6467 self.cancel
6468 .store(false, std::sync::atomic::Ordering::Relaxed);
6469 self.nll_end();
6470 return Err("GPU graph failed during O(1) NLL prefix".into());
6471 }
6472 if pos >= requested_start && pos < n {
6473 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6474 cnt += 1;
6475 }
6476 pos += 1;
6477 }
6478 }
6479 self.o1_seal_checked().map_err(|err| {
6480 self.nll_end();
6481 err
6482 })?;
6483
6484 let batch_k = std::env::var("CMF_BATCH_K")
6493 .ok()
6494 .and_then(|v| v.parse::<usize>().ok())
6495 .unwrap_or(0);
6496 let batch_admitted = batch_k > 0
6497 && self.can_prefill_batched()
6498 && self.o1_active()
6499 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6500 && (0..self.num_layers).all(|li| {
6501 let cache = &self.kv_cache.layers[self.phys_layer(li)];
6502 cache.o1.is_none() || cache.o1_views().is_some()
6503 });
6504 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6505 eprintln!(
6506 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
6507 batch_admitted,
6508 batch_k,
6509 n.saturating_sub(exact_end),
6510 );
6511 }
6512 let mut batch_completed = false;
6513 if batch_admitted && exact_end < n {
6514 let hs = self.hidden_size;
6515 let mut batch_pos = exact_end;
6516 while batch_pos < n {
6517 let end = (batch_pos + batch_k).min(n);
6518 let bk = end - batch_pos;
6519 let mut hiddens = vec![0.0f32; bk * hs];
6520 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
6521 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
6522 }
6523 let positions: Vec<usize> = (batch_pos..end).collect();
6524 let t_batch = std::time::Instant::now();
6525 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6526 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6527 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
6528 eprintln!(
6529 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6530 batch_pos,
6531 end.saturating_sub(1),
6532 bk as f64 / (ms / 1000.0),
6533 );
6534 }
6535 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
6536 self.nll_end();
6537 return Err(err);
6538 }
6539 match outcome {
6540 crate::gpu::BatchGraphOutcome::Completed => {
6541 batch_completed = true;
6542 for row in 0..bk {
6543 nll += self.nll_from_hidden(
6544 &hiddens[row * hs..(row + 1) * hs],
6545 ids[batch_pos + row + 1],
6546 batch_pos + row,
6547 );
6548 cnt += 1;
6549 }
6550 batch_pos = end;
6551 }
6552 crate::gpu::BatchGraphOutcome::Declined => {
6553 if batch_completed {
6554 self.nll_end();
6555 return Err(format!(
6556 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
6557 ));
6558 }
6559 break;
6560 }
6561 crate::gpu::BatchGraphOutcome::Failed => {
6562 self.nll_end();
6563 return Err(format!(
6564 "O(1) NLL batch graph failed after admission at position {batch_pos}"
6565 ));
6566 }
6567 }
6568 }
6569 if batch_completed && cnt == n.saturating_sub(requested_start) {
6570 self.nll_end();
6571 return Ok((nll, cnt));
6572 }
6573 }
6574
6575 for pos in exact_end..n {
6580 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6581 if self
6582 .graph_failed
6583 .swap(false, std::sync::atomic::Ordering::Relaxed)
6584 {
6585 self.cancel
6586 .store(false, std::sync::atomic::Ordering::Relaxed);
6587 self.nll_end();
6588 return Err(format!(
6589 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
6590 ));
6591 }
6592 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6593 cnt += 1;
6594 }
6595 self.nll_end();
6596 Ok((nll, cnt))
6597 }
6598
6599 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
6607 self.clear_sequence_state();
6608 let n = ids.len().saturating_sub(1);
6609 let mut correct = Vec::with_capacity(n);
6610 let mut pmax = Vec::with_capacity(n);
6611 for pos in 0..n {
6612 let emb = self.embed_single(ids[pos]);
6613 let hidden = self.forward_layers(&emb, pos, None);
6614 let normed = inference::rms_norm(
6615 &hidden,
6616 &self.weights.final_norm,
6617 self.rms_eps,
6618 self.norm_style,
6619 );
6620 let logits = self.lm_head_forward(&normed);
6624 let target = ids[pos + 1] as usize;
6625 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
6626 for (i, &v) in logits.iter().enumerate() {
6627 if v > mval {
6628 mval = v;
6629 amax = i;
6630 }
6631 }
6632 correct.push(amax == target);
6633 let row: Vec<f32> = temps
6634 .iter()
6635 .map(|&t| {
6636 let tt = t.max(1e-3);
6637 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
6638 1.0 / s.max(1e-12) })
6640 .collect();
6641 pmax.push(row);
6642 }
6643 self.clear_sequence_state();
6644 (correct, pmax)
6645 }
6646
6647 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
6654 if self.dyn_router.is_none() {
6655 return Ok((self.ppl_ids(ids)?, 0));
6656 }
6657 self.nll_begin()?;
6658 let saved_active = self.dyn_active;
6659 let mut router = self
6660 .dyn_router
6661 .take()
6662 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
6663 router.reset();
6664 self.dyn_phi_seen = 0;
6665 let _ = self.set_active_skill(None);
6666
6667 let result: Result<(f64, usize), String> = (|| {
6668 let mut nll = 0f64;
6669 let mut cnt = 0usize;
6670 for pos in 0..ids.len().saturating_sub(1) {
6671 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6672 self.nll_check_graph("dynamic serial forward", pos)?;
6673 let out_of_band = self.graph_logits.take();
6674 let mut logits = match out_of_band {
6675 Some(lg) => lg,
6676 None => {
6677 let normed = inference::rms_norm(
6678 &hidden,
6679 &self.weights.final_norm,
6680 self.rms_eps,
6681 self.norm_style,
6682 );
6683 self.lm_head_forward(&normed)
6687 }
6688 };
6689 let target = ids[pos + 1] as usize;
6690 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6691 let lse: f64 = logits
6692 .iter()
6693 .map(|&v| ((v - max) as f64).exp())
6694 .sum::<f64>()
6695 .ln()
6696 + max as f64;
6697 let tok_nll = lse - logits[target] as f64;
6698 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6699 let top = logits
6700 .iter()
6701 .enumerate()
6702 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6703 .map(|(i, _)| i)
6704 .unwrap_or(0);
6705 eprintln!(
6706 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6707 logits[target], logits[top]
6708 );
6709 }
6710 nll += tok_nll;
6711 cnt += 1;
6712 attention::recycle_buf(&mut logits);
6713 let phi = self.dyn_phi_ema.clone();
6715 if let Some(new_active) = router.step(&phi, pos) {
6716 let _ = self.set_active_skill(new_active);
6717 }
6718 }
6719 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
6720 })();
6721
6722 let _ = self.set_active_skill(saved_active);
6725 self.dyn_router = Some(router);
6726 self.nll_end();
6727 result
6728 }
6729
6730 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
6732 self.clear_sequence_state();
6733 let mut acc = vec![0f32; self.hidden_size];
6734 for (pos, &id) in ids.iter().enumerate() {
6735 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
6736 for (a, v) in acc.iter_mut().zip(&h) {
6737 *a += v;
6738 }
6739 }
6740 let n = ids.len().max(1) as f32;
6741 for a in acc.iter_mut() {
6742 *a /= n;
6743 }
6744 self.clear_sequence_state();
6745 acc
6746 }
6747
6748 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
6754 self.prefill_batch_masked(ids, start_pos, None)
6755 }
6756
6757 fn prefill_batch_masked(
6763 &mut self,
6764 ids: &[u32],
6765 start_pos: usize,
6766 task_mask: Option<&TaskMask>,
6767 ) -> Vec<f32> {
6768 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
6769 }
6770
6771 fn prefill_batch_span(
6777 &mut self,
6778 input: PrefillIn<'_>,
6779 start_pos: usize,
6780 task_mask: Option<&TaskMask>,
6781 from: usize,
6782 upto_excl: usize,
6783 ) -> Vec<f32> {
6784 let hs = self.hidden_size;
6785 let b = match input {
6786 PrefillIn::Ids(ids) => ids.len(),
6787 PrefillIn::Hidden(hb) => hb.len() / hs,
6788 };
6789 let upto_excl = upto_excl.min(self.num_layers);
6790 let mut h: Vec<f32>;
6794 let mut h_ready;
6795 match input {
6796 PrefillIn::Ids(_) => {
6797 h = vec![0.0; b * hs];
6798 h_ready = false;
6799 }
6800 PrefillIn::Hidden(hb) => {
6801 h = hb.to_vec();
6802 h_ready = true;
6803 }
6804 }
6805 let fill_h = |h: &mut Vec<f32>, me: &Self| {
6806 if let PrefillIn::Ids(ids) = input {
6807 for (bi, &id) in ids.iter().enumerate() {
6808 let e = me.embed_single(id);
6809 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
6810 }
6811 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6812 if let Ok(t) = tp.parse::<usize>() {
6813 if t >= start_pos && t < start_pos + ids.len() {
6814 let bi = t - start_pos;
6815 let row = &h[bi * hs..(bi + 1) * hs];
6816 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6817 eprintln!(
6818 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
6819 ids[bi],
6820 row[0],
6821 row[1],
6822 ids.len(),
6823 &ids[..ids.len().min(8)]
6824 );
6825 }
6826 }
6827 }
6828 }
6829 };
6830 let (_nkv, _hd, _rd, eps) = (
6831 self.num_kv_heads,
6832 self.head_dim,
6833 self.rotary_dim,
6834 self.rms_eps,
6835 );
6836 let pool = self.pool.clone();
6837 let norm_style = self.norm_style;
6838 let automatic_gpu_prefix = self.automatic_gpu_prefix();
6839
6840 #[cfg(target_os = "macos")]
6841 let mut chunk_skip_until = 0usize;
6842 for li in from..upto_excl {
6843 let _capacity_tail = automatic_gpu_prefix
6844 .filter(|&prefix| li >= prefix)
6845 .map(|_| crate::gpu::enter_cpu_scope());
6846 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
6853 if task_mask.is_none() {
6854 if li < chunk_skip_until {
6855 continue;
6856 }
6857 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
6863 fill_h(&mut h, self);
6864 h_ready = true;
6865 }
6866 let ids_for_embed = match input {
6867 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
6868 PrefillIn::Hidden(_) => None,
6869 };
6870 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
6871 if end > li {
6872 h_ready = true;
6873 chunk_skip_until = end;
6874 if self.is_loop_end(end - 1) && end < self.num_layers {
6877 for bi in 0..b {
6878 let normed = inference::rms_norm(
6879 &h[bi * hs..(bi + 1) * hs],
6880 &self.weights.final_norm,
6881 eps,
6882 norm_style,
6883 );
6884 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6885 }
6886 }
6887 continue;
6888 }
6889 }
6890 if !h_ready {
6891 fill_h(&mut h, self);
6892 h_ready = true;
6893 }
6894 let lw = &self.weights.layers[self.phys_layer(li)];
6895 match &lw.attn {
6897 AttnKind::Kda(w) => {
6898 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
6900 let mut normed = vec![0.0f32; b * hs];
6901 for bi in 0..b {
6902 inference::rms_norm_into(
6903 &h[bi * hs..(bi + 1) * hs],
6904 &lw.input_norm,
6905 eps,
6906 norm_style,
6907 &mut normed[bi * hs..(bi + 1) * hs],
6908 );
6909 }
6910 let attn = crate::linear_core::kda_forward_batch(
6911 &normed,
6912 b,
6913 w,
6914 &cfg,
6915 &mut self.kv_cache.layers[li].linear_state,
6916 pool.as_deref(),
6917 );
6918 for (dst, &a) in h.iter_mut().zip(&attn) {
6919 *dst += a;
6920 }
6921 }
6922 AttnKind::LinearGdn(w) => {
6923 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6925 let mut normed = vec![0.0f32; b * hs];
6926 for bi in 0..b {
6927 let r = inference::rms_norm(
6928 &h[bi * hs..(bi + 1) * hs],
6929 &lw.input_norm,
6930 eps,
6931 norm_style,
6932 );
6933 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6934 }
6935 let attn = crate::linear_core::gdn_forward_batch(
6936 &normed,
6937 b,
6938 w,
6939 &cfg,
6940 &mut self.kv_cache.layers[li].linear_state,
6941 pool.as_deref(),
6942 );
6943 for (dst, &a) in h.iter_mut().zip(&attn) {
6944 *dst += a;
6945 }
6946 }
6947 AttnKind::ShortConv(w) => {
6948 let cfg = self
6951 .short_conv_cfg
6952 .expect("short-conv layer without short_conv_cfg");
6953 let mut normed = vec![0.0f32; b * hs];
6954 for bi in 0..b {
6955 inference::rms_norm_into(
6956 &h[bi * hs..(bi + 1) * hs],
6957 &lw.input_norm,
6958 eps,
6959 norm_style,
6960 &mut normed[bi * hs..(bi + 1) * hs],
6961 );
6962 }
6963 let attn = short_conv_forward_batch(
6964 &normed,
6965 b,
6966 w,
6967 &cfg,
6968 &mut self.kv_cache.layers[li].linear_state,
6969 pool.as_deref(),
6970 );
6971 for (dst, &a) in h.iter_mut().zip(&attn) {
6972 *dst += a;
6973 }
6974 }
6975 AttnKind::Mla(w) => {
6976 let inv_freq_l = self.layer_inv_freq(li);
6979 let rs = self.layer_rope_scale(li);
6980 let mut normed = vec![0.0f32; hs];
6981 for bi in 0..b {
6982 inference::rms_norm_into(
6983 &h[bi * hs..(bi + 1) * hs],
6984 &lw.input_norm,
6985 eps,
6986 norm_style,
6987 &mut normed,
6988 );
6989 let ao = mla_attention(
6990 w,
6991 &normed,
6992 &mut self.kv_cache.layers[li],
6993 start_pos + bi,
6994 &inv_freq_l,
6995 rs,
6996 eps,
6997 pool.as_deref(),
6998 );
6999 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
7000 *dst += a;
7001 }
7002 }
7003 }
7004 AttnKind::Full {
7005 wq,
7006 wk,
7007 wv,
7008 wo,
7009 q_norm,
7010 k_norm,
7011 output_gate,
7012 softplus_gate,
7013 bias,
7014 } => {
7015 let mut normed = vec![0.0f32; b * hs];
7019 for bi in 0..b {
7020 inference::rms_norm_into(
7021 &h[bi * hs..(bi + 1) * hs],
7022 &lw.input_norm,
7023 eps,
7024 norm_style,
7025 &mut normed[bi * hs..(bi + 1) * hs],
7026 );
7027 }
7028 let inv_freq_l = self.layer_inv_freq(li);
7029 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7030 let cfg = QwenAttnCfg {
7031 num_heads: self.layer_num_heads(li),
7032 num_kv_heads: nkv_l,
7033 head_dim: hd_l,
7034 hidden_size: hs,
7035 position: start_pos,
7036 inv_freq: &inv_freq_l,
7037 rotary_dim: rd_l,
7038 scale: self.attn_scale,
7039 softcap: self.attn_softcap,
7040 window: self.layer_window(li),
7041 v_norm: self.attn_v_norm,
7042 q_norm: q_norm.as_deref(),
7043 k_norm: k_norm.as_deref(),
7044 output_gate: *output_gate,
7045 softplus_gate: softplus_gate
7046 .as_ref()
7047 .map(|(gate, per_head)| (gate, *per_head)),
7048 rope_scale: self.layer_rope_scale(li),
7049 bias: bias
7050 .as_ref()
7051 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7052 rms_eps: eps,
7053 norm_style,
7054 pool: pool.as_deref(),
7055 };
7056 let mut attn = attention::qwen_attention_batch(
7057 &normed,
7058 b,
7059 wq,
7060 wk,
7061 wv,
7062 wo,
7063 &mut self.kv_cache.layers[li],
7064 &cfg,
7065 );
7066 if let Some(w) = &lw.attn_out_norm {
7067 for bi in 0..b {
7068 inference::rms_norm_into(
7069 &attn[bi * hs..(bi + 1) * hs],
7070 w,
7071 eps,
7072 norm_style,
7073 &mut normed[bi * hs..(bi + 1) * hs],
7074 );
7075 }
7076 attn.copy_from_slice(&normed);
7077 }
7078 for (dst, &a) in h.iter_mut().zip(&attn) {
7079 *dst += a;
7080 }
7081 }
7082 AttnKind::Linear(w) => {
7083 for bi in 0..b {
7084 let normed = inference::rms_norm(
7085 &h[bi * hs..(bi + 1) * hs],
7086 &lw.input_norm,
7087 eps,
7088 norm_style,
7089 );
7090 vmf_phase_forward(
7091 &normed,
7092 w,
7093 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
7094 &mut self.kv_cache.layers[li].linear_state,
7095 pool.as_deref(),
7096 )
7097 .iter()
7098 .enumerate()
7099 .for_each(|(i, &a)| h[bi * hs + i] += a);
7100 }
7101 }
7102 }
7103
7104 let lw = &self.weights.layers[self.phys_layer(li)];
7106 let mut post = vec![0.0f32; b * hs];
7107 for bi in 0..b {
7108 let r =
7109 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
7110 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7111 }
7112 let mask_row = task_mask
7115 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
7116 .and_then(|m| m.ffn_masks.get(li))
7117 .map(|v| v.as_slice());
7118 let mut ffn = match &lw.ffn {
7119 FfnKind::Dense(d) if !d.segs.is_empty() => {
7120 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
7121 }
7122 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
7123 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
7124 FfnKind::DenseMoe(dm) => {
7127 let mut out = vec![0.0f32; b * hs];
7128 for bi in 0..b {
7129 let r = dense_moe_ffn(
7130 dm,
7131 &post[bi * hs..(bi + 1) * hs],
7132 &h[bi * hs..(bi + 1) * hs],
7133 eps,
7134 norm_style,
7135 pool.as_deref(),
7136 );
7137 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7138 }
7139 out
7140 }
7141 };
7142 if let Some(w) = &lw.ffn_out_norm {
7143 for bi in 0..b {
7144 inference::rms_norm_into(
7145 &ffn[bi * hs..(bi + 1) * hs],
7146 w,
7147 eps,
7148 norm_style,
7149 &mut post[bi * hs..(bi + 1) * hs],
7150 );
7151 }
7152 ffn.copy_from_slice(&post);
7153 }
7154 for (dst, &f) in h.iter_mut().zip(&ffn) {
7155 *dst += f;
7156 }
7157 if let Some(sc) = lw.layer_scale {
7158 for v in h.iter_mut() {
7159 *v *= sc;
7160 }
7161 }
7162 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
7163 if let Ok(t) = tp.parse::<usize>() {
7164 if t >= start_pos && t < start_pos + b {
7165 let bi = t - start_pos;
7166 let row = &h[bi * hs..(bi + 1) * hs];
7167 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
7168 eprintln!(
7169 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
7170 row[0], row[1]
7171 );
7172 }
7173 }
7174 }
7175 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
7179 let row = &h[(b - 1) * hs..b * hs];
7180 let rms =
7181 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
7182 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
7183 eprintln!(
7184 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
7185 match &self.weights.layers[self.phys_layer(li)].attn {
7186 AttnKind::LinearGdn(_) => "gdn",
7187 AttnKind::Linear(_) => "vmf",
7188 AttnKind::ShortConv(_) => "conv",
7189 _ => "attn",
7190 },
7191 match &lw.ffn {
7192 FfnKind::Moe(_) => "moe",
7193 FfnKind::Dense(_) => "dense",
7194 FfnKind::DenseMoe(_) => "dense+moe",
7195 },
7196 );
7197 }
7198 if self.is_loop_end(li) && li + 1 < self.num_layers {
7200 for bi in 0..b {
7201 let normed = inference::rms_norm(
7202 &h[bi * hs..(bi + 1) * hs],
7203 &self.weights.final_norm,
7204 eps,
7205 norm_style,
7206 );
7207 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7208 }
7209 }
7210 if std::env::var("CMF_TRACE_H").is_ok() {
7211 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
7212 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
7213 eprintln!(
7214 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
7215 lw.layer_scale
7216 );
7217 }
7218 }
7219 crate::gpu::set_layer(-1); self.o1_progress();
7225 h
7226 }
7227
7228 fn embed_single(&self, id: u32) -> Vec<f32> {
7230 let mut out = vec![0.0f32; self.hidden_size];
7231 if (id as usize) < self.weights.embed_tokens.rows() {
7232 self.weights.embed_tokens.row_f32(id as usize, &mut out);
7233 }
7234 if self.embed_multiplier != 1.0 {
7235 for v in out.iter_mut() {
7236 *v *= self.embed_multiplier;
7237 }
7238 }
7239 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
7243 let mut v = vec![0.0f32; self.hidden_size.max(1)];
7244 v[0] = id as f32;
7245 return v;
7246 }
7247 if let Some(b) = &self.g3n {
7250 return b.0.extend_embedding(id, &out, self.pool.as_deref());
7251 }
7252 out
7253 }
7254
7255 #[cfg(target_os = "macos")]
7261 fn chunk_run_gpu(
7262 &mut self,
7263 li0: usize,
7264 h: &mut [f32],
7265 b: usize,
7266 pos0: usize,
7267 embed_ids: Option<&[u32]>,
7268 cap: usize,
7269 ) -> usize {
7270 if !crate::gpu::enabled_here()
7274 || std::env::var("CMF_GPU_CHUNK")
7275 .map(|v| v == "0")
7276 .unwrap_or(false)
7277 || b < 32
7278 || self.swa.is_some()
7279 || self.global_attn.is_some()
7280 || self.o1_active()
7283 || self.attn_v_norm
7284 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
7285 {
7286 return li0;
7287 }
7288 let Some(model) = self.model.clone() else {
7289 return li0;
7290 };
7291 let inv_freq = self.inv_freq.clone();
7292 let (nh, nkv, hd, hs) = (
7293 self.num_heads,
7294 self.num_kv_heads,
7295 self.head_dim,
7296 self.hidden_size,
7297 );
7298 let loop_end = if self.loop_final_norm {
7302 ((li0 / self.physical_layers) + 1) * self.physical_layers
7303 } else {
7304 self.num_layers
7305 };
7306 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
7307 let mut stored_at: Vec<usize> = Vec::new();
7308 for li in li0..self.num_layers.min(loop_end).min(cap) {
7309 let lw = &self.weights.layers[self.phys_layer(li)];
7310 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7311 break;
7312 }
7313 let AttnKind::Full {
7314 wq,
7315 wk,
7316 wv,
7317 wo,
7318 q_norm,
7319 k_norm,
7320 output_gate: false,
7321 softplus_gate: None,
7322 bias,
7323 } = &lw.attn
7324 else {
7325 break;
7326 };
7327 let FfnKind::Dense(d) = &lw.ffn else { break };
7328 if d.act != Act::Silu || !d.segs.is_empty() {
7329 break;
7330 }
7331 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
7336 t.q8_row_parts()
7337 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7338 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7339 }
7340 let parts = (
7341 cw(wq),
7342 cw(wk),
7343 cw(wv),
7344 cw(wo),
7345 cw(&d.gate_proj),
7346 cw(&d.up_proj),
7347 cw(&d.down_proj),
7348 );
7349 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
7350 else {
7351 break;
7352 };
7353 let layer = &self.kv_cache.layers[li];
7354 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
7355 break;
7356 }
7357 stored_at.push(layer.head_len(0));
7358 layers.push(crate::gpu_metal::ChunkLayer {
7359 model: &model,
7360 kv_id: self.graph_kv_id,
7361 layer: li,
7362 wq: pq,
7363 wk: pk,
7364 wv: pv,
7365 wo: po,
7366 gate: pg,
7367 up: pu,
7368 down: pd,
7369 input_norm: &lw.input_norm,
7370 post_norm: &lw.post_norm,
7371 bias: bias
7372 .as_ref()
7373 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
7374 q_norm: q_norm.as_deref(),
7375 k_norm: k_norm.as_deref(),
7376 inv_freq: &inv_freq,
7377 rd: self.rotary_dim,
7378 nh,
7379 nkv,
7380 hd,
7381 hs,
7382 inter: d.gate_proj.rows(),
7383 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
7384 eps: self.rms_eps as f32,
7385 });
7386 }
7387 if layers.is_empty() {
7388 return li0;
7389 }
7390 let row = nkv * hd;
7391 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
7392 .iter()
7393 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
7394 .collect();
7395 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
7396 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
7397 let li = layers[i].layer;
7398 let layer = &self.kv_cache.layers[li];
7399 io.push(crate::gpu_metal::ChunkIo {
7400 cpu_stored: stored_at[i],
7401 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
7402 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
7403 out_k: ok,
7404 out_v: ov,
7405 imp: oi,
7406 });
7407 }
7408 let n_run = layers.len();
7409 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
7410 let ep = embed_ids.and_then(|ids| {
7413 self.weights
7414 .embed_tokens
7415 .q8_row_parts()
7416 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
7417 idx,
7418 rows,
7419 row_scale: rs,
7420 ids,
7421 mult: self.embed_multiplier,
7422 })
7423 });
7424 if embed_ids.is_some() && ep.is_none() {
7425 return li0;
7426 }
7427 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
7428 return li0;
7429 }
7430 drop(io);
7431 drop(layers);
7432 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
7435 let li = li0 + i;
7436 let layer = &mut self.kv_cache.layers[li];
7437 for bi in 0..b {
7438 layer.append(
7439 &ok[bi * row..(bi + 1) * row],
7440 &ov[bi * row..(bi + 1) * row],
7441 &[],
7442 );
7443 }
7444 layer.accumulate_imp(oi);
7445 }
7446 last
7447 }
7448
7449 fn layer_is_local(&self, li: usize) -> bool {
7452 if let Some(layers) = &self.sliding_layers {
7453 return layers.get(li).copied().unwrap_or(false);
7454 }
7455 match self.swa {
7456 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
7457 None => false,
7458 }
7459 }
7460
7461 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
7464 if self.layer_is_local(li) {
7465 if let Some(f) = &self.inv_freq_local {
7466 return f.clone();
7467 }
7468 } else if let Some(f) = &self.inv_freq_global {
7469 return f.clone();
7470 }
7471 self.inv_freq.clone()
7472 }
7473
7474 fn layer_window(&self, li: usize) -> Option<usize> {
7476 self.swa
7477 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
7478 }
7479
7480 fn layer_num_heads(&self, li: usize) -> usize {
7481 self.attention_heads_per_layer
7482 .as_ref()
7483 .and_then(|v| v.get(li).copied())
7484 .unwrap_or(self.num_heads)
7485 }
7486
7487 fn layer_rope_scale(&self, li: usize) -> f32 {
7488 if self.layer_is_local(li) {
7489 self.rope_scale_local
7490 } else {
7491 self.rope_scale
7492 }
7493 }
7494
7495 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
7498 if !self.layer_is_local(li) {
7499 if let Some((ghd, gkv)) = self.global_attn {
7500 return (gkv, ghd, ghd);
7501 }
7502 }
7503 (
7504 self.num_kv_heads,
7505 self.head_dim,
7506 if self.layer_is_local(li) {
7507 self.rotary_dim_local.unwrap_or(self.rotary_dim)
7508 } else {
7509 self.rotary_dim
7510 },
7511 )
7512 }
7513
7514 fn forward_layers(
7516 &mut self,
7517 hidden: &[f32],
7518 position: usize,
7519 task_mask: Option<&TaskMask>,
7520 ) -> Vec<f32> {
7521 let out = self.forward_layers_upto(hidden, position, task_mask, None);
7522 self.o1_progress();
7523 out
7524 }
7525
7526 pub fn embed_id(&self, id: u32) -> Vec<f32> {
7534 self.embed_single(id)
7535 }
7536
7537 pub fn split_supported(&self) -> Result<(), String> {
7541 if self.dsv4.is_some() {
7542 return Err(
7543 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
7544 );
7545 }
7546 if self.dsv41.is_some() {
7547 return Err(
7548 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
7549 .into(),
7550 );
7551 }
7552 if self.qwen4_exp.is_some() {
7553 return Err(
7554 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
7555 );
7556 }
7557 if self.g3n.is_some() {
7558 return Err(
7559 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
7560 );
7561 }
7562 Ok(())
7563 }
7564
7565 pub fn forward_span(
7570 &mut self,
7571 hidden: &[f32],
7572 position: usize,
7573 from: usize,
7574 upto: usize,
7575 task_mask: Option<&TaskMask>,
7576 ) -> Result<Vec<f32>, String> {
7577 self.split_supported()?;
7578 if from > upto || upto >= self.num_layers {
7579 return Err(format!(
7580 "forward_span: layer range {from}..={upto} outside 0..{}",
7581 self.num_layers
7582 ));
7583 }
7584 if hidden.len() != self.hidden_size {
7585 return Err(format!(
7586 "forward_span: hidden len {} ≠ hidden_size {}",
7587 hidden.len(),
7588 self.hidden_size
7589 ));
7590 }
7591 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
7592 self.o1_progress();
7593 if self
7594 .graph_failed
7595 .swap(false, std::sync::atomic::Ordering::Relaxed)
7596 {
7597 self.cancel
7598 .store(false, std::sync::atomic::Ordering::Relaxed);
7599 self.clear_sequence_state();
7600 return Err("forward_span: deferred O(1) transition failed".into());
7601 }
7602 Ok(out)
7603 }
7604
7605 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
7608 let normed = inference::rms_norm(
7609 hidden,
7610 &self.weights.final_norm,
7611 self.rms_eps,
7612 self.norm_style,
7613 );
7614 self.lm_head_forward(&normed)
7615 }
7616
7617 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
7619 sampler::sample_with_scratch(
7620 logits,
7621 &self.sampler_config,
7622 past_tokens,
7623 &mut self.rng,
7624 &mut self.sampler_scratch,
7625 )
7626 }
7627
7628 pub fn reset_session(&mut self) {
7630 self.clear_sequence_state();
7631 }
7632
7633 pub fn prefill_span_ids(
7639 &mut self,
7640 ids: &[u32],
7641 start_pos: usize,
7642 upto: usize,
7643 task_mask: Option<&TaskMask>,
7644 ) -> Result<Vec<f32>, String> {
7645 self.split_supported()?;
7646 if upto >= self.num_layers {
7647 return Err(format!(
7648 "prefill_span_ids: upto {upto} outside 0..{}",
7649 self.num_layers
7650 ));
7651 }
7652 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7656 let out =
7657 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
7658 self.check_o1_progress_failure("prefill_span_ids")?;
7659 Ok(out)
7660 } else {
7661 let hs = self.hidden_size;
7662 let mut out = Vec::with_capacity(ids.len() * hs);
7663 for (i, &id) in ids.iter().enumerate() {
7664 let emb = self.embed_id(id);
7665 out.extend_from_slice(&self.forward_span(
7666 &emb,
7667 start_pos + i,
7668 0,
7669 upto,
7670 task_mask,
7671 )?);
7672 }
7673 Ok(out)
7674 }
7675 }
7676
7677 pub fn prefill_span_hidden(
7680 &mut self,
7681 hidden: &[f32],
7682 start_pos: usize,
7683 from: usize,
7684 upto: usize,
7685 task_mask: Option<&TaskMask>,
7686 ) -> Result<Vec<f32>, String> {
7687 self.split_supported()?;
7688 let hs = self.hidden_size;
7689 if hidden.is_empty() || hidden.len() % hs != 0 {
7690 return Err(format!(
7691 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
7692 hidden.len()
7693 ));
7694 }
7695 if from > upto || upto >= self.num_layers {
7696 return Err(format!(
7697 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
7698 self.num_layers
7699 ));
7700 }
7701 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7702 let out = self.prefill_batch_span(
7703 PrefillIn::Hidden(hidden),
7704 start_pos,
7705 task_mask,
7706 from,
7707 upto + 1,
7708 );
7709 self.check_o1_progress_failure("prefill_span_hidden")?;
7710 Ok(out)
7711 } else {
7712 let b = hidden.len() / hs;
7713 let mut out = Vec::with_capacity(hidden.len());
7714 for i in 0..b {
7715 let h = self.forward_span(
7716 &hidden[i * hs..(i + 1) * hs],
7717 start_pos + i,
7718 from,
7719 upto,
7720 task_mask,
7721 )?;
7722 out.extend_from_slice(&h);
7723 }
7724 Ok(out)
7725 }
7726 }
7727
7728 fn try_token_graph_wgpu(
7732 &self,
7733 hidden: &[f32],
7734 position: usize,
7735 logits_out: &mut Vec<f32>,
7736 layers_run: &mut usize,
7737 ) -> Option<Result<Vec<f32>, ()>> {
7738 self.try_token_graph_wgpu_steps(
7739 hidden,
7740 position,
7741 logits_out,
7742 1,
7743 None,
7744 Some(layers_run),
7745 0,
7746 self.num_layers,
7747 )
7748 }
7749
7750 fn try_token_graph_wgpu_span(
7754 &self,
7755 hidden: &[f32],
7756 position: usize,
7757 logits_out: &mut Vec<f32>,
7758 from: usize,
7759 upto_excl: usize,
7760 layers_run: &mut usize,
7761 ) -> Option<Result<Vec<f32>, ()>> {
7762 self.try_token_graph_wgpu_steps(
7763 hidden,
7764 position,
7765 logits_out,
7766 1,
7767 None,
7768 Some(layers_run),
7769 from,
7770 upto_excl,
7771 )
7772 }
7773
7774 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
7778 if self.o1_active() || self.attn_softcap > 0.0 {
7779 return None;
7780 }
7781 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
7782 if !graph_on || crate::gpu::graph_unsupported() {
7783 return None;
7790 }
7791 let emb = self.embed_single(t_next);
7792 let mut lg = Vec::new();
7793 let mut ids = Vec::new();
7794 match self.try_token_graph_wgpu_steps(
7795 &emb,
7796 position,
7797 &mut lg,
7798 k,
7799 Some(&mut ids),
7800 None,
7801 0,
7802 self.num_layers,
7803 ) {
7804 Some(Ok(_)) => {}
7805 Some(Err(())) => {
7806 self.graph_failed
7811 .store(true, std::sync::atomic::Ordering::Relaxed);
7812 return None;
7813 }
7814 None => return None,
7815 }
7816 (ids.len() == k).then_some(ids)
7817 }
7818
7819 fn try_token_graph_wgpu_steps(
7823 &self,
7824 hidden: &[f32],
7825 position: usize,
7826 logits_out: &mut Vec<f32>,
7827 steps: usize,
7828 ids_out: Option<&mut Vec<u32>>,
7829 layers_run: Option<&mut usize>,
7830 from: usize,
7831 upto_excl: usize,
7832 ) -> Option<Result<Vec<f32>, ()>> {
7833 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
7836 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
7837 return None;
7841 }
7842 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
7847 .map(|li| {
7848 if !o1_gpu {
7849 return None;
7850 }
7851 self.kv_cache.layers[self.phys_layer(li)].o1_views()
7852 })
7853 .collect();
7854 if self.o1_active() && o1_gpu {
7855 let want: usize = (from..upto_excl)
7858 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
7859 .count();
7860 let have = o1_views.iter().filter(|v| v.is_some()).count();
7861 if want == 0 || have != want {
7862 use std::sync::atomic::{AtomicUsize, Ordering};
7872 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
7873 let code = have * 1000 + want;
7874 if LAST.swap(code, Ordering::Relaxed) != code {
7875 tracing::warn!(
7876 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
7877 );
7878 }
7879 return None;
7880 }
7881 }
7882 let nh = self.num_heads;
7883 let (nkv, hd, rd) = self.layer_geom(0);
7884 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7885 let mut layers = Vec::with_capacity(upto_excl - from);
7886 let mut model = None;
7887 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
7888 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7889 if let Some((m, i, kind, rs)) = t
7890 .graph_weight()
7891 .or_else(|| t.graph_weight_descriptor())
7892 {
7893 let name = &m.tensors[i].name;
7894 let prism = if crate::prism::is_inverse_embedding(m, name) {
7895 crate::gpu::GraphPrismOp::InverseEmbedding
7896 } else if crate::prism::is_forward_weight(m, name) {
7897 crate::gpu::GraphPrismOp::Forward
7898 } else {
7899 crate::gpu::GraphPrismOp::None
7900 };
7901 return Some(crate::gpu::GraphW {
7902 idx: i,
7903 kind,
7904 row_scale: rs,
7905 data: &[],
7906 prism,
7907 affine: crate::prism::is_affine_target(m, name),
7908 });
7909 }
7910 match t.as_f32() {
7912 Some(d) => Some(crate::gpu::GraphW {
7913 idx: 0,
7914 kind: 4,
7915 row_scale: &[],
7916 data: d,
7917 prism: crate::gpu::GraphPrismOp::None,
7918 affine: false,
7919 }),
7920 None => {
7921 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
7922 eprintln!("batch graph: weight has no graph/f32 representation");
7923 }
7924 None
7925 }
7926 }
7927 }
7928 for li in from..upto_excl {
7929 let lw = &self.weights.layers[self.phys_layer(li)];
7930 if dbg {
7931 let ak = match &lw.attn {
7932 AttnKind::Mla(_) => "Mla".into(),
7933 AttnKind::Full {
7934 output_gate, bias, ..
7935 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
7936 AttnKind::LinearGdn(_) => "LinearGdn".into(),
7937 AttnKind::Kda(_) => "Kda".into(),
7938 AttnKind::Linear(_) => "Linear".into(),
7939 AttnKind::ShortConv(_) => "ShortConv".into(),
7940 };
7941 let fk = match &lw.ffn {
7942 FfnKind::Dense(_) => "Dense",
7943 FfnKind::Moe(_) => "Moe",
7944 FfnKind::DenseMoe(_) => "DenseMoe",
7945 };
7946 eprintln!("graph L{li}: attn={ak} ffn={fk}");
7947 }
7948 let gffn = match &lw.ffn {
7949 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7953 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7954 gate: gw(&d.gate_proj)?,
7955 up: gw(&d.up_proj)?,
7956 down: gw(&d.down_proj)?,
7957 },
7958 FfnKind::Moe(m) => {
7959 if m.route_tau.is_some()
7966 || m.mask.is_some()
7967 || (m.routed_scaling - 1.0).abs() > 1e-9
7968 {
7969 return None;
7970 }
7971 let shared = m.shared.as_ref();
7972 let has_shared = shared.is_some();
7973 let sgate = match shared {
7974 Some((_, sg)) => gw(sg.as_ref()?)?,
7975 None => gw(&m.router)?,
7978 };
7979 let router = gw(&m.router)?;
7980 if router.prism != crate::gpu::GraphPrismOp::None
7986 || sgate.prism != crate::gpu::GraphPrismOp::None
7987 || router.affine
7988 || sgate.affine
7989 {
7990 tracing::warn!(
7991 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
7992 );
7993 return None;
7994 }
7995 let inter = m.experts.first()?.gate_proj.rows();
7996 let mut experts = Vec::with_capacity(m.experts.len() + 1);
7997 let mut q4tp: Option<bool> = None;
8000 let mut gu_q2: Option<bool> = None;
8003 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
8004 if !matches!(e.act, Act::Silu)
8005 || e.gate_proj.rows() != inter
8006 || e.up_proj.rows() != inter
8007 {
8008 return None;
8009 }
8010 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
8015 let Some((em, ei, _, _)) = expert_weight
8016 .graph_weight()
8017 .or_else(|| expert_weight.graph_weight_descriptor())
8018 else {
8019 return None;
8020 };
8021 let name = &em.tensors[ei].name;
8022 if crate::prism::is_forward_weight(em, name)
8023 || crate::prism::is_inverse_embedding(em, name)
8024 || crate::prism::is_affine_target(em, name)
8025 {
8026 tracing::warn!(
8027 "resident MoE declined: expert Prism/affine transform is not implemented"
8028 );
8029 return None;
8030 }
8031 }
8032 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8033 Some((mm, gi)) => (
8034 mm,
8035 gi,
8036 e.up_proj.mapped_q4t()?.1,
8037 e.down_proj.mapped_q4t()?.1,
8038 false,
8039 false,
8040 ),
8041 None => match e.gate_proj.mapped_q2tp() {
8042 Some((mm, gi)) => (
8043 mm,
8044 gi,
8045 e.up_proj.mapped_q2tp()?.1,
8046 e.down_proj.mapped_q4tp()?.1,
8047 true,
8048 true,
8049 ),
8050 None => {
8051 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8052 (
8053 mm,
8054 gi,
8055 e.up_proj.mapped_q4tp()?.1,
8056 e.down_proj.mapped_q4tp()?.1,
8057 true,
8058 false,
8059 )
8060 }
8061 },
8062 };
8063 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
8064 {
8065 tracing::warn!(
8071 "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."
8072 );
8073 return None;
8074 }
8075 model.get_or_insert_with(|| mm.clone());
8076 experts.push((gi, ui, di));
8077 }
8078 crate::gpu::GraphFfn::Moe {
8079 router,
8080 shared_gate: sgate,
8081 experts,
8082 n_exp: m.experts.len(),
8083 top_k: std::env::var("CMF_TOPK_PROBE")
8089 .ok()
8090 .and_then(|v| v.parse::<usize>().ok())
8091 .filter(|k| *k > 0 && *k <= m.top_k)
8092 .unwrap_or(m.top_k),
8093 inter,
8094 norm_topk: m.norm_topk_prob,
8095 q4tp: q4tp?,
8096 gu_q2: gu_q2.unwrap_or(false),
8097 sigmoid: m.router_sigmoid,
8098 bias: m.expert_bias.as_deref(),
8099 has_shared,
8100 }
8101 }
8102 };
8103 let attn = match &lw.attn {
8104 AttnKind::Full {
8105 wq,
8106 wk,
8107 wv,
8108 wo,
8109 q_norm,
8110 k_norm,
8111 output_gate,
8112 softplus_gate,
8113 bias,
8114 } => {
8115 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
8116 return None;
8117 }
8118 let (m, _, _, _) = wq
8119 .graph_weight()
8120 .or_else(|| wq.graph_weight_descriptor())?;
8121 model = Some(m.clone());
8122 crate::gpu::GraphAttn::Full {
8123 wq: gw(wq)?,
8124 wk: gw(wk)?,
8125 wv: gw(wv)?,
8126 wo: gw(wo)?,
8127 q_norm: q_norm.as_deref(),
8128 k_norm: k_norm.as_deref(),
8129 bias: bias
8130 .as_ref()
8131 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8132 output_gate: *output_gate,
8133 cpu_k: self.kv_cache.layers[li].k_heads(),
8134 cpu_v: self.kv_cache.layers[li].v_heads(),
8135 }
8136 }
8137 AttnKind::LinearGdn(w) => {
8138 let cfg = self.gdn_cfg?;
8139 let (m, _, _, _) = w
8140 .in_proj_qkv
8141 .graph_weight()
8142 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
8143 model = Some(m.clone());
8144 crate::gpu::GraphAttn::Gdn {
8145 qkv: gw(&w.in_proj_qkv)?,
8146 z: gw(&w.in_proj_z)?,
8147 a: gw(&w.in_proj_a)?,
8148 b: gw(&w.in_proj_b)?,
8149 out: gw(&w.out_proj)?,
8150 conv1d: &w.conv1d,
8151 a_log: &w.a_log,
8152 dt_bias: &w.dt_bias,
8153 norm: &w.norm,
8154 nv: cfg.num_v_heads,
8155 nk: cfg.num_k_heads,
8156 dk: cfg.key_head_dim,
8157 dv: cfg.value_head_dim,
8158 kk: cfg.conv_kernel,
8159 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8160 }
8161 }
8162 AttnKind::ShortConv(w) => {
8163 let cfg = self.short_conv_cfg?;
8164 let (m, _, _, _) = w
8165 .in_proj
8166 .graph_weight()
8167 .or_else(|| w.in_proj.graph_weight_descriptor())?;
8168 model = Some(m.clone());
8169 crate::gpu::GraphAttn::ShortConv {
8170 inp: gw(&w.in_proj)?,
8171 out: gw(&w.out_proj)?,
8172 taps: &w.conv,
8173 kernel: cfg.kernel,
8174 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8175 }
8176 }
8177 _ => return None,
8178 };
8179 layers.push(crate::gpu::GraphLayer {
8180 input_norm: &lw.input_norm,
8181 attn,
8182 post_norm: &lw.post_norm,
8183 ffn: gffn,
8184 });
8185 }
8186 let model = model?;
8187 let lm_gw = if upto_excl == self.num_layers
8193 && self.graph_want_logits
8194 && std::env::var("CMF_GPU_LMHEAD")
8195 .map(|v| v != "0")
8196 .unwrap_or(true)
8197 {
8198 self.weights
8199 .lm_head
8200 .graph_weight()
8201 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
8202 .map(|(m, i, kind, rs)| {
8203 let name = &m.tensors[i].name;
8204 let prism = if crate::prism::is_inverse_embedding(m, name) {
8205 crate::gpu::GraphPrismOp::InverseEmbedding
8206 } else if crate::prism::is_forward_weight(m, name) {
8207 crate::gpu::GraphPrismOp::Forward
8208 } else {
8209 crate::gpu::GraphPrismOp::None
8210 };
8211 (
8212 crate::gpu::GraphW {
8213 idx: i,
8214 kind,
8215 row_scale: rs,
8216 data: &[],
8217 prism,
8218 affine: crate::prism::is_affine_target(m, name),
8219 },
8220 self.weights.lm_head.rows(),
8221 )
8222 })
8223 } else {
8224 None
8225 };
8226 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
8227 let emb_gw = if steps > 1 {
8229 self.weights
8230 .embed_tokens
8231 .graph_weight()
8232 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
8233 .map(|(m, i, kind, rs)| {
8234 let name = &m.tensors[i].name;
8235 let prism = if crate::prism::is_inverse_embedding(m, name) {
8236 crate::gpu::GraphPrismOp::InverseEmbedding
8237 } else if crate::prism::is_forward_weight(m, name) {
8238 crate::gpu::GraphPrismOp::Forward
8239 } else {
8240 crate::gpu::GraphPrismOp::None
8241 };
8242 (
8243 crate::gpu::GraphW {
8244 idx: i,
8245 kind,
8246 row_scale: rs,
8247 data: &[],
8248 prism,
8249 affine: crate::prism::is_affine_target(m, name),
8250 },
8251 self.weights.embed_tokens.rows(),
8252 self.embed_multiplier,
8253 )
8254 })
8255 } else {
8256 None
8257 };
8258
8259 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
8265 (from..upto_excl.min(self.num_layers - 1))
8266 .filter(|&li| (li + 1) % self.physical_layers == 0)
8267 .map(|li| li - from)
8268 .collect()
8269 } else {
8270 Vec::new()
8271 };
8272 let mut h = hidden.to_vec();
8273 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
8279 let outcome = crate::gpu::forward_token_graph(
8280 &model,
8281 self.graph_kv_id,
8282 &layers,
8283 &o1_views,
8284 self.o1_epoch,
8285 &self.inv_freq,
8286 &mut h,
8287 nh,
8288 nkv,
8289 hd,
8290 self.attn_scale,
8291 rd,
8292 self.hidden_size,
8293 self.intermediate_size,
8294 position,
8295 self.kv_cache.max_seq_len,
8296 gemma,
8297 self.rms_eps as f32,
8298 lm,
8299 &self.weights.final_norm,
8300 logits_out,
8301 &loop_norm_at,
8302 steps,
8303 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
8304 ids_out,
8305 layers_run,
8306 from,
8307 dump_hidden,
8308 );
8309 match outcome {
8310 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
8311 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
8312 crate::gpu::TokenGraphOutcome::Declined => None,
8313 }
8314 }
8315
8316 #[cfg(target_os = "macos")]
8325 #[allow(clippy::type_complexity)]
8326 fn metal_rows_plan(
8327 &self,
8328 ) -> Option<(
8329 Vec<MetalRowsItem<'_>>,
8330 std::sync::Arc<cortiq_core::CmfModel>,
8331 Option<crate::gpu_metal::GdnGpuCfg>,
8332 )> {
8333 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
8334 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
8335 if !graph_force
8336 || !crate::gpu::enabled_here()
8337 || std::env::var("CMF_GPU_BLOCK")
8338 .map(|v| v == "0")
8339 .unwrap_or(false)
8340 || self.attn_softcap > 0.0
8341 || self.o1_active()
8342 || self.swa.is_some()
8343 || self.global_attn.is_some()
8344 || self.attention_heads_per_layer.is_some()
8345 || self.attn_v_norm
8346 || self.loop_final_norm
8347 {
8348 return None;
8349 }
8350 let attend_contract = self.head_dim % 4 == 0
8351 && self.head_dim <= 256
8352 && self.rotary_dim >= 2
8353 && self.rotary_dim <= self.head_dim
8354 && (self.rotary_dim / 2) % 32 == 0
8355 && self.num_kv_heads > 0
8356 && self.num_heads % self.num_kv_heads == 0;
8357 if !attend_contract {
8358 return None;
8359 }
8360 let mut plan: Vec<MetalRowsItem> = Vec::new();
8361 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
8362 for li in 0..self.num_layers {
8363 let lw = &self.weights.layers[self.phys_layer(li)];
8364 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8365 return None;
8366 }
8367 let ffn = match &lw.ffn {
8368 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
8369 let (Some(g), Some(u), Some(dn)) = (
8370 d.gate_proj.metal_graph_parts(),
8371 d.up_proj.metal_graph_parts(),
8372 d.down_proj.metal_graph_parts(),
8373 ) else {
8374 return None;
8375 };
8376 MetalFfn::Dense {
8377 gate: g,
8378 up: u,
8379 down: dn,
8380 }
8381 }
8382 _ => return None,
8383 };
8384 match &lw.attn {
8385 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
8386 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
8387 w.in_proj_qkv.metal_graph_parts(),
8388 w.in_proj_z.metal_graph_parts(),
8389 w.in_proj_a.f32_parts(),
8390 w.in_proj_b.f32_parts(),
8391 w.out_proj.metal_graph_parts(),
8392 ) else {
8393 return None;
8394 };
8395 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
8396 model_ref.get_or_insert_with(|| model.clone());
8397 }
8398 let gl = GdnGpuLayer {
8399 attn_norm: &lw.input_norm,
8400 post_norm: &lw.post_norm,
8401 qkv,
8402 z,
8403 a,
8404 b: bb,
8405 out,
8406 ffn,
8407 conv1d: &w.conv1d,
8408 a_log: &w.a_log,
8409 dt_bias: &w.dt_bias,
8410 gnorm: &w.norm,
8411 };
8412 match plan.last_mut() {
8413 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
8414 _ => plan.push(MetalRowsItem::Gdn {
8415 run: vec![gl],
8416 first: li,
8417 }),
8418 }
8419 }
8420 AttnKind::Full {
8421 wq,
8422 wk,
8423 wv,
8424 wo,
8425 q_norm,
8426 k_norm,
8427 output_gate,
8428 softplus_gate: None,
8429 bias: None,
8430 } => {
8431 let (Some(pq), Some(pk), Some(pv), Some(po)) =
8432 (
8433 wq.metal_graph_parts(),
8434 wk.metal_graph_parts(),
8435 wv.metal_graph_parts(),
8436 wo.metal_graph_parts(),
8437 )
8438 else {
8439 return None;
8440 };
8441 if let QTensor::Mapped { model, .. } = wq {
8442 model_ref.get_or_insert_with(|| model.clone());
8443 }
8444 let cache = &self.kv_cache.layers[li];
8445 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
8446 return None;
8447 }
8448 plan.push(MetalRowsItem::Attn {
8449 l: AttnGpuLayer {
8450 attn_norm: &lw.input_norm,
8451 post_norm: &lw.post_norm,
8452 wq: pq,
8453 wk: pk,
8454 wv: pv,
8455 wo: po,
8456 ffn,
8457 },
8458 li,
8459 q_norm: q_norm.as_deref(),
8460 k_norm: k_norm.as_deref(),
8461 output_gate: *output_gate,
8462 });
8463 }
8464 _ => return None,
8465 }
8466 }
8467 let model = model_ref?;
8468 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
8469 nv: cfg.num_v_heads,
8470 nk: cfg.num_k_heads,
8471 dk: cfg.key_head_dim,
8472 dv: cfg.value_head_dim,
8473 kk: cfg.conv_kernel,
8474 hidden: self.hidden_size,
8475 inter: self.intermediate_size,
8476 c_dim: cfg.conv_dim(),
8477 eps: cfg.rms_eps as f32,
8478 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8479 });
8480 Some((plan, model, gcfg))
8481 }
8482
8483 #[cfg(target_os = "macos")]
8485 #[allow(clippy::too_many_arguments)]
8486 fn metal_attn_params<'a>(
8487 li: usize,
8488 cache: &'a crate::kv_cache::LayerKvCache,
8489 q_norm: Option<&'a [f32]>,
8490 k_norm: Option<&'a [f32]>,
8491 output_gate: bool,
8492 inv_freq: &'a [f32],
8493 geom: (usize, usize, usize, usize),
8494 pos0: usize,
8495 kv_id: u64,
8496 scale: f32,
8497 eps: f32,
8498 gemma: bool,
8499 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
8500 let (nh, nkv, hd, rd) = geom;
8501 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8502 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8503 let cpu_stored = cpu_k[0].len() / hd;
8504 (
8505 crate::gpu_metal::AttnDeviceParams {
8506 kv_id,
8507 layer: li,
8508 nh,
8509 nkv,
8510 hd,
8511 rd,
8512 position: pos0,
8513 scale,
8514 eps,
8515 gemma,
8516 output_gate,
8517 q_norm,
8518 k_norm,
8519 inv_freq,
8520 cpu_k,
8521 cpu_v,
8522 cpu_stored,
8523 o1: None,
8524 },
8525 cpu_stored,
8526 )
8527 }
8528
8529 #[cfg(target_os = "macos")]
8534 #[allow(clippy::type_complexity)]
8535 fn metal_rows_run(
8536 &mut self,
8537 hiddens: &mut [f32],
8538 pos0: usize,
8539 b: usize,
8540 prefill: bool,
8541 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8542 ) -> MetalRowsRun {
8543 use crate::gpu_metal::{GraphDims, VerifyGraph};
8544 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
8545 for l in &mut self.kv_cache.layers {
8546 if l.linear_state.len() != want && want > 0 {
8547 l.linear_state = vec![0f32; want];
8548 }
8549 }
8550 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
8551 return MetalRowsRun::Declined;
8552 };
8553 let dims = GraphDims {
8554 hidden: self.hidden_size,
8555 eps: self.rms_eps as f32,
8556 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8557 };
8558 let Some(mut graph) = (if prefill {
8559 VerifyGraph::new_prefill(&model, dims, hiddens, b)
8560 } else {
8561 VerifyGraph::new(&model, dims, hiddens, b)
8562 }) else {
8563 return MetalRowsRun::Declined;
8564 };
8565 let geom = (
8566 self.num_heads,
8567 self.num_kv_heads,
8568 self.head_dim,
8569 self.rotary_dim,
8570 );
8571 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8572 let eps = self.rms_eps as f32;
8573 let kv_id = self.graph_kv_id;
8574 let inv_freq = self.inv_freq.clone();
8575 for item in &plan {
8576 let ok = match item {
8577 MetalRowsItem::Gdn { run, .. } => gcfg
8578 .as_ref()
8579 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
8580 .unwrap_or(false),
8581 MetalRowsItem::Attn {
8582 l,
8583 li,
8584 q_norm,
8585 k_norm,
8586 output_gate,
8587 } => {
8588 let (p, _) = Self::metal_attn_params(
8589 *li,
8590 &self.kv_cache.layers[*li],
8591 *q_norm,
8592 *k_norm,
8593 *output_gate,
8594 &inv_freq,
8595 geom,
8596 pos0,
8597 kv_id,
8598 self.attn_scale,
8599 eps,
8600 gemma,
8601 );
8602 graph.attn_ok(l, &p)
8603 }
8604 };
8605 if !ok {
8606 use std::sync::atomic::{AtomicBool, Ordering};
8607 static SAID: AtomicBool = AtomicBool::new(false);
8608 if !SAID.swap(true, Ordering::Relaxed) {
8609 tracing::warn!("metal rows graph: a layer failed preflight — declining");
8610 }
8611 return MetalRowsRun::Declined;
8612 }
8613 }
8614 let lm = match &spec {
8615 Some((lm, _, _)) => {
8616 if !graph.lm_head_ok(*lm) {
8617 return MetalRowsRun::Declined;
8618 }
8619 Some(*lm)
8620 }
8621 None => None,
8622 };
8623 let mut gdn_layers = Vec::new();
8624 let mut attn_layers = Vec::new();
8625 for item in &plan {
8626 match item {
8627 MetalRowsItem::Gdn { run, first } => {
8628 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
8629 .iter()
8630 .map(|l| l.linear_state.as_slice())
8631 .collect();
8632 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
8633 return MetalRowsRun::Declined;
8634 }
8635 gdn_layers.extend(*first..*first + run.len());
8636 }
8637 MetalRowsItem::Attn {
8638 l,
8639 li,
8640 q_norm,
8641 k_norm,
8642 output_gate,
8643 } => {
8644 let (p, cpu_stored) = Self::metal_attn_params(
8645 *li,
8646 &self.kv_cache.layers[*li],
8647 *q_norm,
8648 *k_norm,
8649 *output_gate,
8650 &inv_freq,
8651 geom,
8652 pos0,
8653 kv_id,
8654 self.attn_scale,
8655 eps,
8656 gemma,
8657 );
8658 if !graph.encode_attn_b(l, &p) {
8659 return MetalRowsRun::Declined;
8660 }
8661 attn_layers.push((*li, cpu_stored));
8662 }
8663 }
8664 }
8665 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
8666 if !graph.encode_lm_head_b(final_norm, lm) {
8667 return MetalRowsRun::Declined;
8668 }
8669 }
8670 if !graph.sync() {
8671 return MetalRowsRun::Failed;
8672 }
8673 if let Some((lm, _, logits)) = spec {
8674 logits.resize(b * lm.1, 0.0);
8675 if !graph.read_logits(logits) {
8676 return MetalRowsRun::Failed;
8677 }
8678 }
8679 if !graph.read_hidden(hiddens) {
8680 return MetalRowsRun::Failed;
8681 }
8682 MetalRowsRun::Completed(MetalVerifyPending {
8683 graph,
8684 gdn_layers,
8685 attn_layers,
8686 })
8687 }
8688
8689 #[cfg(target_os = "macos")]
8695 fn try_batch_graph_metal(
8696 &mut self,
8697 hiddens: &mut [f32],
8698 positions: &[usize],
8699 b: usize,
8700 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8701 ) -> crate::gpu::BatchGraphOutcome {
8702 let _t0 = std::time::Instant::now();
8703 if positions.len() != b
8704 || positions.windows(2).any(|w| w[1] != w[0] + 1)
8705 || hiddens.len() != b * self.hidden_size
8706 {
8707 return crate::gpu::BatchGraphOutcome::Declined;
8708 }
8709 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec) {
8710 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
8711 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
8712 MetalRowsRun::Completed(pending) => pending,
8713 };
8714 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8715 eprintln!(
8716 "metal-verify: {:.1} ms | b={b}",
8717 _t0.elapsed().as_secs_f64() * 1e3
8718 );
8719 }
8720 self.metal_verify = Some(pending);
8721 crate::gpu::BatchGraphOutcome::Completed
8722 }
8723
8724 #[cfg(target_os = "macos")]
8729 fn prefill_rows_metal(
8730 &mut self,
8731 ids: &[u32],
8732 start_pos: usize,
8733 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8734 ) -> MetalPrefillOutcome {
8735 let b = ids.len();
8736 if b == 0 || b > 512 {
8737 return MetalPrefillOutcome::Declined;
8738 }
8739 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8740 let with_head = spec.is_some();
8741 let hs = self.hidden_size;
8742 let mut hiddens = vec![0f32; b * hs];
8743 for (j, &id) in ids.iter().enumerate() {
8744 let e = self.embed_single(id);
8745 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
8746 }
8747 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec) {
8748 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
8749 MetalRowsRun::Failed => {
8750 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8751 return MetalPrefillOutcome::Failed;
8752 }
8753 MetalRowsRun::Completed(pending) => pending,
8754 };
8755 let idxs = pending.gdn_layers.clone();
8757 let mut outs: Vec<&mut [f32]> = self
8758 .kv_cache
8759 .layers
8760 .iter_mut()
8761 .enumerate()
8762 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8763 .map(|(_, l)| l.linear_state.as_mut_slice())
8764 .collect();
8765 if !pending.graph.finish_states(&mut outs) {
8766 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8767 return MetalPrefillOutcome::Failed;
8768 }
8769 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8770 let mut rows = Vec::with_capacity(pending.attn_layers.len());
8774 for (li, cpu_stored) in &pending.attn_layers {
8775 let mut kbuf = vec![0f32; b * nkv * hd];
8776 let mut vbuf = vec![0f32; b * nkv * hd];
8777 if !crate::gpu_metal::kv_mirror_read_rows(
8778 self.graph_kv_id,
8779 *li,
8780 nkv,
8781 hd,
8782 *cpu_stored,
8783 b,
8784 &mut kbuf,
8785 &mut vbuf,
8786 ) {
8787 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8788 return MetalPrefillOutcome::Failed;
8789 }
8790 rows.push((*li, *cpu_stored, kbuf, vbuf));
8791 }
8792 for (li, cpu_stored, kbuf, vbuf) in rows {
8793 let cache = &mut self.kv_cache.layers[li];
8794 for r in 0..b {
8795 cache.append(
8796 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8797 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8798 &[],
8799 );
8800 }
8801 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
8802 }
8803 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8804 if with_head {
8805 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8806 }
8807 MetalPrefillOutcome::Completed(hiddens)
8808 }
8809
8810 #[cfg(target_os = "macos")]
8811 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
8812 self.prefill_rows_metal(ids, start_pos, None)
8813 }
8814
8815 #[cfg(target_os = "macos")]
8820 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
8821 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
8822 return MetalBatchNllOutcome::Declined;
8823 }
8824 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
8825 return MetalBatchNllOutcome::Declined;
8826 };
8827 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
8828 .ok()
8829 .and_then(|v| v.parse::<usize>().ok())
8830 .filter(|&v| (1..=512).contains(&v))
8831 .unwrap_or(32);
8832 let final_norm = self.weights.final_norm.clone();
8833 let mut nll = 0.0f64;
8834 let mut count = 0usize;
8835 let mut pos = 0usize;
8836 let mut completed = 0usize;
8837 while pos < ids.len() {
8838 let end = (pos + chunk).min(ids.len());
8839 let mut logits = Vec::new();
8840 let outcome = self.prefill_rows_metal(
8841 &ids[pos..end],
8842 pos,
8843 Some((lm, &final_norm, &mut logits)),
8844 );
8845 match outcome {
8846 MetalPrefillOutcome::Declined => {
8847 return if completed == 0 {
8848 MetalBatchNllOutcome::Declined
8849 } else {
8850 MetalBatchNllOutcome::Failed(format!(
8851 "ordinary Metal NLL batch declined after {completed} chunks"
8852 ))
8853 };
8854 }
8855 MetalPrefillOutcome::Failed => {
8856 return MetalBatchNllOutcome::Failed(
8857 "ordinary Metal NLL batch failed after admission".to_string(),
8858 );
8859 }
8860 MetalPrefillOutcome::Completed(_) => {}
8861 }
8862 completed += 1;
8863 let vocab = self.vocab_size.min(lm.1);
8864 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
8865 return MetalBatchNllOutcome::Failed(
8866 "ordinary Metal NLL head returned an invalid shape".to_string(),
8867 );
8868 }
8869 for row in 0..(end - pos) {
8870 let absolute = pos + row;
8871 if absolute < start || absolute + 1 >= ids.len() {
8872 continue;
8873 }
8874 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
8875 if let Some(mu) = self.logit_multiplier {
8876 for v in lg.iter_mut() {
8877 *v *= mu;
8878 }
8879 }
8880 if let Some(c) = self.final_softcap {
8881 for v in lg.iter_mut() {
8882 *v = c * (*v / c).tanh();
8883 }
8884 }
8885 let target = ids[absolute + 1] as usize;
8886 if target >= vocab {
8887 return MetalBatchNllOutcome::Failed(format!(
8888 "target token {target} exceeds Metal head rows {vocab}"
8889 ));
8890 }
8891 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8892 let lse: f64 = lg
8893 .iter()
8894 .map(|&v| ((v - max) as f64).exp())
8895 .sum::<f64>()
8896 .ln()
8897 + max as f64;
8898 nll += lse - lg[target] as f64;
8899 count += 1;
8900 }
8901 pos = end;
8902 }
8903 MetalBatchNllOutcome::Completed(nll, count)
8904 }
8905
8906 #[cfg(target_os = "macos")]
8910 fn metal_verify_commit(&mut self, a: usize) -> bool {
8911 let Some(mut pending) = self.metal_verify.take() else {
8912 return false;
8913 };
8914 let n = a + 1;
8915 let idxs = pending.gdn_layers.clone();
8917 let mut outs: Vec<&mut [f32]> = self
8918 .kv_cache
8919 .layers
8920 .iter_mut()
8921 .enumerate()
8922 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8923 .map(|(_, l)| l.linear_state.as_mut_slice())
8924 .collect();
8925 if !pending.graph.commit(n, &mut outs) {
8926 return false;
8927 }
8928 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8929 let mut rows = Vec::with_capacity(pending.attn_layers.len());
8933 for (li, cpu_stored) in &pending.attn_layers {
8934 let mut kbuf = vec![0f32; n * nkv * hd];
8935 let mut vbuf = vec![0f32; n * nkv * hd];
8936 if !crate::gpu_metal::kv_mirror_read_rows(
8937 self.graph_kv_id,
8938 *li,
8939 nkv,
8940 hd,
8941 *cpu_stored,
8942 n,
8943 &mut kbuf,
8944 &mut vbuf,
8945 ) {
8946 return false;
8947 }
8948 rows.push((*li, *cpu_stored, kbuf, vbuf));
8949 }
8950 for (li, cpu_stored, kbuf, vbuf) in rows {
8951 let cache = &mut self.kv_cache.layers[li];
8952 for r in 0..n {
8953 cache.append(
8954 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8955 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8956 &[],
8957 );
8958 }
8959 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
8960 }
8961 true
8962 }
8963
8964 #[cfg(target_os = "macos")]
8970 fn mtp_warm_batch_metal(
8971 &mut self,
8972 m: &mut MtpModule,
8973 pairs: &[(&[f32], u32)],
8974 first_pos: usize,
8975 ) -> bool {
8976 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
8977 let b = pairs.len();
8978 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
8979 return false;
8980 }
8981 let AttnKind::Full {
8982 wq,
8983 wk,
8984 wv,
8985 wo,
8986 q_norm,
8987 k_norm,
8988 output_gate,
8989 softplus_gate: None,
8990 bias: None,
8991 } = &m.layer.attn
8992 else {
8993 return false;
8994 };
8995 let FfnKind::Dense(d) = &m.layer.ffn else {
8996 return false;
8997 };
8998 if !d.segs.is_empty() {
8999 return false;
9000 }
9001 let (Some(pq), Some(pk), Some(pv), Some(po)) =
9002 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
9003 else {
9004 return false;
9005 };
9006 let (Some(g), Some(u), Some(dn)) = (
9007 d.gate_proj.q1_parts(),
9008 d.up_proj.q1_parts(),
9009 d.down_proj.q1_parts(),
9010 ) else {
9011 return false;
9012 };
9013 let Some(eh) = m.eh_proj.q1_parts() else {
9014 return false;
9015 };
9016 let QTensor::Mapped { model, .. } = wq else {
9017 return false;
9018 };
9019 let model = model.clone();
9020 let hs = self.hidden_size;
9021 let mut cat = vec![0f32; b * 2 * hs];
9023 for (j, (h, tok)) in pairs.iter().enumerate() {
9024 let e = self.embed_single(*tok);
9025 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
9026 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
9027 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
9028 }
9029 let dims = GraphDims {
9030 hidden: hs,
9031 eps: self.rms_eps as f32,
9032 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9033 };
9034 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
9035 return false;
9036 };
9037 let l = AttnGpuLayer {
9038 attn_norm: &m.layer.input_norm,
9039 post_norm: &m.layer.post_norm,
9040 wq: pq,
9041 wk: pk,
9042 wv: pv,
9043 wo: po,
9044 ffn: MetalFfn::Dense {
9045 gate: g,
9046 up: u,
9047 down: dn,
9048 },
9049 };
9050 let (nh, nkv, hd, rd) = (
9051 self.num_heads,
9052 self.num_kv_heads,
9053 self.head_dim,
9054 self.rotary_dim,
9055 );
9056 let inv_freq = self.inv_freq.clone();
9057 let cpu_stored;
9058 {
9059 let cache = &m.kv;
9060 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9061 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9062 cpu_stored = cpu_k[0].len() / hd;
9063 if cpu_stored != first_pos {
9064 return false;
9065 }
9066 let p = AttnDeviceParams {
9067 kv_id: self.mtp_kv_id(),
9068 layer: Self::MTP_LAYER_BASE,
9069 nh,
9070 nkv,
9071 hd,
9072 rd,
9073 position: first_pos,
9074 scale: self.attn_scale,
9075 eps: self.rms_eps as f32,
9076 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9077 output_gate: *output_gate,
9078 q_norm: q_norm.as_deref(),
9079 k_norm: k_norm.as_deref(),
9080 inv_freq: &inv_freq,
9081 cpu_k,
9082 cpu_v,
9083 cpu_stored,
9084 o1: None,
9085 };
9086 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
9087 return false;
9088 }
9089 }
9090 if !graph.sync() {
9091 return false;
9092 }
9093 let mut kbuf = vec![0f32; b * nkv * hd];
9094 let mut vbuf = vec![0f32; b * nkv * hd];
9095 if !crate::gpu_metal::kv_mirror_read_rows(
9096 self.mtp_kv_id(),
9097 Self::MTP_LAYER_BASE,
9098 nkv,
9099 hd,
9100 cpu_stored,
9101 b,
9102 &mut kbuf,
9103 &mut vbuf,
9104 ) {
9105 return false;
9106 }
9107 for r in 0..b {
9108 m.kv.append(
9109 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9110 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9111 &[],
9112 );
9113 }
9114 crate::gpu_metal::kv_mirror_set_stored(
9115 self.mtp_kv_id(),
9116 Self::MTP_LAYER_BASE,
9117 cpu_stored + b,
9118 );
9119 true
9120 }
9121
9122 fn draft_vocab_rows(head_rows: usize) -> usize {
9125 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9126 let n = *N.get_or_init(|| {
9127 std::env::var("CMF_DRAFT_VOCAB")
9128 .ok()
9129 .and_then(|v| v.parse().ok())
9130 .unwrap_or(65536)
9131 });
9132 if n == 0 { head_rows } else { n.min(head_rows) }
9133 }
9134
9135 #[cfg(target_os = "macos")]
9140 fn mtp_step_metal(
9141 &mut self,
9142 m: &mut MtpModule,
9143 hidden: &[f32],
9144 next_token: u32,
9145 position: usize,
9146 want_logits: bool,
9147 ) -> Option<(Vec<f32>, Vec<f32>)> {
9148 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
9149 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9150 || !crate::gpu::q1_force()
9151 || !crate::gpu::enabled_here()
9152 || self.attn_softcap > 0.0
9153 || self.attention_heads_per_layer.is_some()
9154 || m.kv.mode != crate::kv_cache::KvMode::F32
9155 || m.kv.o1.is_some()
9156 {
9157 return None;
9158 }
9159 let AttnKind::Full {
9160 wq,
9161 wk,
9162 wv,
9163 wo,
9164 q_norm,
9165 k_norm,
9166 output_gate,
9167 softplus_gate: None,
9168 bias: None,
9169 } = &m.layer.attn
9170 else {
9171 return None;
9172 };
9173 let FfnKind::Dense(d) = &m.layer.ffn else {
9174 return None;
9175 };
9176 if d.act != Act::Silu || !d.segs.is_empty() {
9177 return None;
9178 }
9179 let (pq, pk, pv, po) = (
9180 wq.q1_parts()?,
9181 wk.q1_parts()?,
9182 wv.q1_parts()?,
9183 wo.q1_parts()?,
9184 );
9185 let (g, u, dn) = (
9186 d.gate_proj.q1_parts()?,
9187 d.up_proj.q1_parts()?,
9188 d.down_proj.q1_parts()?,
9189 );
9190 let QTensor::Mapped { model, .. } = wq else {
9191 return None;
9192 };
9193 let model = model.clone();
9194 let lm = if want_logits {
9195 Some(self.weights.lm_head.q1_parts()?)
9196 } else {
9197 None
9198 };
9199 let dims = GraphDims {
9200 hidden: self.hidden_size,
9201 eps: self.rms_eps as f32,
9202 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9203 };
9204 let hs = self.hidden_size;
9207 let mut x = vec![0f32; hs];
9208 let mut graph = TokenGraph::new(&model, dims, &x)?;
9209 let mut folded = false;
9210 if let Some(eh) = m.eh_proj.q1_parts() {
9211 let e = self.embed_single(next_token);
9212 let mut cat = vec![0.0f32; 2 * hs];
9213 let (cat_e, cat_h) = cat.split_at_mut(hs);
9214 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9215 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9216 folded = graph.encode_input_proj(eh, &cat);
9217 }
9218 if !folded {
9219 x = self.mtp_block_input(m, hidden, next_token);
9220 graph = TokenGraph::new(&model, dims, &x)?;
9221 }
9222 let l = AttnGpuLayer {
9223 attn_norm: &m.layer.input_norm,
9224 post_norm: &m.layer.post_norm,
9225 wq: pq,
9226 wk: pk,
9227 wv: pv,
9228 wo: po,
9229 ffn: MetalFfn::Dense {
9230 gate: g,
9231 up: u,
9232 down: dn,
9233 },
9234 };
9235 let (nh, nkv, hd, rd) = (
9236 self.num_heads,
9237 self.num_kv_heads,
9238 self.head_dim,
9239 self.rotary_dim,
9240 );
9241 let inv_freq = self.inv_freq.clone();
9242 {
9243 let cache = &m.kv;
9244 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9245 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9246 let cpu_stored = cpu_k[0].len() / hd;
9247 let p = AttnDeviceParams {
9248 kv_id: self.mtp_kv_id(),
9249 layer: Self::MTP_LAYER_BASE,
9250 nh,
9251 nkv,
9252 hd,
9253 rd,
9254 position,
9255 scale: self.attn_scale,
9256 eps: self.rms_eps as f32,
9257 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9258 output_gate: *output_gate,
9259 q_norm: q_norm.as_deref(),
9260 k_norm: k_norm.as_deref(),
9261 inv_freq: &inv_freq,
9262 cpu_k,
9263 cpu_v,
9264 cpu_stored,
9265 o1: None,
9266 };
9267 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
9268 return None;
9269 }
9270 }
9271 let draft_rows = if let Some(lm) = lm {
9277 Self::draft_vocab_rows(lm.1)
9278 } else {
9279 0
9280 };
9281 if let Some(lm) = lm {
9282 if !graph.lm_head_ok(lm) {
9283 return None;
9284 }
9285 if draft_rows < lm.1 {
9286 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
9287 return None;
9288 }
9289 } else {
9290 graph.encode_lm_head(&m.final_norm, lm);
9291 }
9292 }
9293 if graph.sync_checked().is_err() {
9294 return None;
9295 }
9296 let mut logits = Vec::new();
9297 if let Some(lm) = lm {
9298 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
9299 logits = attention::take_buf(n_read);
9300 graph.read_logits(&mut logits);
9301 logits.resize(self.vocab_size, f32::NEG_INFINITY);
9303 }
9304 graph.finish(&mut x);
9305 let mut krow = attention::take_buf(nkv * hd);
9306 let mut vrow = attention::take_buf(nkv * hd);
9307 if crate::gpu_metal::kv_mirror_read_last(
9308 self.mtp_kv_id(),
9309 Self::MTP_LAYER_BASE,
9310 nkv,
9311 hd,
9312 &mut krow,
9313 &mut vrow,
9314 ) {
9315 m.kv.append(&krow, &vrow, &[]);
9316 }
9317 attention::recycle_buf(&mut krow);
9318 attention::recycle_buf(&mut vrow);
9319 Some((logits, x))
9320 }
9321
9322 fn try_batch_graph_wgpu(
9323 &self,
9324 hiddens: &mut [f32],
9325 positions: &[usize],
9326 k: usize,
9327 spec: Option<crate::gpu::SpecTail<'_>>,
9328 ) -> crate::gpu::BatchGraphOutcome {
9329 let _tb = std::time::Instant::now();
9330 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
9331 if self.attn_softcap > 0.0 {
9332 return crate::gpu::BatchGraphOutcome::Declined; }
9334 let nh = self.num_heads;
9335 let (nkv, hd, rd) = self.layer_geom(0);
9336 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9337 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9338 if let Some((m, i, kind, rs)) = t
9339 .graph_weight()
9340 .or_else(|| t.graph_weight_descriptor())
9341 {
9342 let name = &m.tensors[i].name;
9343 let prism = if crate::prism::is_inverse_embedding(m, name) {
9344 crate::gpu::GraphPrismOp::InverseEmbedding
9345 } else if crate::prism::is_forward_weight(m, name) {
9346 crate::gpu::GraphPrismOp::Forward
9347 } else {
9348 crate::gpu::GraphPrismOp::None
9349 };
9350 return Some(crate::gpu::GraphW {
9351 idx: i,
9352 kind,
9353 row_scale: rs,
9354 data: &[],
9355 prism,
9356 affine: crate::prism::is_affine_target(m, name),
9357 });
9358 }
9359 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
9360 eprintln!(
9361 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
9362 t.rows(),
9363 t.cols()
9364 );
9365 }
9366 t.as_f32().map(|d| crate::gpu::GraphW {
9367 idx: 0,
9368 kind: 4,
9369 row_scale: &[],
9370 data: d,
9371 prism: crate::gpu::GraphPrismOp::None,
9372 affine: false,
9373 })
9374 }
9375 let built: Option<(
9376 Vec<crate::gpu::GraphLayer<'_>>,
9377 std::sync::Arc<cortiq_core::CmfModel>,
9378 )> = (|| {
9379 let mut layers = Vec::with_capacity(self.num_layers);
9380 let mut model = None;
9381 for li in 0..self.num_layers {
9382 let lw = &self.weights.layers[self.phys_layer(li)];
9383 let gffn = match &lw.ffn {
9390 FfnKind::Dense(d) if !d.segs.is_empty() => {
9391 if batch_debug {
9392 eprintln!("batch graph: dense segmented FFN at layer {li}");
9393 }
9394 return None;
9395 }
9396 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
9397 gate: gw(&d.gate_proj)?,
9398 up: gw(&d.up_proj)?,
9399 down: gw(&d.down_proj)?,
9400 },
9401 FfnKind::Moe(m) => {
9402 if m.router_sigmoid
9403 || m.expert_bias.is_some()
9404 || m.route_tau.is_some()
9405 || m.mask.is_some()
9406 {
9407 return None;
9408 }
9409 let (se, sg) = m.shared.as_ref()?;
9410 let sgate = gw(sg.as_ref()?)?;
9411 let router = gw(&m.router)?;
9412 if router.prism != crate::gpu::GraphPrismOp::None
9418 || router.affine
9419 || sgate.prism != crate::gpu::GraphPrismOp::None
9420 || sgate.affine
9421 {
9422 return None;
9423 }
9424 let inter = m.experts.first()?.gate_proj.rows();
9425 let mut experts = Vec::with_capacity(m.experts.len() + 1);
9426 let mut q4tp: Option<bool> = None;
9427 let mut gu_q2: Option<bool> = None;
9428 for e in m.experts.iter().chain(std::iter::once(se)) {
9429 if !matches!(e.act, Act::Silu)
9430 || e.gate_proj.rows() != inter
9431 || e.up_proj.rows() != inter
9432 {
9433 return None;
9434 }
9435 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
9439 Some((mm, gi)) => (
9440 mm,
9441 gi,
9442 e.up_proj.mapped_q4t()?.1,
9443 e.down_proj.mapped_q4t()?.1,
9444 false,
9445 false,
9446 ),
9447 None => match e.gate_proj.mapped_q2tp() {
9448 Some((mm, gi)) => (
9449 mm,
9450 gi,
9451 e.up_proj.mapped_q2tp()?.1,
9452 e.down_proj.mapped_q4tp()?.1,
9453 true,
9454 true,
9455 ),
9456 None => {
9457 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
9458 (
9459 mm,
9460 gi,
9461 e.up_proj.mapped_q4tp()?.1,
9462 e.down_proj.mapped_q4tp()?.1,
9463 true,
9464 false,
9465 )
9466 }
9467 },
9468 };
9469 if *q4tp.get_or_insert(is_p) != is_p
9470 || *gu_q2.get_or_insert(is_q2) != is_q2
9471 {
9472 return None;
9473 }
9474 if [gi, ui, di].into_iter().any(|idx| {
9475 mm.tensors
9476 .get(idx)
9477 .is_some_and(|t| {
9478 crate::prism::is_forward_weight(mm, &t.name)
9479 || crate::prism::is_affine_target(mm, &t.name)
9480 })
9481 }) {
9482 return None;
9483 }
9484 model.get_or_insert_with(|| mm.clone());
9485 experts.push((gi, ui, di));
9486 }
9487 crate::gpu::GraphFfn::Moe {
9488 router,
9489 shared_gate: sgate,
9490 experts,
9491 n_exp: m.experts.len(),
9492 top_k: m.top_k,
9493 inter,
9494 norm_topk: m.norm_topk_prob,
9495 q4tp: q4tp?,
9496 gu_q2: gu_q2.unwrap_or(false),
9497 sigmoid: false,
9498 bias: None,
9499 has_shared: true,
9500 }
9501 }
9502 _ => return None,
9503 };
9504 let attn = match &lw.attn {
9505 AttnKind::Full {
9506 wq,
9507 wk,
9508 wv,
9509 wo,
9510 q_norm,
9511 k_norm,
9512 output_gate,
9513 softplus_gate,
9514 bias,
9515 } => {
9516 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
9517 if batch_debug {
9518 eprintln!(
9519 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
9520 softplus_gate.is_some(),
9521 self.attention_heads_per_layer.is_some()
9522 );
9523 }
9524 return None;
9525 }
9526 let (m, _, _, _) = wq
9527 .graph_weight()
9528 .or_else(|| wq.graph_weight_descriptor())?;
9529 model = Some(m.clone());
9530 crate::gpu::GraphAttn::Full {
9531 wq: gw(wq)?,
9532 wk: gw(wk)?,
9533 wv: gw(wv)?,
9534 wo: gw(wo)?,
9535 q_norm: q_norm.as_deref(),
9536 k_norm: k_norm.as_deref(),
9537 bias: bias
9538 .as_ref()
9539 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9540 output_gate: *output_gate,
9541 cpu_k: self.kv_cache.layers[li].k_heads(),
9542 cpu_v: self.kv_cache.layers[li].v_heads(),
9543 }
9544 }
9545 AttnKind::LinearGdn(w) => {
9546 let Some(cfg) = self.gdn_cfg else {
9547 if batch_debug {
9548 eprintln!("batch graph: no GDN config at layer {li}");
9549 }
9550 return None;
9551 };
9552 let (m, _, _, _) = w
9553 .in_proj_qkv
9554 .graph_weight()
9555 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
9556 model = Some(m.clone());
9557 crate::gpu::GraphAttn::Gdn {
9558 qkv: gw(&w.in_proj_qkv)?,
9559 z: gw(&w.in_proj_z)?,
9560 a: gw(&w.in_proj_a)?,
9561 b: gw(&w.in_proj_b)?,
9562 out: gw(&w.out_proj)?,
9563 conv1d: &w.conv1d,
9564 a_log: &w.a_log,
9565 dt_bias: &w.dt_bias,
9566 norm: &w.norm,
9567 nv: cfg.num_v_heads,
9568 nk: cfg.num_k_heads,
9569 dk: cfg.key_head_dim,
9570 dv: cfg.value_head_dim,
9571 kk: cfg.conv_kernel,
9572 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
9573 }
9574 }
9575 _ => return None,
9576 };
9577 layers.push(crate::gpu::GraphLayer {
9578 input_norm: &lw.input_norm,
9579 attn,
9580 post_norm: &lw.post_norm,
9581 ffn: gffn,
9582 });
9583 }
9584 Some((layers, model?))
9585 })();
9586 let Some((layers, model)) = built else {
9587 {
9588 use std::sync::atomic::{AtomicBool, Ordering};
9589 static SAID: AtomicBool = AtomicBool::new(false);
9590 if !SAID.swap(true, Ordering::Relaxed) {
9591 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
9592 }
9593 }
9594 return crate::gpu::BatchGraphOutcome::Declined;
9595 };
9596 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
9597 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
9598 }
9599 crate::gpu::forward_batch_graph(
9600 &model,
9601 self.graph_kv_id,
9602 &layers,
9603 &self.inv_freq,
9604 hiddens,
9605 nh,
9606 nkv,
9607 hd,
9608 rd,
9609 self.hidden_size,
9610 self.intermediate_size,
9611 positions,
9612 self.kv_cache.max_seq_len,
9613 gemma,
9614 self.rms_eps as f32,
9615 self.attn_scale,
9616 k,
9617 &(0..self.num_layers)
9618 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
9619 .collect::<Vec<_>>(),
9620 self.o1_epoch,
9621 spec,
9622 )
9623 }
9624
9625 fn draft_probe() -> bool {
9629 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9630 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
9631 }
9632
9633 #[cfg(feature = "gpu")]
9645 fn dsv4_spec_on() -> bool {
9646 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9647 *ON.get_or_init(|| {
9648 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
9652 return v != "0";
9653 }
9654 std::env::var("CMF_DSV4_SPEC")
9661 .map(|v| v != "0")
9662 .unwrap_or_else(|_| {
9663 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
9664 })
9665 })
9666 }
9667
9668 #[cfg(feature = "gpu")]
9675 fn dsv4_spec_step(
9676 &mut self,
9677 tip_token: u32,
9678 t_next: u32,
9679 next_pos: usize,
9680 max_extra: usize,
9681 drafted: &mut usize,
9682 accepted_ctr: &mut usize,
9683 ) -> Option<(Vec<u32>, usize)> {
9684 let t_all = std::time::Instant::now();
9685 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9686 thread_local! {
9687 static LAST: std::cell::Cell<Option<std::time::Instant>> =
9688 const { std::cell::Cell::new(None) };
9689 }
9690 LAST.with(|l| {
9691 if let Some(prev) = l.get() {
9692 eprintln!(
9693 "между раундами {:.1} мс",
9694 prev.elapsed().as_secs_f64() * 1e3
9695 );
9696 }
9697 l.set(Some(std::time::Instant::now()));
9698 });
9699 }
9700 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9701 eprintln!("spec_step: вход pos={next_pos}");
9702 }
9703 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
9704 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
9705 if self.dspark.is_none() {
9707 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9708 if t.is_empty() {
9709 return None;
9710 }
9711 crate::dsv4::dspark_arm(&t, cfg.dim);
9712 self.dspark = Some(crate::dsv4::DsparkState::new(
9713 self.dsv4_mtp.len(),
9714 &cfg,
9715 t.len(),
9716 ));
9717 }
9718 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9719 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
9720 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9721 eprintln!("spec_step: пак не построился (targets {targets:?})");
9722 }
9723 let pack = pack?;
9724 let block = crate::dsv4::dspark_block();
9725 let b_box = self.dsv4.as_mut()?;
9726 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
9727 let ds = self.dspark.as_mut()?;
9728 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
9731 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
9732 if dbg {
9733 eprintln!("spec_step: нет захвата");
9734 }
9735 return None;
9736 }
9737 ds.have_hidden = true;
9738 let tip_pos = next_pos.checked_sub(1)?;
9739 let draft_started = std::time::Instant::now();
9740 let mut conf = Vec::new();
9741 let props = crate::dsv4::dspark_draft_gpu(
9742 g,
9743 &self.dsv4_mtp,
9744 &cfg,
9745 ds,
9746 pack,
9747 st.kv_id,
9748 tip_token,
9749 tip_pos,
9750 self.pool.as_deref(),
9751 &mut conf,
9752 );
9753 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9754 *drafted += block;
9755 if props.is_empty() || props[0] != t_next {
9756 if dbg {
9757 eprintln!(
9758 "spec_step: черновик {} (props0={:?} t_next={t_next})",
9759 if props.is_empty() {
9760 "пуст"
9761 } else {
9762 "мимо"
9763 },
9764 props.first()
9765 );
9766 }
9767 return None;
9768 }
9769 let mut k_verify = crate::dsv4::dspark_verify_k()
9776 .min(props.len())
9777 .min(max_extra.saturating_add(1));
9778 let conf_min = {
9784 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
9785 *M.get_or_init(|| {
9786 std::env::var("CMF_DSPARK_CONF_MIN")
9787 .ok()
9788 .and_then(|v| v.parse().ok())
9789 .unwrap_or(0.0)
9790 })
9791 };
9792 if conf_min > 0.0 && conf.len() >= props.len() {
9793 let mut keep = 1usize;
9794 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
9795 keep += 1;
9796 }
9797 k_verify = k_verify.min(keep.max(2));
9798 }
9799 if k_verify < 2 {
9800 return None;
9801 }
9802 let mut fed = Vec::with_capacity(k_verify);
9803 fed.push(t_next);
9804 fed.extend_from_slice(&props[1..k_verify]);
9805 let mut argmax = Vec::new();
9806 let mut logits_all = Vec::new();
9807 let mut walked = Vec::new();
9808 let txn = crate::dsv4::dsv4_verify_chunk(
9809 g,
9810 layers,
9811 &cfg,
9812 st,
9813 &fed,
9814 next_pos,
9815 &self.inv_freq,
9816 self.pool.as_deref(),
9817 &targets,
9818 &mut argmax,
9819 &mut logits_all,
9820 &mut walked,
9821 );
9822 if txn.is_none() && dbg {
9823 eprintln!("spec_step: verify отказал");
9824 }
9825 let txn = txn?;
9826 let spec_gpu_end = txn.gpu_end;
9827 let b = fed.len();
9828 let mut accepted = 1usize;
9829 while accepted < b && fed[accepted] == argmax[accepted - 1] {
9830 accepted += 1;
9831 }
9832 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
9837 accepted = 1;
9838 }
9839 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
9840 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
9841 }
9842 let t_fin = std::time::Instant::now();
9843 if !crate::dsv4::dsv4_spec_finish(
9844 g,
9845 layers,
9846 &cfg,
9847 st,
9848 txn,
9849 accepted,
9850 &fed,
9851 &self.inv_freq,
9852 self.pool.as_deref(),
9853 ) {
9854 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
9855 return None;
9856 }
9857 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9858 eprintln!(
9859 "finish(k={accepted}): {:.1} мс",
9860 t_fin.elapsed().as_secs_f64() * 1e3
9861 );
9862 }
9863 *accepted_ctr += accepted - 1;
9864 let (hc, dim) = (cfg.hc_mult, cfg.dim);
9869 let dev_caps: Vec<usize> = targets
9874 .iter()
9875 .copied()
9876 .filter(|&t| t < spec_gpu_end)
9877 .collect();
9878 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
9879 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
9880 return None;
9881 }
9882 for t in 0..accepted {
9883 let tip = t + 1 == accepted;
9884 for (slot, &tl) in targets.iter().enumerate() {
9885 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
9886 let lo = (di * b + t) * hc * dim;
9887 crate::dsv4::dspark_capture(
9888 &caps_all[lo..lo + hc * dim],
9889 &cfg,
9890 slot,
9891 &mut ds.main_hidden,
9892 );
9893 } else if tip
9894 && crate::dsv4::dspark_peek_slot(slot, dim, {
9895 let lo = slot * dim;
9896 &mut ds.main_hidden[lo..lo + dim]
9897 })
9898 {
9899 } else {
9904 crate::dsv4::dspark_capture(
9908 &walked[t * hc * dim..(t + 1) * hc * dim],
9909 &cfg,
9910 slot,
9911 &mut ds.main_hidden,
9912 );
9913 }
9914 }
9915 crate::dsv4::dspark_ring_append(
9916 g,
9917 &self.dsv4_mtp,
9918 &cfg,
9919 ds,
9920 next_pos + t,
9921 self.pool.as_deref(),
9922 );
9923 }
9924 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
9925 self.graph_logits = Some(row);
9926 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
9931 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
9932 crate::dsv4::pick_tally_arm();
9933 }
9934 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9935 eprintln!(
9936 "spec_step total {:.1} мс (k={accepted})",
9937 t_all.elapsed().as_secs_f64() * 1e3
9938 );
9939 }
9940 Some((fed[1..accepted].to_vec(), next_pos + accepted))
9941 }
9942
9943 fn dspark_probe(&mut self, position: usize, token_id: u32) {
9944 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
9945 return;
9946 }
9947 let trunk_now = crate::dsv4::pick_tally_take();
9949 crate::dsv4::trunk_freq_note(&trunk_now);
9950 if !trunk_now.is_empty() {
9951 self.dspark_trunk_picks.push(trunk_now);
9952 let keep = crate::dsv4::dspark_block();
9953 if self.dspark_trunk_picks.len() > keep {
9954 self.dspark_trunk_picks.remove(0);
9955 }
9956 }
9957 for p in std::mem::take(&mut self.dspark_pending) {
9960 let Some(i) = position.checked_sub(p.0 + 1) else {
9961 continue;
9962 };
9963 let mut p = p;
9964 if i < p.1.len() {
9965 if p.2 && p.1[i] == token_id {
9966 p.3 = i + 1;
9967 } else {
9968 p.2 = false;
9969 }
9970 if i + 1 < p.1.len() {
9971 self.dspark_pending.push(p);
9972 continue;
9973 }
9974 }
9975 self.dspark_hist.push(p.3);
9976 self.dspark_real.push(token_id);
9977 }
9978 let Some(b) = &mut self.dsv4 else { return };
9979 let (g, layers, cfg) = (&b.0, &b.1, b.2);
9980 let n_layers = layers.len();
9981 if self.dspark.is_none() {
9982 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9983 if t.is_empty() {
9984 return;
9985 }
9986 eprintln!(
9987 "DSpark: захват со слоёв {t:?}, блок {}",
9988 crate::dsv4::dspark_block()
9989 );
9990 crate::dsv4::dspark_arm(&t, cfg.dim);
9991 self.dspark = Some(crate::dsv4::DsparkState::new(
9992 self.dsv4_mtp.len(),
9993 &cfg,
9994 t.len(),
9995 ));
9996 }
9997 let ds = self.dspark.as_mut().unwrap();
9998 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
9999 return; }
10001 let mut conf = Vec::new();
10002 crate::dsv4::pick_tally_arm();
10003 let draft_started = std::time::Instant::now();
10008 #[cfg(feature = "gpu")]
10009 let gpu_draft = crate::dsv4::dspark_gpu_on();
10010 #[cfg(not(feature = "gpu"))]
10011 let gpu_draft = false;
10012 let props = if gpu_draft {
10013 #[cfg(feature = "gpu")]
10014 {
10015 let kv_id = b.3.kv_id;
10016 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
10017 Some(pk) => crate::dsv4::dspark_draft_gpu(
10018 g,
10019 &self.dsv4_mtp,
10020 &cfg,
10021 ds,
10022 pk,
10023 kv_id,
10024 token_id,
10025 position,
10026 self.pool.as_deref(),
10027 &mut conf,
10028 ),
10029 None => Vec::new(),
10030 }
10031 }
10032 #[cfg(not(feature = "gpu"))]
10033 Vec::new()
10034 } else {
10035 crate::gpu::cpu_scope(|| {
10036 crate::dsv4::dspark_draft(
10037 g,
10038 &self.dsv4_mtp,
10039 &cfg,
10040 ds,
10041 token_id,
10042 position,
10043 self.pool.as_deref(),
10044 &mut conf,
10045 )
10046 })
10047 };
10048 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
10049 let draft_picks = crate::dsv4::pick_tally_take();
10050 crate::dsv4::dspark_freq_note(&draft_picks);
10051 crate::dsv4::pick_tally_arm();
10054 if !props.is_empty() {
10055 let (tu, tt) = {
10059 let flat: Vec<(usize, Vec<usize>)> = self
10060 .dspark_trunk_picks
10061 .iter()
10062 .flat_map(|v| v.iter().cloned())
10063 .collect();
10064 let mut per: std::collections::HashMap<usize, Vec<usize>> =
10066 std::collections::HashMap::new();
10067 for (li, picks) in flat {
10068 per.entry(li).or_default().extend(picks);
10069 }
10070 let n = per.len().max(1);
10071 let mut u = 0usize;
10072 let mut t = 0usize;
10073 for (_, v) in per {
10074 t += v.len();
10075 u += v.iter().collect::<std::collections::HashSet<_>>().len();
10076 }
10077 (u / n, t / n)
10078 };
10079 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
10080 self.dspark_exp.push((tu, tt, du, dt));
10081 self.dspark_pending.push((position, props, true, 0));
10082 }
10083 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
10084 let n = self.dspark_hist.len() as f32;
10085 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
10086 let block = crate::dsv4::dspark_block();
10087 let mut at = vec![0usize; block + 1];
10088 for &k in &self.dspark_hist {
10089 at[k] += 1;
10090 }
10091 let mut surv = Vec::with_capacity(block);
10093 for i in 1..=block {
10094 let k = at[i..].iter().sum::<usize>() as f32 / n;
10095 surv.push(format!("{k:.2}"));
10096 }
10097 let distinct = self
10098 .dspark_real
10099 .iter()
10100 .collect::<std::collections::HashSet<_>>()
10101 .len();
10102 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
10103 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
10104 });
10105 let m = self.dspark_exp.len().max(1);
10106 eprintln!(
10107 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
10108 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
10109 self.dspark_hist.len(),
10110 mean + 1.0,
10111 surv.join(" ")
10112 );
10113 eprintln!(
10114 "DSpark: разных токенов {distinct} из {} (вырожденность), \
10115 эксперты ствол {}/{} на слой за {block} токенов, \
10116 черновик {}/{} за блок, draft {:.2} мс/блок",
10117 self.dspark_real.len(),
10118 tu / m,
10119 tt / m,
10120 du / m,
10121 dt / m,
10122 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
10123 );
10124 }
10125 }
10126
10127 fn forward_layers_upto(
10128 &mut self,
10129 hidden: &[f32],
10130 position: usize,
10131 task_mask: Option<&TaskMask>,
10132 upto: Option<usize>,
10133 ) -> Vec<f32> {
10134 if let Some(plan) = self.gpu_plan.clone() {
10140 if upto.is_none() && plan.len() > 1 {
10141 let mut h = hidden.to_vec();
10142 for &(dev, from, upto_incl) in plan.iter() {
10143 h = crate::gpu::with_device(dev, || {
10144 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
10145 });
10146 }
10147 return h;
10148 }
10149 }
10150 self.forward_layers_span(hidden, position, task_mask, 0, upto)
10151 }
10152
10153 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
10158 self.set_gpu_plan_at(devices, None)
10159 }
10160
10161 pub fn set_gpu_plan_at(
10165 &mut self,
10166 devices: Option<&[usize]>,
10167 at: Option<usize>,
10168 ) -> Result<(), String> {
10169 let Some(devs) = devices.filter(|d| d.len() > 1) else {
10170 self.gpu_plan = None;
10171 return Ok(());
10172 };
10173 self.split_supported()?;
10174 let n = self.num_layers;
10175 if devs.len() > n {
10176 return Err(format!("{} devices for {n} layers", devs.len()));
10177 }
10178 if let Some(k) = at {
10179 if k == 0 || k >= n {
10180 return Err(format!("split at {k}: the model has {n} layers"));
10181 }
10182 if devs.len() == 2 {
10183 self.gpu_plan = Some(std::sync::Arc::new(vec![
10184 (devs[0], 0, k - 1),
10185 (devs[1], k, n - 1),
10186 ]));
10187 return Ok(());
10188 }
10189 return Err(format!(
10190 "an explicit split point takes exactly 2 devices, got {}",
10191 devs.len()
10192 ));
10193 }
10194 let per = n.div_ceil(devs.len());
10195 let mut plan = Vec::with_capacity(devs.len());
10196 let mut from = 0usize;
10197 for &d in devs {
10198 if from >= n {
10199 break;
10200 }
10201 let upto = (from + per - 1).min(n - 1);
10202 plan.push((d, from, upto));
10203 from = upto + 1;
10204 }
10205 self.gpu_plan = Some(std::sync::Arc::new(plan));
10206 Ok(())
10207 }
10208
10209 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
10211 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
10212 }
10213
10214 fn forward_layers_span(
10220 &mut self,
10221 hidden: &[f32],
10222 position: usize,
10223 task_mask: Option<&TaskMask>,
10224 from: usize,
10225 upto: Option<usize>,
10226 ) -> Vec<f32> {
10227 debug_assert!(
10228 from == 0
10229 || (self.dsv4.is_none()
10230 && self.dsv41.is_none()
10231 && self.qwen4_exp.is_none()
10232 && self.g3n.is_none())
10233 );
10234 if let Some(b) = &mut self.qwen4_exp {
10235 let _ = (task_mask, upto);
10236 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10237 let mut logits = Vec::new();
10238 crate::qwen4_exp::forward_token(
10239 &b.0,
10240 &b.1,
10241 &b.2,
10242 &mut b.3,
10243 token_id,
10244 position,
10245 &self.inv_freq,
10246 self.pool.as_deref(),
10247 &mut logits,
10248 true,
10249 );
10250 self.graph_logits = Some(logits);
10251 return vec![0.0; self.hidden_size];
10252 }
10253 if let Some(b) = &mut self.dsv4 {
10259 let _ = (task_mask, upto);
10260 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10261 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
10262 st.pos = position;
10263 let mut logits = Vec::new();
10264 crate::dsv4::forward_token(
10265 g,
10266 layers,
10267 &cfg,
10268 st,
10269 token_id,
10270 &self.inv_freq,
10271 self.pool.as_deref(),
10272 &mut logits,
10273 );
10274 self.graph_logits = Some(logits);
10275 self.dspark_probe(position, token_id);
10276 return vec![0.0; self.hidden_size];
10279 }
10280 if let Some(b) = &mut self.dsv41 {
10282 let _ = (task_mask, upto);
10283 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10284 let mut logits = Vec::new();
10285 crate::dsv41::forward_token(
10286 &b.0,
10287 &b.1,
10288 &b.2,
10289 &mut b.3,
10290 token_id,
10291 position,
10292 self.pool.as_deref(),
10293 &mut logits,
10294 );
10295 self.graph_logits = Some(logits);
10296 return vec![0.0; self.hidden_size];
10297 }
10298 if let Some(b) = &self.g3n {
10301 let _ = (task_mask, upto);
10302 return crate::g3n::g3n_forward(
10303 &b.0,
10304 &b.1,
10305 hidden,
10306 position,
10307 &mut self.kv_cache.layers,
10308 self.num_heads,
10309 self.num_kv_heads,
10310 self.head_dim,
10311 self.pool.as_deref(),
10312 );
10313 }
10314 let mut h = hidden.to_vec();
10315 let (nh, _nkv, _hd, hs, _rd, eps) = (
10318 self.num_heads,
10319 self.num_kv_heads,
10320 self.head_dim,
10321 self.hidden_size,
10322 self.rotary_dim,
10323 self.rms_eps,
10324 );
10325 let pool = self.pool.clone();
10326 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
10338 let graph_on = match graph_env.as_deref() {
10339 Some("0") => false,
10340 Some("prefill") => false, Some(_) => true,
10342 None => crate::gpu::wgpu_graph_default(),
10348 };
10349 let graph_trusted =
10350 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
10351 let race_eligible = graph_on
10352 && upto.is_none()
10353 && task_mask.is_none()
10354 && from == 0
10355 && !crate::gpu::graph_unsupported();
10356 let mut tail_start = 0usize;
10357 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
10358 let t_graph = std::time::Instant::now();
10359 let mut lg = Vec::new();
10360 let mut gl = 0usize;
10361 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
10362 let declined = built.is_none();
10363 let built = match built {
10364 Some(Ok(hh)) => Some(hh),
10365 Some(Err(())) => {
10366 self.clear_sequence_state();
10370 self.graph_failed
10371 .store(true, std::sync::atomic::Ordering::Relaxed);
10372 self.cancel
10373 .store(true, std::sync::atomic::Ordering::Relaxed);
10374 tracing::error!("token graph failed after admission; sequence state cleared");
10375 return vec![0.0; self.hidden_size];
10376 }
10377 None => None,
10378 };
10379 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
10384 crate::gpu::graph_mark_unsupported();
10385 }
10386 graph_note(built.is_some(), gl, self.num_layers);
10387 if let Some(hh) = built {
10388 let dur = t_graph.elapsed();
10389 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10390 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
10391 }
10392 if gl > 0 && gl < self.num_layers {
10393 h = hh;
10399 tail_start = gl;
10400 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
10401 if !graph_trusted {
10402 crate::gpu::graph_race_record(true, dur);
10403 }
10404 if !lg.is_empty() {
10405 lg.resize(self.vocab_size, 0.0);
10408 if let Some(c) = self.final_softcap {
10409 for l in lg.iter_mut() {
10410 *l = c * (*l / c).tanh();
10411 }
10412 }
10413 self.graph_logits = Some(lg);
10414 }
10415 return hh;
10416 }
10417 }
10423 }
10424 let span = from > 0 || upto.is_some();
10448 if span && graph_on && task_mask.is_none() && graph_trusted {
10449 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
10450 let mut lg = Vec::new();
10451 let mut gl = 0usize;
10452 let span_res =
10453 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
10454 let span_res = match span_res {
10455 Some(Ok(hh)) => Some(hh),
10456 Some(Err(())) => {
10457 self.clear_sequence_state();
10458 self.graph_failed
10459 .store(true, std::sync::atomic::Ordering::Relaxed);
10460 self.cancel
10461 .store(true, std::sync::atomic::Ordering::Relaxed);
10462 tracing::error!(
10463 "span token graph failed after admission; sequence state cleared"
10464 );
10465 return vec![0.0; self.hidden_size];
10466 }
10467 None => None,
10468 };
10469 graph_note(span_res.is_some(), gl, upto_excl - from);
10470 if std::env::var("CMF_GPU_DEBUG").is_ok() {
10471 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
10475 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
10476 eprintln!(
10477 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
10478 upto_excl - from,
10479 span_res.is_some()
10480 );
10481 }
10482 }
10483 if let Some(hh) = span_res {
10484 if gl == upto_excl - from {
10485 if !lg.is_empty() {
10486 lg.resize(self.vocab_size, 0.0);
10487 if let Some(c) = self.final_softcap {
10488 for l in lg.iter_mut() {
10489 *l = c * (*l / c).tanh();
10490 }
10491 }
10492 self.graph_logits = Some(lg);
10493 }
10494 crate::gpu::set_layer(-1);
10495 return hh;
10496 }
10497 h = hh;
10499 tail_start = from + gl;
10500 }
10501 }
10502 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
10503
10504 let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
10510 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10511
10512 #[cfg(target_os = "macos")]
10513 let mut gpu_skip_until = 0usize;
10514 for li in tail_start.max(from)..self.num_layers {
10515 let _capacity_tail = automatic_gpu_prefix
10516 .filter(|&prefix| li >= prefix)
10517 .map(|_| crate::gpu::enter_cpu_scope());
10518 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
10520 if li > u {
10521 break;
10522 }
10523 }
10524 if let Some(mask) = task_mask {
10525 if !mask.layer_alive(li) {
10526 continue; }
10528 }
10529 #[cfg(target_os = "macos")]
10533 {
10534 if li < gpu_skip_until {
10535 continue;
10536 }
10537 if task_mask.is_none() {
10538 let end = self.q1_graph_gpu(li, upto, position, &mut h);
10539 if self
10540 .graph_failed
10541 .load(std::sync::atomic::Ordering::Relaxed)
10542 {
10543 return vec![0.0; self.hidden_size];
10547 }
10548 if end > li {
10549 gpu_skip_until = end;
10550 if self.is_loop_end(end - 1) && end < self.num_layers {
10553 h = inference::rms_norm(
10554 &h,
10555 &self.weights.final_norm,
10556 self.rms_eps,
10557 self.norm_style,
10558 );
10559 }
10560 continue;
10561 }
10562 }
10563 }
10564
10565 let lw = &self.weights.layers[self.phys_layer(li)];
10566 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10567 if tp.parse::<usize>().ok() == Some(position) {
10568 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
10569 eprintln!(
10570 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10571 h[0], h[1]
10572 );
10573 }
10574 }
10575 inference::rms_norm_into(
10578 &h,
10579 &lw.input_norm,
10580 self.rms_eps,
10581 self.norm_style,
10582 &mut self.ws.n1,
10583 );
10584
10585 let attn_out = match &lw.attn {
10586 AttnKind::Mla(w) => {
10587 let inv_freq_l = self.layer_inv_freq(li);
10588 let rs = self.layer_rope_scale(li);
10589 let eps = self.rms_eps;
10590 let pool = self.pool.clone();
10591 mla_attention(
10592 w,
10593 &self.ws.n1,
10594 &mut self.kv_cache.layers[li],
10595 position,
10596 &inv_freq_l,
10597 rs,
10598 eps,
10599 pool.as_deref(),
10600 )
10601 }
10602 AttnKind::Linear(w) => {
10603 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
10604 vmf_phase_forward(
10605 &self.ws.n1,
10606 w,
10607 &cfg,
10608 &mut self.kv_cache.layers[li].linear_state,
10609 self.pool.as_deref(),
10610 )
10611 }
10612 AttnKind::Kda(w) => {
10613 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10614 crate::linear_core::kda_forward(
10615 &self.ws.n1,
10616 w,
10617 &cfg,
10618 &mut self.kv_cache.layers[li].linear_state,
10619 self.pool.as_deref(),
10620 )
10621 }
10622 AttnKind::LinearGdn(w) => {
10623 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10624 gdn_forward(
10625 &self.ws.n1,
10626 w,
10627 &cfg,
10628 &mut self.kv_cache.layers[li].linear_state,
10629 self.pool.as_deref(),
10630 )
10631 }
10632 AttnKind::ShortConv(w) => {
10633 let cfg = self
10634 .short_conv_cfg
10635 .expect("short-conv layer without short_conv_cfg");
10636 short_conv_forward(
10637 &self.ws.n1,
10638 w,
10639 &cfg,
10640 &mut self.kv_cache.layers[li].linear_state,
10641 self.pool.as_deref(),
10642 )
10643 }
10644 AttnKind::Full {
10645 wq,
10646 wk,
10647 wv,
10648 wo,
10649 q_norm,
10650 k_norm,
10651 output_gate,
10652 softplus_gate,
10653 bias,
10654 } if self.kv_cache.layers[li].o1_sealed() => {
10655 let inv_freq_l = self.layer_inv_freq(li);
10658 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10659 let cfg = QwenAttnCfg {
10660 num_heads: self.layer_num_heads(li),
10661 num_kv_heads: nkv_l,
10662 head_dim: hd_l,
10663 hidden_size: hs,
10664 position,
10665 inv_freq: &inv_freq_l,
10666 rotary_dim: rd_l,
10667 scale: self.attn_scale,
10668 softcap: self.attn_softcap,
10669 window: None,
10670 v_norm: self.attn_v_norm,
10671 q_norm: q_norm.as_deref(),
10672 k_norm: k_norm.as_deref(),
10673 output_gate: *output_gate,
10674 softplus_gate: softplus_gate
10675 .as_ref()
10676 .map(|(gate, per_head)| (gate, *per_head)),
10677 rope_scale: self.layer_rope_scale(li),
10678 bias: bias
10679 .as_ref()
10680 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10681 rms_eps: eps,
10682 norm_style: self.norm_style,
10683 pool: pool.as_deref(),
10684 };
10685 attention::qwen_attention_nystrom(
10686 &self.ws.n1,
10687 wq,
10688 wk,
10689 wv,
10690 wo,
10691 &mut self.kv_cache.layers[li],
10692 &cfg,
10693 )
10694 }
10695 AttnKind::Full {
10696 wq,
10697 wk,
10698 wv,
10699 wo,
10700 q_norm,
10701 k_norm,
10702 output_gate,
10703 softplus_gate,
10704 bias,
10705 } => 'attn: {
10706 if graph_on
10709 && !*output_gate
10710 && softplus_gate.is_none()
10711 && self.attention_heads_per_layer.is_none()
10712 && bias.is_none()
10713 && task_mask.is_none()
10714 {
10715 let inv_freq_l = self.layer_inv_freq(li);
10716 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10717 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10718 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
10719 wq.mapped_q1(),
10720 wk.mapped_q1(),
10721 wv.mapped_q1(),
10722 wo.mapped_q1(),
10723 ) {
10724 let gm = gm.clone();
10725 let mut out = vec![0f32; hs];
10726 let cache = &self.kv_cache.layers[li];
10727 if crate::gpu::attn_dropin(
10728 &gm,
10729 self.graph_kv_id,
10730 li,
10731 &self.ws.n1,
10732 qi,
10733 ki,
10734 vi,
10735 oi,
10736 q_norm.as_deref(),
10737 k_norm.as_deref(),
10738 &inv_freq_l,
10739 nh,
10740 nkv_l,
10741 hd_l,
10742 rd_l,
10743 hs,
10744 position,
10745 self.kv_cache.max_seq_len,
10746 gemma,
10747 eps as f32,
10748 cache.k_heads(),
10749 cache.v_heads(),
10750 &mut out,
10751 ) {
10752 break 'attn out;
10753 }
10754 }
10755 }
10756 let masked = task_mask
10757 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
10758 .unwrap_or(false);
10759 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
10760 match (masked, f32_view) {
10761 (true, (Some(q), Some(k), Some(v), Some(o))) => {
10764 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
10765 attention::multi_head_attention(
10766 &self.ws.n1,
10767 q,
10768 k,
10769 v,
10770 o,
10771 &mut self.kv_cache.layers[li],
10772 self.num_heads,
10773 self.num_kv_heads,
10774 self.head_dim,
10775 self.hidden_size,
10776 position,
10777 &active_heads,
10778 &self.inv_freq,
10779 )
10780 }
10781 (masked, _) => {
10782 if masked {
10783 tracing::warn!(
10784 "layer {li}: head mask on quantized weights not \
10785 supported yet — executing dense"
10786 );
10787 }
10788 let inv_freq_l = self.layer_inv_freq(li);
10789 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10790 let cfg = QwenAttnCfg {
10791 num_heads: self.layer_num_heads(li),
10792 num_kv_heads: nkv_l,
10793 head_dim: hd_l,
10794 hidden_size: hs,
10795 position,
10796 inv_freq: &inv_freq_l,
10797 rotary_dim: rd_l,
10798 scale: self.attn_scale,
10799 softcap: self.attn_softcap,
10800 window: self.layer_window(li),
10801 v_norm: self.attn_v_norm,
10802 q_norm: q_norm.as_deref(),
10803 k_norm: k_norm.as_deref(),
10804 output_gate: *output_gate,
10805 softplus_gate: softplus_gate
10806 .as_ref()
10807 .map(|(gate, per_head)| (gate, *per_head)),
10808 rope_scale: self.layer_rope_scale(li),
10809 bias: bias
10810 .as_ref()
10811 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10812 rms_eps: eps,
10813 norm_style: self.norm_style,
10814 pool: pool.as_deref(),
10815 };
10816 attention::qwen_attention(
10817 &self.ws.n1,
10818 wq,
10819 wk,
10820 wv,
10821 wo,
10822 &mut self.kv_cache.layers[li],
10823 &cfg,
10824 )
10825 }
10826 }
10827 }
10828 };
10829 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
10832 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
10833 None => attn_out,
10834 };
10835 let lw = &self.weights.layers[self.phys_layer(li)];
10836 inference::add_rmsnorm_fused_into(
10837 &mut h,
10838 &attn_out,
10839 &lw.post_norm,
10840 self.rms_eps,
10841 self.norm_style,
10842 &mut self.ws.p1,
10843 );
10844 let mut attn_out = attn_out;
10845 attention::recycle_buf(&mut attn_out);
10846 let post_normed = &self.ws.p1;
10847
10848 let ffn_masked = task_mask
10849 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
10850 .unwrap_or(false);
10851 let ffn_out = match (ffn_masked, &lw.ffn) {
10863 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
10867 let row = task_mask
10868 .and_then(|tm| tm.ffn_masks.get(li))
10869 .map(|v| v.as_slice());
10870 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
10871 }
10872 (true, FfnKind::Dense(d)) => {
10873 let tm = task_mask.unwrap();
10874 let alive = tm.ffn_active_count(li);
10875 let deep = alive * 2 <= self.intermediate_size;
10876 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
10877 let active = tm.ffn_active_indices(li);
10878 sparse_ffn_quant(
10879 d,
10880 post_normed,
10881 &active,
10882 self.hidden_size,
10883 self.pool.as_deref(),
10884 )
10885 } else if deep
10886 && let (Some(g), Some(u), Some(dn)) = (
10887 d.gate_proj.as_f32(),
10888 d.up_proj.as_f32(),
10889 d.down_proj.as_f32(),
10890 )
10891 {
10892 let active = tm.ffn_active_indices(li);
10893 inference::sparse_ffn_forward(
10894 post_normed,
10895 g,
10896 u,
10897 dn,
10898 self.hidden_size,
10899 self.intermediate_size,
10900 &active,
10901 self.pool.as_deref(),
10902 )
10903 } else {
10904 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
10905 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
10906 }
10907 }
10908 (true, FfnKind::Moe(m)) => {
10909 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
10913 ffn_forward(
10914 &lw.ffn,
10915 post_normed,
10916 self.pool.as_deref(),
10917 allowed.as_deref(),
10918 )
10919 }
10920 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
10921 dm,
10922 post_normed,
10923 &h,
10924 self.rms_eps,
10925 self.norm_style,
10926 self.pool.as_deref(),
10927 ),
10928 (false, _) => match &lw.ffn {
10929 FfnKind::DenseMoe(dm) => dense_moe_ffn(
10930 dm,
10931 post_normed,
10932 &h,
10933 self.rms_eps,
10934 self.norm_style,
10935 self.pool.as_deref(),
10936 ),
10937 _ => {
10938 let allowed = match (&lw.ffn, task_mask) {
10939 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
10940 _ => None,
10941 };
10942 ffn_forward(
10943 &lw.ffn,
10944 post_normed,
10945 self.pool.as_deref(),
10946 allowed.as_deref(),
10947 )
10948 }
10949 },
10950 };
10951 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
10952 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
10953 None => ffn_out,
10954 };
10955 for (i, &f) in ffn_out.iter().enumerate() {
10956 h[i] += f;
10957 }
10958 let mut ffn_out = ffn_out;
10959 attention::recycle_buf(&mut ffn_out);
10960
10961 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
10963 for v in h.iter_mut() {
10964 *v *= sc;
10965 }
10966 }
10967
10968 if self.is_loop_end(li) && li + 1 < self.num_layers {
10971 h = inference::rms_norm(
10972 &h,
10973 &self.weights.final_norm,
10974 self.rms_eps,
10975 self.norm_style,
10976 );
10977 }
10978
10979 if self.dyn_phi_layer == Some(li) {
10983 self.update_dyn_phi(&h);
10984 }
10985 }
10986 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
10988 crate::gpu::graph_race_record(false, t.elapsed());
10989 }
10990
10991 h
10992 }
10993
10994 fn update_dyn_phi(&mut self, h: &[f32]) {
10997 const A: f32 = 0.2;
10998 if self.dyn_phi_ema.len() != h.len() {
10999 self.dyn_phi_ema = vec![0.0; h.len()];
11000 self.dyn_phi_seen = 0;
11001 }
11002 if self.dyn_phi_seen == 0 {
11003 self.dyn_phi_ema.copy_from_slice(h);
11004 } else {
11005 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
11006 *e = (1.0 - A) * *e + A * v;
11007 }
11008 }
11009 self.dyn_phi_seen += 1;
11010 }
11011
11012 pub fn dyn_phi(&self) -> &[f32] {
11014 &self.dyn_phi_ema
11015 }
11016
11017 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
11019 self.dyn_phi_layer = layer;
11020 self.dyn_phi_ema.clear();
11021 self.dyn_phi_seen = 0;
11022 }
11023
11024 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
11026 let Some(model) = &self.model else {
11027 return Vec::new();
11028 };
11029 model
11030 .header
11031 .skills
11032 .iter()
11033 .enumerate()
11034 .filter_map(|(i, sk)| {
11035 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
11036 let sel = sk.selection.as_ref()?;
11037 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
11038 })
11039 .collect()
11040 }
11041
11042 pub fn active_skill(&self) -> Option<usize> {
11044 self.dyn_active
11045 }
11046
11047 pub fn enable_dynamic_routing(&mut self) -> usize {
11052 use crate::swarm::{DynRouter, RoutableSkill};
11053 let Some(model) = self.model.clone() else {
11054 return 0;
11055 };
11056 if self.dyn_blend_loaded {
11059 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
11060 return 0;
11061 }
11062 if let Some(a) = self.dyn_active {
11066 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
11067 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
11068 return 0;
11069 }
11070 }
11071 let hidden = self.hidden_size;
11072 let mut skills = Vec::new();
11073 for (idx, id, _phi) in self.dynamic_skills() {
11074 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
11075 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
11076 skills.push(rs);
11077 }
11078 }
11079 }
11080 if skills.is_empty() {
11081 return 0;
11082 }
11083 let phi = skills[0].phi_layer;
11085 if skills.iter().any(|s| s.phi_layer != phi) {
11086 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
11087 }
11088 let n = skills.len();
11089 self.set_dyn_phi_layer(Some(phi));
11090 self.dyn_router = Some(DynRouter::new(skills));
11091 n
11092 }
11093
11094 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
11096 self.dyn_router
11097 .as_ref()
11098 .map(|r| r.switches.clone())
11099 .unwrap_or_default()
11100 }
11101
11102 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
11105 let rows = self.weights.lm_head.rows();
11106 let mut logits = attention::take_buf(rows.min(self.vocab_size));
11107 self.weights
11108 .lm_head
11109 .matvec(hidden, &mut logits, self.pool.as_deref());
11110 logits.resize(self.vocab_size, 0.0);
11111 if let Some(m) = self.logit_multiplier {
11112 for l in logits.iter_mut() {
11113 *l *= m;
11114 }
11115 }
11116 if let Some(c) = self.final_softcap {
11117 for l in logits.iter_mut() {
11118 *l = c * (*l / c).tanh();
11119 }
11120 }
11121 if let Some(cm) = self.head_clusters.as_ref() {
11122 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
11123 }
11124 logits
11125 }
11126
11127 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
11130 let h = hidden.len();
11131 let ncl = cm.len() / h.max(1);
11132 if ncl == 0 || logits.len() % ncl != 0 {
11133 return;
11134 }
11135 let cs = logits.len() / ncl;
11136 let mut lc = vec![0.0f32; ncl];
11138 for c in 0..ncl {
11139 let row = &cm[c * h..(c + 1) * h];
11140 let mut s = 0.0f32;
11141 for j in 0..h {
11142 s += row[j] * hidden[j];
11143 }
11144 lc[c] = s;
11145 }
11146 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11147 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
11148 for c in 0..ncl {
11149 let blk = &mut logits[c * cs..(c + 1) * cs];
11150 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11151 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
11152 let add = lc[c] - lse - bl;
11153 for v in blk.iter_mut() {
11154 *v += add;
11155 }
11156 }
11157 }
11158
11159 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
11164 self.clear_sequence_state();
11165 crate::gpu::graph_race_begin_generation();
11169 if task_mask.is_none() {
11170 self.o1_begin();
11171 }
11172 let mut hidden = vec![0.0f32; self.hidden_size];
11173 for (pos, &id) in ids.iter().enumerate() {
11174 let emb = self.embed_single(id);
11175 hidden = self.forward_layers(&emb, pos, task_mask);
11176 }
11177 if let Err(err) = self.o1_seal_checked() {
11178 self.o1_fail(err);
11179 }
11180 inference::rms_norm_into(
11181 &hidden,
11182 &self.weights.final_norm,
11183 self.rms_eps,
11184 self.norm_style,
11185 &mut self.ws.n1,
11186 );
11187 self.lm_head_forward(&self.ws.n1)
11188 }
11189}
11190
11191pub fn create_test_pipeline(
11193 hidden_size: usize,
11194 intermediate_size: usize,
11195 num_heads: usize,
11196 num_kv_heads: usize,
11197 head_dim: usize,
11198 num_layers: usize,
11199 vocab_size: usize,
11200) -> Pipeline {
11201 let synth = |n: usize, salt: usize| -> Vec<f32> {
11204 (0..n)
11205 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
11206 .collect()
11207 };
11208 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
11209 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
11210 };
11211 let layer_weights: Vec<LayerWeights> = (0..num_layers)
11212 .map(|li| LayerWeights {
11213 input_norm: vec![1.0; hidden_size],
11214 post_norm: vec![1.0; hidden_size],
11215 attn_out_norm: None,
11216 ffn_out_norm: None,
11217 layer_scale: None,
11218 ffn: FfnKind::Dense(DenseFfn {
11219 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
11220 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
11221 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
11222 act: Act::Silu,
11223 down_t: None,
11224 segs: Vec::new(),
11225 }),
11226 attn: AttnKind::Full {
11227 bias: None,
11228 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
11229 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
11230 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
11231 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
11232 q_norm: None,
11233 k_norm: None,
11234 output_gate: false,
11235 softplus_gate: None,
11236 },
11237 })
11238 .collect();
11239
11240 Pipeline::new(
11241 Tokenizer::byte_level(),
11242 PipelineWeights {
11243 embed_tokens: qt(vocab_size, hidden_size, 100),
11244 layers: layer_weights,
11245 lm_head: qt(vocab_size, hidden_size, 200),
11246 final_norm: vec![1.0; hidden_size],
11247 },
11248 hidden_size,
11249 intermediate_size,
11250 num_heads,
11251 num_kv_heads,
11252 head_dim,
11253 num_layers,
11254 num_layers, false, vocab_size,
11257 1e-6,
11258 10_000.0,
11259 NormStyle::Qwen,
11260 4096,
11261 SamplerConfig {
11262 seed: Some(42),
11263 ..Default::default()
11264 },
11265 )
11266}
11267
11268#[inline]
11273fn mask_bit(row: &[u8], j: usize) -> bool {
11274 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
11275}
11276
11277fn mask_gain() -> f32 {
11288 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
11289 *G.get_or_init(|| {
11290 std::env::var("CMF_FFN_MASK_GAIN")
11291 .ok()
11292 .and_then(|v| v.parse().ok())
11293 .unwrap_or(1.0)
11294 })
11295}
11296
11297fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
11298 let fill = meanfill().and_then(|(i, v)| {
11301 let li = crate::gpu::cur_layer();
11302 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
11303 });
11304 for r in 0..rows {
11305 let base = r * inter;
11306 for (bi, &byte) in row.iter().enumerate() {
11307 if byte == 0xFF {
11308 continue;
11309 }
11310 let j0 = bi * 8;
11311 for bit in 0..8 {
11312 let j = j0 + bit;
11313 if j < inter && byte & (1 << bit) == 0 {
11314 g[base + j] = fill.map_or(0.0, |f| f[j]);
11315 }
11316 }
11317 }
11318 }
11319 let gain = mask_gain();
11320 if gain != 1.0 {
11321 for v in g[..rows * inter].iter_mut() {
11322 *v *= gain;
11323 }
11324 }
11325}
11326
11327#[inline]
11329fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
11330 row.is_none_or(|r| mask_bit(r, i))
11331}
11332
11333fn all_bits_on(row: &[u8], n: usize) -> bool {
11336 (0..n).all(|i| mask_bit(row, i))
11337}
11338
11339fn tube_topk() -> usize {
11347 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11348 *K.get_or_init(|| {
11349 std::env::var("CMF_TUBE_TOPK")
11350 .ok()
11351 .and_then(|v| v.parse().ok())
11352 .unwrap_or(0)
11353 })
11354}
11355
11356fn tube_score_oracle() -> bool {
11357 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11358 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
11359}
11360
11361fn tube_ffn_routed(
11368 d: &DenseFfn,
11369 xs: &[f32],
11370 b: usize,
11371 pool: Option<&Pool>,
11372 mask_row: Option<&[u8]>,
11373 k: usize,
11374) -> Vec<f32> {
11375 let hidden = d.down_proj.rows();
11376 let core = d.gate_proj.rows();
11377 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11378 let mut out = match (b, core_full, mask_row) {
11379 (1, true, _) => dense_ffn(d, xs, pool),
11380 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11381 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11382 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11383 };
11384 let cand: Vec<usize> = (0..d.segs.len())
11385 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
11386 .collect();
11387 if cand.is_empty() {
11388 return out;
11389 }
11390 let oracle = tube_score_oracle();
11394 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
11395 let mut scores = vec![0f32; b * cand.len()];
11396 for (ci, &i) in cand.iter().enumerate() {
11397 let seg = &d.segs[i];
11398 let w = seg.width;
11399 let mut g = vec![0.0f32; b * w];
11400 if b == 1 {
11401 seg.gate.matvec(xs, &mut g, pool);
11402 } else {
11403 seg.gate.matmat(xs, b, &mut g, pool);
11404 }
11405 for v in g.iter_mut() {
11406 *v = Act::Silu.combine(*v, 1.0);
11407 }
11408 if !oracle {
11409 for t in 0..b {
11410 scores[t * cand.len() + ci] =
11411 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11412 }
11413 }
11414 if oracle || b > 1 {
11415 let mut u = vec![0.0f32; b * w];
11416 if b == 1 {
11417 seg.up.matvec(xs, &mut u, pool);
11418 } else {
11419 seg.up.matmat(xs, b, &mut u, pool);
11420 }
11421 for (a, &v) in g.iter_mut().zip(u.iter()) {
11422 *a *= v;
11423 }
11424 if oracle {
11425 for t in 0..b {
11426 scores[t * cand.len() + ci] =
11427 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11428 }
11429 }
11430 }
11431 acts.push(g);
11432 }
11433 let keep = k.min(cand.len());
11435 let mut scratch: Vec<f32> = Vec::new();
11436 for t in 0..b {
11437 let mut sc: Vec<(f32, usize)> = (0..cand.len())
11438 .map(|ci| (scores[t * cand.len() + ci], ci))
11439 .collect();
11440 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
11441 let mut alive = vec![false; cand.len()];
11442 for &(_, ci) in sc.iter().take(keep) {
11443 alive[ci] = true;
11444 }
11445 if b > 1 {
11446 for (ci, a) in acts.iter_mut().enumerate() {
11447 if !alive[ci] {
11448 let w = d.segs[cand[ci]].width;
11449 a[t * w..(t + 1) * w].fill(0.0);
11450 }
11451 }
11452 } else {
11453 for (ci, &i) in cand.iter().enumerate() {
11457 if !alive[ci] {
11458 continue;
11459 }
11460 let seg = &d.segs[i];
11461 let w = seg.width;
11462 let g = &mut acts[ci];
11463 if !tube_score_oracle() {
11464 scratch.clear();
11465 scratch.resize(w, 0.0);
11466 seg.up.matvec(xs, &mut scratch, pool);
11467 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
11468 *a *= v;
11469 }
11470 }
11471 let mut acc = vec![0.0f32; hidden];
11472 seg.down.matvec(g, &mut acc, pool);
11473 for (o, a) in out.iter_mut().zip(&acc) {
11474 *o += *a;
11475 }
11476 }
11477 }
11478 }
11479 if b > 1 {
11480 for (ci, &i) in cand.iter().enumerate() {
11481 let seg = &d.segs[i];
11482 let mut acc = vec![0.0f32; b * hidden];
11483 seg.down.matmat(&acts[ci], b, &mut acc, pool);
11484 for (o, a) in out.iter_mut().zip(&acc) {
11485 *o += *a;
11486 }
11487 }
11488 }
11489 out
11490}
11491
11492fn tube_ffn(
11498 d: &DenseFfn,
11499 xs: &[f32],
11500 b: usize,
11501 pool: Option<&Pool>,
11502 mask_row: Option<&[u8]>,
11503) -> Vec<f32> {
11504 if tube_topk() > 0 {
11505 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
11506 }
11507 let hidden = d.down_proj.rows();
11508 let core = d.gate_proj.rows();
11509 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11510 let mut out = match (b, core_full, mask_row) {
11511 (1, true, _) => dense_ffn(d, xs, pool),
11512 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11513 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11514 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11515 };
11516 TUBE_SCRATCH.with(|sc| {
11517 let mut sc = sc.borrow_mut();
11518 let [g, u, acc] = &mut *sc;
11519 for seg in &d.segs {
11520 if !tube_bit(mask_row, seg.start) {
11521 continue;
11522 }
11523 let w = seg.width;
11524 g.resize(b * w, 0.0);
11525 if b == 1
11526 && d.act == Act::Silu
11527 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
11528 {
11529 } else {
11531 u.resize(b * w, 0.0);
11532 if b == 1 {
11533 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
11534 } else {
11535 seg.gate.matmat(xs, b, g, pool);
11536 seg.up.matmat(xs, b, u, pool);
11537 }
11538 for i in 0..b * w {
11539 g[i] = d.act.combine(g[i], u[i]);
11540 }
11541 }
11542 acc.resize(b * hidden, 0.0);
11543 acc.fill(0.0);
11544 if b == 1 {
11545 seg.down.matvec(g, acc, pool);
11546 } else {
11547 seg.down.matmat(g, b, acc, pool);
11548 }
11549 for (o, a) in out.iter_mut().zip(acc.iter()) {
11550 *o += *a;
11551 }
11552 }
11553 out
11554 })
11555}
11556
11557thread_local! {
11558 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
11562 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
11563}
11564
11565fn dense_ffn_batch(
11566 d: &DenseFfn,
11567 xs: &[f32],
11568 b: usize,
11569 pool: Option<&Pool>,
11570 mask_row: Option<&[u8]>,
11571) -> Vec<f32> {
11572 let inter = d.gate_proj.rows();
11573 let hidden = d.down_proj.rows();
11574 if mask_row.is_none()
11582 && d.act == Act::Silu
11583 && b >= 32
11584 && crate::gpu::enabled_here()
11585 && !crate::gpu::mm_killed()
11586 && refit_dir().is_none()
11591 && !ffn_probe_active()
11596 {
11597 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11598 d.gate_proj.mapped_q4t(),
11599 d.up_proj.mapped_q4t(),
11600 d.down_proj.mapped_q4t(),
11601 ) {
11602 let mut out = vec![0.0f32; b * hidden];
11603 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11604 return out;
11605 }
11606 }
11607 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11612 d.gate_proj.mapped_q4tp(),
11613 d.up_proj.mapped_q4tp(),
11614 d.down_proj.mapped_q4tp(),
11615 ) {
11616 let mut out = vec![0.0f32; b * hidden];
11617 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11618 return out;
11619 }
11620 }
11621 }
11622 let mut g = vec![0.0f32; b * inter];
11623 d.gate_proj.matmat(xs, b, &mut g, pool);
11624 let mut u = vec![0.0f32; b * inter];
11625 d.up_proj.matmat(xs, b, &mut u, pool);
11626 if gate_topk() > 0 && d.act == Act::Silu {
11627 for t in 0..b {
11628 let row = &mut g[t * inter..(t + 1) * inter];
11629 for v in row.iter_mut() {
11630 *v = Act::Silu.combine(*v, 1.0);
11631 }
11632 keep_top_k(row, gate_topk());
11633 }
11634 for i in 0..b * inter {
11635 g[i] *= u[i];
11636 }
11637 } else {
11638 for i in 0..b * inter {
11639 g[i] = d.act.combine(g[i], u[i]);
11640 }
11641 }
11642 if let Some(row) = mask_row {
11643 zero_masked_cols(&mut g, b, inter, row);
11644 }
11645 if oracle_topk() > 0 {
11646 for t in 0..b {
11647 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
11648 }
11649 }
11650 let mut out = vec![0.0f32; b * hidden];
11651 d.down_proj.matmat(&g, b, &mut out, pool);
11652 if refit_dir().is_some() {
11653 let li = crate::gpu::cur_layer();
11654 if li >= 0 {
11655 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
11656 }
11657 }
11658 FFN_PROBE.with(|pr| {
11662 if let Some(acc) = pr.borrow_mut().as_mut() {
11663 let li = crate::gpu::cur_layer();
11664 if li < 0 {
11665 return;
11666 }
11667 let Some(row) = acc.get_mut(li as usize) else {
11668 return;
11669 };
11670 let sq = probe_sq();
11671 for t in 0..b {
11672 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
11673 *a += if sq {
11674 (v as f64) * (v as f64)
11675 } else {
11676 (v as f64).abs()
11677 };
11678 }
11679 }
11680 }
11681 });
11682 out
11683}
11684
11685fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
11690 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11691 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11692 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
11693 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
11694 if (!on && !dump) || b == 0 {
11695 return;
11696 }
11697 let hidden = xs.len() / b;
11698 if on {
11699 let mut acc = m.act_sq.borrow_mut();
11700 if acc.len() < hidden {
11701 acc.resize(hidden, 0.0);
11702 }
11703 for t in 0..b {
11704 let row = &xs[t * hidden..(t + 1) * hidden];
11705 for (a, &v) in acc.iter_mut().zip(row) {
11706 *a += (v as f64) * (v as f64);
11707 }
11708 }
11709 }
11710 if dump {
11711 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
11714 .ok()
11715 .and_then(|v| v.parse().ok())
11716 .unwrap_or(4096);
11717 let mut rows = m.act_rows.borrow_mut();
11718 if rows.len() < cap * hidden {
11719 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
11720 rows.extend_from_slice(&xs[..take * hidden]);
11721 }
11722 }
11723}
11724
11725#[derive(Clone, Copy)]
11728struct SendVecs(*mut Vec<f32>);
11729unsafe impl Send for SendVecs {}
11730unsafe impl Sync for SendVecs {}
11731impl SendVecs {
11732 #[inline]
11733 fn at(self, i: usize) -> *mut Vec<f32> {
11734 unsafe { self.0.add(i) }
11735 }
11736}
11737
11738fn moe_ffn_batch(
11739 m: &MoeFfn,
11740 xs: &[f32],
11741 b: usize,
11742 hidden: usize,
11743 pool: Option<&Pool>,
11744 allowed: Option<&[bool]>,
11745) -> Vec<f32> {
11746 accumulate_act(m, xs, b);
11747 let ne = m.experts.len();
11748 let mut logits = vec![0.0f32; b * ne];
11749 match &m.resonance {
11750 Some(r) => {
11751 let hdim = xs.len() / b.max(1);
11752 for bi in 0..b {
11753 r.scores(
11754 &xs[bi * hdim..(bi + 1) * hdim],
11755 &mut logits[bi * ne..(bi + 1) * ne],
11756 );
11757 }
11758 }
11759 None => m.router.matmat(xs, b, &mut logits, pool),
11760 }
11761
11762 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
11765 {
11766 let mut st = m.stats.borrow_mut();
11767 if st.len() < ne {
11768 st.resize(ne, 0);
11769 }
11770 for bi in 0..b {
11771 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
11772 for &e in &idx {
11773 st[e] += 1;
11774 assign[e].push((bi, p[e] / wsum));
11775 }
11776 }
11777 }
11778
11779 let mut out = vec![0.0f32; b * hidden];
11780 let cols = m.experts[0].gate_proj.cols();
11781 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
11782 let sb = list.len();
11783 let mut sub = vec![0.0f32; sb * cols];
11784 for (k, &(bi, _)) in list.iter().enumerate() {
11785 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11786 }
11787 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
11788 for (k, &(bi, w)) in list.iter().enumerate() {
11789 for i in 0..hidden {
11790 out[bi * hidden + i] += w * eo[k * hidden + i];
11791 }
11792 }
11793 };
11794 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
11800 if pool.is_some() && active.len() >= 8 {
11801 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
11802 {
11803 let panel_ptr = SendVecs(panels.as_mut_ptr());
11804 let experts = &m.experts;
11807 let (active_r, assign_r) = (&active, &assign);
11808 let run = |start: usize, end: usize| {
11809 for ai in start..end {
11810 let e = active_r[ai];
11811 let list = &assign_r[e];
11812 let sb = list.len();
11813 let mut sub = vec![0.0f32; sb * cols];
11814 for (k, &(bi, _)) in list.iter().enumerate() {
11815 sub[k * cols..(k + 1) * cols]
11816 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11817 }
11818 unsafe {
11820 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
11821 }
11822 }
11823 };
11824 match pool {
11825 Some(p) => p.run_rows(active.len(), &run),
11826 None => run(0, active.len()),
11827 }
11828 }
11829 for (ai, &e) in active.iter().enumerate() {
11830 for (k, &(bi, w)) in assign[e].iter().enumerate() {
11831 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
11832 for i in 0..hidden {
11833 out[bi * hidden + i] += w * eo[i];
11834 }
11835 }
11836 }
11837 } else {
11838 for &e in &active {
11839 run_expert(&m.experts[e], &assign[e], &mut out);
11840 }
11841 }
11842 if let Some((se, gate)) = &m.shared {
11843 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
11844 let mut gl = vec![0.0f32; b];
11845 gate.matmat(xs, b, &mut gl, pool);
11846 (0..b)
11847 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
11848 .collect()
11849 } else {
11850 (0..b).map(|bi| (bi, 1.0)).collect()
11851 };
11852 run_expert(se, &all, &mut out);
11853 }
11854 out
11855}
11856
11857thread_local! {
11858 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
11862 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
11863}
11864
11865fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11867 if gate_topk() > 0
11870 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
11871 {
11872 return out;
11873 }
11874 let prism_body = d.gate_proj.has_prism_contract()
11890 || d.up_proj.has_prism_contract()
11891 || d.down_proj.has_prism_contract();
11892 if !prism_body
11893 && crate::gpu::enabled_here()
11894 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
11895 {
11896 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
11897 crate::gpu::ProbeArm::Gpu
11898 } else {
11899 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
11900 };
11901 match arm {
11902 crate::gpu::ProbeArm::Gpu => {
11903 let t0 = std::time::Instant::now();
11904 if let Some(out) = dense_ffn_gpu(d, x, pool) {
11905 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
11906 return out;
11907 }
11908 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
11912 }
11913 crate::gpu::ProbeArm::CpuTimed => {
11914 let t0 = std::time::Instant::now();
11915 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11916 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
11917 return out;
11918 }
11919 crate::gpu::ProbeArm::Cpu => {
11920 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11921 }
11922 }
11923 }
11924 dense_ffn_cpu(d, x, pool)
11925}
11926
11927fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11929 let inter = d.gate_proj.rows();
11930 FFN_SCRATCH.with(|s| {
11931 let mut s = s.borrow_mut();
11932 let [g, u, ..] = &mut *s;
11933 g.resize(inter, 0.0);
11934 if gate_topk() > 0 {
11937 u.resize(inter, 0.0);
11941 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11942 for i in 0..inter {
11943 g[i] = Act::Silu.combine(g[i], 1.0);
11944 }
11945 keep_top_k(g, gate_topk());
11946 for i in 0..inter {
11947 g[i] *= u[i];
11948 }
11949 } else if d.act == Act::Silu
11950 && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
11951 {
11952 } else {
11954 u.resize(inter, 0.0);
11955 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11957 for i in 0..inter {
11958 g[i] = d.act.combine(g[i], u[i]);
11959 }
11960 }
11961 FFN_PROBE.with(|pr| {
11969 if let Some(acc) = pr.borrow_mut().as_mut() {
11970 let li = crate::gpu::cur_layer();
11971 if li >= 0 {
11972 if let Some(row) = acc.get_mut(li as usize) {
11973 match probe_topk() {
11974 0 if probe_sq() => {
11975 for (a, &v) in row.iter_mut().zip(g.iter()) {
11976 *a += (v as f64) * (v as f64);
11977 }
11978 }
11979 0 if probe_signed() => {
11980 for (a, &v) in row.iter_mut().zip(g.iter()) {
11981 *a += v as f64;
11982 }
11983 }
11984 0 => {
11985 for (a, &v) in row.iter_mut().zip(g.iter()) {
11986 *a += (v as f64).abs();
11987 }
11988 }
11989 k => {
11990 let n = g.len();
11991 let k = k.min(n);
11992 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
11993 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
11994 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11995 });
11996 let thr = *kth;
11997 for (a, &v) in row.iter_mut().zip(g.iter()) {
11998 if v.abs() >= thr {
11999 *a += 1.0;
12000 }
12001 }
12002 }
12003 }
12004 }
12005 }
12006 }
12007 });
12008 if oracle_topk() > 0 {
12009 keep_top_k(g, oracle_topk());
12010 }
12011 {
12012 let li = crate::gpu::cur_layer();
12013 if li >= 0 {
12014 adump_row(li as usize, g);
12015 }
12016 }
12017 let mut out = attention::take_buf(d.down_proj.rows());
12018 d.down_proj.matvec(g, &mut out, pool);
12019 out
12020 })
12021}
12022
12023pub struct RefitAcc {
12036 pub support: Vec<u32>,
12037 pub gss: Vec<f32>,
12038 pub ya: Vec<f32>,
12039 pub hidden: usize,
12040 pub tokens: u64,
12041 pub buf_g: Vec<f32>,
12047 pub buf_o: Vec<f32>,
12048 pub buf_t: usize,
12049}
12050
12051type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
12055
12056static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
12057 std::sync::OnceLock::new();
12058
12059fn ffn_probe_active() -> bool {
12062 FFN_PROBE.with(|p| p.borrow().is_some())
12063}
12064
12065fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
12066 REFIT
12067 .get_or_init(|| {
12068 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
12069 (
12070 d,
12071 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
12072 )
12073 })
12074 })
12075 .as_ref()
12076}
12077
12078fn refit_accumulate(
12080 li: usize,
12081 g: &[f32],
12082 b: usize,
12083 inter: usize,
12084 out: &[f32],
12085 hidden: usize,
12086 pool: Option<&Pool>,
12087) {
12088 let Some((dir, map)) = refit_dir() else {
12089 return;
12090 };
12091 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12092 let (from, to) = *SPAN.get_or_init(|| {
12093 let g = |k: &str, d: usize| {
12094 std::env::var(k)
12095 .ok()
12096 .and_then(|v| v.parse().ok())
12097 .unwrap_or(d)
12098 };
12099 (
12100 g("CMF_FFN_REFIT_FROM", 0),
12101 g("CMF_FFN_REFIT_TO", usize::MAX),
12102 )
12103 });
12104 if li < from || li > to {
12105 return;
12106 }
12107 let mut guard = map.lock().unwrap();
12108 let (map, shared) = &mut *guard;
12109 let acc = match map.entry(li) {
12110 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
12111 std::collections::hash_map::Entry::Vacant(e) => {
12112 let path = format!("{dir}/support.{li}.u32");
12113 let Ok(bytes) = std::fs::read(&path) else {
12114 eprintln!("refit: no {path} — layer {li} skipped");
12115 return;
12116 };
12117 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
12118 let support: Vec<u32> = bytes[4..4 + n * 4]
12119 .chunks_exact(4)
12120 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12121 .collect();
12122 eprintln!(
12123 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
12124 (n * n + hidden * n) as f64 * 4.0 / 1e6
12125 );
12126 e.insert(RefitAcc {
12127 gss: vec![0.0; n * n],
12128 ya: vec![0.0; hidden * n],
12129 buf_g: Vec::new(),
12130 buf_o: Vec::new(),
12131 buf_t: 0,
12132 support,
12133 hidden,
12134 tokens: 0,
12135 })
12136 }
12137 };
12138 let ns = acc.support.len();
12139 let cap = refit_batch();
12141 if acc.buf_g.is_empty() {
12142 acc.buf_g = vec![0.0; ns * cap];
12143 acc.buf_o = vec![0.0; hidden * cap];
12144 }
12145 let take = b.min(cap - acc.buf_t);
12146 for t in 0..take {
12147 let col = acc.buf_t + t;
12148 for (j, &n) in acc.support.iter().enumerate() {
12149 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
12150 }
12151 for h in 0..hidden {
12152 acc.buf_o[h * cap + col] = out[t * hidden + h];
12153 }
12154 }
12155 acc.buf_t += take;
12156 acc.tokens += take as u64;
12157 if acc.buf_t < cap {
12158 return;
12159 }
12160 let bt = acc.buf_t;
12161 acc.buf_t = 0;
12162 let RefitAcc {
12172 gss,
12173 ya,
12174 buf_g,
12175 buf_o,
12176 ..
12177 } = acc;
12178 let need = (ns * ns).max(hidden * ns);
12179 if shared.len() < need {
12180 shared.resize(need, 0.0);
12181 }
12182 let scratch = &mut shared[..];
12183 let _ = bt;
12184 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
12185 add_into(gss, &scratch[..ns * ns], pool);
12186 if crate::gpu::gemm_nt_f32_transient(
12187 buf_o,
12188 buf_g,
12189 &mut scratch[..hidden * ns],
12190 hidden,
12191 cap,
12192 ns,
12193 ) {
12194 add_into(ya, &scratch[..hidden * ns], pool);
12195 } else {
12196 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12197 }
12198 } else {
12199 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
12200 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12201 }
12202 }
12206
12207fn refit_batch() -> usize {
12209 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12210 *B.get_or_init(|| {
12211 std::env::var("CMF_FFN_REFIT_BATCH")
12212 .ok()
12213 .and_then(|v| v.parse().ok())
12214 .unwrap_or(4096)
12215 })
12216}
12217
12218fn accum_outer_t(
12221 c: &mut [f32],
12222 m: usize,
12223 n: usize,
12224 b: usize,
12225 left: &[f32],
12226 right: &[f32],
12227 pool: Option<&Pool>,
12228) {
12229 let ptr = SendMut(c.as_mut_ptr());
12230 let body = |i: usize| {
12231 let ptr = &ptr;
12232 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
12233 for t in 0..b {
12234 let a = left[i * b + t];
12235 if a == 0.0 {
12236 continue;
12237 }
12238 for (j, o) in row.iter_mut().enumerate() {
12239 *o += a * right[j * b + t];
12240 }
12241 }
12242 };
12243 match pool {
12244 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
12245 for i in s..e {
12246 body(i);
12247 }
12248 }),
12249 _ => {
12250 for i in 0..m {
12251 body(i);
12252 }
12253 }
12254 }
12255}
12256
12257fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
12260 let n = dst.len().min(src.len());
12261 match pool {
12262 Some(p) if n >= 1 << 16 => {
12263 let ptr = SendMut(dst.as_mut_ptr());
12264 let f = |s: usize, e: usize| {
12265 let ptr = &ptr;
12266 for blk in s..e {
12267 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
12268 for i in a..b {
12269 unsafe { *ptr.0.add(i) += src[i] };
12270 }
12271 }
12272 };
12273 p.run_rows(n.div_ceil(4096), &f);
12274 }
12275 _ => {
12276 for (d, v) in dst.iter_mut().zip(&src[..n]) {
12277 *d += *v;
12278 }
12279 }
12280 }
12281}
12282
12283fn accum_outer(
12288 c: &mut [f32],
12289 m: usize,
12290 n: usize,
12291 b: usize,
12292 left: &[f32],
12293 right: &[f32],
12294 pool: Option<&Pool>,
12295) {
12296 const TILE: usize = 32;
12297 let tiles = m.div_ceil(TILE);
12298 let cp = SendMut(c.as_mut_ptr());
12299 let body = |ti: usize| {
12300 let cp = &cp;
12301 let i0 = ti * TILE;
12302 let i1 = (i0 + TILE).min(m);
12303 for t in 0..b {
12304 let r = &right[t * n..t * n + n];
12305 for i in i0..i1 {
12306 let a = left[i * b + t];
12307 if a == 0.0 {
12308 continue;
12309 }
12310 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
12312 for (o, v) in row.iter_mut().zip(r) {
12313 *o += a * *v;
12314 }
12315 }
12316 }
12317 };
12318 match pool {
12319 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
12320 for ti in s..e {
12321 body(ti);
12322 }
12323 }),
12324 _ => {
12325 for ti in 0..tiles {
12326 body(ti);
12327 }
12328 }
12329 }
12330}
12331
12332pub fn refit_flush() -> usize {
12334 let Some((dir, map)) = refit_dir() else {
12335 return 0;
12336 };
12337 let guard = map.lock().unwrap();
12338 let mut n = 0;
12339 for (li, acc) in guard.0.iter() {
12340 let w = |name: &str, v: &[f32]| {
12343 let path = format!("{dir}/{name}.{li}.f32");
12344 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
12345 match std::fs::write(&path, &bytes) {
12346 Ok(()) => {}
12347 Err(e) => eprintln!(
12348 "refit: FAILED to write {path} ({} MB): {e}",
12349 bytes.len() / 1_000_000
12350 ),
12351 }
12352 };
12353 w("gss", &acc.gss);
12354 w("ya", &acc.ya);
12355 println!(
12356 "refit L{li}: {} support, {} tokens, hidden {}",
12357 acc.support.len(),
12358 acc.tokens,
12359 acc.hidden
12360 );
12361 n += 1;
12362 }
12363 n
12364}
12365
12366fn adump_row(li: usize, g: &[f32]) {
12371 use std::io::Write as _;
12372 static FILES: std::sync::OnceLock<
12373 Option<(
12374 String,
12375 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
12376 )>,
12377 > = std::sync::OnceLock::new();
12378 let Some((prefix, map)) = FILES
12379 .get_or_init(|| {
12380 std::env::var("CMF_FFN_ADUMP")
12381 .ok()
12382 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
12383 })
12384 .as_ref()
12385 else {
12386 return;
12387 };
12388 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12391 let (from, to) = *SPAN.get_or_init(|| {
12392 let g = |k: &str, d: usize| {
12393 std::env::var(k)
12394 .ok()
12395 .and_then(|v| v.parse().ok())
12396 .unwrap_or(d)
12397 };
12398 (
12399 g("CMF_FFN_ADUMP_FROM", 0),
12400 g("CMF_FFN_ADUMP_TO", usize::MAX),
12401 )
12402 });
12403 if li < from || li > to {
12404 return;
12405 }
12406 let mut map = map.lock().unwrap();
12407 let f = map.entry(li).or_insert_with(|| {
12408 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
12409 });
12410 let mut bytes = Vec::with_capacity(g.len() * 2);
12411 for v in g {
12412 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
12413 }
12414 let _ = f.write_all(&bytes);
12415}
12416
12417fn oracle_topk() -> usize {
12423 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12424 *K.get_or_init(|| {
12425 std::env::var("CMF_FFN_ORACLE_TOPK")
12426 .ok()
12427 .and_then(|v| v.parse().ok())
12428 .unwrap_or(0)
12429 })
12430}
12431
12432fn gate_topk() -> usize {
12438 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12439 *K.get_or_init(|| {
12440 std::env::var("CMF_FFN_GATE_TOPK")
12441 .ok()
12442 .and_then(|v| v.parse().ok())
12443 .unwrap_or(0)
12444 })
12445}
12446
12447fn gate_block() -> usize {
12454 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12455 *B.get_or_init(|| {
12456 std::env::var("CMF_FFN_GATE_BLOCK")
12457 .ok()
12458 .and_then(|v| v.parse().ok())
12459 .unwrap_or(1)
12460 })
12461}
12462
12463fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
12465 let n = g.len();
12466 let nb = n.div_ceil(block);
12467 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
12468 if kb >= nb {
12469 return;
12470 }
12471 let mut score: Vec<f32> = (0..nb)
12472 .map(|b| {
12473 g[b * block..((b + 1) * block).min(n)]
12474 .iter()
12475 .map(|v| v * v)
12476 .sum::<f32>()
12477 })
12478 .collect();
12479 let mut ord = score.clone();
12480 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
12481 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12482 });
12483 let thr = *kth;
12484 for b in 0..nb {
12485 if score[b] < thr {
12486 g[b * block..((b + 1) * block).min(n)].fill(0.0);
12487 }
12488 }
12489 score.clear();
12490}
12491
12492fn keep_top_k(g: &mut [f32], k: usize) {
12494 if gate_block() > 1 {
12495 return keep_top_blocks(g, k, gate_block());
12496 }
12497 let n = g.len();
12498 if k == 0 || k >= n {
12499 return;
12500 }
12501 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12502 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12503 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12504 });
12505 let thr = *kth;
12506 for v in g.iter_mut() {
12507 if v.abs() < thr {
12508 *v = 0.0;
12509 }
12510 }
12511}
12512
12513fn probe_sq() -> bool {
12517 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12518 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
12519}
12520
12521fn probe_signed() -> bool {
12525 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12526 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
12527}
12528
12529fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
12537 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
12538 M.get_or_init(|| {
12539 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
12540 let b = std::fs::read(&p).ok()?;
12541 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
12542 let vals: Vec<f32> = b[8..]
12543 .chunks_exact(4)
12544 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12545 .collect();
12546 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
12547 Some((inter, vals))
12548 })
12549 .as_ref()
12550}
12551
12552fn probe_topk() -> usize {
12555 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12556 *K.get_or_init(|| {
12557 std::env::var("CMF_FFN_PROBE_TOPK")
12558 .ok()
12559 .and_then(|v| v.parse().ok())
12560 .unwrap_or(0)
12561 })
12562}
12563
12564thread_local! {
12565 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
12568 const { std::cell::RefCell::new(None) };
12569}
12570
12571fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
12584 if d.gate_proj.has_prism_contract()
12589 || d.up_proj.has_prism_contract()
12590 || d.down_proj.has_prism_contract()
12591 {
12592 return None;
12593 }
12594 let dt = d.down_t.as_ref()?;
12595 let inter = d.gate_proj.rows();
12596 let hidden = dt.cols();
12597 if k == 0 || k >= inter || d.act != Act::Silu {
12598 return None;
12599 }
12600 DYN_SCRATCH.with(|sc| {
12601 let mut sc = sc.borrow_mut();
12602 let DynScratch {
12603 g,
12604 mag,
12605 live,
12606 parts,
12607 } = &mut *sc;
12608 g.resize(inter, 0.0);
12609 d.gate_proj.matvec(x, g, pool);
12610 for v in g.iter_mut() {
12611 *v = inference::silu(*v);
12612 }
12613 mag.clear();
12616 mag.extend(g.iter().map(|v| v.abs()));
12617 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12618 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12619 });
12620 let thr = *kth;
12621 live.clear();
12622 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
12623 let mut out = vec![0.0f32; hidden];
12624 match pool {
12625 Some(p) if live.len() >= 64 => {
12626 let nw = p.n_workers() + 1;
12627 parts.clear();
12628 parts.resize(nw * hidden, 0.0);
12629 let ptr = SendMut(parts.as_mut_ptr());
12630 let n = live.len();
12631 let live_ref: &[u32] = live;
12632 let g_ref: &[f32] = g;
12633 p.run(&|w, workers| {
12634 let chunk = n.div_ceil(workers);
12635 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
12636 if s >= e {
12637 return;
12638 }
12639 WORKER_SCRATCH.with(|ws| {
12640 let mut ws = ws.borrow_mut();
12641 let [scratch, acc] = &mut *ws;
12642 scratch.resize(hidden.max(x.len()), 0.0);
12643 acc.clear();
12644 acc.resize(hidden, 0.0);
12645 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
12646 if let Some(&nx) = live_ref[s..e].get(o + 1) {
12649 d.up_proj.prefetch_row(nx as usize);
12650 dt.prefetch_row(nx as usize);
12651 }
12652 let idx = nrm as usize;
12653 let up = d.up_proj.row_dot(idx, x, scratch);
12654 let a = g_ref[idx] * up;
12655 if a != 0.0 {
12656 dt.add_row_scaled(idx, a, acc, scratch);
12657 }
12658 }
12659 for (j, v) in acc.iter().enumerate() {
12660 unsafe { *ptr.at(w * hidden + j) = *v };
12661 }
12662 });
12663 });
12664 for w in 0..nw {
12665 for (j, o) in out.iter_mut().enumerate() {
12666 *o += parts[w * hidden + j];
12667 }
12668 }
12669 }
12670 _ => {
12671 WORKER_SCRATCH.with(|ws| {
12672 let mut ws = ws.borrow_mut();
12673 let [scratch, _acc] = &mut *ws;
12674 scratch.resize(hidden.max(x.len()), 0.0);
12675 for &nrm in live.iter() {
12676 let idx = nrm as usize;
12677 let up = d.up_proj.row_dot(idx, x, scratch);
12678 let a = g[idx] * up;
12679 if a != 0.0 {
12680 dt.add_row_scaled(idx, a, &mut out, scratch);
12681 }
12682 }
12683 });
12684 }
12685 }
12686 Some(out)
12687 })
12688}
12689
12690struct DynScratch {
12693 g: Vec<f32>,
12694 mag: Vec<f32>,
12695 live: Vec<u32>,
12696 parts: Vec<f32>,
12697}
12698
12699thread_local! {
12700 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
12701 std::cell::RefCell::new(DynScratch {
12702 g: Vec::new(),
12703 mag: Vec::new(),
12704 live: Vec::new(),
12705 parts: Vec::new(),
12706 })
12707 };
12708 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
12710 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
12711}
12712
12713fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
12718 let inter = d.gate_proj.rows();
12719 FFN_SCRATCH.with(|s| {
12720 let mut s = s.borrow_mut();
12721 let [g, u, ..] = &mut *s;
12722 g.resize(inter, 0.0);
12723 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
12724 } else {
12726 u.resize(inter, 0.0);
12727 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12728 for i in 0..inter {
12729 g[i] = d.act.combine(g[i], u[i]);
12730 }
12731 }
12732 zero_masked_cols(g, 1, inter, mask_row);
12733 let mut out = attention::take_buf(d.down_proj.rows());
12734 d.down_proj.matvec(g, &mut out, pool);
12735 out
12736 })
12737}
12738
12739fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
12745 if d.gate_proj.has_prism_contract()
12746 || d.up_proj.has_prism_contract()
12747 || d.down_proj.has_prism_contract()
12748 {
12749 return None;
12750 }
12751 if d.act != Act::Silu {
12753 return None;
12754 }
12755 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
12758 return None;
12759 }
12760 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
12761 let mut model_ref = None;
12762 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
12763 let model = model_ref?;
12764 let hidden = jobs[0].down.1;
12765 let mut out = attention::take_buf(hidden);
12766 if crate::gpu::moe_block(&model, &jobs, &mut out) {
12767 Some(out)
12768 } else {
12769 let mut out = out;
12770 attention::recycle_buf(&mut out);
12771 None
12772 }
12773}
12774
12775#[allow(clippy::type_complexity)]
12780#[allow(clippy::type_complexity)]
12781pub(crate) fn moe_parts(
12782 t: &QTensor,
12783) -> Option<(
12784 &std::sync::Arc<cortiq_core::CmfModel>,
12785 usize,
12786 usize,
12787 usize,
12788 &[f32],
12789 &[f32],
12790 bool,
12791 bool,
12792 bool,
12793)> {
12794 match t {
12795 QTensor::Mapped {
12796 model,
12797 idx,
12798 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
12799 rows,
12800 cols,
12801 row_scale,
12802 col_field,
12803 ..
12804 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
12805 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
12806 )),
12807 QTensor::Mapped {
12809 model,
12810 idx,
12811 dtype: cortiq_core::TensorDtype::Q1,
12812 rows,
12813 cols,
12814 ..
12815 } => Some((
12816 model,
12817 *idx,
12818 *rows,
12819 *cols,
12820 &[][..],
12821 &[][..],
12822 true,
12823 false,
12824 false,
12825 )),
12826 QTensor::Mapped {
12828 model,
12829 idx,
12830 dtype: cortiq_core::TensorDtype::Q4Tiled,
12831 rows,
12832 cols,
12833 ..
12834 } => Some((
12835 model,
12836 *idx,
12837 *rows,
12838 *cols,
12839 &[][..],
12840 &[][..],
12841 false,
12842 true,
12843 false,
12844 )),
12845 QTensor::Mapped {
12847 model,
12848 idx,
12849 dtype: cortiq_core::TensorDtype::Q4TiledP,
12850 rows,
12851 cols,
12852 ..
12853 } => Some((
12854 model,
12855 *idx,
12856 *rows,
12857 *cols,
12858 &[][..],
12859 &[][..],
12860 false,
12861 true,
12862 false,
12863 )),
12864 QTensor::Mapped {
12868 model,
12869 idx,
12870 dtype: cortiq_core::TensorDtype::Q2TiledP,
12871 rows,
12872 cols,
12873 ..
12874 } => Some((
12875 model,
12876 *idx,
12877 *rows,
12878 *cols,
12879 &[][..],
12880 &[][..],
12881 false,
12882 true,
12883 true,
12884 )),
12885 _ => None,
12886 }
12887}
12888
12889#[cfg(target_os = "macos")]
12895fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
12896 if m.router_sigmoid
12897 || m.router_input_norm
12898 || m.expert_bias.is_some()
12899 || m.route_tau.is_some()
12900 || m.mask.is_some()
12901 || m.per_expert_scale.is_some()
12902 || m.experts.is_empty()
12903 || m.top_k == 0
12904 || m.resonance.is_some()
12905 {
12906 return None;
12907 }
12908 let (sh, sg) = match &m.shared {
12911 Some((sh, Some(sg))) => (sh, sg),
12912 _ => return None,
12913 };
12914 let (rf, rr, rc) = m.router.f32_parts()?;
12915 if rr != m.experts.len() || rc != hidden {
12916 return None;
12917 }
12918 let (sf, sr, sc) = sg.f32_parts()?;
12919 if sr * sc != hidden {
12920 return None;
12921 }
12922 let inter = m.experts[0].gate_proj.rows();
12923 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
12926 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
12927 if e.act != Act::Silu
12928 || e.gate_proj.rows() != inter
12929 || e.gate_proj.cols() != hidden
12930 || e.up_proj.rows() != inter
12931 || e.up_proj.cols() != hidden
12932 || e.down_proj.rows() != hidden
12933 || e.down_proj.cols() != inter
12934 {
12935 return None;
12936 }
12937 let pick = |t: &QTensor| -> Option<usize> {
12938 if gu_q2 {
12939 t.mapped_q2tp().map(|(_, i)| i)
12940 } else {
12941 t.mapped_q4tp().map(|(_, i)| i)
12942 }
12943 };
12944 Some((
12945 pick(&e.gate_proj)?,
12946 pick(&e.up_proj)?,
12947 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
12948 ))
12949 };
12950 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
12951 let shared = trio(sh)?;
12952 Some(crate::gpu::GpuMoe {
12953 router: rf,
12954 sgate: sf,
12955 experts,
12956 shared,
12957 n_exp: m.experts.len(),
12958 top_k: m.top_k,
12959 inter,
12960 norm_topk: m.norm_topk_prob,
12961 route_scale: m.routed_scaling,
12962 gu_q2,
12963 })
12964}
12965
12966pub(crate) fn moe_push_job_parts<'a>(
12970 gate: &'a QTensor,
12971 up: &'a QTensor,
12972 down: &'a QTensor,
12973 x: &[f32],
12974 w: f32,
12975 swiglu_limit: f32,
12976 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
12977 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
12978) -> Option<()> {
12979 use crate::qtensor::prescale;
12980 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
12981 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
12982 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
12983 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
12984 return None; }
12986 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
12989 return None;
12990 }
12991 if !gq2 && dq2 {
12992 return None;
12993 }
12994 model_ref.get_or_insert_with(|| gm.clone());
12995 let dt = |cf: &[f32]| {
12996 if cf.is_empty() {
12997 cortiq_core::TensorDtype::Q8Row
12998 } else {
12999 cortiq_core::TensorDtype::Q8_2f
13000 }
13001 };
13002 jobs.push(crate::gpu::MoeJob {
13003 gate: (gi, gr, gc, grs),
13004 up: (ui, ur, uc, urs),
13005 down: (di, dr, dc, drs),
13006 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
13007 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
13008 down_col: dcf,
13009 w,
13010 q1: gq1,
13011 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
13012 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
13013 gu_q2: gq2,
13014 swiglu_limit,
13015 });
13016 Some(())
13017}
13018
13019fn moe_push_job<'a>(
13021 d: &'a DenseFfn,
13022 x: &[f32],
13023 w: f32,
13024 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13025 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13026) -> Option<()> {
13027 use crate::qtensor::prescale;
13028 if d.act != Act::Silu {
13029 return None; }
13031 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
13032 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
13033 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
13034 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13035 return None; }
13037 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
13038 return None;
13039 }
13040 if !gq2 && dq2 {
13041 return None;
13042 }
13043 model_ref.get_or_insert_with(|| gm.clone());
13044 let gdt = if gcf.is_empty() {
13045 cortiq_core::TensorDtype::Q8Row
13046 } else {
13047 cortiq_core::TensorDtype::Q8_2f
13048 };
13049 let udt = if ucf.is_empty() {
13050 cortiq_core::TensorDtype::Q8Row
13051 } else {
13052 cortiq_core::TensorDtype::Q8_2f
13053 };
13054 jobs.push(crate::gpu::MoeJob {
13055 gate: (gi, gr, gc, grs),
13056 up: (ui, ur, uc, urs),
13057 down: (di, dr, dc, drs),
13058 xs_gate: prescale(x, gcf, gdt).into_owned(),
13059 xs_up: prescale(x, ucf, udt).into_owned(),
13060 down_col: dcf,
13061 w,
13062 q1: gq1,
13063 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
13064 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
13065 gu_q2: gq2,
13066 swiglu_limit: 0.0,
13067 });
13068 Some(())
13069}
13070
13071fn sparse_ffn_quant(
13078 d: &DenseFfn,
13079 x: &[f32],
13080 active: &[u16],
13081 hidden: usize,
13082 pool: Option<&Pool>,
13083) -> Vec<f32> {
13084 let n = active.len();
13085 let inter = d.gate_proj.rows();
13086 let mut act = vec![0.0f32; n];
13087 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
13090 let compute = |ai: usize| -> f32 {
13091 let idx = active[ai] as usize;
13092 if idx >= inter {
13093 return 0.0; }
13095 let mut s = if need_scratch {
13096 vec![0.0f32; hidden]
13097 } else {
13098 Vec::new()
13099 };
13100 let gate = d.gate_proj.row_dot(idx, x, &mut s);
13101 let up = d.up_proj.row_dot(idx, x, &mut s);
13102 d.act.combine(gate, up)
13103 };
13104 match pool {
13105 Some(p) if n >= 256 => {
13106 let ptr = SendMut(act.as_mut_ptr());
13107 p.run(&|widx, nw| {
13108 let chunk = n.div_ceil(nw);
13109 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
13110 for ai in s..e {
13111 unsafe { *ptr.at(ai) = compute(ai) };
13112 }
13113 });
13114 }
13115 _ => {
13116 for (ai, a) in act.iter_mut().enumerate() {
13117 *a = compute(ai);
13118 }
13119 }
13120 }
13121 let mut out = vec![0.0f32; hidden];
13123 for (ai, &idx) in active.iter().enumerate() {
13124 let w = act[ai];
13125 if w.abs() >= 1e-12 && (idx as usize) < inter {
13126 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
13127 }
13128 }
13129 out
13130}
13131
13132#[doc(hidden)]
13134pub fn sparse_ffn_quant_for_test(
13135 d: &DenseFfn,
13136 x: &[f32],
13137 active: &[u16],
13138 hidden: usize,
13139) -> Vec<f32> {
13140 sparse_ffn_quant(d, x, active, hidden, None)
13141}
13142
13143fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
13147 let deq = |t: &QTensor| -> Vec<f32> {
13148 let (rows, cols) = (t.rows(), t.cols());
13149 let mut out = vec![0.0f32; rows * cols];
13150 for r in 0..rows {
13151 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
13152 }
13153 out
13154 };
13155 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
13156}
13157
13158struct SendMut(*mut f32);
13160unsafe impl Send for SendMut {}
13161unsafe impl Sync for SendMut {}
13162impl SendMut {
13163 #[inline]
13164 #[allow(clippy::mut_from_ref)]
13167 unsafe fn at(&self, i: usize) -> &mut f32 {
13168 unsafe { &mut *self.0.add(i) }
13169 }
13170}
13171
13172pub(crate) fn moe_route(
13182 logits: &[f32],
13183 m: &MoeFfn,
13184 allowed: Option<&[bool]>,
13185) -> (Vec<usize>, Vec<f32>, f32) {
13186 let ne = logits.len();
13187 let p: Vec<f32> = if m.router_sigmoid {
13188 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
13189 } else {
13190 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13191 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
13192 let s: f32 = e.iter().sum();
13193 for v in &mut e {
13194 *v /= s;
13195 }
13196 e
13197 };
13198 let admit = |e: usize| {
13204 m.mask.as_ref().is_none_or(|mk| mk[e])
13205 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
13206 };
13207 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
13208 match &m.expert_bias {
13210 Some(b) => idx.sort_unstable_by(|&x, &y| {
13211 (p[y] + b[y])
13212 .partial_cmp(&(p[x] + b[x]))
13213 .unwrap()
13214 .then(x.cmp(&y))
13215 }),
13216 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
13217 }
13218 idx.truncate(m.top_k);
13219 if let Some(tau) = m.route_tau {
13223 let total: f32 = idx.iter().map(|&e| p[e]).sum();
13224 if total > 0.0 {
13225 let mut acc = 0.0f32;
13226 let mut keep = idx.len();
13227 for (i, &e) in idx.iter().enumerate() {
13228 acc += p[e];
13229 if acc >= tau * total {
13230 keep = i + 1;
13231 break;
13232 }
13233 }
13234 idx.truncate(keep);
13235 }
13236 }
13237 let wsum: f32 = if m.norm_topk_prob {
13238 let s: f32 = idx.iter().map(|&e| p[e]).sum();
13239 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
13242 } else {
13243 1.0 / m.routed_scaling
13244 };
13245 (idx, p, wsum)
13246}
13247
13248fn moe_trace(idx: &[usize]) {
13250 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
13251}
13252
13253pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
13256 use std::io::Write;
13257 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
13258 std::sync::OnceLock::new();
13259 let Some(f) = F.get_or_init(|| {
13260 let p = std::env::var("CMF_MOE_TRACE").ok()?;
13261 Some(std::sync::Mutex::new(
13262 std::fs::OpenOptions::new()
13263 .create(true)
13264 .append(true)
13265 .open(p)
13266 .ok()?,
13267 ))
13268 }) else {
13269 return;
13270 };
13271 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
13272 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
13273}
13274
13275pub(crate) fn moe_ffn(
13278 m: &MoeFfn,
13279 x: &[f32],
13280 pool: Option<&Pool>,
13281 allowed: Option<&[bool]>,
13282) -> Vec<f32> {
13283 accumulate_act(m, x, 1);
13284 let ne = m.experts.len();
13285 let mut logits = vec![0.0f32; ne];
13286 match &m.resonance {
13287 Some(r) => r.scores(x, &mut logits),
13288 None => m.router.matvec(x, &mut logits, pool),
13289 }
13290 let (idx, p, wsum) = moe_route(&logits, m, allowed);
13291 {
13292 let mut st = m.stats.borrow_mut();
13293 if st.len() < ne {
13294 st.resize(ne, 0);
13295 }
13296 for &e in &idx {
13297 st[e] += 1;
13298 }
13299 }
13300 moe_trace(&idx);
13306 if crate::gpu::enabled_here() {
13311 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
13312 crate::gpu::ProbeArm::Gpu => {
13313 let t0 = std::time::Instant::now();
13314 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
13315 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
13316 return out;
13317 }
13318 }
13319 crate::gpu::ProbeArm::CpuTimed => {
13320 let t0 = std::time::Instant::now();
13321 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13322 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
13323 return out;
13324 }
13325 crate::gpu::ProbeArm::Cpu => {
13326 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13327 }
13328 }
13329 }
13330 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
13331}
13332
13333fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
13338 use std::sync::atomic::{AtomicBool, Ordering};
13339 if built {
13340 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
13341 if total_layers > 0 && layers_run < total_layers {
13342 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
13343 } else {
13344 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
13345 }
13346 } else {
13347 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
13348 }
13349 static SAID: AtomicBool = AtomicBool::new(false);
13350 if !SAID.swap(true, Ordering::Relaxed) {
13351 if built {
13352 tracing::info!("wgpu whole-token graph: ACTIVE");
13353 } else {
13354 tracing::warn!("wgpu whole-token graph refused — per-op path");
13355 }
13356 }
13357}
13358
13359pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13363pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13364pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13368pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13370
13371pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
13375 std::sync::atomic::AtomicU64::new(0);
13376pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
13377 std::sync::atomic::AtomicU64::new(0);
13378pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
13379 std::sync::atomic::AtomicU64::new(0);
13380pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
13381 std::sync::atomic::AtomicU64::new(0);
13382pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
13383 std::sync::atomic::AtomicU64::new(0);
13384pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
13388 std::sync::atomic::AtomicU64::new(0);
13389pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
13390 std::sync::atomic::AtomicU64::new(0);
13391pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
13392 std::sync::atomic::AtomicU64::new(0);
13393pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
13394 std::sync::atomic::AtomicU64::new(0);
13395
13396fn moe_batch_enabled() -> bool {
13399 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13400 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
13401}
13402
13403fn moe_ffn_cpu_batched(
13409 m: &MoeFfn,
13410 x: &[f32],
13411 idx: &[usize],
13412 p: &[f32],
13413 wsum: f32,
13414 pool: Option<&Pool>,
13415) -> Option<Vec<f32>> {
13416 if idx.is_empty() || !moe_batch_enabled() {
13417 return None;
13418 }
13419 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
13423 return None;
13424 }
13425 let n = idx.len() + usize::from(m.shared.is_some());
13426 let mut pairs = Vec::with_capacity(n);
13427 let mut downs = Vec::with_capacity(n);
13428 let mut ws = Vec::with_capacity(n);
13429 for &e in idx {
13430 let d = &m.experts[e];
13431 if d.act != Act::Silu {
13432 return None;
13433 }
13434 pairs.push((&d.gate_proj, &d.up_proj));
13435 downs.push(&d.down_proj);
13436 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
13437 }
13438 if let Some((se, gate)) = &m.shared {
13441 if se.act != Act::Silu {
13442 return None;
13443 }
13444 let g = gate.as_ref().map_or(1.0, |gate| {
13445 let mut gl = [0.0f32; 1];
13446 gate.matvec(x, &mut gl, pool);
13447 1.0 / (1.0 + (-gl[0]).exp())
13448 });
13449 pairs.push((&se.gate_proj, &se.up_proj));
13450 downs.push(&se.down_proj);
13451 ws.push(g);
13452 }
13453 let inter = pairs[0].0.rows();
13454 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
13455 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
13456 return None;
13457 }
13458 let mut out = attention::take_buf(x.len());
13459 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
13460 attention::recycle_buf(&mut out);
13461 return None;
13462 }
13463 Some(out)
13464}
13465
13466pub(crate) fn moe_cold_experts_cpu(
13472 experts: &[(&DenseFfn, f32)],
13473 x: &[f32],
13474 pool: Option<&Pool>,
13475) -> Vec<f32> {
13476 let mut out = attention::take_buf(x.len());
13477 if experts.is_empty() {
13478 return out;
13479 }
13480 let pairs: Vec<_> = experts
13481 .iter()
13482 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
13483 .collect();
13484 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
13485 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
13486 let inter = experts[0].0.gate_proj.rows();
13487 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
13488 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
13489 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
13490 {
13491 return out;
13492 }
13493 out.fill(0.0);
13494 for &(expert, weight) in experts {
13495 let mut one = dense_ffn(expert, x, pool);
13496 for (o, v) in out.iter_mut().zip(&one) {
13497 *o += weight * v;
13498 }
13499 attention::recycle_buf(&mut one);
13500 }
13501 out
13502}
13503
13504fn moe_ffn_cpu(
13506 m: &MoeFfn,
13507 x: &[f32],
13508 idx: &[usize],
13509 p: &[f32],
13510 wsum: f32,
13511 pool: Option<&Pool>,
13512) -> Vec<f32> {
13513 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
13514 return out;
13515 }
13516 let mut out = attention::take_buf(x.len());
13517 for &e in idx {
13518 let mut eo = dense_ffn(&m.experts[e], x, pool);
13519 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
13520 for i in 0..out.len() {
13521 out[i] += w * eo[i];
13522 }
13523 attention::recycle_buf(&mut eo);
13524 }
13525 if let Some((se, gate)) = &m.shared {
13526 let mut so = dense_ffn(se, x, pool);
13527 let g = gate.as_ref().map_or(1.0, |gate| {
13528 let mut gl = [0.0f32; 1];
13529 gate.matvec(x, &mut gl, pool);
13530 1.0 / (1.0 + (-gl[0]).exp())
13531 });
13532 for i in 0..out.len() {
13533 out[i] += g * so[i];
13534 }
13535 attention::recycle_buf(&mut so);
13536 }
13537 out
13538}
13539
13540#[allow(clippy::too_many_arguments)]
13548fn mla_attention(
13549 w: &MlaWeights,
13550 normed: &[f32],
13551 cache: &mut crate::kv_cache::LayerKvCache,
13552 position: usize,
13553 inv_freq: &[f32],
13554 rope_scale: f32,
13555 eps: f64,
13556 pool: Option<&Pool>,
13557) -> Vec<f32> {
13558 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
13559 let hd = dr + dn;
13560 let mut q = vec![0.0f32; nh * hd];
13561 match (&w.q_a, &w.q_a_norm) {
13562 (Some(qa), Some(qn)) => {
13563 let mut t = vec![0.0f32; qa.rows()];
13564 qa.matvec(normed, &mut t, pool);
13565 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
13566 w.q_proj.matvec(&tn, &mut q, pool);
13567 }
13568 _ => w.q_proj.matvec(normed, &mut q, pool),
13569 }
13570 let mut ca = vec![0.0f32; lora + dr];
13571 w.kv_a.matvec(normed, &mut ca, pool);
13572 let (c_lat, k_rope) = ca.split_at_mut(lora);
13573 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
13574 let mut kvb = vec![0.0f32; nh * (dn + dv)];
13575 w.kv_b.matvec(&latn, &mut kvb, pool);
13576 if !w.nope {
13577 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
13578 }
13579 for h in 0..nh {
13580 if !w.nope {
13581 attention::rope_rotate_scaled(
13582 &mut q[h * hd..h * hd + dr],
13583 position,
13584 inv_freq,
13585 rope_scale,
13586 );
13587 }
13588 }
13589 let mut k = vec![0.0f32; nh * hd];
13590 let mut v = vec![0.0f32; nh * hd];
13591 for h in 0..nh {
13592 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
13593 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
13594 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
13595 }
13596 cache.append(&k, &v, &vec![true; nh]);
13597 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
13598 attention::recycle_buf(&mut imp);
13599 let mut ov = vec![0.0f32; nh * dv];
13600 for h in 0..nh {
13601 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
13602 }
13603 let mut out = vec![0.0f32; w.o_proj.rows()];
13604 w.o_proj.matvec(&ov, &mut out, pool);
13605 out
13606}
13607
13608fn dense_moe_ffn(
13615 dm: &DenseMoeFfn,
13616 x_normed: &[f32],
13617 h_raw: &[f32],
13618 eps: f64,
13619 norm_style: NormStyle,
13620 pool: Option<&Pool>,
13621) -> Vec<f32> {
13622 let mut d = dense_ffn(&dm.dense, x_normed, pool);
13623 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
13624 let m = &dm.moe;
13625 let ne = m.experts.len();
13626 let mut logits = vec![0.0f32; ne];
13627 if m.router_input_norm {
13628 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
13629 let inv = 1.0 / (ss + eps as f32).sqrt();
13630 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
13631 m.router.matvec(&xr, &mut logits, pool);
13632 } else {
13633 m.router.matvec(h_raw, &mut logits, pool);
13634 }
13635 let (idx, p, wsum) = moe_route(&logits, m, None);
13636 {
13637 let mut st = m.stats.borrow_mut();
13638 if st.len() < ne {
13639 st.resize(ne, 0);
13640 }
13641 for &e in &idx {
13642 st[e] += 1;
13643 }
13644 }
13645 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
13646 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
13647 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
13648 for (di, mi) in d.iter_mut().zip(&mo) {
13649 *di += mi;
13650 }
13651 d
13652}
13653
13654fn moe_gpu_refused(why: &'static str) {
13661 use std::sync::atomic::{AtomicBool, Ordering};
13662 static SAID: AtomicBool = AtomicBool::new(false);
13663 if !SAID.swap(true, Ordering::Relaxed) {
13664 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
13665 }
13666}
13667
13668fn moe_ffn_gpu(
13669 m: &MoeFfn,
13670 x: &[f32],
13671 idx: &[usize],
13672 p: &[f32],
13673 wsum: f32,
13674 pool: Option<&Pool>,
13675) -> Option<Vec<f32>> {
13676 use crate::gpu::MoeJob;
13677
13678 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
13679 let mut model_ref = None;
13680 for &e in idx {
13681 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
13682 moe_gpu_refused("push_job(expert)");
13683 return None;
13684 }
13685 }
13686 if let Some((se, gate)) = &m.shared {
13687 let g = gate.as_ref().map_or(1.0, |gate| {
13688 let mut gl = [0.0f32; 1];
13689 gate.matvec(x, &mut gl, pool);
13690 1.0 / (1.0 + (-gl[0]).exp())
13691 });
13692 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
13693 moe_gpu_refused("push_job(shared)");
13694 return None;
13695 }
13696 }
13697 let Some(model) = model_ref else {
13698 moe_gpu_refused("no model_ref");
13699 return None;
13700 };
13701 let hidden = jobs[0].down.1;
13702 let mut out = vec![0.0f32; hidden];
13703 if crate::gpu::moe_block(&model, &jobs, &mut out) {
13704 Some(out)
13705 } else {
13706 moe_gpu_refused("gpu::moe_block");
13707 None
13708 }
13709}
13710
13711fn ffn_forward(
13713 ffn: &FfnKind,
13714 x: &[f32],
13715 pool: Option<&Pool>,
13716 experts_allowed: Option<&[bool]>,
13717) -> Vec<f32> {
13718 match ffn {
13719 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
13720 FfnKind::Dense(d) => dense_ffn(d, x, pool),
13721 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
13722 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13726 }
13727}
13728
13729fn ffn_forward_pair(
13733 ffn: &FfnKind,
13734 x1: &[f32],
13735 x2: &[f32],
13736 pool: Option<&Pool>,
13737 experts_allowed: Option<&[bool]>,
13738) -> (Vec<f32>, Vec<f32>) {
13739 let d = match ffn {
13740 FfnKind::Dense(d) if !d.segs.is_empty() => {
13743 return (
13744 tube_ffn(d, x1, 1, pool, None),
13745 tube_ffn(d, x2, 1, pool, None),
13746 );
13747 }
13748 FfnKind::Dense(d) => d,
13749 FfnKind::Moe(m) => {
13750 return (
13751 moe_ffn(m, x1, pool, experts_allowed),
13752 moe_ffn(m, x2, pool, experts_allowed),
13753 );
13754 }
13755 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13756 };
13757 let inter = d.gate_proj.rows();
13758 FFN_SCRATCH.with(|s| {
13759 let mut s = s.borrow_mut();
13760 let [g1, g2, u1, u2] = &mut *s;
13761 g1.resize(inter, 0.0);
13762 g2.resize(inter, 0.0);
13763 u1.resize(inter, 0.0);
13764 u2.resize(inter, 0.0);
13765 QTensor::matvec2_many(
13768 [&d.gate_proj, &d.up_proj],
13769 x1,
13770 x2,
13771 [g1.as_mut_slice(), u1.as_mut_slice()],
13772 [g2.as_mut_slice(), u2.as_mut_slice()],
13773 pool,
13774 );
13775 for i in 0..inter {
13776 g1[i] = d.act.combine(g1[i], u1[i]);
13777 g2[i] = d.act.combine(g2[i], u2[i]);
13778 }
13779 let mut o1 = attention::take_buf(d.down_proj.rows());
13780 let mut o2 = attention::take_buf(d.down_proj.rows());
13781 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
13782 (o1, o2)
13783 })
13784}
13785
13786#[cfg(test)]
13787mod tests {
13788
13789 #[test]
13790 fn nll_graph_policy_scopes_only_the_fused_head() {
13791 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
13792 ("vulkan graph", true, true, false, true, false),
13794 ("native Metal graph", true, true, true, true, true),
13796 ("masked", false, true, false, false, false),
13798 ("graph disabled", true, false, true, false, false),
13799 ] {
13800 let (graph_quality, graph_head_required) =
13801 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
13802 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
13803 assert_eq!(graph_head_required, want_head, "{label}: fused head");
13804 }
13805 }
13806
13807 #[test]
13808 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
13809 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
13810 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
13811 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
13812 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
13813 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
13814 }
13815
13816 #[test]
13817 fn cancel_flag_stops_generation() {
13818 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
13819 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13822 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
13823 assert_eq!(r.finish_reason, "cancelled");
13824 assert!(
13825 r.token_ids.is_empty(),
13826 "no tokens after cancel: {:?}",
13827 r.token_ids
13828 );
13829 assert_eq!(p.kv_cache.seq_len(), 0);
13830 assert!(p.kv_history.is_empty());
13831 assert!(!p.graph_want_logits);
13832 assert!(p.graph_logits.is_none());
13833 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
13835 assert_ne!(r2.finish_reason, "cancelled");
13836 }
13837 use super::*;
13838
13839 #[test]
13847 fn dynamic_ffn_equals_the_zeroing_arm() {
13848 let (hidden, inter) = (8usize, 32usize);
13849 let synth = |n: usize, salt: usize| -> Vec<f32> {
13850 (0..n)
13851 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
13852 .collect()
13853 };
13854 let down = synth(hidden * inter, 3);
13855 let mut down_t = vec![0.0f32; inter * hidden];
13856 for r in 0..hidden {
13857 for c in 0..inter {
13858 down_t[c * hidden + r] = down[r * inter + c];
13859 }
13860 }
13861 let d = DenseFfn {
13862 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13863 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13864 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
13865 act: Act::Silu,
13866 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
13867 segs: Vec::new(),
13868 };
13869 let x = synth(hidden, 11);
13870 let k = 12usize;
13871 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
13872 let mut g = vec![0.0f32; inter];
13874 d.gate_proj.matvec(&x, &mut g, None);
13875 let mut u = vec![0.0f32; inter];
13876 d.up_proj.matvec(&x, &mut u, None);
13877 for v in g.iter_mut() {
13878 *v = inference::silu(*v);
13879 }
13880 keep_top_k(&mut g, k);
13881 for i in 0..inter {
13882 g[i] *= u[i];
13883 }
13884 let mut want = vec![0.0f32; hidden];
13885 d.down_proj.matvec(&g, &mut want, None);
13886 for (a, b) in want.iter().zip(&got) {
13887 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
13888 }
13889 }
13890
13891 #[test]
13897 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
13898 let (hidden, core, tube) = (8usize, 12usize, 8usize);
13899 let inter = core + tube;
13900 let synth = |n: usize, salt: usize| -> Vec<f32> {
13901 (0..n)
13902 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
13903 .collect()
13904 };
13905 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
13906 let d_all = synth(hidden * inter, 3);
13907 let dense = DenseFfn {
13909 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
13910 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
13911 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
13912 act: Act::Silu,
13913 down_t: None,
13914 segs: Vec::new(),
13915 };
13916 let rows =
13917 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
13918 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
13919 let mut o = Vec::with_capacity(hidden * (b - a));
13920 for r in 0..hidden {
13921 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
13922 }
13923 o
13924 };
13925 let tubed = DenseFfn {
13926 down_t: None,
13927 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
13928 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
13929 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
13930 act: Act::Silu,
13931 segs: vec![FfnSeg {
13932 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
13933 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
13934 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
13935 start: core,
13936 width: tube,
13937 }],
13938 };
13939 let x = synth(hidden, 7);
13940 let want = dense_ffn(&dense, &x, None);
13941 let got = tube_ffn(&tubed, &x, 1, None, None);
13942 for (a, b) in want.iter().zip(&got) {
13943 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
13944 }
13945 let mut bits = vec![0u8; inter.div_ceil(8)];
13947 for n in 0..core {
13948 bits[n / 8] |= 1 << (n % 8);
13949 }
13950 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13951 let masked = dense_ffn_masked(&dense, &x, None, &bits);
13952 for (a, b) in masked.iter().zip(&closed) {
13953 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
13954 }
13955 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13957 for (a, b) in closed.iter().zip(&batch) {
13958 assert_eq!(a, b, "batch arm disagrees with decode arm");
13959 }
13960 }
13961
13962 #[test]
13964 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
13965 let (hidden, inter) = (16usize, 40usize);
13966 let synth = |n: usize, salt: usize| -> Vec<f32> {
13967 (0..n)
13968 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
13969 .collect()
13970 };
13971 let d = DenseFfn {
13972 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13973 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13974 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
13975 act: Act::Silu,
13976 down_t: None,
13977 segs: Vec::new(),
13978 };
13979 let x = synth(hidden, 9);
13980 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
13982
13983 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
13984
13985 let mut g = vec![0.0f32; inter];
13987 d.gate_proj.matvec(&x, &mut g, None);
13988 let mut u = vec![0.0f32; inter];
13989 d.up_proj.matvec(&x, &mut u, None);
13990 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
13991 for i in 0..inter {
13992 g[i] = if act_set.contains(&(i as u16)) {
13993 inference::silu(g[i]) * u[i]
13994 } else {
13995 0.0
13996 };
13997 }
13998 let mut reference = vec![0.0f32; hidden];
13999 d.down_proj.matvec(&g, &mut reference, None);
14000
14001 let max_d = sparse
14002 .iter()
14003 .zip(&reference)
14004 .map(|(a, b)| (a - b).abs())
14005 .fold(0.0f32, f32::max);
14006 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
14007 }
14008
14009 fn attach_test_mtp(p: &mut Pipeline) {
14011 let (h, inter, heads, kv, hd) = (
14012 p.hidden_size,
14013 p.intermediate_size,
14014 p.num_heads,
14015 p.num_kv_heads,
14016 p.head_dim,
14017 );
14018 let synth = |n: usize, salt: usize| -> Vec<f32> {
14019 (0..n)
14020 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
14021 .collect()
14022 };
14023 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
14024 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14025 };
14026 p.mtp = Some(MtpModule {
14027 enorm: vec![1.0; h],
14028 hnorm: vec![1.0; h],
14029 eh_proj: qt(h, 2 * h, 301),
14030 layer: LayerWeights {
14031 input_norm: vec![1.0; h],
14032 post_norm: vec![1.0; h],
14033 attn_out_norm: None,
14034 ffn_out_norm: None,
14035 layer_scale: None,
14036 ffn: FfnKind::Dense(DenseFfn {
14037 gate_proj: qt(inter, h, 315),
14038 up_proj: qt(inter, h, 316),
14039 down_proj: qt(h, inter, 317),
14040 act: Act::Silu,
14041 down_t: None,
14042 segs: Vec::new(),
14043 }),
14044 attn: AttnKind::Full {
14045 bias: None,
14046 wq: qt(heads * hd, h, 311),
14047 wk: qt(kv * hd, h, 312),
14048 wv: qt(kv * hd, h, 313),
14049 wo: qt(h, heads * hd, 314),
14050 q_norm: None,
14051 k_norm: None,
14052 output_gate: false,
14053 softplus_gate: None,
14054 },
14055 },
14056 final_norm: vec![1.0; h],
14057 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
14058 });
14059 }
14060
14061 #[test]
14062 fn speculative_equals_vanilla_greedy() {
14063 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14067 let run = |spec: bool| {
14068 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14069 p.sampler_config.temperature = 0.0;
14070 attach_test_mtp(&mut p);
14071 p.speculative = spec;
14072 let r = p.generate("abcdef", 12, None, None).unwrap();
14073 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
14074 };
14075 let (vanilla, d0, _) = run(false);
14076 let (spec, d1, a1) = run(true);
14077 assert_eq!(d0, 0, "vanilla path must not draft");
14078 assert!(d1 > 0, "speculative path must draft");
14079 assert_eq!(
14080 vanilla, spec,
14081 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
14082 );
14083 }
14084
14085 #[test]
14086 fn speculative_accepts_constant_oracle() {
14087 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14089 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14090 p.sampler_config.temperature = 0.0;
14091 p.sampler_config.repetition_penalty = 1.0;
14092 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
14095 attach_test_mtp(&mut p);
14096 p.speculative = true;
14097 let r = p.generate("abcd", 10, None, None).unwrap();
14098 assert!(r.mtp_drafted > 0);
14099 assert_eq!(
14100 r.mtp_accepted, r.mtp_drafted,
14101 "constant logits → every draft accepted"
14102 );
14103 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
14106 }
14107
14108 #[test]
14109 fn empty_prompt_is_an_error_not_a_panic() {
14110 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14111 let r = p.generate("", 4, None, None);
14112 assert!(r.is_err(), "empty prompt must be a clean error");
14113 }
14114
14115 #[test]
14116 fn every_token_enters_kv_exactly_once() {
14117 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14118 p.sampler_config.temperature = 0.0;
14120 let r = p.generate("abc", 2, None, None).unwrap();
14121 assert_eq!(r.prompt_tokens, 3);
14122 assert_eq!(
14126 p.kv_cache.seq_len(),
14127 3 + r.tokens_generated - 1,
14128 "each token must be cached exactly once (v1 cached the last prompt token twice)"
14129 );
14130 }
14131
14132 #[test]
14133 fn generation_is_reproducible_with_seed() {
14134 let run = || {
14135 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14136 p.generate("hello", 8, None, None).unwrap().token_ids
14137 };
14138 assert_eq!(run(), run());
14139 }
14140
14141 #[test]
14142 fn resetting_sampler_restarts_the_seeded_stream() {
14143 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14144 let config = SamplerConfig {
14145 seed: Some(1234),
14146 ..SamplerConfig::default()
14147 };
14148 p.set_sampler_config(config.clone());
14149 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
14150 p.set_sampler_config(config);
14151 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
14152 assert_eq!(first, second);
14153 }
14154
14155 #[test]
14156 fn eviction_bounds_the_cache() {
14157 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14158 p.kv_cache.max_seq_len = 6;
14159 p.sampler_config.temperature = 0.0;
14160 let _ = p.generate("abcd", 12, None, None).unwrap();
14161 assert!(
14162 p.kv_cache.seq_len() <= 6 + 1,
14163 "cache must stay bounded by max_seq_len (got {})",
14164 p.kv_cache.seq_len()
14165 );
14166 }
14167
14168 #[test]
14169 fn confidence_matches_tokens_and_is_a_probability() {
14170 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14171 p.sampler_config.temperature = 0.0;
14172 p.sampler_config.repetition_penalty = 1.0;
14173 let r = p.generate("abcd", 10, None, None).unwrap();
14174 assert_eq!(
14175 r.token_confidence.len(),
14176 r.token_ids.len(),
14177 "one confidence per emitted token"
14178 );
14179 for &c in &r.token_confidence {
14180 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
14181 }
14182 let logits = [1.0f32, 3.0, 0.5, 3.0];
14184 let p0 = top1_prob_t(&logits, 1, 1.0);
14185 let p1 = top1_prob_t(&logits, 3, 1.0);
14186 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
14187 assert!(p0 > 0.0 && p0 < 1.0);
14188 let sharp = top1_prob_t(&logits, 1, 1.0);
14190 let soft = top1_prob_t(&logits, 1, 2.0);
14191 assert!(soft < sharp, "higher temperature lowers peak confidence");
14192 }
14193
14194 #[test]
14195 fn trace_is_opt_in_and_parallels_the_output() {
14196 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14198 p.sampler_config.temperature = 0.0;
14199 p.sampler_config.repetition_penalty = 1.0;
14200 let r = p.generate("abcd", 10, None, None).unwrap();
14201 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
14202
14203 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14205 p.sampler_config.temperature = 0.0;
14206 p.sampler_config.repetition_penalty = 1.0;
14207 p.set_trace(true);
14208 let r = p.generate("abcd", 10, None, None).unwrap();
14209 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
14210 for (i, tr) in r.traces.iter().enumerate() {
14211 assert_eq!(tr.t, i, "trace index is sequential");
14212 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
14213 assert_eq!(
14214 tr.confidence, r.token_confidence[i],
14215 "trace confidence matches the confidence channel"
14216 );
14217 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
14219 }
14220 }
14221
14222 #[test]
14223 fn explain_prefill_logits_match_greedy_first_token() {
14224 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14228 p.sampler_config.temperature = 0.0;
14229 p.sampler_config.repetition_penalty = 1.0;
14230 let ids = p.tokenizer.encode("abcd");
14231 let logits = p.prefill_next_logits(&ids, None);
14232 let argmax = logits
14233 .iter()
14234 .enumerate()
14235 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14236 .unwrap()
14237 .0 as u32;
14238 let r = p.generate("abcd", 1, None, None).unwrap();
14239 assert_eq!(
14240 argmax, r.token_ids[0],
14241 "explain preview must match greedy emit"
14242 );
14243 }
14244
14245 #[test]
14246 fn laguna_shared_expert_is_unconditionally_added() {
14247 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
14248 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
14249 let zero_dense = || DenseFfn {
14250 gate_proj: matrix(vec![0.0; 4]),
14251 up_proj: matrix(vec![0.0; 4]),
14252 down_proj: matrix(vec![0.0; 4]),
14253 act: Act::Silu,
14254 down_t: None,
14255 segs: Vec::new(),
14256 };
14257 let shared = DenseFfn {
14258 gate_proj: identity(),
14259 up_proj: identity(),
14260 down_proj: identity(),
14261 act: Act::Silu,
14262 down_t: None,
14263 segs: Vec::new(),
14264 };
14265 let x = [1.0, 2.0];
14266 let expected = dense_ffn(&shared, &x, None);
14267 let moe = MoeFfn {
14268 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
14269 experts: vec![zero_dense()],
14270 top_k: 1,
14271 norm_topk_prob: true,
14272 router_sigmoid: true,
14273 expert_bias: None,
14274 routed_scaling: 1.0,
14275 route_tau: None,
14276 shared: Some((shared, None)),
14277 stats: std::cell::RefCell::new(Vec::new()),
14278 act_sq: std::cell::RefCell::new(Vec::new()),
14279 act_rows: std::cell::RefCell::new(Vec::new()),
14280 mask: None,
14281 per_expert_scale: None,
14282 router_input_norm: false,
14283 resonance: None,
14284 };
14285 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
14286 for (actual, expected) in actual.iter().zip(expected) {
14287 assert!((actual - expected).abs() < 1e-6);
14288 }
14289 }
14290
14291 #[test]
14292 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
14293 const B: usize = 19;
14294 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14295 p.set_o1(Some(crate::nystrom::O1Cfg {
14296 layers: crate::nystrom::O1Layers::All,
14297 m: 4,
14298 w: 8,
14299 sink: 2,
14300 rect: crate::nystrom::O1Rect::Aggregate,
14301 }));
14302 p.o1_begin_with_prefix(Some(B));
14303 let ids: Vec<u32> = (0..B as u32).collect();
14304 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
14305
14306 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
14307 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
14308 let next = p.embed_single(B as u32);
14309 let _ = p.forward_layers(&next, B, None);
14310 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
14311 }
14312
14313 #[test]
14314 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
14315 const B: usize = 19;
14316 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14317 let gdn_cfg = crate::linear_core::GdnCfg {
14321 num_v_heads: 2,
14322 num_k_heads: 1,
14323 key_head_dim: 2,
14324 value_head_dim: 4,
14325 conv_kernel: 3,
14326 hidden_size: 8,
14327 rms_eps: 1e-6,
14328 output_gate_sigmoid: false,
14329 };
14330 let synth = |n: usize, salt: usize| -> Vec<f32> {
14331 (0..n)
14332 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
14333 .collect()
14334 };
14335 let qt = |rows: usize, cols: usize, salt: usize| {
14336 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14337 };
14338 let c_dim = gdn_cfg.conv_dim();
14339 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
14340 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
14341 in_proj_qkv: qt(c_dim, 8, 1),
14342 in_proj_z: qt(vd, 8, 2),
14343 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
14344 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
14345 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
14346 a_log: vec![0.2, 0.5],
14347 dt_bias: synth(gdn_cfg.num_v_heads, 6),
14348 norm: vec![1.0; gdn_cfg.value_head_dim],
14349 out_proj: qt(8, vd, 7),
14350 });
14351 p.gdn_cfg = Some(gdn_cfg);
14352 p.set_o1(Some(crate::nystrom::O1Cfg {
14353 layers: crate::nystrom::O1Layers::All,
14354 m: 4,
14355 w: 8,
14356 sink: 2,
14357 rect: crate::nystrom::O1Rect::Aggregate,
14358 }));
14359 p.o1_begin_with_prefix(Some(B));
14360 for pos in 0..B - 2 {
14361 let emb = p.embed_single(pos as u32);
14362 let _ = p.forward_layers(&emb, pos, None);
14363 }
14364 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
14365
14366 let e1 = p.embed_single((B - 2) as u32);
14367 let e2 = p.embed_single((B - 1) as u32);
14368 let _ = p.forward_pair(&e1, &e2, B - 2);
14369
14370 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
14371 assert!(
14372 p.kv_cache
14373 .layers
14374 .iter()
14375 .enumerate()
14376 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
14377 );
14378 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
14379 assert_ne!(
14380 p.kv_cache.layers[0].linear_state, lane1_state,
14381 "real pair must commit GDN lane 2 before returning"
14382 );
14383 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
14384 let next = p.embed_single(B as u32);
14385 let _ = p.forward_layers(&next, B, None);
14386 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
14387 }
14388
14389 #[test]
14390 fn o1_error_observation_stays_terminal_until_reset() {
14391 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14392 p.set_o1(Some(crate::nystrom::O1Cfg {
14393 layers: crate::nystrom::O1Layers::All,
14394 m: 4,
14395 w: 8,
14396 sink: 2,
14397 rect: crate::nystrom::O1Rect::Aggregate,
14398 }));
14399 p.o1_begin();
14400 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
14401
14402 assert!(p.o1_seal_checked().is_err());
14403 assert!(
14404 p.o1_seal_checked().is_err(),
14405 "retry must see the sticky error"
14406 );
14407 let k = vec![0.2f32; 4];
14408 let v = vec![0.3f32; 4];
14409 p.kv_cache.layers[0].append(&k, &v, &[]);
14410 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
14411
14412 p.reset_session();
14413 p.o1_begin();
14414 p.kv_cache.layers[0].append(&k, &v, &[]);
14415 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
14416 }
14417
14418 #[test]
14419 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
14420 let ids = vec![1u32, 2, 3, 4, 5, 6];
14421 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14422 p.graph_logits = Some(vec![123.0]);
14423 p.graph_want_logits = true;
14424 p.graph_failed
14425 .store(true, std::sync::atomic::Ordering::Relaxed);
14426 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14427 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
14428 assert!(err.contains("before NLL"));
14429 assert!(p.graph_logits.is_none());
14430 assert!(!p.graph_want_logits);
14431 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14432 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14433
14434 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14435 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14436 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14437 assert_eq!(actual.1, expected.1);
14438 assert!((actual.0 - expected.0).abs() < 1e-9);
14439 }
14440
14441 #[test]
14442 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
14443 let ids = vec![1u32, 2, 3, 4, 5, 6];
14444 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14445 p.nll_test_fail_at = Some(1);
14446 let err = p
14447 .nll_ids_from(&ids, 0)
14448 .expect_err("one-shot forward failure");
14449 assert!(err.contains("forward") || err.contains("score row"));
14450 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14451 assert!(!p.graph_want_logits);
14452 assert!(p.graph_logits.is_none());
14453 assert!(p.kv_history.is_empty());
14454
14455 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14456 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14457 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14458 assert_eq!(actual.1, expected.1);
14459 assert!((actual.0 - expected.0).abs() < 1e-9);
14460 }
14461
14462 #[test]
14463 fn nll_serial_failure_before_first_row_is_reported() {
14464 let ids = vec![1u32, 2, 3, 4];
14465 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14466 p.nll_test_force_serial = true;
14467 p.nll_test_fail_at = Some(0);
14468 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
14469 assert!(err.contains("serial forward"));
14470 assert!(p.kv_history.is_empty());
14471 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14472 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14473 }
14474
14475 #[test]
14476 fn ffn_probe_failure_discards_recorder_and_state() {
14477 let ids = vec![1u32, 2, 3, 4];
14478 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14479 p.nll_test_fail_at = Some(0);
14480 let err = p
14481 .probe_ffn_mass_batch(&ids)
14482 .expect_err("probe forward failure");
14483 assert!(err.contains("NLL"));
14484 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
14485 assert!(p.kv_history.is_empty());
14486 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14487 }
14488
14489 #[test]
14490 fn nll_test_controls_are_pipeline_scoped() {
14491 let ids = vec![1u32, 2, 3, 4];
14492 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14493 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14494 failing.nll_test_force_serial = true;
14495 failing.nll_test_fail_at = Some(0);
14496
14497 assert!(!failing.can_prefill_batched());
14498 assert!(unaffected.can_prefill_batched());
14499 let expected = unaffected
14500 .nll_ids_from(&ids, 0)
14501 .expect("unaffected pipeline remains usable");
14502 let err = failing
14503 .nll_ids_from(&ids, 0)
14504 .expect_err("failure injection belongs to failing pipeline");
14505 assert!(err.contains("serial forward"));
14506 assert!(failing.nll_test_fail_at.is_none());
14507 assert!(unaffected.can_prefill_batched());
14508 let actual = unaffected
14509 .nll_ids_from(&ids, 0)
14510 .expect("unaffected pipeline remains reusable");
14511 assert_eq!(actual.1, expected.1);
14512 assert!((actual.0 - expected.0).abs() < 1e-9);
14513 }
14514
14515 #[test]
14516 fn forward_ids_failure_channel_is_terminal_and_reusable() {
14517 let ids = vec![1u32, 2, 3, 4, 5, 6];
14518 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14519 p.graph_logits = Some(vec![123.0]);
14520 p.graph_want_logits = true;
14521 p.graph_failed
14522 .store(true, std::sync::atomic::Ordering::Relaxed);
14523 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14524
14525 let err = p
14526 .forward_ids(&ids, None)
14527 .expect_err("a failed forward must not become a valid head result");
14528 assert!(err.contains("forward_ids setup"));
14529 assert!(p.graph_logits.is_none());
14530 assert!(!p.graph_want_logits);
14531 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14532 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14533 assert_eq!(p.kv_cache.seq_len(), 0);
14534
14535 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
14536 .forward_ids(&ids, None)
14537 .expect("fresh forward_ids");
14538 let actual = p
14539 .forward_ids(&ids, None)
14540 .expect("pipeline remains reusable after a failed forward");
14541 assert_eq!(actual.len(), expected.len());
14542 assert!(
14543 actual
14544 .iter()
14545 .zip(expected)
14546 .all(|(a, b)| (a - b).abs() < 1e-9)
14547 );
14548 assert_eq!(p.kv_cache.seq_len(), ids.len());
14549 }
14550}