1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16#[path = "clover.rs"]
17pub mod clover;
18#[path = "pipeline_masked.rs"]
20pub mod masked;
21#[path = "pipeline_speech.rs"]
22pub mod speech;
23use crate::kv_cache::KvCache;
24use crate::linear_core::{
25 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
26 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
27 vmf_phase_pair,
28};
29use crate::pool::Pool;
30use crate::qtensor::QTensor;
31use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
32use crate::tokenizer::Tokenizer;
33use cortiq_core::mask::TaskMask;
34use cortiq_core::types::NormStyle;
35
36pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
37 std::sync::atomic::AtomicBool::new(false);
38
39#[derive(Clone)]
46struct GraphMoeBank {
47 experts: Vec<(usize, usize, usize)>,
48 q4tp: Option<bool>,
49 gu_q2: Option<bool>,
50 q82: Option<bool>,
51 act: Option<crate::gpu::GraphAct>,
52 model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
53}
54
55type GraphMoeKey = (usize, usize, usize, usize, usize, usize, usize);
61
62thread_local! {
67 static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
68}
69
70struct ExactMtpCalibrationGuard;
71
72impl ExactMtpCalibrationGuard {
73 fn enter() -> Self {
74 EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
75 Self
76 }
77}
78
79impl Drop for ExactMtpCalibrationGuard {
80 fn drop(&mut self) {
81 EXACT_MTP_CALIBRATION.with(|depth| {
82 debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
83 depth.set(depth.get().saturating_sub(1));
84 });
85 }
86}
87
88fn exact_mtp_calibration_active() -> bool {
89 EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
90}
91
92fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
96 logits.resize(vocab_size, f32::NEG_INFINITY);
97}
98
99struct ForwardScratch {
103 n1: Vec<f32>,
104 n2: Vec<f32>,
105 p1: Vec<f32>,
106 p2: Vec<f32>,
107}
108
109impl ForwardScratch {
110 fn new(hidden: usize) -> Self {
111 Self {
112 n1: vec![0.0; hidden],
113 n2: vec![0.0; hidden],
114 p1: vec![0.0; hidden],
115 p2: vec![0.0; hidden],
116 }
117 }
118}
119
120pub struct Pipeline {
122 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
127 pub tokenizer: std::sync::Arc<Tokenizer>,
130 pub kv_cache: KvCache,
131 pub sampler_config: SamplerConfig,
132 pub weights: PipelineWeights,
133 pub hidden_size: usize,
134 pub intermediate_size: usize,
135 pub num_heads: usize,
136 pub num_kv_heads: usize,
137 pub head_dim: usize,
138 pub num_layers: usize,
140 pub physical_layers: usize,
142 pub loop_final_norm: bool,
144 pub vocab_size: usize,
145 pub rms_eps: f64,
146 pub rope_base: f32,
147 pub norm_style: NormStyle,
148 pub rotary_dim: usize,
150 pub attention_heads_per_layer: Option<Vec<usize>>,
152 pub kv_heads_per_layer: Option<Vec<usize>>,
157 pub v_head_dim: Option<usize>,
162 pub layer_dump: Option<std::path::PathBuf>,
175 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
178 pub(crate) mimo_moe: crate::mimo_moe::Slot,
181 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
186 pub vmf_cfg: Option<VmfPhaseCfg>,
188 pub gdn_cfg: Option<GdnCfg>,
190 pub logit_multiplier: Option<f32>,
192 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
197 graph_failed: std::sync::atomic::AtomicBool,
202 graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
205 pub kv_history: Vec<u32>,
210 pub kv_history_device: bool,
213 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
215 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
218 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
221 pub dsv4: Option<
225 Box<(
226 crate::dsv4::Dsv4Globals,
227 Vec<crate::dsv4::Dsv4Layer>,
228 crate::dsv4::Dsv4Cfg,
229 crate::dsv4::Dsv4State,
230 )>,
231 >,
232 pub dsv41: Option<
236 Box<(
237 crate::dsv41::Dsv41Globals,
238 Vec<crate::dsv41::Dsv41Layer>,
239 crate::dsv41::Dsv41Cfg,
240 crate::dsv41::Dsv41State,
241 )>,
242 >,
243 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
245 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
247 pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
252 clover_stats: clover::CloverStats,
253 pub qwen4_exp: Option<
254 Box<(
255 crate::qwen4_exp::Globals,
256 Vec<crate::qwen4_exp::Layer>,
257 crate::qwen4_exp::Cfg,
258 crate::qwen4_exp::State,
259 )>,
260 >,
261 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
265 pub dspark: Option<crate::dsv4::DsparkState>,
267 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
270 pub dspark_hist: Vec<usize>,
272 pub dspark_real: Vec<u32>,
276 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
280 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
282 pub dspark_draft_ns: u128,
286 m3_winners: Vec<Vec<u32>>,
288 m3_counts: Vec<u32>,
290 m3_n_exp: usize,
291 m3_tokens: u64,
292 m3_prewarmed: bool,
293 pub short_conv_cfg: Option<ShortConvCfg>,
296 pub mtp: Option<MtpModule>,
298 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
302 verify_exact_moe: bool,
305 pub speculative: bool,
307 pub ignore_eos: bool,
313 pub draft_full_streak: u32,
319 pub spec_k_adapt: Option<usize>,
327 pub spec_acc_ewma: f32,
329 rng: SplitMix64,
330 sampler_scratch: SamplerScratch,
331 spec_forced: Option<u32>,
337 spec_q: Vec<Vec<f32>>,
338 spec_p: Vec<f32>,
339 spec_res: Vec<f32>,
340 spec_qs: Vec<sampler::Sparse>,
342 spec_ps: sampler::Sparse,
343 spec_ress: sampler::Sparse,
344 mtp_graph_mode: Option<bool>,
351 #[cfg(target_os = "macos")]
354 metal_verify: Option<MetalVerifyPending>,
355 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
359 ws: ForwardScratch,
363 pool: Option<std::sync::Arc<Pool>>,
365 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
369 pub(crate) dyn_force_f32: bool,
371 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
376 pub(crate) dyn_active: Option<usize>,
382 pub(crate) dyn_blend_loaded: bool,
386 pub(crate) dyn_phi_layer: Option<usize>,
389 dyn_phi_ema: Vec<f32>,
391 dyn_phi_seen: usize,
392 pub dyn_router: Option<crate::swarm::DynRouter>,
395 o1_cfg: Option<crate::nystrom::O1Cfg>,
398 o1_epoch: u64,
401 o1_flags: Vec<bool>,
403 trace: bool,
406 calib_temp: f32,
409 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
411 graph_kv_id: u64,
412 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
415 graph_want_logits: bool,
416 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
420 graph_head_required: bool,
421 graph_logits: Option<Vec<f32>>,
424 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
427 graph_refused: std::sync::atomic::AtomicBool,
433 pub embed_multiplier: f32,
435 pub attn_scale: f32,
438 pub swa: Option<(usize, usize)>,
441 pub swa_trim: Option<(usize, usize)>,
448 swa_trim_anchor: usize,
452 pub chunk_min_rows: usize,
456 pub sliding_layers: Option<Vec<bool>>,
459 pub no_rope_layers: Option<Vec<bool>>,
464 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
470 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
473 pub kv_prefix: KvPrefix,
477 pub last_prefill_tokens: usize,
480 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
483 pub rotary_dim_local: Option<usize>,
484 pub rope_scale: f32,
485 pub rope_scale_local: f32,
486 pub global_attn: Option<(usize, usize)>,
489 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
492 pub attn_v_norm: bool,
494 pub qk_norm_after_rope: bool,
496 pub proj_gate_sigmoid: bool,
499 pub final_softcap: Option<f32>,
501 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
505 pub attn_softcap: f32,
507 confidence_on: bool,
511 #[cfg(test)]
514 nll_test_fail_at: Option<usize>,
515 #[cfg(test)]
518 nll_test_force_serial: bool,
519}
520
521#[cfg(target_os = "macos")]
522impl Drop for Pipeline {
523 fn drop(&mut self) {
524 let _ = crate::gpu_metal::wait_replay();
526 crate::gpu::kv_mirror_drop(self.graph_kv_id);
527 }
528}
529
530#[cfg(not(target_os = "macos"))]
531impl Drop for Pipeline {
532 fn drop(&mut self) {
533 crate::gpu::graph_kv_reset(self.graph_kv_id);
537 }
538}
539
540pub struct PipelineWeights {
545 pub embed_tokens: QTensor,
547 pub layers: Vec<LayerWeights>,
549 pub lm_head: QTensor,
551 pub final_norm: Vec<f32>,
553}
554
555pub struct LayerWeights {
557 pub input_norm: Vec<f32>,
558 pub post_norm: Vec<f32>,
561 pub attn_out_norm: Option<Vec<f32>>,
564 pub layer_scale: Option<f32>,
566 pub ffn_out_norm: Option<Vec<f32>>,
569 pub ffn: FfnKind,
570 pub attn: AttnKind,
571}
572
573#[derive(Clone, Copy, PartialEq, Debug, Default)]
577pub enum Act {
578 #[default]
579 Silu,
580 GeluTanh,
581 Gelu,
583 Situ {
586 beta: f32,
587 linear_beta: f32,
588 },
589 SwiGluOai {
594 alpha: f32,
595 limit: f32,
596 },
597}
598
599impl Act {
600 pub fn from_arch(name: &str) -> Self {
601 if name == "gelu_tanh" {
602 Self::GeluTanh
603 } else if name == "gelu" {
604 Self::Gelu
605 } else {
606 Self::Silu
607 }
608 }
609
610 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
612 match arch.hidden_act.as_str() {
613 "situ" => Self::Situ {
614 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
615 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
616 },
617 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
622 alpha: 1.702,
623 limit: 7.0,
624 },
625 other => Self::from_arch(other),
626 }
627 }
628
629 #[inline]
634 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
635 Self::SwiGluOai { alpha, limit }
636 }
637
638 #[inline]
639 pub fn apply(self, x: f32) -> f32 {
640 match self {
641 Self::Silu => inference::silu(x),
642 Self::GeluTanh => inference::gelu_tanh(x),
643 Self::Gelu => inference::gelu_erf(x),
644 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
645 Self::SwiGluOai { alpha, limit } => {
646 let gate = x.min(limit);
647 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
648 }
649 }
650 }
651
652 #[inline]
655 pub fn combine(self, g: f32, u: f32) -> f32 {
656 match self {
657 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
658 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
659 }
660 Self::SwiGluOai { alpha, limit } => {
661 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
662 }
663 _ => self.apply(g) * u,
664 }
665 }
666
667 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
671 match self {
672 Self::Silu => Some(crate::gpu::GraphAct::Silu),
673 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
674 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
675 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
676 }
677 }
678
679 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
685 self.moe_job_graph_contract().map(|(act, _)| act)
686 }
687
688 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
694 match self {
695 Self::SwiGluOai { alpha, limit }
696 if alpha.to_bits() == 1.702f32.to_bits()
697 && limit.to_bits() == 7.0f32.to_bits() =>
698 {
699 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
700 }
701 _ => self.graph_act().map(|act| (act, 0.0)),
702 }
703 }
704}
705
706pub struct DenseFfn {
708 pub gate_proj: QTensor,
709 pub up_proj: QTensor,
710 pub down_proj: QTensor,
711 pub act: Act,
713 pub down_t: Option<QTensor>,
719 pub segs: Vec<FfnSeg>,
726}
727
728pub struct FfnSeg {
733 pub gate: QTensor,
734 pub up: QTensor,
735 pub down: QTensor,
736 pub start: usize,
737 pub width: usize,
738}
739
740pub enum FfnKind {
743 Dense(DenseFfn),
744 Moe(MoeFfn),
748 DenseMoe(Box<DenseMoeFfn>),
755}
756
757pub struct DenseMoeFfn {
759 pub dense: DenseFfn,
760 pub moe: MoeFfn,
761 pub post_norm_1: Vec<f32>,
763 pub pre_norm_2: Vec<f32>,
766 pub post_norm_2: Vec<f32>,
768}
769
770pub struct MoeFfn {
771 pub router: QTensor,
773 pub experts: Vec<DenseFfn>,
774 pub top_k: usize,
775 pub norm_topk_prob: bool,
776 pub router_sigmoid: bool,
779 pub sigmoid_denom_eps: f32,
785 pub expert_bias: Option<Vec<f32>>,
789 pub routed_scaling: f32,
792 pub route_tau: Option<f32>,
798 pub shared: Option<(DenseFfn, Option<QTensor>)>,
801 pub stats: std::cell::RefCell<Vec<u64>>,
805 pub act_sq: std::cell::RefCell<Vec<f64>>,
812 pub act_rows: std::cell::RefCell<Vec<f32>>,
818 pub mask: Option<Vec<bool>>,
823 pub per_expert_scale: Option<Vec<f32>>,
826 pub router_input_norm: bool,
830 pub resonance: Option<Resonance>,
834 pub grown: Vec<GrownExpert>,
840}
841
842#[derive(Debug, Clone, PartialEq, Eq)]
844pub struct GrownExpert {
845 pub record: String,
847 pub record_index: usize,
849 pub layer: usize,
850 pub expert: usize,
854}
855
856pub struct Resonance {
858 pub mu: Vec<f32>,
860 pub u: Vec<f32>,
862 pub k: usize,
863 pub bias: Vec<f32>,
865 pub shell: Vec<f32>,
871}
872
873static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
876
877pub fn growth_shell_enabled() -> bool {
882 use std::sync::atomic::Ordering;
883 match GROWTH_SHELL.load(Ordering::Relaxed) {
884 1 => true,
885 2 => false,
886 _ => {
887 let off = std::env::var("CMF_GROWTH_SHELL")
888 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
889 .unwrap_or(false);
890 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
891 !off
892 }
893 }
894}
895
896pub fn set_growth_shell(on: Option<bool>) {
901 GROWTH_SHELL.store(
902 match on {
903 Some(true) => 1,
904 Some(false) => 2,
905 None => 0,
906 },
907 std::sync::atomic::Ordering::Relaxed,
908 );
909}
910
911impl Resonance {
912 pub fn has_shell(&self) -> bool {
914 self.shell.iter().any(|s| s.is_finite())
915 }
916
917 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
920 let mut out = vec![f32::INFINITY; ne];
921 if growth_shell_enabled() {
922 for (o, s) in out.iter_mut().zip(&self.shell) {
923 *o = *s;
924 }
925 }
926 out
927 }
928
929 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
934 let h = x.len();
935 let ne = out.len();
936 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
937 for e in 0..ne {
938 let mu = &self.mu[e * h..(e + 1) * h];
939 let mut d2 = 0.0f32;
940 for j in 0..h {
941 let d = x[j] - mu[j];
942 d2 += d * d;
943 }
944 let mut proj = 0.0f32;
945 for i in 0..self.k {
946 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
947 let mut p = 0.0f32;
948 for j in 0..h {
949 p += (x[j] - mu[j]) * u[j];
950 }
951 proj += p * p;
952 }
953 let err = d2 - proj;
954 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
955 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
956 out[e] = f32::NEG_INFINITY;
957 }
958 }
959 }
960}
961
962pub enum AttnKind {
965 Full {
967 wq: QTensor,
968 wk: QTensor,
969 wv: QTensor,
970 wo: QTensor,
971 q_norm: Option<Vec<f32>>,
972 k_norm: Option<Vec<f32>>,
973 output_gate: bool,
974 softplus_gate: Option<(QTensor, bool)>,
978 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
980 },
981 Linear(VmfPhaseWeights),
983 LinearGdn(GdnWeights),
985 ShortConv(ShortConvWeights),
988 Mla(Box<MlaWeights>),
996 Kda(Box<crate::linear_core::KdaWeights>),
1000 Bounded(Box<crate::bounded::BoundedWeights>),
1005}
1006
1007pub struct MlaWeights {
1009 pub q_proj: QTensor,
1013 pub q_a: Option<QTensor>,
1016 pub q_a_norm: Option<Vec<f32>>,
1017 pub kv_a: QTensor,
1019 pub kv_a_norm: Vec<f32>,
1021 pub kv_b: QTensor,
1023 pub o_proj: QTensor,
1025 pub nh: usize,
1026 pub qk_rope: usize,
1027 pub qk_nope: usize,
1028 pub v_dim: usize,
1029 pub lora: usize,
1030 pub scale: f32,
1032 pub nope: bool,
1034}
1035
1036pub struct MtpModule {
1041 pub enorm: Vec<f32>,
1042 pub hnorm: Vec<f32>,
1043 pub eh_proj: QTensor,
1045 pub layer: LayerWeights,
1046 pub final_norm: Vec<f32>,
1047 pub kv: crate::kv_cache::LayerKvCache,
1048}
1049
1050const MTP_PROBE_FFN_ROWS: usize = 128;
1059
1060#[derive(Clone, Debug, PartialEq)]
1065pub struct MtpFfnProbe {
1066 pub mass: Vec<f64>,
1067 pub squared_mass: Vec<f64>,
1068 pub observed_positions: usize,
1069}
1070
1071#[cfg(target_os = "macos")]
1078enum MetalRowsItem<'a> {
1079 Gdn {
1080 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1081 first: usize,
1082 },
1083 Attn {
1084 l: crate::gpu_metal::AttnGpuLayer<'a>,
1085 li: usize,
1086 q_norm: Option<&'a [f32]>,
1087 k_norm: Option<&'a [f32]>,
1088 output_gate: bool,
1089 },
1090}
1091
1092#[cfg(target_os = "macos")]
1093struct MetalVerifyPending {
1094 graph: crate::gpu_metal::VerifyGraph,
1095 gdn_layers: Vec<usize>,
1096 attn_layers: Vec<(usize, usize)>,
1097}
1098
1099#[cfg(target_os = "macos")]
1103struct MetalWarmPending {
1104 graph: crate::gpu_metal::VerifyGraph,
1105 cpu_stored: usize,
1106 b: usize,
1107}
1108
1109#[cfg(target_os = "macos")]
1110enum MetalRowsRun {
1111 Declined,
1113 Failed,
1116 Completed(MetalVerifyPending),
1117}
1118
1119#[cfg(target_os = "macos")]
1120enum MetalPrefillOutcome {
1121 Declined,
1122 Failed,
1123 Completed(Vec<f32>),
1124}
1125
1126#[cfg(target_os = "macos")]
1127enum MetalBatchNllOutcome {
1128 Declined,
1129 Failed(String),
1130 Completed(f64, usize),
1131}
1132
1133#[derive(Clone, Copy)]
1139enum SpecTrial {
1140 Spec {
1141 t0: std::time::Instant,
1142 gen0: usize,
1143 rounds: usize,
1144 },
1145 Plain {
1146 t0: std::time::Instant,
1147 gen0: usize,
1148 },
1149 Decided {
1150 spec: bool,
1151 recheck_at: usize,
1152 },
1153}
1154
1155pub(crate) fn spec_time_level() -> u8 {
1159 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1160 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1161 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1162 Err(_) => 0,
1163 })
1164}
1165
1166fn graph_moe_bank_on() -> bool {
1169 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1170 *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1171}
1172
1173fn graph_spec_wanted_by_policy(
1181 spec_env: Option<&str>,
1182 spec_default_ok: bool,
1183 penalized: bool,
1184 metal_wgpu: bool,
1185 native_metal: bool,
1186) -> bool {
1187 match spec_env {
1188 Some("0") => false,
1189 Some("1") => true,
1194 Some(_) => !native_metal,
1195 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1198 }
1199}
1200
1201struct SpecStampLog {
1207 t_last: std::time::Instant,
1208 items: Vec<(&'static str, f32)>,
1209}
1210
1211static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1212
1213pub(crate) fn spec_stamp(name: &'static str) {
1214 if spec_time_level() == 0 {
1215 return;
1216 }
1217 if let Ok(mut g) = SPEC_STAMPS.lock() {
1218 if let Some(log) = g.as_mut() {
1219 let now = std::time::Instant::now();
1220 log.items
1221 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1222 log.t_last = now;
1223 }
1224 }
1225}
1226
1227fn spec_stamps_begin() {
1228 if spec_time_level() == 0 {
1229 return;
1230 }
1231 if let Ok(mut g) = SPEC_STAMPS.lock() {
1232 *g = Some(SpecStampLog {
1233 t_last: std::time::Instant::now(),
1234 items: Vec::with_capacity(64),
1235 });
1236 }
1237}
1238
1239fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1240 SPEC_STAMPS
1241 .lock()
1242 .ok()
1243 .and_then(|mut g| g.take())
1244 .map(|l| l.items)
1245 .unwrap_or_default()
1246}
1247
1248fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1251 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1252 for &(n, ms) in items {
1253 match agg.iter_mut().find(|e| e.0 == n) {
1254 Some(e) => {
1255 e.1 += ms;
1256 e.2 += 1;
1257 }
1258 None => agg.push((n, ms, 1)),
1259 }
1260 }
1261 let mut s = String::with_capacity(agg.len() * 16);
1262 for (n, ms, k) in agg {
1263 if k > 1 {
1264 s.push_str(&format!("{n} {ms:.1}/{k} "));
1265 } else {
1266 s.push_str(&format!("{n} {ms:.1} "));
1267 }
1268 }
1269 s
1270}
1271
1272#[derive(Default, Clone, Copy)]
1294struct SpecMon {
1295 round_ms: f64,
1296 tokens: f64,
1297 plain_ms: f64,
1298 n: u32,
1299 fails: u32,
1300 zero_accept_rounds: u8,
1305 metal: bool,
1306}
1307
1308const SPEC_PROXY_TOKENS: f64 = 3.5;
1311const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1314
1315impl SpecMon {
1316 fn round(&mut self, dt_ms: f64, produced: usize) {
1317 if produced <= 1 {
1318 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1319 } else {
1320 self.zero_accept_rounds = 0;
1321 }
1322 self.n += 1;
1323 if self.n == 1 {
1324 return; }
1326 let a = if self.n == 2 { 1.0 } else { 0.3 };
1327 self.round_ms += a * (dt_ms - self.round_ms);
1328 self.tokens += a * (produced as f64 - self.tokens);
1329 }
1330 fn pays(&self) -> bool {
1331 if self.plain_ms > 0.0 {
1332 self.tokens * self.plain_ms > self.round_ms * 1.03
1333 } else {
1334 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1335 }
1336 }
1337 fn repeated_zero_acceptance(&self) -> bool {
1338 self.zero_accept_rounds >= 2
1339 }
1340 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1342 let n = generated.saturating_sub(gen0);
1343 if n >= 8 {
1344 return true;
1345 }
1346 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1347 }
1348}
1349
1350pub const KV_PREFIX_TAIL: usize = 128;
1353
1354#[derive(Debug, Clone, Default)]
1360pub struct KvPrefix {
1361 len: usize,
1362 hash: u64,
1363 tail: Vec<u32>,
1364 device: bool,
1369}
1370
1371impl KvPrefix {
1372 #[inline]
1373 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1374 for &id in ids {
1375 h ^= id as u64;
1376 h = h.wrapping_mul(0x100000001b3);
1377 h ^= h >> 29;
1378 }
1379 h
1380 }
1381
1382 pub fn clear(&mut self) {
1383 self.len = 0;
1384 self.hash = 0xcbf29ce484222325;
1385 self.tail.clear();
1386 self.device = false;
1387 }
1388
1389 pub fn on_device(&self) -> bool {
1391 self.device
1392 }
1393
1394 pub fn set_on_device(&mut self, device: bool) {
1396 self.device = device;
1397 }
1398
1399 pub fn len(&self) -> usize {
1401 self.len
1402 }
1403
1404 pub fn is_empty(&self) -> bool {
1405 self.len == 0
1406 }
1407
1408 pub fn tail_len(&self) -> usize {
1410 self.tail.len()
1411 }
1412
1413 pub fn set(&mut self, ids: &[u32]) {
1415 self.clear();
1416 self.extend(ids);
1417 }
1418
1419 pub fn extend(&mut self, more: &[u32]) {
1421 if self.len == 0 && self.hash == 0 {
1422 self.hash = 0xcbf29ce484222325;
1423 }
1424 self.hash = Self::fold(self.hash, more);
1425 self.len += more.len();
1426 if more.len() >= KV_PREFIX_TAIL {
1427 self.tail.clear();
1428 self.tail
1429 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1430 } else {
1431 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1432 self.tail.drain(..drop);
1433 self.tail.extend_from_slice(more);
1434 }
1435 }
1436
1437 pub fn extension(&self, ids: &[u32]) -> usize {
1441 if self.len == 0 || ids.len() <= self.len {
1442 return 0;
1443 }
1444 let t = self.tail.len();
1445 if ids[self.len - t..self.len] != self.tail[..] {
1446 return 0;
1447 }
1448 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1449 return 0;
1450 }
1451 self.len
1452 }
1453}
1454
1455pub struct GenerateResult {
1457 pub text: String,
1458 pub token_ids: Vec<u32>,
1459 pub prompt_tokens: usize,
1460 pub tokens_generated: usize,
1461 pub finish_reason: String,
1462 pub mtp_drafted: usize,
1464 pub mtp_accepted: usize,
1465 pub token_confidence: Vec<f32>,
1470 pub traces: Vec<TokenTrace>,
1473}
1474
1475#[derive(Clone, Debug)]
1480pub struct TokenTrace {
1481 pub t: usize,
1483 pub token_id: u32,
1485 pub confidence: f32,
1487 pub active_skill: Option<String>,
1489 pub recon: Option<f32>,
1493 pub switched: bool,
1496}
1497
1498#[cfg_attr(not(test), allow(dead_code))]
1503fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1504 let t = if temp > 1e-3 { temp } else { 1.0 };
1505 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1506 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1507 if sum > 0.0 {
1508 (((logits[id as usize] - max) / t).exp()) / sum
1509 } else {
1510 0.0
1511 }
1512}
1513
1514fn prefill_batched() -> bool {
1517 std::env::var("CMF_PREFILL")
1518 .map(|v| v != "seq")
1519 .unwrap_or(true)
1520}
1521
1522#[inline]
1526fn rel_l2(a: &[f32], b: &[f32]) -> f64 {
1528 if a.len() != b.len() {
1529 return f64::INFINITY;
1530 }
1531 let (mut d, mut r) = (0f64, 0f64);
1532 for (&x, &y) in a.iter().zip(b) {
1533 let e = (x - y) as f64;
1534 d += e * e;
1535 r += y as f64 * y as f64;
1536 }
1537 (d / r.max(1e-30)).sqrt()
1538}
1539
1540fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1541 let graph_quality = unmasked && prefer_graph;
1542 let fused_head_quality = graph_quality && native_metal;
1543 (graph_quality, fused_head_quality)
1544}
1545
1546#[derive(Clone, Copy)]
1550enum PrefillIn<'a> {
1551 Ids(&'a [u32]),
1552 Hidden(&'a [f32]),
1553}
1554
1555impl Pipeline {
1562 pub(crate) fn can_prefill_batched(&self) -> bool {
1563 #[cfg(test)]
1564 let force_serial = self.nll_test_force_serial;
1565 #[cfg(not(test))]
1566 let force_serial = false;
1567 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1568 }
1569
1570 fn automatic_gpu_prefix(&self) -> Option<usize> {
1573 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1574 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1575 }
1576
1577 pub fn prefill_chunk(&self) -> usize {
1581 let env = env_prefill_chunk();
1582 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1583 return prefill_chunk_rule(env, ChunkHost::here(), false);
1584 }
1585 prefill_chunk_rule(
1586 None,
1587 ChunkHost::Other,
1588 self.chunk_stack_facts().dense_on_discrete(),
1589 )
1590 }
1591
1592 fn per_token_stack(&self) -> bool {
1596 self.g3n.is_some() || self.g4e.is_some()
1597 }
1598
1599 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1600 let plain_dense = !self.weights.layers.is_empty()
1601 && !self.per_token_stack()
1602 && self.dsv4.is_none()
1603 && self.dsv41.is_none()
1604 && self.qwen4_exp.is_none()
1605 && self.weights.layers.iter().all(|lw| {
1606 matches!(lw.attn, AttnKind::Full { .. } | AttnKind::ShortConv(_))
1611 && matches!(lw.ffn, FfnKind::Dense(_))
1612 });
1613 let gpu_on = crate::gpu::enabled();
1614 ChunkStackFacts {
1615 plain_dense,
1616 discrete: gpu_on && crate::gpu::discrete(),
1617 gpu_on,
1618 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1621 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1622 multi_gpu: self.gpu_plan.is_some(),
1623 o1: self.o1_active(),
1624 }
1625 }
1626}
1627
1628pub fn prefill_chunk() -> usize {
1637 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1638}
1639
1640fn env_prefill_chunk() -> Option<usize> {
1641 std::env::var("CMF_PREFILL_CHUNK")
1642 .ok()
1643 .and_then(|v| v.parse::<usize>().ok())
1644}
1645
1646#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1648enum ChunkHost {
1649 Macos,
1650 Aarch64,
1652 Other,
1654}
1655
1656impl ChunkHost {
1657 fn here() -> Self {
1658 if cfg!(target_os = "macos") {
1659 ChunkHost::Macos
1660 } else if cfg!(target_arch = "aarch64") {
1661 ChunkHost::Aarch64
1662 } else {
1663 ChunkHost::Other
1664 }
1665 }
1666}
1667
1668const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1675
1676fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1682 if let Some(n) = env {
1683 return n.max(1);
1684 }
1685 match host {
1686 ChunkHost::Macos => 512,
1687 ChunkHost::Aarch64 => 256,
1690 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1691 ChunkHost::Other => 48,
1692 }
1693}
1694
1695#[derive(Clone, Copy, Debug, Default)]
1697struct ChunkStackFacts {
1698 plain_dense: bool,
1702 discrete: bool,
1704 gpu_on: bool,
1706 capacity_split: bool,
1708 multi_gpu: bool,
1710 o1: bool,
1712}
1713
1714impl ChunkStackFacts {
1715 fn dense_on_discrete(self) -> bool {
1716 self.plain_dense
1717 && self.discrete
1718 && self.gpu_on
1719 && !self.capacity_split
1720 && !self.multi_gpu
1721 && !self.o1
1722 }
1723}
1724
1725#[inline]
1731fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1732 if end <= start || start >= input_len {
1733 return 0;
1734 }
1735 let rows = (end.min(input_len) - start).min(input_len - start);
1736 if end < input_len {
1737 rows
1738 } else {
1739 rows.saturating_sub(1)
1740 }
1741}
1742
1743pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1745
1746#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1748pub(crate) struct ReuseLayer {
1749 pub full: bool,
1752 pub host_rows: usize,
1755 pub device_rows: Option<usize>,
1757 pub device_state: bool,
1759}
1760
1761#[derive(Debug, Clone, PartialEq, Eq)]
1763pub(crate) enum ReusePlan {
1764 Ready,
1766 Pull(Vec<(usize, usize, usize)>),
1769 Fresh,
1771}
1772
1773pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1783 let mut pulls = Vec::new();
1784 for (li, l) in layers.iter().enumerate() {
1785 if !l.full {
1786 if l.device_state {
1787 return ReusePlan::Fresh;
1788 }
1789 continue;
1790 }
1791 if l.host_rows == reuse_from {
1792 continue;
1793 }
1794 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1795 pulls.push((li, l.host_rows, reuse_from));
1796 continue;
1797 }
1798 return ReusePlan::Fresh;
1799 }
1800 if pulls.is_empty() {
1801 ReusePlan::Ready
1802 } else {
1803 ReusePlan::Pull(pulls)
1804 }
1805}
1806
1807impl Pipeline {
1808 fn reset_fresh_mtp_draft_state(&mut self) {
1811 self.draft_full_streak = 0;
1812 }
1813
1814 fn clear_sequence_state(&mut self) {
1822 #[cfg(target_os = "macos")]
1825 let _ = crate::gpu_metal::wait_replay();
1826 self.kv_cache.clear();
1827 self.clear_history();
1830 self.graph_logits = None;
1831 if let Some(b) = &mut self.dsv41 {
1832 b.3.clear();
1833 }
1834 crate::gpu::graph_kv_reset(self.graph_kv_id);
1835 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1840 }
1841
1842 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1849 if self.graph_prefill_preferred() {
1850 return true;
1851 }
1852 let kv_id = self.graph_kv_id;
1853 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1854 .map(|li| {
1855 let full = matches!(
1856 self.weights.layers[self.phys_layer(li)].attn,
1857 AttnKind::Full { .. }
1858 );
1859 ReuseLayer {
1860 full,
1861 host_rows: self.kv_cache.layers[li].pos_len(),
1864 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1865 device_state: crate::gpu::graph_state_resident(kv_id, li),
1866 }
1867 })
1868 .collect();
1869 if layers
1873 .iter()
1874 .all(|l| l.device_rows.is_none() && !l.device_state)
1875 {
1876 return true;
1877 }
1878 let plan = kv_reuse_plan(reuse_from, &layers);
1879 let (what, rows, n) = match &plan {
1880 ReusePlan::Ready => ("host ready", 0, 0),
1881 ReusePlan::Fresh => ("fresh", 0, 0),
1882 ReusePlan::Pull(p) => (
1883 "pull",
1884 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1885 p.len(),
1886 ),
1887 };
1888 let t0 = std::time::Instant::now();
1889 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1890 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1891 eprintln!(
1892 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1893 if ok { "" } else { " (failed → fresh)" },
1894 t0.elapsed().as_secs_f64() * 1e3
1895 );
1896 }
1897 ok
1898 }
1899
1900 fn apply_kv_reuse_plan(
1901 &mut self,
1902 reuse_from: usize,
1903 plan: ReusePlan,
1904 layers: &[ReuseLayer],
1905 ) -> bool {
1906 let kv_id = self.graph_kv_id;
1907 match plan {
1908 ReusePlan::Fresh => return false,
1909 ReusePlan::Ready => {}
1910 ReusePlan::Pull(pulls) => {
1911 let (nkv, hd) = {
1916 let c = &self.kv_cache.layers[pulls[0].0];
1917 (c.num_kv_heads, c.head_dim)
1918 };
1919 let uniform = pulls.iter().all(|&(li, _, _)| {
1920 let c = &self.kv_cache.layers[li];
1921 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1922 });
1923 let batched = if uniform {
1924 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1925 } else {
1926 None
1927 };
1928 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1929 Some(rows) => rows,
1930 None => {
1931 let mut rows = Vec::with_capacity(pulls.len());
1932 for &(li, from, to) in &pulls {
1933 let (lnkv, lhd) = {
1934 let c = &self.kv_cache.layers[li];
1935 (c.num_kv_heads, c.head_dim)
1936 };
1937 let Some((k, v, first_valid)) =
1938 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1939 else {
1940 return false;
1941 };
1942 let need_from = match self.layer_window(li) {
1946 Some(w) => from.max((to + 1).saturating_sub(w)),
1947 None => from,
1948 };
1949 if first_valid > need_from {
1950 return false;
1951 }
1952 rows.push((k, v));
1953 }
1954 rows
1955 }
1956 };
1957 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1958 let cache = &mut self.kv_cache.layers[li];
1959 let row = cache.num_kv_heads * cache.head_dim;
1960 for p in 0..to - from {
1961 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1962 }
1963 if cache.pos_len() != to {
1964 return false;
1965 }
1966 }
1967 }
1968 }
1969 for (li, l) in layers.iter().enumerate() {
1973 if l.full
1974 && l.device_rows.is_some_and(|d| d > reuse_from)
1975 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1976 {
1977 return false;
1978 }
1979 }
1980 true
1981 }
1982
1983 fn finish_generation(
1989 &mut self,
1990 mtp: &mut Option<MtpModule>,
1991 router: &mut Option<crate::swarm::DynRouter>,
1992 clear_sequence: bool,
1993 ) {
1994 if router.is_some() {
1998 let _ = self.set_active_skill(None);
1999 }
2000 #[cfg(target_os = "macos")]
2007 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
2008 if clear_sequence {
2009 self.clear_sequence_state();
2010 if let Some(m) = mtp.as_mut() {
2011 m.kv.clear();
2017 }
2018 if let Some(m) = self.mtp.as_mut() {
2019 m.kv.clear();
2023 }
2024 }
2025 self.graph_want_logits = false;
2026 self.graph_head_required = false;
2027 self.graph_logits = None;
2028 self.graph_failed
2029 .store(false, std::sync::atomic::Ordering::Relaxed);
2030 self.cancel
2031 .store(false, std::sync::atomic::Ordering::Relaxed);
2032 self.dyn_router = router.take().or(self.dyn_router.take());
2033 self.mtp = mtp.take().or(self.mtp.take());
2034 self.mtp_graph_mode = None;
2035 self.spec_forced = None;
2036 }
2037
2038 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2042 if self
2043 .graph_failed
2044 .swap(false, std::sync::atomic::Ordering::Relaxed)
2045 {
2046 self.cancel
2047 .store(false, std::sync::atomic::Ordering::Relaxed);
2048 self.clear_sequence_state();
2049 self.graph_logits = None;
2050 self.graph_want_logits = false;
2051 self.graph_head_required = false;
2052 return Err(format!("GPU graph failed during {phase} at position {pos}"));
2053 }
2054 Ok(())
2055 }
2056
2057 #[cfg(target_os = "macos")]
2058 fn fail_metal_graph(&mut self, reason: &str) {
2059 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2060 self.clear_sequence_state();
2061 self.graph_logits = None;
2062 self.graph_failed
2063 .store(true, std::sync::atomic::Ordering::Relaxed);
2064 self.cancel
2065 .store(true, std::sync::atomic::Ordering::Relaxed);
2066 tracing::error!("native Metal TokenGraph failed closed: {reason}");
2067 }
2068
2069 fn nll_begin(&mut self) -> Result<(), String> {
2074 if self
2075 .graph_failed
2076 .swap(false, std::sync::atomic::Ordering::Relaxed)
2077 {
2078 self.cancel
2079 .store(false, std::sync::atomic::Ordering::Relaxed);
2080 self.clear_sequence_state();
2081 self.graph_logits = None;
2082 self.graph_want_logits = false;
2083 self.graph_head_required = false;
2084 return Err("GPU graph failed before NLL scoring".to_string());
2085 }
2086 self.clear_sequence_state();
2087 self.graph_logits = None;
2088 self.graph_want_logits = false;
2089 self.graph_head_required = false;
2090 Ok(())
2091 }
2092
2093 fn nll_end(&mut self) {
2097 self.clear_sequence_state();
2098 self.graph_logits = None;
2099 self.graph_want_logits = false;
2100 self.graph_head_required = false;
2101 self.graph_failed
2102 .store(false, std::sync::atomic::Ordering::Relaxed);
2103 }
2104
2105 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2108 #[cfg(test)]
2109 if self.nll_test_fail_at == Some(pos) {
2110 self.nll_test_fail_at = None;
2111 self.graph_failed
2112 .store(true, std::sync::atomic::Ordering::Relaxed);
2113 self.cancel
2114 .store(true, std::sync::atomic::Ordering::Relaxed);
2115 }
2116 if self
2117 .graph_failed
2118 .swap(false, std::sync::atomic::Ordering::Relaxed)
2119 {
2120 self.cancel
2121 .store(false, std::sync::atomic::Ordering::Relaxed);
2122 self.clear_sequence_state();
2123 self.graph_logits = None;
2124 self.graph_want_logits = false;
2125 return Err(format!(
2126 "GPU graph failed during NLL {phase} at position {pos}"
2127 ));
2128 }
2129 Ok(())
2130 }
2131
2132 #[inline]
2136 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2137 virtual_idx % self.physical_layers
2138 }
2139
2140 #[inline]
2143 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2144 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2145 }
2146
2147 #[allow(clippy::too_many_arguments)]
2149
2150 #[cfg(target_os = "macos")]
2169 fn graph_prefill_preferred(&self) -> bool {
2170 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2171 if !crate::gpu::enabled_here()
2172 || !graph_force
2173 || std::env::var("CMF_GPU_BLOCK")
2174 .map(|v| v == "0")
2175 .unwrap_or(false)
2176 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2179 {
2180 return false;
2181 }
2182 self.weights
2183 .layers
2184 .iter()
2185 .any(|lw| {
2186 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2187 })
2188 }
2189
2190 #[cfg(not(target_os = "macos"))]
2197 fn batch_prefix_prefill(&self) -> bool {
2198 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2199 Ok("0") => return false,
2200 Ok("1") => true,
2201 _ => false,
2202 };
2203 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2204 && crate::gpu::enabled_here()
2205 && !self.graph_refused()
2206 && (forced || self.graph_attn_decline_reason().is_some())
2207 && self.wgpu_graph_attn_decline().is_none()
2208 && self.attn_softcap == 0.0
2209 && self
2210 .weights
2211 .layers
2212 .iter()
2213 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2214 && self.automatic_gpu_prefix().is_some()
2215 }
2216
2217 fn g4_batch_prefill(&self) -> bool {
2223 #[cfg(target_os = "macos")]
2224 {
2225 false
2226 }
2227 #[cfg(not(target_os = "macos"))]
2228 {
2229 !self.weights.layers.is_empty()
2233 && !self.per_token_stack()
2234 && (self.global_attn.is_some() || self.attn_v_norm)
2235 && crate::gpu::g4_batch_on()
2236 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2237 && crate::gpu::enabled_here()
2238 && crate::gpu::discrete()
2239 && !self.graph_refused()
2240 && !self.o1_active()
2241 && self.attn_softcap == 0.0
2242 && self.wgpu_graph_attn_decline().is_none()
2243 && self.weights.layers.iter().all(|lw| {
2244 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2245 })
2246 && self.automatic_gpu_prefix().is_none()
2247 }
2248 }
2249
2250 #[cfg(not(target_os = "macos"))]
2257 fn g4_batch_ingest(
2258 &mut self,
2259 ids: &[u32],
2260 pos0: usize,
2261 keep_all: bool,
2262 ) -> Result<(usize, Vec<f32>), String> {
2263 let hs = self.hidden_size;
2264 let chunk = self.generation_batch_k().max(1);
2265 let end_all = pos0 + ids.len();
2266 let mut pos = pos0;
2267 let mut out = Vec::new();
2268 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2269 let end = (pos + chunk).min(end_all);
2270 let bk = end - pos;
2271 let mut hiddens = vec![0f32; bk * hs];
2272 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2273 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2274 }
2275 let positions: Vec<usize> = (pos..end).collect();
2276 let t_chunk = std::time::Instant::now();
2277 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2278 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2279 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2280 eprintln!(
2281 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2282 bk as f64 / (ms / 1000.0)
2283 );
2284 }
2285 match outcome {
2286 crate::gpu::BatchGraphOutcome::Completed => {
2287 if keep_all {
2288 out.extend_from_slice(&hiddens);
2289 } else {
2290 out = hiddens[(bk - 1) * hs..].to_vec();
2291 }
2292 pos = end;
2293 }
2294 crate::gpu::BatchGraphOutcome::Failed => {
2295 return Err("Gemma-4 batched prefill failed after admission".into());
2296 }
2297 crate::gpu::BatchGraphOutcome::Declined => {
2298 #[cfg(feature = "gpu")]
2299 if pos > pos0 {
2300 self.pull_lagging_host_kv(0, self.num_layers, pos);
2301 }
2302 break;
2303 }
2304 }
2305 }
2306 Ok((pos - pos0, out))
2307 }
2308
2309 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2314 #[cfg(not(target_os = "macos"))]
2315 {
2316 if ids.is_empty() || !self.g4_batch_prefill() {
2317 return None;
2318 }
2319 self.clear_sequence_state();
2320 match self.g4_batch_ingest(ids, 0, true) {
2321 Ok((n, rows)) if n == ids.len() => Some(rows),
2322 _ => {
2323 self.clear_sequence_state();
2324 None
2325 }
2326 }
2327 }
2328 #[cfg(target_os = "macos")]
2329 {
2330 let _ = ids;
2331 None
2332 }
2333 }
2334
2335 #[cfg(not(target_os = "macos"))]
2336 fn graph_prefill_preferred(&self) -> bool {
2337 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2345 if !graph_on || !crate::gpu::enabled_here() {
2346 return false;
2347 }
2348 if self.embryo_resident_eligible() {
2352 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2357 }
2358 if self.o1_active() {
2372 return false;
2373 }
2374 if self.wgpu_graph_attn_decline().is_some() {
2378 return false;
2379 }
2380 if self
2381 .weights
2382 .layers
2383 .iter()
2384 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2385 {
2386 return true;
2387 }
2388 self.weights
2399 .layers
2400 .iter()
2401 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2402 && self.automatic_gpu_prefix().is_none()
2403 }
2404
2405 #[cfg(target_os = "macos")]
2406 fn q1_graph_gpu(
2407 &mut self,
2408 start: usize,
2409 upto: Option<usize>,
2410 position: usize,
2411 h: &mut [f32],
2412 ) -> usize {
2413 let _mt0 = std::time::Instant::now(); use crate::gpu::{
2415 AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, SconvGpuLayer, TokenGraph,
2416 };
2417 const MOE_WORKING_SET_REASON: &str =
2418 "model weights exceed Metal working set for native MoE graph";
2419 if self
2423 .graph_declines
2424 .borrow()
2425 .iter()
2426 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2427 {
2428 return start;
2429 }
2430 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2431 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2433 || !crate::gpu::enabled_here()
2434 || !graph_force
2435 || std::env::var("CMF_GPU_BLOCK")
2436 .map(|v| v == "0")
2437 .unwrap_or(false)
2438 {
2439 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2440 eprintln!(
2441 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2442 self.attn_softcap > 0.0,
2443 crate::gpu::enabled_here(),
2444 graph_force,
2445 );
2446 }
2447 if self.graph_head_required {
2448 self.fail_metal_graph("native graph front gate refused");
2449 }
2450 return start;
2451 }
2452 let gemma4 = self.metal_gemma4();
2462 let swa_graph = self.metal_graph_swa() || gemma4;
2463 let graph_attn_reason = self.graph_attn_decline_reason();
2470 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2471 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2472 let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
2476 if (self.swa.is_some() && !swa_graph)
2477 || (self.global_attn.is_some() && !gemma4)
2478 || self.attention_heads_per_layer.is_some()
2479 || (self.attn_v_norm && !gemma4)
2480 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2483 || self.weights.layers.iter().any(|lw| {
2484 ((lw.layer_scale.is_some()
2485 || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())))
2486 && !gemma4)
2487 || matches!(&lw.ffn, FfnKind::Dense(d)
2488 if !(matches!(d.act, Act::Silu | Act::Gelu)
2489 || (gemma4 && d.act == Act::GeluTanh)))
2490 })
2491 {
2492 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2495 self.note_graph_decline("metal block graph", reason);
2496 }
2497 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2498 eprintln!(
2499 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2500 self.swa.is_some(),
2501 self.global_attn.is_some(),
2502 self.attention_heads_per_layer.is_some(),
2503 self.attn_v_norm,
2504 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2505 );
2506 }
2507 if self.graph_head_required {
2508 self.fail_metal_graph("native graph architecture gate refused");
2509 }
2510 return start;
2511 }
2512 let limit = upto
2515 .map(|u| u + 1)
2516 .unwrap_or(self.num_layers)
2517 .min(self.num_layers);
2518
2519 enum Item<'a> {
2520 Gdn {
2521 run: Vec<GdnGpuLayer<'a>>,
2522 first: usize,
2523 },
2524 Sconv {
2526 run: Vec<SconvGpuLayer<'a>>,
2527 first: usize,
2528 },
2529 Attn {
2530 l: AttnGpuLayer<'a>,
2531 li: usize,
2532 q_norm: Option<&'a [f32]>,
2533 k_norm: Option<&'a [f32]>,
2534 output_gate: bool,
2535 proj_gate: Option<(&'a QTensor, bool)>,
2539 head_gate_w: Option<&'a [f32]>,
2542 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2543 full_gpu: bool,
2546 },
2547 }
2548
2549 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2556 let attend_contract = attend_mode != "0"
2557 && attend_mode != "off"
2558 && self.head_dim % 4 == 0
2559 && self.head_dim <= 256
2560 && (self.rotary_dim == 0
2564 || (self.rotary_dim >= 2
2565 && self.rotary_dim <= self.head_dim
2566 && (self.rotary_dim / 2) % 32 == 0))
2567 && self.num_kv_heads > 0
2568 && self.num_heads % self.num_kv_heads == 0;
2569
2570 let mut plan: Vec<Item> = Vec::new();
2571 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2572 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2574 let mut scan = start;
2575 while scan < limit {
2576 let lw = &self.weights.layers[self.phys_layer(scan)];
2577 let ffn = match &lw.ffn {
2578 FfnKind::Dense(d) if d.segs.is_empty() => {
2579 let (Some(g), Some(u), Some(dn)) = (
2580 d.gate_proj.metal_graph_parts(),
2581 d.up_proj.metal_graph_parts(),
2582 d.down_proj.metal_graph_parts(),
2583 ) else {
2584 if block_diag {
2585 eprintln!(
2586 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2587 );
2588 }
2589 break;
2590 };
2591 MetalFfn::Dense {
2592 gate: g,
2593 up: u,
2594 down: dn,
2595 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2598 }
2599 }
2600 FfnKind::Moe(m) => {
2601 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2607 {
2608 MetalFfn::FlintMoe(flint)
2609 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2610 MetalFfn::Moe(moe)
2611 } else {
2612 if block_diag {
2613 eprintln!(
2614 "block-graph: L{scan} MoE outside the graph contract — run ends"
2615 );
2616 }
2617 break;
2618 };
2619 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2620 model_ref.get_or_insert_with(|| model.clone());
2621 }
2622 ffn
2623 }
2624 _ => {
2625 if block_diag {
2626 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2627 }
2628 break;
2629 }
2630 };
2631 match &lw.attn {
2632 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2633 let parts = (
2634 w.in_proj_qkv.metal_graph_parts(),
2635 w.in_proj_z.metal_graph_parts(),
2636 w.in_proj_a.f32_parts(),
2637 w.in_proj_b.f32_parts(),
2638 w.out_proj.metal_graph_parts(),
2639 );
2640 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2641 if block_diag {
2642 eprintln!(
2643 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2644 w.in_proj_qkv.metal_graph_parts().is_some(),
2645 w.in_proj_z.metal_graph_parts().is_some(),
2646 w.in_proj_a.f32_parts().is_some(),
2647 w.in_proj_b.f32_parts().is_some(),
2648 w.out_proj.metal_graph_parts().is_some(),
2649 );
2650 }
2651 break;
2652 };
2653 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2654 model_ref.get_or_insert_with(|| model.clone());
2655 }
2656 let gl = GdnGpuLayer {
2657 attn_norm: &lw.input_norm,
2658 post_norm: &lw.post_norm,
2659 qkv,
2660 z,
2661 a,
2662 b,
2663 out,
2664 ffn,
2665 conv1d: &w.conv1d,
2666 a_log: &w.a_log,
2667 dt_bias: &w.dt_bias,
2668 gnorm: &w.norm,
2669 };
2670 match plan.last_mut() {
2671 Some(Item::Gdn { run, .. }) => run.push(gl),
2672 _ => plan.push(Item::Gdn {
2673 run: vec![gl],
2674 first: scan,
2675 }),
2676 }
2677 }
2678 AttnKind::ShortConv(w) if self.short_conv_cfg.is_some() => {
2679 let (Some(inp), Some(out)) =
2680 (w.in_proj.metal_graph_parts(), w.out_proj.metal_graph_parts())
2681 else {
2682 if block_diag {
2683 eprintln!("block-graph: L{scan} short-conv parts refused");
2684 }
2685 break;
2686 };
2687 if let QTensor::Mapped { model, .. } = &w.in_proj {
2688 model_ref.get_or_insert_with(|| model.clone());
2689 }
2690 let sl = SconvGpuLayer {
2691 attn_norm: &lw.input_norm,
2692 post_norm: &lw.post_norm,
2693 in_proj: inp,
2694 taps: &w.conv,
2695 out,
2696 attn_out_norm: lw.attn_out_norm.as_deref(),
2697 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2698 ffn,
2699 };
2700 match plan.last_mut() {
2701 Some(Item::Sconv { run, .. }) => run.push(sl),
2702 _ => plan.push(Item::Sconv {
2703 run: vec![sl],
2704 first: scan,
2705 }),
2706 }
2707 }
2708 AttnKind::Full {
2709 wq,
2710 wk,
2711 wv,
2712 wo,
2713 q_norm,
2714 k_norm,
2715 output_gate,
2716 softplus_gate,
2717 bias,
2718 } if (!self.kv_cache.layers[scan].o1_sealed()
2719 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2724 && softplus_gate
2728 .as_ref()
2729 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2730 {
2731 let parts = (
2732 wq.metal_graph_parts(),
2733 wk.metal_graph_parts(),
2734 wv.metal_graph_parts(),
2735 wo.metal_graph_parts(),
2736 );
2737 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2738 break;
2739 };
2740 if let QTensor::Mapped { model, .. } = wq {
2741 model_ref.get_or_insert_with(|| model.clone());
2742 }
2743 let cache = &self.kv_cache.layers[scan];
2744 let o1_metal = cache.o1.is_some()
2748 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2749 && cache.o1_views().is_some();
2750 let head_gate_w = softplus_gate
2754 .as_ref()
2755 .and_then(|(g, _)| g.f32_parts())
2756 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2757 .map(|(d, _, _)| d);
2758 let (nkv_g, hd_g) = if gemma4 {
2761 let (a, b, _) = self.layer_geom(scan);
2762 (a, b)
2763 } else {
2764 (self.num_kv_heads, self.head_dim)
2765 };
2766 let full_gpu = attend_contract
2767 && softplus_gate.is_none() == head_gate_w.is_none()
2768 && cache.mode == crate::kv_cache::KvMode::F32
2769 && (cache.o1.is_none() || o1_metal)
2770 && bias.is_none()
2771 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2772 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2773 && pk.1 == nkv_g * hd_g
2774 && pv.1 == nkv_g * hd_g
2775 && po.2 == self.num_heads * hd_g;
2776 plan.push(Item::Attn {
2777 l: AttnGpuLayer {
2778 attn_norm: &lw.input_norm,
2779 post_norm: &lw.post_norm,
2780 wq: pq,
2781 wk: pk,
2782 wv: pv,
2783 wo: po,
2784 ffn,
2785 attn_out_norm: lw.attn_out_norm.as_deref(),
2786 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2787 layer_scale: lw.layer_scale,
2788 },
2789 li: scan,
2790 q_norm: q_norm.as_deref(),
2791 k_norm: k_norm.as_deref(),
2792 output_gate: *output_gate,
2793 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2794 head_gate_w,
2795 bias: bias
2796 .as_ref()
2797 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2798 full_gpu,
2799 });
2800 }
2801 _ => break,
2802 }
2803 scan += 1;
2804 }
2805 let Some(model) = model_ref else {
2806 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2807 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2808 }
2809 if self.graph_head_required {
2810 self.fail_metal_graph("native graph has no mapped model reference");
2811 }
2812 return start;
2813 };
2814 if plan.is_empty() {
2815 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2816 eprintln!("q1-graph: empty plan at layer {start}");
2817 }
2818 if self.graph_head_required {
2819 self.fail_metal_graph("native graph plan is empty");
2820 }
2821 return start;
2822 }
2823 let has_moe = plan.iter().any(|it| match it {
2824 Item::Gdn { run, .. } => run
2825 .iter()
2826 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2827 Item::Sconv { run, .. } => run
2828 .iter()
2829 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2830 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2831 });
2832 let has_flint_moe = plan.iter().any(|it| match it {
2833 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2834 Item::Sconv { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2835 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2836 });
2837 let metal_working_set = crate::gpu_metal::working_set_bytes();
2838 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2839 let model_bytes = model.primary_bytes().len() as u64;
2840 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2846 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2847 let pager_cap = if !has_moe {
2856 None
2857 } else if has_flint_moe {
2858 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2862 } else {
2863 crate::gpu_metal::moe_pager_cap_bytes()
2864 };
2865 let pager_eligible =
2866 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2867 let mut pager_span: Option<(usize, usize)> = None;
2868 if pager_eligible {
2869 let cap = pager_cap.expect("checked above");
2870 let tail_span = (self.graph_want_logits
2874 && upto.is_none()
2875 && scan == self.num_layers
2876 && std::env::var("CMF_GPU_LMHEAD")
2877 .map(|v| v != "0")
2878 .unwrap_or(true))
2879 .then(|| self.weights.lm_head.metal_graph_parts())
2880 .flatten()
2881 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2882 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2883 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2884 Some(match span {
2885 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2886 None => (lo, hi),
2887 })
2888 };
2889 let page_span = |span: (usize, usize), include_tail: bool| {
2890 let (mut lo, mut hi) = span;
2891 if include_tail {
2892 if let Some((tail_lo, tail_hi)) = tail_span {
2893 lo = lo.min(tail_lo);
2894 hi = hi.max(tail_hi);
2895 }
2896 }
2897 crate::gpu_metal::page_round_span(lo, hi)
2898 };
2899 let mut selected = 0usize;
2900 let mut selected_span: Option<(usize, usize)> = None;
2901 let mut gdn_prefix: Option<(usize, usize)> = None;
2909 'items: for (i, item) in plan.iter().enumerate() {
2910 match item {
2911 Item::Sconv { .. } => break,
2914 Item::Attn { li, .. } => {
2915 let Some(span) = add_layer_span(selected_span, *li) else {
2916 break;
2917 };
2918 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2919 break;
2920 };
2921 if page_hi - page_lo > cap {
2922 break;
2923 }
2924 selected = i + 1;
2925 selected_span = Some((page_lo, page_hi));
2926 }
2927 Item::Gdn { first, run } => {
2928 let mut whole = None;
2935 for take in 1..=run.len() {
2936 whole = add_layer_span(whole, *first + take - 1);
2937 if whole.is_none() {
2938 break;
2939 }
2940 }
2941 let whole_fits_empty_page = whole
2942 .and_then(|span| page_span(span, false))
2943 .is_some_and(|(lo, hi)| hi - lo <= cap);
2944 if whole_fits_empty_page {
2945 let mut span = selected_span;
2946 for take in 1..=run.len() {
2947 let Some(next) = add_layer_span(span, *first + take - 1) else {
2948 break 'items;
2949 };
2950 span = Some(next);
2951 }
2952 let Some(span) = span else {
2953 break;
2954 };
2955 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2956 else {
2957 break;
2958 };
2959 if page_hi - page_lo > cap {
2960 break;
2961 }
2962 selected = i + 1;
2963 selected_span = Some((page_lo, page_hi));
2964 continue;
2965 }
2966 if selected != 0 {
2967 break;
2968 }
2969 let mut span = None;
2970 let mut best: Option<(usize, (usize, usize))> = None;
2971 for take in 1..=run.len() {
2972 let Some(next) = add_layer_span(span, *first + take - 1) else {
2973 break;
2974 };
2975 let Some((page_lo, page_hi)) = page_span(next, false) else {
2978 break;
2979 };
2980 if page_hi - page_lo > cap {
2981 break;
2982 }
2983 span = Some((page_lo, page_hi));
2984 best = Some((take, (page_lo, page_hi)));
2985 }
2986 let Some((take, page)) = best else {
2987 break;
2988 };
2989 selected = i + 1;
2990 selected_span = Some(page);
2991 if take != run.len() {
2992 gdn_prefix = Some((i, take));
2993 break 'items;
2994 }
2995 }
2996 }
2997 }
2998 if selected != 0 {
2999 if let Some((item, take)) = gdn_prefix {
3000 match &mut plan[item] {
3001 Item::Gdn { run, .. } => run.truncate(take),
3002 Item::Sconv { .. } | Item::Attn { .. } => {
3003 unreachable!("only a GDN run is splittable")
3004 }
3005 }
3006 }
3007 let (page_lo, page_hi) = selected_span.expect("selected page span");
3008 tracing::info!(
3009 page_start = page_lo,
3010 page_end = page_hi,
3011 page_mb = (page_hi - page_lo) / (1024 * 1024),
3012 cap_mb = cap / (1024 * 1024),
3013 graph_items = selected,
3014 "native Metal MoE graph uses transient bounded weight page"
3015 );
3016 plan.truncate(selected);
3017 pager_span = Some((page_lo, page_hi));
3018 } else {
3019 tracing::warn!(
3020 cap_mb = cap / (1024 * 1024),
3021 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
3022 );
3023 }
3024 }
3025 if has_moe && needs_bounded_pager && pager_span.is_none() {
3034 tracing::info!(
3035 model_bytes,
3036 metal_working_set,
3037 "native Metal MoE graph declined before allocation"
3038 );
3039 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
3040 if self.graph_head_required {
3041 self.fail_metal_graph(MOE_WORKING_SET_REASON);
3042 }
3043 return start;
3044 }
3045 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
3046 let dev_attend = attend_contract
3047 && (self.head_dim <= 128
3048 || has_moe
3049 || (self.head_dim <= 256 && has_gdn)
3055 || (self.head_dim <= 256 && swa_graph)
3059 || attend_mode == "force"
3060 || attend_mode == "256");
3061 if !dev_attend {
3062 for it in &mut plan {
3063 if let Item::Attn { li, full_gpu, .. } = it {
3064 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
3067 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
3068 if !keep_o1 {
3069 *full_gpu = false;
3070 }
3071 }
3072 }
3073 }
3074 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3075 use std::sync::atomic::{AtomicBool, Ordering};
3076 static SAID: AtomicBool = AtomicBool::new(false);
3077 if !SAID.swap(true, Ordering::Relaxed) {
3078 let fg = plan
3079 .iter()
3080 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
3081 .count();
3082 let att = plan
3083 .iter()
3084 .filter(|it| matches!(it, Item::Attn { .. }))
3085 .count();
3086 eprintln!(
3087 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
3088 plan.len(),
3089 self.head_dim,
3090 self.rotary_dim,
3091 self.num_kv_heads,
3092 self.num_heads,
3093 );
3094 }
3095 }
3096 let dims = GraphDims {
3097 hidden: self.hidden_size,
3098 eps: self.rms_eps as f32,
3099 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3100 };
3101 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
3105 crate::gpu_metal::MoePagerPageScope::new(
3106 _mt0,
3107 position,
3108 start,
3109 page_lo,
3110 page_hi,
3111 pager_cap.unwrap_or(0),
3115 )
3116 });
3117 let graph_new = match pager_span {
3118 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3119 None => TokenGraph::new(&model, dims, h),
3120 };
3121 let Some(mut graph) = graph_new else {
3122 if self.graph_head_required {
3123 self.fail_metal_graph("native TokenGraph allocation refused");
3124 }
3125 return start;
3126 };
3127 if swa_graph && self.head_dim > 128 {
3128 graph.set_attend_blk(1, 32);
3138 }
3139 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3140 nv: cfg.num_v_heads,
3141 nk: cfg.num_k_heads,
3142 dk: cfg.key_head_dim,
3143 dv: cfg.value_head_dim,
3144 kk: cfg.conv_kernel,
3145 hidden: self.hidden_size,
3146 inter: self.intermediate_size,
3147 c_dim: cfg.conv_dim(),
3148 eps: cfg.rms_eps as f32,
3149 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3150 });
3151 let mut valid = 0usize;
3155 let mut end = start;
3156 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3158 static ONCE: std::sync::Once = std::sync::Once::new();
3159 ONCE.call_once(|| {
3160 for it in &plan {
3161 match it {
3162 Item::Gdn { first, run } => {
3163 eprintln!("plan: Gdn first={first} len={}", run.len())
3164 }
3165 Item::Sconv { first, run } => {
3166 eprintln!("plan: Sconv first={first} len={}", run.len())
3167 }
3168 Item::Attn { li, full_gpu, .. } => {
3169 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3170 }
3171 }
3172 }
3173 });
3174 }
3175 for item in &plan {
3176 let ok = match item {
3177 Item::Gdn { run, .. } => gcfg
3178 .as_ref()
3179 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3180 .unwrap_or(false),
3181 Item::Sconv { run, .. } => self
3182 .short_conv_cfg
3183 .is_some_and(|sc| run.iter().all(|l| graph.sconv_ok(l, sc.kernel))),
3184 Item::Attn { l, .. } => graph.attn_ok(l),
3185 };
3186 if !ok {
3187 if block_diag {
3188 eprintln!(
3189 "block-graph: plan item {} ({}) failed graph preflight",
3190 valid,
3191 match item {
3192 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3193 Item::Sconv { run, first } => {
3194 format!("short-conv run L{first}+{}", run.len())
3195 }
3196 Item::Attn { li, .. } => format!("Attn L{li}"),
3197 }
3198 );
3199 }
3200 break;
3201 }
3202 valid += 1;
3203 end += match item {
3204 Item::Gdn { run, .. } => run.len(),
3205 Item::Sconv { run, .. } => run.len(),
3206 Item::Attn { .. } => 1,
3207 };
3208 }
3209 plan.truncate(valid);
3210 if plan.is_empty() {
3211 if self.graph_head_required {
3212 self.fail_metal_graph("native graph preflight produced no valid items");
3213 }
3214 return start;
3215 }
3216
3217 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3218 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3219 return start;
3220 }
3221
3222 let one_pass = |t: (usize, usize, usize)| {
3231 use cortiq_core::TensorDtype as D;
3232 matches!(
3233 model.tensors[t.0].dtype,
3234 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3235 )
3236 };
3237 let dense_fast = plan.iter().all(|it| match it {
3238 Item::Attn {
3239 l, li, full_gpu, ..
3240 } => {
3241 *full_gpu
3242 && self.kv_cache.layers[*li].o1.is_none()
3243 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3244 && match l.ffn {
3245 MetalFfn::Dense { gate, up, down, .. } => {
3246 one_pass(gate) && one_pass(up) && one_pass(down)
3247 }
3248 _ => false,
3249 }
3250 }
3251 Item::Sconv { run, .. } => run.iter().all(|l| {
3254 one_pass(l.in_proj)
3255 && one_pass(l.out)
3256 && match l.ffn {
3257 MetalFfn::Dense { gate, up, down, .. } => {
3258 one_pass(gate) && one_pass(up) && one_pass(down)
3259 }
3260 _ => false,
3261 }
3262 }),
3263 Item::Gdn { .. } => false,
3264 });
3265 let moe_plan = !dense_fast
3269 && plan.iter().all(|it| match it {
3270 Item::Attn {
3271 l, li, full_gpu, ..
3272 } => {
3273 *full_gpu
3274 && self.kv_cache.layers[*li].o1.is_none()
3275 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3276 }
3277 Item::Gdn { .. } | Item::Sconv { .. } => false,
3278 });
3279 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3280 if block_diag {
3281 use std::sync::atomic::{AtomicBool, Ordering};
3282 static SAID_AB: AtomicBool = AtomicBool::new(false);
3283 if !SAID_AB.swap(true, Ordering::Relaxed) {
3284 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3285 }
3286 }
3287 let _mv_fast = match ab {
3288 Some((bits, _)) => {
3289 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3290 crate::gpu_metal::MvFastGuard::set_raw(bits)
3291 }
3292 None if moe_plan => {
3293 graph.set_dense_concurrent(
3294 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3295 );
3296 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3297 }
3298 None => {
3299 graph.set_dense_concurrent(dense_fast);
3300 let q8r4 = if swa_graph {
3303 crate::gpu_metal::DENSE_Q8R4
3304 } else {
3305 0
3306 };
3307 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3308 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3309 } else {
3310 0
3311 })
3312 }
3313 };
3314
3315 let pool = self.pool.clone();
3318 let (nh, nkv, hd, hs, eps) = (
3319 self.num_heads,
3320 self.num_kv_heads,
3321 self.head_dim,
3322 self.hidden_size,
3323 self.rms_eps,
3324 );
3325 let norm_style = self.norm_style;
3326 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3327 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3328 let kv_id = self.graph_kv_id;
3329 let mut pending: Vec<(usize, usize)> = Vec::new();
3332 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3335 for item in &plan {
3336 let _xt0 = std::time::Instant::now();
3337 let _xkind: u32 = match item {
3338 Item::Gdn { .. } | Item::Sconv { .. } => 2,
3339 Item::Attn { .. } => 3,
3340 };
3341 if self.loop_final_norm {
3343 let item_start = match item {
3344 Item::Gdn { first, .. } | Item::Sconv { first, .. } => *first,
3345 Item::Attn { li, .. } => *li,
3346 };
3347 if item_start > start && self.is_loop_end(item_start - 1) {
3348 graph.encode_loop_norm(&self.weights.final_norm);
3349 }
3350 }
3351 match item {
3352 Item::Gdn { run, first } => {
3353 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3354 if l.linear_state.len() != want {
3355 l.linear_state = vec![0f32; want];
3356 }
3357 }
3358 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3359 .iter()
3360 .map(|l| l.linear_state.as_slice())
3361 .collect();
3362 let _ig = std::time::Instant::now();
3363 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3364 tracing::error!("q1 graph: GDN run refused after validation");
3366 return start;
3367 }
3368 graph.commit_kind = 2;
3371 graph.commit();
3372 crate::gpu::stageprof(0, _ig.elapsed());
3373 pending.push((*first, run.len()));
3374 }
3375 Item::Sconv { run, first } => {
3376 let sc = self.short_conv_cfg.expect("validated short-conv run");
3377 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3378 if l.linear_state.len() != sc.state_len() {
3379 l.linear_state = vec![0f32; sc.state_len()];
3380 }
3381 }
3382 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3383 .iter()
3384 .map(|l| l.linear_state.as_slice())
3385 .collect();
3386 if !graph.encode_sconv_run(run, &ro, sc.kernel) {
3387 tracing::error!("q1 graph: short-conv run refused after validation");
3388 return start;
3389 }
3390 graph.commit_kind = 2;
3391 graph.commit();
3392 pending.push((*first, run.len()));
3393 }
3394 Item::Attn {
3395 l,
3396 li,
3397 q_norm,
3398 k_norm,
3399 output_gate,
3400 proj_gate,
3401 head_gate_w,
3402 bias,
3403 full_gpu,
3404 } => {
3405 let _ia = std::time::Instant::now();
3406 let inv_freq_l = self.layer_inv_freq(*li);
3412 let rd_l = self.layer_geom(*li).2;
3413 let window_l = self.layer_window(*li);
3414 let head_gate_w = *head_gate_w;
3415 let (nkv, hd) = if gemma4 {
3418 let (a, b, _) = self.layer_geom(*li);
3419 (a, b)
3420 } else {
3421 (nkv, hd)
3422 };
3423 if *full_gpu {
3425 let cache = &self.kv_cache.layers[*li];
3426 let o1p = if cache.o1.is_some() {
3427 match cache.o1_views() {
3428 Some(views) => Some(crate::gpu::O1AttnParams {
3429 views,
3430 epoch: self.o1_epoch,
3431 }),
3432 None => None,
3434 }
3435 } else {
3436 None
3437 };
3438 let o1_layer = cache.o1.is_some();
3439 if o1_layer && o1p.is_none() {
3440 }
3442 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3443 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3444 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3445 debug_assert!(
3448 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3449 );
3450 let p = crate::gpu::AttnDeviceParams {
3451 kv_id,
3452 layer: *li,
3453 nh,
3454 nkv,
3455 hd,
3456 rd: rd_l,
3457 position,
3458 scale: self.attn_scale,
3459 eps: eps as f32,
3460 gemma,
3461 late_qk_norm: self.qk_norm_after_rope,
3462 output_gate: *output_gate,
3463 q_norm: *q_norm,
3464 k_norm: *k_norm,
3465 inv_freq: &inv_freq_l,
3466 rope_scale: self.layer_rope_scale(*li),
3467 cpu_k,
3468 cpu_v,
3469 cpu_stored,
3470 cpu_gen: cache.generation(),
3471 o1: o1p,
3472 window: window_l,
3473 softmax_sink: cache.sinks.as_deref(),
3474 head_gate: head_gate_w,
3475 v_norm: self.attn_v_norm,
3476 };
3477 let o1_bad = o1_layer && p.o1.is_none();
3478 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3479 {
3480 if p.o1.is_none() {
3482 dev_attn.push((*li, nkv, hd));
3483 }
3484 graph.commit_kind = 3;
3485 graph.commit();
3486 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3490 continue;
3491 }
3492 }
3494 graph.encode_attn_prefix(l);
3495 if let Err(err) = graph.sync_checked() {
3496 self.fail_metal_graph(&err);
3497 return start;
3498 }
3499 if !pending.is_empty() {
3500 let idxs: Vec<usize> =
3501 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3502 let mut outs: Vec<&mut [f32]> = self
3503 .kv_cache
3504 .layers
3505 .iter_mut()
3506 .enumerate()
3507 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3508 .map(|(_, s)| s.linear_state.as_mut_slice())
3509 .collect();
3510 graph.read_states(&mut outs);
3511 }
3512 let mut q_raw = attention::take_buf(l.wq.1);
3513 let mut k = attention::take_buf(l.wk.1);
3514 let mut v = attention::take_buf(l.wv.1);
3515 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3516 let mut gate_raw = proj_gate.map(|(gp, _)| {
3519 let mut normed = attention::take_buf(hs);
3520 graph.read_normed(&mut normed);
3521 let mut raw = attention::take_buf(gp.rows());
3522 gp.matvec(&normed, &mut raw, pool.as_deref());
3523 attention::recycle_buf(&mut normed);
3524 raw
3525 });
3526 let cfg = QwenAttnCfg {
3527 num_heads: nh,
3528 num_kv_heads: nkv,
3529 head_dim: hd,
3530 hidden_size: hs,
3531 position,
3532 inv_freq: &inv_freq_l,
3533 rotary_dim: rd_l,
3534 scale: self.attn_scale,
3535 softcap: self.attn_softcap,
3536 window: window_l,
3537 v_norm: self.attn_v_norm,
3540 qk_norm_after_rope: self.qk_norm_after_rope,
3541 gate_sigmoid: self.proj_gate_sigmoid,
3542 q_norm: *q_norm,
3543 k_norm: *k_norm,
3544 output_gate: *output_gate,
3545 softplus_gate: None,
3546 rope_scale: self.layer_rope_scale(*li),
3547 bias: *bias,
3548 rms_eps: eps,
3549 norm_style,
3550 pool: pool.as_deref(),
3551 v_head_dim: hd,
3552 };
3553 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3556 || std::env::var("CMF_ATTN_DUMP").is_ok();
3557 let _ = full_gpu;
3558 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3559 let mut ao = attention::qwen_attention_core(
3560 q_raw,
3561 k,
3562 v,
3563 &mut self.kv_cache.layers[*li],
3564 &cfg,
3565 );
3566 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3570 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3571 let (cq, _cg, _ck, _cv) =
3572 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3573 let cache = &self.kv_cache.layers[*li];
3574 let n = cache.head_keys(0).len() / hd;
3575 let mut bytes: Vec<u8> = Vec::new();
3576 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3577 bytes.extend_from_slice(&v.to_le_bytes());
3578 }
3579 for v in &cq {
3580 bytes.extend_from_slice(&v.to_le_bytes());
3581 }
3582 for g in 0..nkv {
3583 for v in cache.head_keys(g) {
3584 bytes.extend_from_slice(&v.to_le_bytes());
3585 }
3586 }
3587 for g in 0..nkv {
3588 for v in cache.head_values(g) {
3589 bytes.extend_from_slice(&v.to_le_bytes());
3590 }
3591 }
3592 let _ =
3593 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3594 }
3595 }
3596 if let Some((qr0, k0, v0)) =
3597 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3598 {
3599 let (cq, _cg, ck, cv) =
3600 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3601 let mut h_now = vec![0f32; hs];
3602 graph.read_h(&mut h_now);
3603 let cache = &self.kv_cache.layers[*li];
3604 let n_after = cache.head_keys(0).len() / hd;
3605 let stored = n_after.saturating_sub(1);
3609 let cpu_k: Vec<&[f32]> = (0..nkv)
3610 .map(|g| &cache.head_keys(g)[..stored * hd])
3611 .collect();
3612 let cpu_v: Vec<&[f32]> = (0..nkv)
3613 .map(|g| &cache.head_values(g)[..stored * hd])
3614 .collect();
3615 let p = crate::gpu::AttnDeviceParams {
3616 kv_id,
3617 layer: *li,
3618 nh,
3619 nkv,
3620 hd,
3621 rd: rd_l,
3625 position,
3626 scale: self.attn_scale,
3627 eps: eps as f32,
3628 gemma,
3629 late_qk_norm: self.qk_norm_after_rope,
3630 output_gate: *output_gate,
3631 q_norm: *q_norm,
3632 k_norm: *k_norm,
3633 inv_freq: &inv_freq_l,
3634 rope_scale: self.layer_rope_scale(*li),
3635 cpu_k,
3636 cpu_v,
3637 cpu_stored: stored,
3638 cpu_gen: cache.generation(),
3639 o1: None,
3640 window: window_l,
3641 softmax_sink: cache.sinks.as_deref(),
3642 head_gate: None,
3643 v_norm: self.attn_v_norm,
3644 };
3645 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3646 let md = |a: &[f32], b: &[f32]| {
3647 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3648 };
3649 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3650 eprintln!(
3651 "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}",
3652 nn(&cq),
3653 md(&cq, &dq),
3654 nn(&ck),
3655 md(&ck, &dk),
3656 nn(&cv),
3657 md(&cv, &dv),
3658 nn(&ao),
3659 md(&ao, &dao)
3660 );
3661 } else {
3662 eprintln!("attn-oracle L{li}: device probe declined");
3663 }
3664 }
3665 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3666 attention::apply_projected_gate(
3669 &mut ao,
3670 raw,
3671 per_head,
3672 hd,
3673 self.proj_gate_sigmoid,
3674 );
3675 }
3676 if let Some(mut raw) = gate_raw.take() {
3677 attention::recycle_buf(&mut raw);
3678 }
3679 graph.encode_attn_suffix(l, &ao);
3680 graph.commit();
3683 attention::recycle_buf(&mut ao);
3684 }
3685 }
3686
3687 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3688 }
3689 let mut lm_rows = None;
3694 if self.graph_want_logits
3695 && upto.is_none()
3696 && end == self.num_layers
3697 && std::env::var("CMF_GPU_LMHEAD")
3698 .map(|v| v != "0")
3699 .unwrap_or(true)
3700 {
3701 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3702 if graph.lm_head_ok(lm) {
3703 graph.encode_lm_head(&self.weights.final_norm, lm);
3704 lm_rows = Some(lm.1);
3705 }
3706 }
3707 }
3708 if self.graph_head_required && lm_rows.is_none() {
3709 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3710 self.fail_metal_graph("fused graph head was requested but not encodable");
3711 return start;
3712 }
3713 let _sy0 = std::time::Instant::now();
3714 if let Err(err) = graph.sync_checked() {
3715 if let Some(profile) = pager_profile_page.as_mut() {
3716 profile.finish(start, graph.pager_page_stats(), "sync-error");
3717 }
3718 self.fail_metal_graph(&err);
3719 return start;
3720 }
3721 let _rs0 = std::time::Instant::now();
3722 if !pending.is_empty() {
3723 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3724 let mut outs: Vec<&mut [f32]> = self
3725 .kv_cache
3726 .layers
3727 .iter_mut()
3728 .enumerate()
3729 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3730 .map(|(_, s)| s.linear_state.as_mut_slice())
3731 .collect();
3732 graph.read_states(&mut outs);
3733 }
3734 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3735 use std::sync::atomic::{AtomicU64, Ordering};
3736 static SY: AtomicU64 = AtomicU64::new(0);
3737 static RS: AtomicU64 = AtomicU64::new(0);
3738 static N: AtomicU64 = AtomicU64::new(0);
3739 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3740 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3741 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3742 if n % 100 == 0 {
3743 eprintln!(
3744 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3745 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3746 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3747 );
3748 }
3749 }
3750 if let Some(rows) = lm_rows {
3751 crate::gpu::hostprof_encode_done(_mt0);
3752 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3753 graph.read_logits(&mut lg);
3754 crate::gpu::hostprof_total(_mt0);
3755 lg.resize(self.vocab_size, 0.0);
3756 if let Some(c) = self.final_softcap {
3757 softcap_in_place(&mut lg, c, self.pool.as_deref());
3758 }
3759 self.graph_logits = Some(lg);
3760 }
3761 graph.read_h(h);
3762 if self.graph_head_required && self.graph_logits.is_none() {
3763 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3764 self.fail_metal_graph("fused graph head completed without logits readback");
3765 return start;
3766 }
3767 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3768 METAL_GRAPH_LAYERS.fetch_add(
3769 end.saturating_sub(start) as u64,
3770 std::sync::atomic::Ordering::Relaxed,
3771 );
3772 if self.graph_head_required {
3773 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3774 }
3775 for (li, nkv, hd) in dev_attn {
3779 let mut krow = attention::take_buf(nkv * hd);
3780 let mut vrow = attention::take_buf(nkv * hd);
3781 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3782 let cache = &mut self.kv_cache.layers[li];
3783 cache.append(&krow, &vrow, &[]);
3784 let n = cache.seq_len;
3785 let mut imp = attention::take_buf(n);
3786 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3787 cache.accumulate_imp(&imp);
3788 attention::recycle_buf(&mut imp);
3789 }
3790 attention::recycle_buf(&mut krow);
3791 attention::recycle_buf(&mut vrow);
3792 }
3793 if let Some(profile) = pager_profile_page.as_mut() {
3794 profile.finish(end, graph.pager_page_stats(), "page-complete");
3795 }
3796 if let Some((_, arm)) = ab {
3797 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3798 }
3799 end
3800 }
3801
3802 pub fn new(
3803 tokenizer: Tokenizer,
3804 weights: PipelineWeights,
3805 hidden_size: usize,
3806 intermediate_size: usize,
3807 num_heads: usize,
3808 num_kv_heads: usize,
3809 head_dim: usize,
3810 num_layers: usize,
3811 physical_layers: usize,
3812 loop_final_norm: bool,
3813 vocab_size: usize,
3814 rms_eps: f64,
3815 rope_base: f32,
3816 norm_style: NormStyle,
3817 max_seq_len: usize,
3818 sampler_config: SamplerConfig,
3819 ) -> Self {
3820 let rng = match sampler_config.seed {
3821 Some(s) => SplitMix64::new(s),
3822 None => SplitMix64::from_entropy(),
3823 };
3824 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3825 let pool = Pool::from_env();
3826 if let Some(p) = &pool {
3827 tracing::info!("worker pool: {} threads", p.n_workers());
3828 if let Some(model) = weights
3830 .lm_head
3831 .model_arc()
3832 .or_else(|| weights.embed_tokens.model_arc())
3833 {
3834 let regions: Vec<&[u8]> =
3835 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3836 p.bind_numa(®ions);
3837 }
3838 }
3839 Self {
3840 gpu_plan: None,
3841 tokenizer: std::sync::Arc::new(tokenizer),
3842 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3843 sampler_config,
3844 weights,
3845 hidden_size,
3846 intermediate_size,
3847 num_heads,
3848 num_kv_heads,
3849 head_dim,
3850 num_layers,
3851 physical_layers,
3852 loop_final_norm,
3853 vocab_size,
3854 rms_eps,
3855 rope_base,
3856 norm_style,
3857 rotary_dim: head_dim,
3858 attention_heads_per_layer: None,
3859 kv_heads_per_layer: None,
3860 v_head_dim: None,
3861 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3862 .filter(|v| !v.is_empty())
3863 .map(std::path::PathBuf::from),
3864 graph_declines: std::cell::RefCell::new(Vec::new()),
3865 mimo_moe: Default::default(),
3866 minicpm_tail: Default::default(),
3867 vmf_cfg: None,
3868 gdn_cfg: None,
3869 kda_cfg: None,
3870 g3n: None,
3871 g4e: None,
3872 dsv4: None,
3873 dsv41: None,
3874 dsv41_vision: None,
3875 dsv41_prefill: None,
3876 qwen4_exp: None,
3877 clover: None,
3878 clover_stats: clover::CloverStats::default(),
3879 dsv4_mtp: Vec::new(),
3880 dspark: None,
3881 dspark_pending: Vec::new(),
3882 dspark_hist: Vec::new(),
3883 dspark_real: Vec::new(),
3884 dspark_trunk_picks: Vec::new(),
3885 dspark_exp: Vec::new(),
3886 dspark_draft_ns: 0,
3887 m3_winners: Vec::new(),
3888 m3_counts: Vec::new(),
3889 m3_n_exp: 0,
3890 m3_tokens: 0,
3891 m3_prewarmed: false,
3892 logit_multiplier: None,
3893 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3894 graph_failed: std::sync::atomic::AtomicBool::new(false),
3895 graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
3896 kv_history: Vec::new(),
3897 kv_history_device: false,
3898 short_conv_cfg: None,
3899 mtp: None,
3900 mimo_mtp: None,
3901 verify_exact_moe: false,
3902 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3903 ignore_eos: false,
3904 draft_full_streak: 0,
3905 spec_k_adapt: None,
3906 spec_acc_ewma: 0.7,
3907 rng,
3908 sampler_scratch: SamplerScratch::default(),
3909 spec_forced: None,
3910 spec_q: Vec::new(),
3911 spec_p: Vec::new(),
3912 spec_res: Vec::new(),
3913 spec_qs: Vec::new(),
3914 spec_ps: Vec::new(),
3915 spec_ress: Vec::new(),
3916 mtp_graph_mode: None,
3917 #[cfg(target_os = "macos")]
3918 metal_verify: None,
3919 inv_freq,
3920 ws: ForwardScratch::new(hidden_size),
3921 pool,
3922 model: None,
3923 dyn_force_f32: false,
3924 dyn_skill_layers: Vec::new(),
3925 dyn_active: None,
3926 dyn_blend_loaded: false,
3927 dyn_phi_layer: None,
3928 dyn_phi_ema: Vec::new(),
3929 dyn_phi_seen: 0,
3930 dyn_router: None,
3931 o1_cfg: None,
3932 o1_epoch: 0,
3933 o1_flags: Vec::new(),
3934 trace: false,
3935 calib_temp: 1.0,
3936 confidence_on: true,
3937 embed_multiplier: 1.0,
3938 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3939 swa: None,
3940 swa_trim: None,
3941 swa_trim_anchor: usize::MAX,
3942 chunk_min_rows: 32,
3943 sliding_layers: None,
3944 no_rope_layers: None,
3945 anchor_core: None,
3946 bounded_rope: None,
3947 kv_prefix: KvPrefix::default(),
3948 last_prefill_tokens: 0,
3949 inv_freq_local: None,
3950 rotary_dim_local: None,
3951 rope_scale: 1.0,
3952 rope_scale_local: 1.0,
3953 global_attn: None,
3954 inv_freq_global: None,
3955 attn_v_norm: false,
3956 qk_norm_after_rope: false,
3957 proj_gate_sigmoid: false,
3958 final_softcap: None,
3959 head_clusters: None,
3960 attn_softcap: 0.0,
3961 graph_want_logits: false,
3962 graph_head_required: false,
3963 graph_logits: None,
3964 embryo_graph: None,
3965 graph_refused: std::sync::atomic::AtomicBool::new(false),
3966 graph_kv_id: {
3967 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3968 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3969 },
3970 #[cfg(test)]
3971 nll_test_fail_at: None,
3972 #[cfg(test)]
3973 nll_test_force_serial: false,
3974 }
3975 }
3976
3977 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3985 if let Err(e) = self.try_set_o1(cfg) {
3986 tracing::error!("{e}");
3987 }
3988 }
3989
3990 pub fn bounded_native(&self) -> bool {
3994 self.anchor_core.is_some()
3995 }
3996
3997 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
4000 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
4001 }
4002
4003 pub fn o1_refusal(&self) -> Option<String> {
4005 self.anchor_core.as_ref().map(|ac| {
4006 format!(
4007 "--o1 / CMF_O1 refused: the anchor is native bounded \
4008 (anchor_core kind={} window={} sink={}); the file's operator \
4009 is executed as-is and no post-hoc Nyström overlay applies",
4010 ac.kind, ac.window, ac.sink
4011 )
4012 })
4013 }
4014
4015 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
4017 if let Some(c) = &cfg {
4018 if let Some(why) = self.o1_refusal() {
4019 self.o1_flags = Vec::new();
4020 self.o1_cfg = None;
4021 return Err(why);
4022 }
4023 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
4024 self.o1_flags.clear();
4025 self.o1_cfg = None;
4026 return Err(format!(
4027 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
4028 c.w, c.sink
4029 ));
4030 }
4031 }
4032 self.o1_flags = match &cfg {
4033 Some(c) => {
4034 let mut flags = c.layer_flags(self.num_layers);
4035 for (li, f) in flags.iter_mut().enumerate() {
4036 if *f
4042 && (!matches!(
4043 self.weights.layers[self.phys_layer(li)].attn,
4044 AttnKind::Full { .. }
4045 ) || self.layer_window(li).is_some()
4046 || self.kv_cache.layers[li].sinks.is_some()
4047 || self.layer_v_dim(li) != self.layer_geom(li).1)
4048 {
4049 *f = false;
4050 }
4051 }
4052 flags
4053 }
4054 None => Vec::new(),
4055 };
4056 if let Some(c) = &cfg {
4057 let n = self.o1_flags.iter().filter(|&&f| f).count();
4058 tracing::info!(
4059 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
4060 self.num_layers,
4061 c.m,
4062 c.w,
4063 c.sink,
4064 c.rect
4065 );
4066 }
4067 self.o1_cfg = cfg;
4068 Ok(())
4069 }
4070
4071 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
4077 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
4078 return Err(format!(
4079 "anchor_core kind '{}' is not executable by this runtime",
4080 cfg.kind
4081 ));
4082 }
4083 if cfg.window == 0 {
4084 return Err("anchor_core.window must be >= 1".into());
4085 }
4086 let mut n = 0usize;
4087 for li in 0..self.num_layers {
4088 let pl = self.phys_layer(li);
4089 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
4090 if w.window != cfg.window || w.sink != cfg.sink {
4091 return Err(format!(
4092 "layer {li}: bounded weights (window {} sink {}) disagree with \
4093 anchor_core (window {} sink {})",
4094 w.window, w.sink, cfg.window, cfg.sink
4095 ));
4096 }
4097 self.kv_cache.layers[li].install_bounded(cfg.window);
4098 n += 1;
4099 }
4100 }
4101 if n == 0 {
4102 return Err("anchor_core is present but no layer executes it".into());
4103 }
4104 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
4105 self.bounded_rope = Some(std::sync::Arc::new(rope));
4106 self.anchor_core = Some(cfg.clone());
4107 self.embryo_graph = None;
4108 tracing::info!(
4109 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
4110 cfg.kind,
4111 cfg.window,
4112 cfg.sink,
4113 self.kv_cache
4114 .layers
4115 .iter()
4116 .map(|l| l.bounded_state_bytes())
4117 .max()
4118 .unwrap_or(0)
4119 );
4120 Ok(())
4121 }
4122
4123 pub fn install_wire_identity(&mut self, identity: u64) {
4127 for li in 0..self.kv_cache.layers.len() {
4128 let pl = self.phys_layer(li);
4129 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
4130 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
4131 Some(AttnKind::Linear(_))
4132 | Some(AttnKind::LinearGdn(_))
4133 | Some(AttnKind::ShortConv(_))
4134 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
4135 _ => crate::kv_cache::WireKind::Full,
4136 };
4137 let l = &mut self.kv_cache.layers[li];
4138 l.wire_kind = kind;
4139 l.wire_identity = identity;
4140 l.wire_layer = li as u32;
4143 }
4144 }
4145
4146 pub fn clear_history(&mut self) {
4148 self.kv_history.clear();
4149 self.kv_history_device = false;
4150 self.kv_prefix.clear();
4151 }
4152
4153 pub fn graph_refused(&self) -> bool {
4157 self.graph_refused
4158 .load(std::sync::atomic::Ordering::Relaxed)
4159 }
4160
4161 pub fn mark_graph_refused(&self) {
4163 if !self
4164 .graph_refused
4165 .swap(true, std::sync::atomic::Ordering::Relaxed)
4166 {
4167 tracing::info!(
4168 "token graph: unsupported for this pipeline (seq {}) — not retrying",
4169 self.graph_kv_id
4170 );
4171 }
4172 }
4173
4174 pub fn device_sequence_position(&self) -> Option<usize> {
4178 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4179 }
4180
4181 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4186 let dev = self.device_sequence_position();
4187 if recorded_on_device {
4188 dev == Some(n) && self.embryo_resident_wanted()
4189 } else {
4190 dev.is_none()
4191 }
4192 }
4193
4194 pub(crate) fn invalidate_for_weight_change(&mut self) {
4202 self.clear_sequence_state();
4203 self.embryo_graph = None;
4204 }
4205
4206 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4212 let (n, on_device) = if self.bounded_native() {
4213 (
4214 self.kv_prefix.extension(input_ids),
4215 self.kv_prefix.on_device(),
4216 )
4217 } else {
4218 let h = &self.kv_history;
4219 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4220 (h.len(), self.kv_history_device)
4221 } else if !h.is_empty() && !self.kv_history_device {
4222 (self.rollback_prefix_len(input_ids), false)
4223 } else {
4224 (0, false)
4225 }
4226 };
4227 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4230 tracing::warn!(
4231 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4232 the device now holds {:?} — re-prefilling from zero",
4233 if on_device { "resident device" } else { "host" },
4234 self.device_sequence_position()
4235 );
4236 return 0;
4237 }
4238 n
4239 }
4240
4241 fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4252 if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4253 || self.device_sequence_position().is_some()
4254 {
4255 return 0;
4256 }
4257 let h = &self.kv_history;
4258 let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4259 let n = lcp.min(input_ids.len().saturating_sub(1));
4260 if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4261 return 0;
4262 }
4263 let kv_id = self.graph_kv_id;
4264 for li in 0..self.num_layers {
4265 let l = &self.kv_cache.layers[li];
4266 let tail_ok = l.base() == 0
4267 || self
4268 .layer_window(li)
4269 .is_some_and(|w| l.base() + w <= n + 1);
4270 if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4271 || l.o1.is_some()
4272 || l.bounded.is_some()
4273 || !l.linear_state.is_empty()
4274 || l.mode != crate::kv_cache::KvMode::F32
4275 || l.pos_len() != h.len()
4276 || !tail_ok
4277 || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4278 || crate::gpu::graph_state_resident(kv_id, li)
4279 {
4280 return 0;
4281 }
4282 }
4283 n
4284 }
4285
4286 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4289 self.cached_prefix_len(input_ids)
4290 }
4291
4292 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4298 let on_device = self.device_sequence_position().is_some();
4299 if self.bounded_native() {
4300 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4301 let prev_device = self.kv_prefix.on_device();
4302 self.kv_history.clear();
4303 self.kv_history_device = false;
4304 if keep && prev_device == on_device {
4305 self.kv_prefix.extend(&consumed[reused..]);
4306 } else {
4307 self.kv_prefix.set(consumed);
4308 }
4309 self.kv_prefix.set_on_device(on_device);
4310 } else {
4311 self.kv_history = consumed.to_vec();
4312 self.kv_history_device = on_device;
4313 }
4314 }
4315
4316 pub fn o1_active(&self) -> bool {
4318 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4319 }
4320
4321 pub fn generation_batch_k(&self) -> usize {
4339 if let Some(k) = std::env::var("CMF_BATCH_K")
4340 .ok()
4341 .and_then(|v| v.parse::<usize>().ok())
4342 {
4343 return k;
4344 }
4345 if self.g4_batch_prefill() {
4348 return 128;
4349 }
4350 #[cfg(not(target_os = "macos"))]
4351 if self.graph_prefill_preferred() && !self.o1_active() {
4352 let moe = self
4356 .weights
4357 .layers
4358 .iter()
4359 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4360 let q82 = crate::gpu::moe_q82_graph_on()
4366 && self.weights.layers.iter().any(|lw| {
4367 matches!(&lw.ffn, FfnKind::Moe(m)
4368 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4369 });
4370 let q4tp_moe = self.weights.layers.iter().any(|lw| {
4377 matches!(&lw.ffn, FfnKind::Moe(m)
4378 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
4379 });
4380 return if q82 {
4381 128
4382 } else if q4tp_moe {
4383 256
4384 } else if moe {
4385 32
4386 } else {
4387 128
4388 };
4389 }
4390 0
4391 }
4392
4393 pub fn generation_graph_prefill(&self) -> bool {
4394 let graph = self.graph_prefill_preferred();
4395 #[cfg(not(target_os = "macos"))]
4406 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4407 return false;
4408 }
4409 graph
4410 }
4411
4412 pub fn o1_device_stats(&self) -> (usize, u64) {
4417 crate::gpu::o1_device_stats(self.graph_kv_id)
4418 }
4419
4420 pub fn o1_begin(&mut self) {
4425 self.o1_begin_with_prefix(None);
4426 }
4427
4428 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4432 if let Some(c) = &self.o1_cfg {
4433 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4434 let boundary = requested_prefix.map(|p| {
4435 p.max(
4436 crate::nystrom::o1_deferred_boundary(w, sink)
4437 .expect("o1 config boundary validated in set_o1"),
4438 )
4439 });
4440 for (li, &f) in self.o1_flags.iter().enumerate() {
4441 if f {
4442 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4443 }
4444 }
4445 }
4446 }
4447
4448 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4450 self.o1_cfg.as_ref().and_then(|c| {
4451 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4452 .map(|floor| requested_prefix.max(floor))
4453 })
4454 }
4455
4456 fn o1_note_transition(&mut self) {
4457 let mut transitioned = false;
4461 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4462 if flagged {
4463 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4464 }
4465 }
4466 if transitioned {
4467 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4468 }
4469 }
4470
4471 fn o1_pending(&self) -> bool {
4472 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4473 f && self.kv_cache.layers[li].seq_len > 0
4474 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4475 })
4476 }
4477
4478 fn o1_fail(&mut self, err: String) {
4479 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4480 self.clear_sequence_state();
4481 self.graph_failed
4482 .store(true, std::sync::atomic::Ordering::Relaxed);
4483 self.cancel
4484 .store(true, std::sync::atomic::Ordering::Relaxed);
4485 }
4486
4487 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4492 if self.o1_cfg.is_none() {
4493 return Ok(false);
4494 }
4495 let mut participating = false;
4496 for li in 0..self.num_layers {
4497 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4498 continue;
4499 }
4500 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4501 return Err(err);
4502 }
4503 if self.kv_cache.layers[li].seq_len == 0 {
4504 continue;
4505 }
4506 participating = true;
4507 let num_heads = self.layer_num_heads(li);
4508 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4509 }
4510 self.o1_note_transition();
4511 for li in 0..self.num_layers {
4512 if self.o1_flags.get(li).copied().unwrap_or(false) {
4513 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4514 return Err(err);
4515 }
4516 }
4517 }
4518 Ok(participating
4519 && (0..self.num_layers).all(|li| {
4520 !self.o1_flags.get(li).copied().unwrap_or(false)
4521 || self.kv_cache.layers[li].seq_len == 0
4522 || self.kv_cache.layers[li].o1_sealed()
4523 }))
4524 }
4525
4526 fn o1_progress(&mut self) {
4529 if !self.o1_active() {
4530 return;
4531 }
4532 for li in 0..self.num_layers {
4533 if self.o1_flags.get(li).copied().unwrap_or(false) {
4534 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4535 self.o1_fail(err);
4536 return;
4537 }
4538 }
4539 }
4540 self.o1_note_transition();
4544 if !self.o1_pending() {
4545 return;
4546 }
4547 if let Err(err) = self.o1_seal_checked() {
4548 self.o1_fail(err);
4549 }
4550 }
4551
4552 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4557 if self
4558 .graph_failed
4559 .swap(false, std::sync::atomic::Ordering::Relaxed)
4560 {
4561 self.cancel
4562 .store(false, std::sync::atomic::Ordering::Relaxed);
4563 self.clear_sequence_state();
4564 return Err(format!("{phase}: deferred O(1) transition failed"));
4565 }
4566 Ok(())
4567 }
4568
4569 pub fn o1_seal(&mut self) {
4573 if let Err(err) = self.o1_seal_checked() {
4574 self.o1_fail(err);
4575 }
4576 }
4577
4578 pub fn set_trace(&mut self, on: bool) {
4580 self.trace = on;
4581 }
4582
4583 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4586 self.rng = match config.seed {
4587 Some(seed) => SplitMix64::new(seed),
4588 None => SplitMix64::from_entropy(),
4589 };
4590 self.sampler_config = config;
4591 }
4592
4593 pub fn set_confidence(&mut self, on: bool) {
4598 self.confidence_on = on;
4599 }
4600
4601 pub fn set_calib_temp(&mut self, t: f32) {
4604 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4605 }
4606
4607 pub fn calib_temp(&self) -> f32 {
4609 self.calib_temp
4610 }
4611
4612 pub fn rope_inv_freq(&self) -> &[f32] {
4616 self.inv_freq.as_slice()
4617 }
4618
4619 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4622 self.rotary_dim = rotary_dim.min(self.head_dim);
4623 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4624 self.embryo_graph = None;
4628 }
4629
4630 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4631 QwenAttnCfg {
4632 num_heads: self.num_heads,
4633 num_kv_heads: self.num_kv_heads,
4634 head_dim: self.head_dim,
4635 hidden_size: self.hidden_size,
4636 position,
4637 inv_freq: &self.inv_freq,
4638 rotary_dim: self.rotary_dim,
4639 scale: self.attn_scale,
4640 softcap: self.attn_softcap,
4641 window: None,
4642 v_norm: false,
4643 qk_norm_after_rope: self.qk_norm_after_rope,
4644 gate_sigmoid: self.proj_gate_sigmoid,
4645 q_norm: None,
4646 k_norm: None,
4647 output_gate: false,
4648 softplus_gate: None,
4649 rope_scale: self.rope_scale,
4650 bias: None,
4651 rms_eps: self.rms_eps,
4652 norm_style: self.norm_style,
4653 pool: self.pool.as_deref(),
4654 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4655 }
4656 }
4657
4658 pub fn generate(
4660 &mut self,
4661 prompt: &str,
4662 max_tokens: usize,
4663 task_mask: Option<&TaskMask>,
4664 on_token: Option<TokenCallback>,
4665 ) -> Result<GenerateResult, String> {
4666 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4667 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4668 }
4669
4670 pub fn generate_from_vl(
4673 &mut self,
4674 input: &crate::dsv41_vision::PreparedVlInputs,
4675 max_tokens: usize,
4676 task_mask: Option<&TaskMask>,
4677 on_token: Option<TokenCallback>,
4678 ) -> Result<GenerateResult, String> {
4679 let Some(dsv41) = &self.dsv41 else {
4680 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4681 };
4682 if input.token_ids.is_empty() {
4683 return Err("empty V4.1 multimodal prompt".into());
4684 }
4685 if input.token_types.len() != input.token_ids.len() {
4686 return Err(format!(
4687 "V4.1 token type count {} != token count {}",
4688 input.token_types.len(),
4689 input.token_ids.len()
4690 ));
4691 }
4692 let dim = dsv41.2.dim;
4693 let mut embeddings = vec![None; input.token_ids.len()];
4694 let mut participates = vec![true; input.token_ids.len()];
4695 if !input.images.is_empty() {
4696 let vision = self
4697 .dsv41_vision
4698 .as_ref()
4699 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4700 for image in &input.images {
4701 let end = image.start.saturating_add(image.types.len());
4702 if end > input.token_ids.len() {
4703 return Err(format!(
4704 "V4.1 image span {}..{} exceeds prompt length {}",
4705 image.start,
4706 end,
4707 input.token_ids.len()
4708 ));
4709 }
4710 let mut span = vec![0.0f32; image.types.len() * dim];
4711 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4712 for (offset, &kind) in image.types.iter().enumerate() {
4713 let pos = image.start + offset;
4714 if input.token_types[pos] != kind {
4715 return Err(format!(
4716 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4717 input.token_types[pos]
4718 ));
4719 }
4720 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4721 participates[pos] = false;
4722 }
4723 }
4724 }
4725 for (pos, &kind) in input.token_types.iter().enumerate() {
4726 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4727 return Err(format!("V4.1 text position {pos} has an image embedding"));
4728 }
4729 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4730 return Err(format!("V4.1 image position {pos} has no image embedding"));
4731 }
4732 }
4733 self.dsv41_prefill = Some((embeddings, participates));
4734 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4735 self.dsv41_prefill = None;
4736 result
4737 }
4738
4739 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4741 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4742 }
4743
4744 pub fn generate_from_ids(
4752 &mut self,
4753 input_ids: &[u32],
4754 max_tokens: usize,
4755 task_mask: Option<&TaskMask>,
4756 on_token: Option<TokenCallback>,
4757 ) -> Result<GenerateResult, String> {
4758 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4759 }
4760
4761 pub fn generate_from_embeds(
4767 &mut self,
4768 input_ids: &[u32],
4769 prompt_rows: &[f32],
4770 max_tokens: usize,
4771 task_mask: Option<&TaskMask>,
4772 on_token: Option<TokenCallback>,
4773 ) -> Result<GenerateResult, String> {
4774 if input_ids.is_empty()
4775 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4776 {
4777 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4778 }
4779 if prompt_rows.iter().any(|x| !x.is_finite()) {
4780 return Err("embedded prompt contains non-finite values".into());
4781 }
4782 if !self.can_prefill_batched()
4783 || self.dyn_router.is_some()
4784 || self.o1_active()
4785 || self.mtp.is_some()
4786 || self.gpu_plan.is_some()
4787 {
4788 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4789 }
4790 self.generate_with_prompt_rows(
4791 input_ids,
4792 Some(prompt_rows),
4793 max_tokens,
4794 task_mask,
4795 on_token,
4796 )
4797 }
4798
4799 fn generate_with_prompt_rows(
4800 &mut self,
4801 input_ids: &[u32],
4802 prompt_rows: Option<&[f32]>,
4803 max_tokens: usize,
4804 task_mask: Option<&TaskMask>,
4805 mut on_token: Option<TokenCallback>,
4806 ) -> Result<GenerateResult, String> {
4807 #[cfg(target_os = "macos")]
4808 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4809 if std::env::var("CMF_TRACE_H").is_ok() {
4810 eprintln!("input_ids: {input_ids:?}");
4811 }
4812 if input_ids.is_empty() {
4813 return Err("empty prompt: nothing to generate from".to_string());
4814 }
4815 self.graph_failed
4819 .store(false, std::sync::atomic::Ordering::Relaxed);
4820 let task_mask = self.drop_open_mask(task_mask);
4825 if self.clover.is_some() {
4828 if prompt_rows.is_some() || task_mask.is_some() {
4829 return Err("block-diffusion generation takes token prompts without task masks".into());
4830 }
4831 return self.clover_generate(input_ids, max_tokens, on_token);
4832 }
4833
4834 let mut reuse_from = {
4842 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4843 if on
4844 && prompt_rows.is_none()
4845 && task_mask.is_none()
4846 && self.mtp.is_none()
4847 && !(self.mimo_mtp.is_some() && self.speculative)
4848 && self.o1_cfg.is_none()
4849 && self.dsv41.is_none()
4850 {
4851 self.cached_prefix_len(input_ids)
4852 } else {
4853 0
4854 }
4855 };
4856 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4859 reuse_from = 0;
4860 }
4861 if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
4864 for l in &mut self.kv_cache.layers {
4865 let d = l.pos_len() - reuse_from;
4866 l.truncate_last(d);
4867 }
4868 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4869 eprintln!(
4870 "kv-reuse: rolled back {} answer position(s) to the common prefix",
4871 self.kv_history.len() - reuse_from
4872 );
4873 }
4874 self.kv_history.truncate(reuse_from);
4875 self.graph_logits = None;
4876 }
4877 self.swa_trim_anchor = input_ids.len();
4878 self.last_prefill_tokens = input_ids.len() - reuse_from;
4879 let bounded_native = self.bounded_native();
4880 if reuse_from == 0 {
4881 self.reset_fresh_mtp_draft_state();
4886 self.clear_sequence_state();
4887 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4888 eprintln!(
4889 "kv-reuse: {} of {} prompt positions already cached",
4890 reuse_from,
4891 input_ids.len()
4892 );
4893 }
4894 crate::gpu::graph_race_begin_generation();
4895 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4899 std::env::var("CMF_O1_PREFILL")
4900 .ok()
4901 .and_then(|v| v.parse::<usize>().ok())
4902 .filter(|&p| p > 0)
4903 } else {
4904 None
4905 };
4906 if task_mask.is_none() {
4907 self.o1_begin_with_prefix(o1_prefill);
4908 }
4909
4910 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4916 #[cfg(target_os = "macos")]
4954 let metal_graph = crate::gpu::q1_force()
4955 && crate::gpu::enabled_here()
4956 && std::env::var("CMF_GPU_BLOCK")
4957 .map(|v| v != "0")
4958 .unwrap_or(true);
4959 #[cfg(target_os = "macos")]
4965 let native_metal_backend = crate::gpu::q1_force();
4966 #[cfg(not(target_os = "macos"))]
4967 let metal_graph = false;
4968 #[cfg(not(target_os = "macos"))]
4969 let native_metal_backend = false;
4970 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4971 let spec_cheap_round =
4975 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4976 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4977 || match spec_sample_env.as_deref() {
4978 Some("1") => true,
4979 Some(_) => false,
4980 None => metal_graph && spec_cheap_round,
4981 };
4982 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4998 for lw in &self.weights.layers {
4999 if let FfnKind::Dense(d) = &lw.ffn {
5000 dense_n += 1;
5001 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
5002 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
5003 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
5004 {
5005 dense_q4tp += 1;
5006 }
5007 }
5008 }
5009 #[cfg(feature = "gpu")]
5018 let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| self.mtp_graph_ok(m));
5019 #[cfg(not(feature = "gpu"))]
5020 let mtp_on_graph = true;
5021 let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
5022 let penalized = !metal_graph
5034 && (self.sampler_config.repetition_penalty != 1.0
5035 || self.sampler_config.presence_penalty != 0.0
5036 || !self.sampler_config.suppress_tokens.is_empty());
5037 #[cfg(feature = "gpu")]
5042 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
5043 #[cfg(not(feature = "gpu"))]
5044 let metal_wgpu = false;
5045 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
5046 let spec_wanted = graph_spec_wanted_by_policy(
5047 spec_env.as_deref(),
5048 spec_default_ok,
5049 penalized,
5050 metal_wgpu,
5051 native_metal_backend,
5052 );
5053 if spec_wanted && metal_wgpu {
5054 tracing::warn!(
5055 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
5056 verified on this backend (garbage measured on Qwen3.5-0.8B)"
5057 );
5058 }
5059 let graph_spec = self.speculative
5063 && (graph_on || metal_graph)
5064 && self.mtp.is_some()
5065 && task_mask.is_none()
5066 && !self.o1_active()
5067 && spec_sampling_ok
5068 && spec_wanted;
5069 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
5082 let pair_pays = host_pair_opt_in
5083 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
5084 #[cfg(target_os = "macos")]
5088 if metal_graph {
5089 static SAID: std::sync::Once = std::sync::Once::new();
5090 SAID.call_once(|| {
5091 let spec = if graph_spec {
5092 let k = std::env::var("CMF_GRAPH_SPEC_K")
5093 .ok()
5094 .and_then(|v| v.parse::<usize>().ok())
5095 .filter(|&v| (1..=8).contains(&v))
5096 .unwrap_or(7);
5097 let arm = if self.sampler_config.temperature < 1e-6 {
5098 "greedy"
5099 } else {
5100 "sampling"
5101 };
5102 format!(
5103 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
5104 Self::draft_vocab_rows(usize::MAX)
5105 )
5106 } else if !self.speculative {
5107 "spec off (CMF_MTP=0)".to_string()
5108 } else if self.mtp.is_none() {
5109 "spec off (no MTP head)".to_string()
5110 } else if !spec_sampling_ok {
5111 if spec_cheap_round {
5112 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
5113 } else {
5114 "spec off (sampling without a top-k: the dense chain \
5115 costs more than it saves)"
5116 .to_string()
5117 }
5118 } else if !spec_wanted {
5119 if spec_env.is_none() {
5120 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
5121 } else {
5122 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
5123 }
5124 } else if task_mask.is_some() {
5125 "spec off (task mask)".to_string()
5126 } else if !graph_spec && !pair_pays {
5127 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
5128 .to_string()
5129 } else {
5130 "spec off (O(1) attention)".to_string()
5131 };
5132 let on = |var: &str| {
5133 if std::env::var(var).as_deref() == Ok("0") {
5134 "off"
5135 } else {
5136 "on"
5137 }
5138 };
5139 tracing::info!(
5140 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
5141 MTP graph {}, attend {}, probe {}",
5142 if crate::gpu_metal::state4_on() {
5143 "on"
5144 } else {
5145 "off"
5146 },
5147 if crate::gpu_metal::async_replay_on() {
5148 "on"
5149 } else {
5150 "off"
5151 },
5152 on("CMF_METAL_PREFILL"),
5153 on("CMF_MTP_GRAPH"),
5154 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
5155 if crate::gpu::probe_enabled() {
5156 "bypassed (q1 force)"
5157 } else {
5158 "off"
5159 },
5160 );
5161 });
5162 }
5163 let spec_active = self.speculative
5164 && self.mtp.is_some()
5165 && task_mask.is_none()
5166 && !self.o1_active()
5167 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
5168 let mut mtp = if spec_active { self.mtp.take() } else { None };
5171 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
5172 eprintln!(
5173 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
5174 mtp.is_some(),
5175 self.speculative,
5176 self.sampler_config.temperature < 1e-6,
5177 );
5178 }
5179 if let Some(m) = &mut mtp {
5180 m.kv.clear();
5181 crate::gpu::graph_kv_reset(self.mtp_kv_id());
5183 self.mtp_graph_mode = None;
5184 }
5185 let mimo_spec = self.speculative
5189 && self.mimo_mtp.is_some()
5190 && task_mask.is_none()
5191 && !self.o1_active()
5192 && self.dyn_router.is_none()
5193 && self.sampler_config.temperature < 1e-6
5194 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5195 if let Some(st) = self.mimo_mtp.as_mut() {
5196 st.reset();
5197 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5198 Self::mimo_mtp_hist_cap(st, input_ids.len());
5199 }
5200 }
5201 let mut router = if mtp.is_none() {
5205 self.dyn_router.take()
5206 } else {
5207 None
5208 };
5209 let mut reuse_from = reuse_from;
5210 if let Some(r) = &mut router {
5211 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
5214 let _ = self.set_active_skill(None);
5217 reuse_from = 0;
5218 self.last_prefill_tokens = input_ids.len();
5219 }
5220 }
5221
5222 let mut all_ids = input_ids.to_vec();
5223 let mut generated = 0usize;
5224 let mut finish_reason = "max_tokens".to_string();
5225 let mut drafted = 0usize;
5226 let mut accepted = 0usize;
5227 let mut dsv4_spec_bad = 0usize;
5234 let mut dsv4_spec_retry_at = 0usize;
5235 let mut confidence: Vec<f32> = Vec::new();
5236 let trace_on = self.trace;
5237 let calib_temp = self.calib_temp;
5238 let mut traces: Vec<TokenTrace> = Vec::new();
5239
5240 let mut hidden = vec![0.0f32; self.hidden_size];
5246 let mut pos = reuse_from;
5247 let fuse_lm = mtp.is_none()
5256 && router.is_none()
5257 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5258 self.graph_logits = None;
5259 self.graph_want_logits = false;
5260 let _tpf = std::time::Instant::now();
5261 let batch_k = self.generation_batch_k();
5262 if let Some(rows) = prompt_rows {
5263 let hs = self.hidden_size;
5264 let chunk = self.prefill_chunk().max(1);
5265 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5266 let end = (pos + chunk).min(input_ids.len());
5267 let hb = match self.prefill_input_rows(
5268 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5269 pos,
5270 task_mask,
5271 ) {
5272 Ok(hb) => hb,
5273 Err(err) => {
5274 self.finish_generation(&mut mtp, &mut router, true);
5275 return Err(err);
5276 }
5277 };
5278 if mimo_spec {
5279 self.mimo_note_rows(&hb, pos);
5280 }
5281 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5282 pos = end;
5283 }
5284 }
5285 while self.qwen4_exp.is_some()
5296 && mtp.is_none()
5297 && pos < input_ids.len()
5298 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5299 {
5300 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5303 let want_logits = end == input_ids.len();
5304 let mut lg = Vec::new();
5305 if let Some(b) = &mut self.qwen4_exp {
5306 crate::qwen4_exp::forward_tokens(
5307 &b.0,
5308 &b.1,
5309 &b.2,
5310 &mut b.3,
5311 &input_ids[pos..end],
5312 pos,
5313 &self.inv_freq,
5314 self.pool.as_deref(),
5315 &mut lg,
5316 want_logits,
5317 );
5318 }
5319 if want_logits {
5320 self.graph_logits = Some(lg);
5321 }
5322 pos = end;
5323 hidden.fill(0.0);
5324 }
5325 while self.dsv4.is_some()
5326 && mtp.is_none()
5327 && pos < input_ids.len()
5328 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5329 {
5330 let end = (pos + prefill_chunk()).min(input_ids.len());
5331 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5332 let mut lg = Vec::new();
5333 if let Some(b) = &mut self.dsv4 {
5334 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5335 crate::dsv4::forward_chunk(
5336 g,
5337 layers,
5338 &cfg,
5339 st,
5340 &ids,
5341 pos,
5342 &self.inv_freq,
5343 self.pool.as_deref(),
5344 &mut lg,
5345 end == input_ids.len(),
5346 );
5347 }
5348 if end == input_ids.len() {
5349 self.graph_logits = Some(lg);
5350 }
5351 pos = end;
5352 hidden = vec![0.0; self.hidden_size];
5353 }
5354 let dsv41_prefill = self.dsv41_prefill.take();
5355 while self.dsv41.is_some()
5356 && mtp.is_none()
5357 && pos < input_ids.len()
5358 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5359 {
5360 let end = (pos + prefill_chunk()).min(input_ids.len());
5361 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5362 let mut lg = Vec::new();
5363 if let Some(b) = &mut self.dsv41 {
5364 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5365 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5366 crate::dsv41::forward_chunk_masked_with_embeddings(
5367 g,
5368 layers,
5369 cfg,
5370 st,
5371 &ids,
5372 pos,
5373 &embeddings[pos..end],
5374 &participates[pos..end],
5375 self.pool.as_deref(),
5376 &mut lg,
5377 );
5378 } else {
5379 crate::dsv41::forward_chunk(
5380 g,
5381 layers,
5382 cfg,
5383 st,
5384 &ids,
5385 pos,
5386 self.pool.as_deref(),
5387 &mut lg,
5388 );
5389 }
5390 }
5391 if end == input_ids.len() {
5392 self.graph_logits = Some(lg);
5393 }
5394 pos = end;
5395 hidden = vec![0.0; self.hidden_size];
5396 }
5397 let dyn_prefill = router.is_some();
5402 let o1_prefill_limit = o1_prefill
5410 .and_then(|requested| self.o1_effective_boundary(requested))
5411 .map(|boundary| boundary.min(input_ids.len()));
5412 let mut o1_sealed = false;
5413 if let Some(limit) = o1_prefill_limit {
5414 if self.can_prefill_batched() && limit > 2 {
5417 let chunk = self.prefill_chunk();
5418 let hs = self.hidden_size;
5419 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5420 let end = (pos + chunk).min(limit);
5421 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5422 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5423 pos = end;
5424 }
5425 } else {
5426 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5427 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5428 pos += 1;
5429 }
5430 }
5431 if pos >= limit {
5432 o1_sealed = match self.o1_seal_checked() {
5433 Ok(sealed) => sealed,
5434 Err(err) => {
5435 self.finish_generation(&mut mtp, &mut router, true);
5436 return Err(err);
5437 }
5438 };
5439 tracing::info!(
5440 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5441 o1_prefill.unwrap_or(0),
5442 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5443 .unwrap_or(limit),
5444 limit,
5445 input_ids.len()
5446 );
5447 }
5448 }
5449 let graph_prefill = self.graph_prefill_preferred();
5455 #[cfg(target_os = "macos")]
5463 if task_mask.is_none()
5464 && !dyn_prefill
5465 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5466 && crate::gpu::enabled_here()
5467 && self.gdn_cfg.is_some()
5468 && !self.per_token_stack()
5469 && input_ids.len() > 8
5470 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5471 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5472 {
5473 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5474 .ok()
5475 .and_then(|v| v.parse().ok())
5476 .filter(|&v| (16..=512).contains(&v))
5477 .unwrap_or(256);
5478 let hs = self.hidden_size;
5479 let _tp = std::time::Instant::now();
5480 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5481 let end = (pos + chunk).min(input_ids.len());
5482 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5483 MetalPrefillOutcome::Completed(hb) => hb,
5484 MetalPrefillOutcome::Declined => break,
5485 MetalPrefillOutcome::Failed => {
5486 self.finish_generation(&mut mtp, &mut router, true);
5487 return Err("ordinary Metal prefill failed after admission".into());
5488 }
5489 };
5490 if let Some(m) = &mut mtp {
5491 let n_pairs = if end < input_ids.len() {
5492 end - pos
5493 } else {
5494 end - pos - 1
5495 };
5496 if n_pairs > 0 {
5497 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5498 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5499 .collect();
5500 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5501 for (j, (h, t)) in pairs.iter().enumerate() {
5502 let h = h.to_vec();
5503 let _ = self.mtp_step(m, &h, *t, pos + j);
5504 }
5505 }
5506 }
5507 }
5508 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5509 pos = end;
5510 }
5511 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5512 eprintln!(
5513 "metal-prefill: {} of {} tokens in {:.1} ms",
5514 pos,
5515 input_ids.len(),
5516 _tp.elapsed().as_secs_f64() * 1e3
5517 );
5518 }
5519 }
5520 #[cfg(not(target_os = "macos"))]
5525 if task_mask.is_none()
5526 && !dyn_prefill
5527 && mtp.is_none()
5528 && o1_prefill.is_none()
5529 && batch_k > 0
5530 && pos + 1 < input_ids.len()
5531 && self.g4_batch_prefill()
5532 {
5533 let t_g4 = std::time::Instant::now();
5534 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5535 Ok((n, last)) => {
5536 if n > 0 {
5537 hidden.copy_from_slice(&last);
5538 }
5539 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5540 eprintln!(
5541 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5542 input_ids.len() - pos,
5543 t_g4.elapsed().as_secs_f64() * 1e3
5544 );
5545 }
5546 pos += n;
5547 }
5548 Err(e) => {
5549 self.finish_generation(&mut mtp, &mut router, true);
5550 return Err(e);
5551 }
5552 }
5553 }
5554 self.mimo_moe_prepare();
5555 #[cfg(not(target_os = "macos"))]
5561 if task_mask.is_none()
5562 && !dyn_prefill
5563 && !graph_prefill
5564 && mtp.is_none()
5565 && o1_prefill.is_none()
5566 && !self.o1_active()
5567 && input_ids.len() > 2
5568 && self.batch_prefix_prefill()
5569 {
5570 let chunk = self.prefill_chunk().max(1);
5571 let hs = self.hidden_size;
5572 let t_bp = std::time::Instant::now();
5573 let pos0 = pos;
5574 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5575 let end = (pos + chunk).min(input_ids.len());
5576 let bk = end - pos;
5577 let mut hiddens = vec![0f32; bk * hs];
5578 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5579 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5580 }
5581 let positions: Vec<usize> = (pos..end).collect();
5582 let mut run = 0usize;
5583 let outcome = self.try_batch_graph_wgpu_prefix(
5584 &mut hiddens,
5585 &positions,
5586 bk,
5587 None,
5588 Some(&mut run),
5589 );
5590 match outcome {
5591 crate::gpu::BatchGraphOutcome::Completed => {
5592 let hb = if run < self.num_layers {
5593 self.prefill_batch_span(
5594 PrefillIn::Hidden(&hiddens),
5595 pos,
5596 None,
5597 run,
5598 self.num_layers,
5599 )
5600 } else {
5601 hiddens
5602 };
5603 if mimo_spec {
5604 self.mimo_note_rows(&hb, pos);
5605 }
5606 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5607 pos = end;
5608 }
5609 crate::gpu::BatchGraphOutcome::Failed => {
5610 self.finish_generation(&mut mtp, &mut router, true);
5611 return Err("batched prefix prefill failed after admission".into());
5612 }
5613 crate::gpu::BatchGraphOutcome::Declined => {
5614 #[cfg(feature = "gpu")]
5617 if pos > pos0 {
5618 self.pull_lagging_host_kv(0, self.num_layers, pos);
5619 }
5620 break;
5621 }
5622 }
5623 }
5624 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5625 eprintln!(
5626 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5627 pos - pos0,
5628 input_ids.len(),
5629 t_bp.elapsed().as_secs_f64() * 1e3
5630 );
5631 }
5632 }
5633 if task_mask.is_none()
5634 && !dyn_prefill
5635 && !graph_prefill
5636 && self.can_prefill_batched()
5637 && !self.per_token_stack()
5638 && o1_prefill.is_none()
5639 && input_ids.len() > 2
5640 {
5641 let chunk = self.prefill_chunk();
5647 let hs = self.hidden_size;
5648 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5649 let end = (pos + chunk).min(input_ids.len());
5650 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5651 if mimo_spec {
5652 self.mimo_note_rows(&hb, pos);
5653 }
5654 if let Some(m) = &mut mtp {
5655 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5656 .ok()
5657 .and_then(|v| v.parse().ok())
5658 .unwrap_or(0);
5659 for p in pos..end {
5660 if p + 1 < input_ids.len() {
5661 if probe >= 1 && p + 2 < input_ids.len() {
5662 let (d1, mut hx) = self.mtp_step_h(
5666 m,
5667 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5668 input_ids[p + 1],
5669 p,
5670 );
5671 let mut ok = d1 == input_ids[p + 2];
5672 Self::chain_probe_note(0, ok);
5673 let mut d_prev = d1;
5674 let mut extra = 0usize;
5675 for j in 1..probe {
5676 if p + 2 + j >= input_ids.len() {
5677 break;
5678 }
5679 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5680 extra += 1;
5681 ok = ok && dj == input_ids[p + 2 + j];
5682 Self::chain_probe_note(j, ok);
5683 d_prev = dj;
5684 hx = hj;
5685 }
5686 m.kv.truncate_last(extra);
5687 } else {
5688 let _ = self.mtp_step(
5689 m,
5690 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5691 input_ids[p + 1],
5692 p,
5693 );
5694 }
5695 }
5696 }
5697 }
5698 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5699 pos = end;
5700 }
5701 }
5702 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5703 if task_mask.is_none()
5704 && !dyn_prefill
5705 && !graph_prefill
5706 && !pair_off
5707 && self.pair_supported()
5708 && o1_prefill.is_none()
5709 {
5710 while pos + 1 < input_ids.len()
5711 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5712 {
5713 let e1 = self.embed_single(input_ids[pos]);
5714 let e2 = self.embed_single(input_ids[pos + 1]);
5715 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5716 if mimo_spec {
5717 self.mimo_note_rows(&h1, pos);
5718 self.mimo_note_rows(&h2, pos + 1);
5719 }
5720 self.commit_linear_scratch();
5722 if let Some(m) = &mut mtp {
5723 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5724 if pos + 2 < input_ids.len() {
5725 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5726 .ok()
5727 .and_then(|v| v.parse().ok())
5728 .unwrap_or(0);
5729 if probe >= 1 && pos + 3 < input_ids.len() {
5730 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5734 let mut ok = d1 == input_ids[pos + 3];
5735 Self::chain_probe_note(0, ok);
5736 let mut d_prev = d1;
5737 let mut extra = 0usize;
5738 for j in 1..probe {
5739 if pos + 3 + j >= input_ids.len() {
5740 break;
5741 }
5742 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5743 extra += 1;
5744 ok = ok && dj == input_ids[pos + 3 + j];
5745 Self::chain_probe_note(j, ok);
5746 d_prev = dj;
5747 hx = hj;
5748 }
5749 m.kv.truncate_last(extra);
5750 } else {
5751 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5752 }
5753 }
5754 }
5755 hidden = h2;
5756 pos += 2;
5757 }
5758 }
5759 let o1_batch_ready = o1_sealed
5772 && o1_prefill.is_some()
5773 && mtp.is_none()
5774 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5775 && (0..self.num_layers).all(|li| {
5776 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5777 cache.o1.is_none() || cache.o1_views().is_some()
5778 });
5779 let mtp_batch_prefill = mtp.is_some()
5784 && graph_prefill
5785 && task_mask.is_none()
5786 && !dyn_prefill
5787 && !self.o1_active()
5788 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5789 if batch_k > 0
5790 && (graph_prefill || o1_batch_ready)
5791 && task_mask.is_none()
5792 && (!self.o1_active() || o1_batch_ready)
5793 && (mtp.is_none() || mtp_batch_prefill)
5794 && !dyn_prefill
5795 && pos + 1 < input_ids.len()
5796 {
5797 let hs = self.hidden_size;
5798 let chunk = batch_k;
5799 while pos < input_ids.len() {
5800 let end = (pos + chunk).min(input_ids.len());
5801 let bk = end - pos;
5802 let mut hiddens = vec![0f32; bk * hs];
5803 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5804 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5805 }
5806 let positions: Vec<usize> = (pos..end).collect();
5807 let t_chunk = std::time::Instant::now();
5808 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5809 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5810 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5811 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5812 eprintln!(
5813 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5814 if o1_batch_ready {
5815 "o1"
5816 } else if mtp_batch_prefill {
5817 "ordinary_mtp"
5818 } else {
5819 "ordinary"
5820 },
5821 bk as f64 / (ms / 1000.0)
5822 );
5823 }
5824 {
5825 use std::sync::atomic::{AtomicBool, Ordering};
5826 static SAID: AtomicBool = AtomicBool::new(false);
5827 if !SAID.swap(true, Ordering::Relaxed) {
5828 if ok_b {
5829 tracing::info!(
5830 "batched prefill: ACTIVE mode={} (k={bk})",
5831 if o1_batch_ready {
5832 "o1"
5833 } else if mtp_batch_prefill {
5834 "ordinary_mtp"
5835 } else {
5836 "ordinary"
5837 }
5838 );
5839 } else {
5840 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5841 }
5842 }
5843 }
5844 if ok_b {
5845 if mimo_spec {
5846 self.mimo_note_rows(&hiddens, pos);
5847 }
5848 if mtp_batch_prefill {
5849 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5850 if n_pairs > 0 {
5851 let rows: Vec<Vec<f32>> = (0..n_pairs)
5857 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5858 .collect();
5859 let pairs: Vec<(&[f32], u32)> = rows
5860 .iter()
5861 .enumerate()
5862 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5863 .collect();
5864 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5865 eprintln!(
5866 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5867 pos,
5868 n_pairs,
5869 pos + n_pairs - 1,
5870 );
5871 }
5872 let warm_error = if let Some(m) = mtp.as_mut() {
5873 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5874 } else {
5875 None
5876 };
5877 if let Some(err) = warm_error {
5878 self.finish_generation(&mut mtp, &mut router, true);
5883 return Err(err.to_string());
5884 }
5885 }
5886 }
5887 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5888 pos = end;
5889 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5890 self.finish_generation(&mut mtp, &mut router, true);
5895 return Err(if o1_batch_ready {
5896 "sealed O(1) batch graph failed after admission".to_string()
5897 } else {
5898 "ordinary recurrent batch graph failed after admission".to_string()
5899 });
5900 } else {
5901 break; }
5903 }
5904 }
5905 if graph_prefill
5909 && task_mask.is_none()
5910 && mtp.is_none()
5911 && !dyn_prefill
5912 && pos == 0
5913 && input_ids.len() > 1
5914 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5915 {
5916 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5917 self.graph_logits = Some(lg);
5918 hidden = vec![0.0; self.hidden_size];
5919 pos = input_ids.len();
5920 }
5921 }
5922 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5923 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5924 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5925 if mimo_spec {
5926 self.mimo_note_rows(&hidden, pos);
5927 }
5928 if let Some(m) = &mut mtp {
5929 if pos + 1 < input_ids.len() {
5930 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5936 .ok()
5937 .and_then(|v| v.parse().ok())
5938 .unwrap_or(0);
5939 if probe >= 1 && pos + 2 < input_ids.len() {
5940 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5941 let mut ok = d1 == input_ids[pos + 2];
5942 Self::chain_probe_note(0, ok);
5943 let mut d_prev = d1;
5944 let mut extra = 0usize;
5945 for j in 1..probe {
5946 if pos + 2 + j >= input_ids.len() {
5947 break;
5948 }
5949 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5950 extra += 1;
5951 ok = ok && dj == input_ids[pos + 2 + j];
5952 Self::chain_probe_note(j, ok);
5953 d_prev = dj;
5954 hx = hj;
5955 }
5956 m.kv.truncate_last(extra);
5959 } else {
5960 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5961 }
5962 }
5963 }
5964 pos += 1;
5965 }
5966 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5967 eprintln!(
5968 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5969 input_ids.len(),
5970 _tpf.elapsed().as_secs_f64() * 1000.0
5971 );
5972 }
5973 if self
5974 .graph_failed
5975 .swap(false, std::sync::atomic::Ordering::Relaxed)
5976 {
5977 self.finish_generation(&mut mtp, &mut router, true);
5982 return Err("GPU token graph failed during prefill".to_string());
5983 }
5984 if self
5987 .cancel
5988 .swap(false, std::sync::atomic::Ordering::Relaxed)
5989 {
5990 self.finish_generation(&mut mtp, &mut router, true);
5994 return Ok(GenerateResult {
5995 text: String::new(),
5996 token_ids: Vec::new(),
5997 prompt_tokens: input_ids.len(),
5998 tokens_generated: 0,
5999 finish_reason: "cancelled".to_string(),
6000 mtp_drafted: 0,
6001 mtp_accepted: 0,
6002 token_confidence: Vec::new(),
6003 traces: Vec::new(),
6004 });
6005 }
6006
6007 if !o1_sealed {
6010 match self.o1_seal_checked() {
6011 Ok(_) => {}
6012 Err(err) => {
6013 self.finish_generation(&mut mtp, &mut router, true);
6014 return Err(err);
6015 }
6016 }
6017 }
6018
6019 macro_rules! commit {
6021 ($id:expr) => {{
6022 all_ids.push($id);
6023 generated += 1;
6024 self.note_draft_id($id);
6025 if self.tokenizer.is_eos($id) && !self.ignore_eos {
6026 finish_reason = "stop".to_string();
6027 false
6028 } else {
6029 let token_text = self.tokenizer.decode_token($id);
6030 let mut go = true;
6031 if let Some(ref mut cb) = on_token {
6032 if !cb(&token_text) {
6033 finish_reason = "cancelled".to_string();
6034 go = false;
6035 }
6036 }
6037 go
6038 }
6039 }};
6040 }
6041
6042 let mut spec_trial = SpecTrial::Spec {
6053 t0: std::time::Instant::now(),
6054 gen0: generated,
6055 rounds: 0,
6056 };
6057 let mut spec_mon = SpecMon {
6063 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
6064 ..SpecMon::default()
6065 };
6066 let mut spec_watchdog_off = false;
6067 let mut spec_walls: Vec<f32> = Vec::new();
6070 let mut spec_round_end: Option<std::time::Instant> = None;
6073 if mimo_spec {
6074 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
6075 if let Some(mut st) = self.mimo_mtp.take() {
6076 self.mimo_mtp_probe(&mut st, input_ids, &path);
6077 self.mimo_mtp = Some(st);
6078 }
6079 }
6080 }
6081 let mut next_pos = input_ids.len();
6083 'decode: while generated < max_tokens {
6084 if self
6085 .graph_failed
6086 .swap(false, std::sync::atomic::Ordering::Relaxed)
6087 {
6088 self.finish_generation(&mut mtp, &mut router, true);
6093 return Err("GPU token graph failed during decode".to_string());
6094 }
6095 if self
6096 .cancel
6097 .swap(false, std::sync::atomic::Ordering::Relaxed)
6098 {
6099 finish_reason = "cancelled".to_string();
6100 break 'decode;
6101 }
6102 if mimo_spec && next_pos > 0 {
6107 self.mimo_note_rows(&hidden, next_pos - 1);
6110 }
6111 let forced = self.spec_forced.take();
6112 let mut logits = match (forced, self.graph_logits.take()) {
6113 (Some(_), _) => Vec::new(),
6114 (None, Some(lg)) => lg,
6115 (None, None) => {
6116 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
6117 #[cfg(feature = "gpu")]
6122 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
6123 lg
6124 } else {
6125 inference::rms_norm_into(
6126 &hidden,
6127 &self.weights.final_norm,
6128 self.rms_eps,
6129 self.norm_style,
6130 &mut self.ws.n1,
6131 );
6132 self.lm_head_forward(&self.ws.n1)
6133 };
6134 #[cfg(not(feature = "gpu"))]
6135 let logits_out = {
6136 inference::rms_norm_into(
6137 &hidden,
6138 &self.weights.final_norm,
6139 self.rms_eps,
6140 self.norm_style,
6141 &mut self.ws.n1,
6142 );
6143 self.lm_head_forward(&self.ws.n1)
6144 };
6145 logits_out
6146 }
6147 };
6148 if generated
6151 == std::env::var("CMF_LOGIT_DUMP_STEP")
6152 .ok()
6153 .and_then(|v| v.parse().ok())
6154 .unwrap_or(0)
6155 {
6156 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
6157 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
6158 for v in hidden.iter().chain(logits.iter()) {
6159 bytes.extend_from_slice(&v.to_le_bytes());
6160 }
6161 if let Err(e) = std::fs::write(&path, &bytes) {
6162 eprintln!("logit dump: failed to write {path}: {e}");
6163 self.finish_generation(&mut mtp, &mut router, true);
6164 return Err(format!("logit dump write failed: {e}"));
6165 }
6166 }
6167 }
6168 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
6172 if !logits.is_empty() {
6173 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
6174 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
6175 if let Err(e) =
6176 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6177 {
6178 eprintln!("logit dump: failed to write {}: {e}", path.display());
6179 }
6180 }
6181 }
6182 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6183 Some(c) => c,
6184 None => {
6185 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6186 sampler::sample_with_scratch_pool(
6187 &logits,
6188 &self.sampler_config,
6189 self.sampler_config.penalty_past(&all_ids, bounded_native),
6190 &mut self.rng,
6191 &mut self.sampler_scratch,
6192 self.pool.as_deref(),
6193 )
6194 }
6195 };
6196 if self.confidence_on {
6197 confidence.push(if logits.is_empty() {
6198 0.0
6199 } else {
6200 sampler::top1_prob_pool(
6201 self.pool.as_deref(),
6202 &mut self.sampler_scratch,
6203 &logits,
6204 t_next,
6205 calib_temp,
6206 )
6207 });
6208 }
6209 if !logits.is_empty() {
6210 attention::recycle_buf(&mut logits);
6211 }
6212 if trace_on {
6213 let skill = router.as_ref().and_then(|r| r.active_id());
6217 traces.push(TokenTrace {
6218 t: generated,
6219 token_id: t_next,
6220 confidence: confidence.last().copied().unwrap_or(0.0),
6221 active_skill: skill,
6222 recon: None,
6223 switched: false,
6224 });
6225 }
6226 if !commit!(t_next) {
6227 break 'decode;
6228 }
6229 if generated >= max_tokens {
6230 break 'decode;
6231 }
6232
6233 self.swa_trim_tails();
6236 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6237 static SAID: std::sync::Once = std::sync::Once::new();
6243 SAID.call_once(|| {
6244 tracing::warn!(
6245 "KV cache full at {} positions — evicting half; quality \
6246 will degrade. Raise CMF_MAX_SEQ.",
6247 self.kv_cache.max_seq_len,
6248 );
6249 });
6250 let keep = (self.kv_cache.max_seq_len / 2).max(1);
6251 self.kv_cache.evict(keep);
6252 }
6253
6254 if graph_spec {
6257 match spec_trial {
6258 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6259 spec_mon.plain_ms =
6260 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6261 let keep = spec_mon.pays();
6262 tracing::info!(
6263 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6264 spec_mon.tokens,
6265 spec_mon.round_ms,
6266 spec_mon.plain_ms,
6267 if keep { "speculating" } else { "plain" }
6268 );
6269 spec_mon.fails = 0;
6270 spec_trial = SpecTrial::Decided {
6271 spec: keep,
6272 recheck_at: if keep { usize::MAX } else { generated + 128 },
6273 };
6274 }
6275 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6276 spec_mon.n = 0;
6277 spec_mon.zero_accept_rounds = 0;
6278 spec_trial = SpecTrial::Spec {
6279 t0: std::time::Instant::now(),
6280 gen0: generated,
6281 rounds: 0,
6282 };
6283 }
6284 _ => {}
6285 }
6286 spec_watchdog_off = matches!(
6287 spec_trial,
6288 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6289 );
6290 }
6291 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6293 let budget = max_tokens - generated - 1;
6294 if let Some(mut st) = self.mimo_mtp.take() {
6295 let k = st.depth.min(budget);
6296 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6297 self.mimo_mtp = Some(st);
6298 let r = match r {
6299 Ok(r) => r,
6300 Err(err) => {
6301 self.finish_generation(&mut mtp, &mut router, true);
6302 return Err(err);
6303 }
6304 };
6305 if let Some(r) = r {
6306 drafted += r.drafted;
6307 accepted += r.accepted.len();
6308 let mut stopped = false;
6309 for &id in &r.accepted {
6310 if self.confidence_on {
6311 confidence.push(0.0);
6312 }
6313 if !commit!(id) {
6314 stopped = true;
6315 break;
6316 }
6317 }
6318 if stopped {
6319 break 'decode;
6320 }
6321 next_pos += r.accepted.len() + 1;
6322 hidden = r.hidden;
6323 self.graph_logits = Some(r.logits);
6326 continue 'decode;
6327 }
6328 }
6329 }
6330 #[cfg(feature = "gpu")]
6333 if self.speculative
6334 && self.qwen4_exp.is_some()
6335 && task_mask.is_none()
6336 && self.sampler_config.temperature < 1e-6
6337 && generated + 1 < max_tokens
6338 && next_pos > 0
6339 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6340 {
6341 let r = match &mut self.qwen4_exp {
6342 Some(b) => crate::qwen4_exp::spec_round(
6343 &b.0,
6344 &b.1,
6345 &b.2,
6346 &mut b.3,
6347 next_pos,
6348 &all_ids,
6349 &self.inv_freq,
6350 self.pool.as_deref(),
6351 ),
6352 None => None,
6353 };
6354 if let Some(r) = r {
6355 drafted += r.drafted;
6356 accepted += r.accepted.len();
6357 let mut stopped = false;
6358 for &id in &r.accepted {
6359 if self.confidence_on {
6360 confidence.push(0.0);
6361 }
6362 if !commit!(id) {
6363 stopped = true;
6364 break;
6365 }
6366 }
6367 if stopped {
6368 break 'decode;
6369 }
6370 next_pos += r.accepted.len() + 1;
6371 hidden.fill(0.0);
6372 self.graph_logits = Some(r.logits);
6373 continue 'decode;
6374 }
6375 }
6376 match &mut mtp {
6377 #[cfg(feature = "gpu")]
6379 Some(m)
6380 if graph_spec
6381 && !spec_watchdog_off
6382 && generated + 1 < max_tokens
6383 && next_pos > 0 =>
6384 {
6385 let t_round = std::time::Instant::now();
6386 if spec_time_level() >= 2 {
6387 if let Some(t) = spec_round_end.take() {
6388 eprintln!(
6389 "spec-gap {:.2} ms (host between rounds)",
6390 t.elapsed().as_secs_f64() * 1e3
6391 );
6392 }
6393 }
6394 spec_stamps_begin();
6395 #[cfg(target_os = "macos")]
6400 let allocs0 =
6401 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6402 #[cfg(not(target_os = "macos"))]
6403 let allocs0 = 0u64;
6404 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6405 m,
6406 &hidden,
6407 t_next,
6408 next_pos,
6409 &mut drafted,
6410 &mut accepted,
6411 &mut all_ids,
6412 max_tokens - generated,
6413 ) {
6414 next_pos = n_pos;
6415 hidden = new_h;
6416 let level = spec_time_level();
6417 if level > 0 {
6418 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6419 let stamps = spec_stamps_take();
6420 let median = if spec_walls.len() >= 3 {
6423 let mut s = spec_walls.clone();
6424 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6425 Some(s[s.len() / 2])
6426 } else {
6427 None
6428 };
6429 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6430 #[cfg(target_os = "macos")]
6431 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6432 .load(std::sync::atomic::Ordering::Relaxed)
6433 - allocs0;
6434 #[cfg(not(target_os = "macos"))]
6435 let allocs = allocs0;
6436 eprintln!(
6437 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6438 extra.len() + 1,
6439 if allocs > 0 {
6440 format!(" [{allocs} new device buffers]")
6441 } else {
6442 String::new()
6443 },
6444 match (outlier, median) {
6445 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6446 _ => String::new(),
6447 }
6448 );
6449 if level >= 2 || outlier {
6450 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6451 eprintln!(
6452 "spec-stamps: {}| untracked {:.1}",
6453 spec_stamps_format(&stamps),
6454 wall - sum
6455 );
6456 }
6457 if spec_mon.n >= 1 {
6458 spec_walls.push(wall);
6459 }
6460 }
6461 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6465 spec_trial = Self::spec_trial_round(
6468 spec_trial,
6469 &mut spec_mon,
6470 generated + extra.len() + 1,
6471 );
6472 let mut stopped = false;
6473 for &id in &extra {
6474 if self.confidence_on {
6475 confidence.push(0.0);
6476 }
6477 if trace_on {
6490 traces.push(TokenTrace {
6491 t: generated,
6492 token_id: id,
6493 confidence: confidence.last().copied().unwrap_or(0.0),
6494 active_skill: None,
6495 recon: None,
6496 switched: false,
6497 });
6498 }
6499 if !commit!(id) {
6500 stopped = true;
6501 break;
6502 }
6503 }
6504 if stopped {
6505 break 'decode;
6506 }
6507 if spec_time_level() >= 2 {
6508 spec_round_end = Some(std::time::Instant::now());
6509 }
6510 continue 'decode;
6511 }
6512 if self
6513 .graph_failed
6514 .swap(false, std::sync::atomic::Ordering::Relaxed)
6515 {
6516 self.finish_generation(&mut mtp, &mut router, true);
6522 return Err("GPU MTP graph failed during speculative decode".to_string());
6523 }
6524 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6535 spec_mon.tokens = 0.0;
6536 spec_mon.fails = 3;
6537 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6538 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6539 next_pos += 1;
6540 continue 'decode;
6541 }
6542 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6544 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6545 drafted += 1;
6546 let emb1 = self.embed_single(t_next);
6547 let emb2 = self.embed_single(draft);
6548 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6549
6550 inference::rms_norm_into(
6551 &h1,
6552 &self.weights.final_norm,
6553 self.rms_eps,
6554 self.norm_style,
6555 &mut self.ws.n1,
6556 );
6557 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6558 let t_after = sampler::sample_with_scratch_pool(
6559 &logits1,
6560 &self.sampler_config,
6561 self.sampler_config.penalty_past(&all_ids, bounded_native),
6562 &mut self.rng,
6563 &mut self.sampler_scratch,
6564 self.pool.as_deref(),
6565 );
6566 if self.confidence_on {
6567 confidence.push(sampler::top1_prob_pool(
6568 self.pool.as_deref(),
6569 &mut self.sampler_scratch,
6570 &logits1,
6571 t_after,
6572 calib_temp,
6573 ));
6574 }
6575 attention::recycle_buf(&mut logits1);
6576 if trace_on {
6577 traces.push(TokenTrace {
6580 t: generated,
6581 token_id: t_after,
6582 confidence: confidence.last().copied().unwrap_or(0.0),
6583 active_skill: None,
6584 recon: None,
6585 switched: false,
6586 });
6587 }
6588 let stop = !commit!(t_after);
6589
6590 if t_after == draft {
6591 accepted += 1;
6592 self.commit_linear_scratch();
6593 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6594 hidden = h2;
6595 next_pos += 2;
6596 } else {
6597 for layer in &mut self.kv_cache.layers {
6599 layer.truncate_last(1);
6600 }
6601 if !stop {
6602 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6603 hidden = self.forward_layers(
6604 &self.embed_single(t_after),
6605 next_pos + 1,
6606 None,
6607 );
6608 }
6609 next_pos += 2;
6610 }
6611 if stop {
6612 break 'decode;
6613 }
6614 }
6615 _ => {
6617 #[cfg(feature = "gpu")]
6622 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6623 static SAID: std::sync::Once = std::sync::Once::new();
6624 SAID.call_once(|| {
6625 eprintln!(
6626 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6627 !self.dsv4_mtp.is_empty(),
6628 task_mask.is_none(),
6629 router.is_none(),
6630 !trace_on,
6631 self.sampler_config.temperature < 1e-6,
6632 self.sampler_config.repetition_penalty == 1.0,
6633 );
6634 });
6635 }
6636 #[cfg(feature = "gpu")]
6637 if Self::dsv4_spec_on()
6638 && self.dsv4.is_some()
6639 && !self.dsv4_mtp.is_empty()
6640 && task_mask.is_none()
6641 && router.is_none()
6642 && !trace_on
6643 && self.sampler_config.temperature < 1e-6
6644 && self.sampler_config.repetition_penalty == 1.0
6645 && generated + 1 < max_tokens
6646 && all_ids.len() >= 2
6647 && generated >= dsv4_spec_retry_at
6648 {
6649 let tip_token = all_ids[all_ids.len() - 2];
6650 let drafted0 = drafted;
6651 let round = self.dsv4_spec_step(
6652 tip_token,
6653 t_next,
6654 next_pos,
6655 max_tokens.saturating_sub(generated),
6656 &mut drafted,
6657 &mut accepted,
6658 );
6659 if drafted > drafted0 {
6660 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6661 if useful {
6662 dsv4_spec_bad = 0;
6663 } else {
6664 dsv4_spec_bad += 1;
6665 if dsv4_spec_bad >= 2 {
6666 dsv4_spec_bad = 0;
6667 dsv4_spec_retry_at = generated.saturating_add(32);
6668 tracing::info!(
6669 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6670 );
6671 }
6672 }
6673 }
6674 if let Some((extra, n_pos)) = round {
6675 next_pos = n_pos;
6676 let mut stopped = false;
6677 for &id in &extra {
6678 if self.confidence_on {
6679 confidence.push(0.0);
6680 }
6681 if !commit!(id) {
6682 stopped = true;
6683 break;
6684 }
6685 }
6686 if stopped {
6687 break 'decode;
6688 }
6689 continue 'decode;
6690 }
6691 }
6692 self.graph_want_logits = fuse_lm;
6693 let mut t_fwd = t_next;
6699 let pure_greedy = self.sampler_config.temperature < 1e-6
6700 && self.sampler_config.repetition_penalty == 1.0
6701 && self.sampler_config.presence_penalty == 0.0
6702 && self.sampler_config.suppress_tokens.is_empty();
6703 let burst_k = std::env::var("CMF_MULTISTEP")
6708 .ok()
6709 .and_then(|v| v.parse::<usize>().ok())
6710 .unwrap_or(0);
6711 if pure_greedy
6712 && burst_k >= 1
6713 && fuse_lm
6714 && task_mask.is_none()
6715 && router.is_none()
6716 && !trace_on
6717 && !self.confidence_on
6718 && std::env::var_os("CMF_FORCE_IDS").is_none()
6723 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6724 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6725 {
6726 let mut stopped = false;
6727 loop {
6728 if self
6733 .cancel
6734 .swap(false, std::sync::atomic::Ordering::Relaxed)
6735 {
6736 finish_reason = "cancelled".to_string();
6737 stopped = true;
6738 break;
6739 }
6740 let room = max_tokens.saturating_sub(generated);
6741 if room <= 2 {
6742 break;
6743 }
6744 let k = burst_k.min(room - 1);
6745 if k < 1 {
6746 break;
6747 }
6748 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6749 if self
6750 .graph_failed
6751 .swap(false, std::sync::atomic::Ordering::Relaxed)
6752 {
6753 self.finish_generation(&mut mtp, &mut router, true);
6754 return Err(
6755 "GPU token graph failed during greedy burst".to_string()
6756 );
6757 }
6758 break;
6759 };
6760 next_pos += k;
6761 for &id in &ids {
6762 if !commit!(id) {
6763 stopped = true;
6764 break;
6765 }
6766 }
6767 if stopped {
6768 break;
6769 }
6770 t_fwd = *ids.last().unwrap();
6771 }
6772 if stopped {
6773 break 'decode;
6774 }
6775 }
6776 #[cfg(target_os = "macos")]
6786 if graph_spec
6787 && spec_watchdog_off
6788 && next_pos > 0
6789 && self.mtp_graph_mode == Some(true)
6790 && crate::gpu::q1_force()
6791 {
6792 if let Some(m) = mtp.as_mut() {
6793 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6794 }
6795 }
6796 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6797 next_pos += 1;
6798 if let Some(r) = &mut router {
6801 let phi = self.dyn_phi_ema.clone();
6802 let decision = r.step(&phi, generated);
6803 if let Some(new_active) = decision {
6804 let _ = self.set_active_skill(new_active);
6805 }
6806 if trace_on {
6809 if let Some(last) = traces.last_mut() {
6810 let e = r.last_best_e();
6811 last.recon = e.is_finite().then_some(e);
6812 last.switched = decision.is_some();
6813 }
6814 }
6815 }
6816 }
6817 }
6818 }
6819
6820 let cancelled = finish_reason == "cancelled";
6821 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6825 if mimo_spec {
6826 if let Some(st) = self.mimo_mtp.as_ref() {
6827 let line = st.stats.line();
6828 tracing::info!("{line}");
6829 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6830 eprintln!("{line}");
6831 }
6832 }
6833 }
6834 self.finish_generation(&mut mtp, &mut router, cancelled);
6835
6836 let output_ids = &all_ids[input_ids.len()..];
6837 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6841 let consumed = std::mem::take(&mut all_ids);
6846 if dyn_switched {
6847 self.clear_sequence_state();
6848 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6849 self.clear_history();
6850 } else {
6851 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6852 }
6853 all_ids = consumed;
6854 let output_ids = &all_ids[input_ids.len()..];
6855 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6857 Ok(GenerateResult {
6858 text: self.tokenizer.decode(output_ids),
6859 token_ids: output_ids.to_vec(),
6860 prompt_tokens: input_ids.len(),
6861 tokens_generated: generated,
6862 finish_reason,
6863 mtp_drafted: drafted,
6864 mtp_accepted: accepted,
6865 token_confidence: confidence,
6866 traces,
6867 })
6868 }
6869
6870 fn mtp_step(
6874 &mut self,
6875 m: &mut MtpModule,
6876 hidden: &[f32],
6877 next_token: u32,
6878 position: usize,
6879 ) -> u32 {
6880 self.mtp_step_h(m, hidden, next_token, position).0
6881 }
6882
6883 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6887 use std::sync::Mutex;
6888 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6889 let mut t = T.lock().unwrap();
6890 if t.len() <= depth {
6891 t.resize(depth + 1, (0, 0));
6892 }
6893 t[depth].0 += 1;
6894 t[depth].1 += prefix_ok as u64;
6895 if depth == 0 && t[0].0 % 128 == 0 {
6896 let line: Vec<String> = t
6897 .iter()
6898 .enumerate()
6899 .map(|(d, (n, k))| {
6900 format!(
6901 "d{}={:.0}%({n})",
6902 d + 1,
6903 100.0 * *k as f64 / (*n).max(1) as f64
6904 )
6905 })
6906 .collect();
6907 eprintln!("mtp-chain: {}", line.join(" "));
6908 }
6909 }
6910
6911 fn mtp_step_hl(
6919 &mut self,
6920 m: &mut MtpModule,
6921 hidden: &[f32],
6922 next_token: u32,
6923 position: usize,
6924 ) -> (Vec<f32>, Vec<f32>) {
6925 #[cfg(target_os = "macos")]
6930 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6931 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6932 self.mtp_graph_mode = Some(true);
6933 return r;
6934 }
6935 if self.mtp_graph_mode == Some(true) {
6936 tracing::error!("mtp Metal graph failed after admission");
6937 self.clear_sequence_state();
6938 self.graph_failed
6939 .store(true, std::sync::atomic::Ordering::Relaxed);
6940 self.cancel
6941 .store(true, std::sync::atomic::Ordering::Relaxed);
6942 return (Vec::new(), Vec::new());
6943 }
6944 self.mtp_graph_mode = Some(false);
6945 }
6946 #[cfg(feature = "gpu")]
6947 if self.mtp_graph_mode != Some(false) {
6948 if !self.mtp_graph_ok(m) {
6949 if self.mtp_graph_mode == Some(true) {
6950 tracing::error!("mtp graph became unavailable after admission");
6955 self.clear_sequence_state();
6956 self.graph_failed
6957 .store(true, std::sync::atomic::Ordering::Relaxed);
6958 self.cancel
6959 .store(true, std::sync::atomic::Ordering::Relaxed);
6960 return (Vec::new(), Vec::new());
6961 }
6962 self.mtp_graph_mode = Some(false);
6963 } else {
6964 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6965 self.mtp_graph_mode = Some(true);
6966 return r;
6967 }
6968 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6969 return (Vec::new(), Vec::new());
6976 }
6977 tracing::error!("mtp graph failed or declined after admission");
6981 self.clear_sequence_state();
6982 self.graph_failed
6983 .store(true, std::sync::atomic::Ordering::Relaxed);
6984 self.cancel
6985 .store(true, std::sync::atomic::Ordering::Relaxed);
6986 return (Vec::new(), Vec::new());
6987 }
6988 }
6989 let e = self.embed_single(next_token);
6993 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6994 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6995 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6996 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6997 let mut x = vec![0.0f32; self.hidden_size];
6998 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6999
7000 let lw = &m.layer;
7002 inference::rms_norm_into(
7003 &x,
7004 &lw.input_norm,
7005 self.rms_eps,
7006 self.norm_style,
7007 &mut self.ws.n1,
7008 );
7009 let attn = match &lw.attn {
7010 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
7012 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
7013 AttnKind::Full {
7014 wq,
7015 wk,
7016 wv,
7017 wo,
7018 q_norm,
7019 k_norm,
7020 output_gate,
7021 softplus_gate,
7022 bias,
7023 } => {
7024 let mut cfg = self.attn_cfg(position);
7025 cfg.q_norm = q_norm.as_deref();
7026 cfg.k_norm = k_norm.as_deref();
7027 cfg.output_gate = *output_gate;
7028 cfg.softplus_gate = softplus_gate
7029 .as_ref()
7030 .map(|(gate, per_head)| (gate, *per_head));
7031 cfg.bias = bias
7032 .as_ref()
7033 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7034 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7035 }
7036 AttnKind::Linear(_)
7037 | AttnKind::LinearGdn(_)
7038 | AttnKind::ShortConv(_)
7039 | AttnKind::Bounded(_) => {
7040 unreachable!("MTP block is full attention")
7041 }
7042 };
7043 for (i, &a) in attn.iter().enumerate() {
7044 x[i] += a;
7045 }
7046 inference::rms_norm_into(
7047 &x,
7048 &lw.post_norm,
7049 self.rms_eps,
7050 self.norm_style,
7051 &mut self.ws.p1,
7052 );
7053 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
7054 for (i, &f) in ffn.iter().enumerate() {
7055 x[i] += f;
7056 }
7057
7058 inference::rms_norm_into(
7059 &x,
7060 &m.final_norm,
7061 self.rms_eps,
7062 self.norm_style,
7063 &mut self.ws.n1,
7064 );
7065 let lg = self.lm_head_forward(&self.ws.n1);
7066 (lg, x)
7067 }
7068
7069 fn mtp_step_h(
7071 &mut self,
7072 m: &mut MtpModule,
7073 hidden: &[f32],
7074 next_token: u32,
7075 position: usize,
7076 ) -> (u32, Vec<f32>) {
7077 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
7078 let draft = sampler::argmax(&lg);
7079 attention::recycle_buf(&mut lg);
7080 (draft, x)
7081 }
7082
7083 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
7089 match trial {
7090 SpecTrial::Spec { t0, gen0, rounds } => {
7091 let rounds = rounds + 1;
7092 if mon.repeated_zero_acceptance() {
7100 tracing::info!(
7101 "speculation trial: no accepted drafts in two rounds — timing plain"
7102 );
7103 return SpecTrial::Plain {
7104 t0: std::time::Instant::now(),
7105 gen0: generated,
7106 };
7107 }
7108 if rounds >= 5 {
7109 if mon.plain_ms > 0.0 {
7110 let keep = mon.pays();
7111 mon.fails = 0;
7112 tracing::info!(
7113 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
7114 mon.tokens,
7115 mon.round_ms,
7116 mon.plain_ms,
7117 if keep { "speculating" } else { "plain" }
7118 );
7119 SpecTrial::Decided {
7120 spec: keep,
7121 recheck_at: if keep { usize::MAX } else { generated + 128 },
7122 }
7123 } else if mon.pays() {
7124 mon.fails = 0;
7129 tracing::info!(
7130 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
7131 mon.tokens,
7132 mon.round_ms,
7133 );
7134 SpecTrial::Decided {
7135 spec: true,
7136 recheck_at: usize::MAX,
7137 }
7138 } else {
7139 SpecTrial::Plain {
7140 t0: std::time::Instant::now(),
7141 gen0: generated,
7142 }
7143 }
7144 } else {
7145 SpecTrial::Spec { t0, gen0, rounds }
7146 }
7147 }
7148 SpecTrial::Decided { spec: true, .. } => {
7149 if mon.pays() {
7150 mon.fails = 0;
7151 trial
7152 } else {
7153 mon.fails += 1;
7154 if mon.fails >= 4 {
7155 if mon.plain_ms <= 0.0 {
7156 tracing::info!(
7160 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
7161 mon.tokens,
7162 mon.round_ms,
7163 );
7164 return SpecTrial::Plain {
7165 t0: std::time::Instant::now(),
7166 gen0: generated,
7167 };
7168 }
7169 tracing::info!(
7170 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
7171 mon.tokens,
7172 mon.round_ms,
7173 mon.plain_ms
7174 );
7175 SpecTrial::Decided {
7176 spec: false,
7177 recheck_at: generated + 128,
7178 }
7179 } else {
7180 trial
7181 }
7182 }
7183 }
7184 other => other,
7185 }
7186 }
7187
7188 fn mtp_kv_id(&self) -> u64 {
7191 self.graph_kv_id | (1u64 << 40)
7192 }
7193
7194 const MTP_LAYER_BASE: usize = 0;
7199
7200 #[cfg(feature = "gpu")]
7207 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7208 self.mtp_graph_mode != Some(true)
7209 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7210 }
7211
7212 #[cfg(feature = "gpu")]
7218 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7219 let mut ok = true;
7220 let mut expected = false;
7221 for li in 0..self.num_layers {
7222 if matches!(
7223 self.weights.layers[self.phys_layer(li)].attn,
7224 AttnKind::Full { .. }
7225 ) {
7226 expected = true;
7227 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7228 }
7229 }
7230 !expected || ok
7231 }
7232
7233 fn graph_gdn_layer_count(&self) -> usize {
7237 (0..self.num_layers)
7238 .filter(|&li| {
7239 matches!(
7240 &self.weights.layers[self.phys_layer(li)].attn,
7241 AttnKind::LinearGdn(_)
7242 )
7243 })
7244 .count()
7245 }
7246
7247 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7250 let e = self.embed_single(next_token);
7251 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7252 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7253 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7254 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7255 let mut x = vec![0.0f32; self.hidden_size];
7256 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7257 x
7258 }
7259
7260 #[cfg(feature = "gpu")]
7263 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7264 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7265 return false;
7266 }
7267 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7268 || !crate::gpu::enabled_here()
7269 || self.attn_softcap > 0.0
7270 || self.attention_heads_per_layer.is_some()
7271 || self.v_head_dim.is_some()
7274 {
7275 return false;
7276 }
7277 matches!(
7278 &m.layer.attn,
7279 AttnKind::Full {
7280 softplus_gate: None,
7281 ..
7282 }
7283 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7284 }
7285
7286 #[cfg(feature = "gpu")]
7290 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7291 if !self.mtp_block_graph_ok(m) {
7292 return false;
7293 }
7294 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7295 return false;
7296 };
7297 let FfnKind::Dense(d) = &m.layer.ffn else {
7298 return false;
7299 };
7300 d.segs.is_empty()
7301 && wq.graph_weight().is_some()
7302 && wk.graph_weight().is_some()
7303 && wv.graph_weight().is_some()
7304 && wo.graph_weight().is_some()
7305 && d.gate_proj.graph_weight().is_some()
7306 && d.up_proj.graph_weight().is_some()
7307 && d.down_proj.graph_weight().is_some()
7308 && self.weights.lm_head.graph_weight().is_some()
7309 }
7310
7311 #[cfg(feature = "gpu")]
7317 fn mtp_step_graph(
7318 &mut self,
7319 m: &mut MtpModule,
7320 hidden: &[f32],
7321 next_token: u32,
7322 position: usize,
7323 ) -> Option<(Vec<f32>, Vec<f32>)> {
7324 if !self.mtp_graph_ok(m) {
7325 return None;
7326 }
7327 let lw = &m.layer;
7328 let AttnKind::Full {
7329 wq,
7330 wk,
7331 wv,
7332 wo,
7333 q_norm,
7334 k_norm,
7335 output_gate,
7336 softplus_gate,
7337 bias,
7338 } = &lw.attn
7339 else {
7340 return None;
7341 };
7342 if softplus_gate.is_some() {
7343 return None;
7344 }
7345 let FfnKind::Dense(d) = &lw.ffn else {
7346 return None;
7347 };
7348 if !d.segs.is_empty() {
7349 return None; }
7351 let mut x = self.mtp_block_input(m, hidden, next_token);
7354 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7355 let (_, i, kind, rs) = t.graph_weight()?;
7356 Some(crate::gpu::GraphW {
7357 idx: i,
7358 kind,
7359 row_scale: rs,
7360 data: &[],
7361 prism: crate::gpu::GraphPrismOp::None,
7362 affine: false,
7363 })
7364 }
7365 let (model, _, _, _) = wq.graph_weight()?;
7366 let model = model.clone();
7367 let (lm_gw, lm_rows) = {
7368 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7369 let rows = if kind == 6 {
7373 self.draft_head_rows(self.weights.lm_head.rows())
7374 } else {
7375 self.weights.lm_head.rows()
7376 };
7377 (
7378 crate::gpu::GraphW {
7379 idx: i,
7380 kind,
7381 row_scale: rs,
7382 data: &[],
7383 prism: crate::gpu::GraphPrismOp::None,
7384 affine: false,
7385 },
7386 rows,
7387 )
7388 };
7389 let layer = crate::gpu::GraphLayer {
7390 input_norm: &lw.input_norm,
7391 attn: crate::gpu::GraphAttn::Full {
7392 wq: gw(wq)?,
7393 wk: gw(wk)?,
7394 wv: gw(wv)?,
7395 wo: gw(wo)?,
7396 q_norm: q_norm.as_deref(),
7397 k_norm: k_norm.as_deref(),
7398 late_qk_norm: self.qk_norm_after_rope,
7399 bias: bias
7400 .as_ref()
7401 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7402 output_gate: *output_gate,
7403 cpu_k: m.kv.k_heads(),
7404 cpu_v: m.kv.v_heads(),
7405 cpu_base: m.kv.base(),
7406 geom: None,
7407 head_gate: None,
7408 },
7409 post_norm: &lw.post_norm,
7410 attn_out_norm: None,
7411 ffn_out_norm: None,
7412 layer_scale: None,
7413 ffn: crate::gpu::GraphFfn::Dense {
7414 gate: gw(&d.gate_proj)?,
7415 up: gw(&d.up_proj)?,
7416 down: gw(&d.down_proj)?,
7417 act: d.act.graph_act()?,
7418 },
7419 };
7420 let nh = self.num_heads;
7421 let (nkv, hd, rd) = self.layer_geom(0);
7422 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7423 let mut logits = Vec::new();
7424 let ok = crate::gpu::forward_token_graph(
7425 &model,
7426 self.mtp_kv_id(),
7427 std::slice::from_ref(&layer),
7428 &[None],
7429 self.o1_epoch,
7430 &self.inv_freq,
7431 &mut x,
7432 nh,
7433 nkv,
7434 hd,
7435 self.attn_scale,
7436 rd,
7437 self.hidden_size,
7438 d.gate_proj.rows(),
7442 position,
7443 self.kv_cache.max_seq_len,
7444 gemma,
7445 self.rms_eps as f32,
7446 Some((&lm_gw, lm_rows)),
7447 &m.final_norm,
7448 &mut logits,
7449 &[],
7450 1,
7451 None,
7452 None,
7453 None,
7454 Self::MTP_LAYER_BASE,
7455 true,
7456 );
7457 match ok {
7458 crate::gpu::TokenGraphOutcome::Completed => {}
7459 crate::gpu::TokenGraphOutcome::Declined => return None,
7460 crate::gpu::TokenGraphOutcome::Failed => {
7461 self.clear_sequence_state();
7465 self.graph_failed
7466 .store(true, std::sync::atomic::Ordering::Relaxed);
7467 self.cancel
7468 .store(true, std::sync::atomic::Ordering::Relaxed);
7469 return None;
7470 }
7471 }
7472 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7476 Some((logits, x))
7477 }
7478
7479 #[cfg(feature = "gpu")]
7487 fn mtp_warm_graph(
7488 &mut self,
7489 m: &mut MtpModule,
7490 pairs: &[(&[f32], u32)],
7491 first_pos: usize,
7492 ) -> crate::gpu::BatchGraphOutcome {
7493 if pairs.is_empty() {
7494 return crate::gpu::BatchGraphOutcome::Completed;
7495 }
7496 if !self.mtp_block_graph_ok(m) {
7497 return crate::gpu::BatchGraphOutcome::Declined;
7498 }
7499 let hs = self.hidden_size;
7500 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7503 for (h, t) in pairs {
7504 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7505 }
7506 let lw = &m.layer;
7507 let AttnKind::Full {
7508 wq,
7509 wk,
7510 wv,
7511 wo,
7512 q_norm,
7513 k_norm,
7514 output_gate,
7515 bias,
7516 ..
7517 } = &lw.attn
7518 else {
7519 return crate::gpu::BatchGraphOutcome::Declined;
7520 };
7521 let FfnKind::Dense(d) = &lw.ffn else {
7522 return crate::gpu::BatchGraphOutcome::Declined;
7523 };
7524 if !d.segs.is_empty() {
7525 return crate::gpu::BatchGraphOutcome::Declined; }
7527 let (
7530 AttnKind::Full {
7531 softplus_gate: None,
7532 ..
7533 },
7534 Some(gact),
7535 ) = (&lw.attn, d.act.graph_act())
7536 else {
7537 return crate::gpu::BatchGraphOutcome::Declined;
7538 };
7539 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7540 let (_, i, kind, rs) = t.graph_weight()?;
7541 Some(crate::gpu::GraphW {
7542 idx: i,
7543 kind,
7544 row_scale: rs,
7545 data: &[],
7546 prism: crate::gpu::GraphPrismOp::None,
7547 affine: false,
7548 })
7549 }
7550 let Some((model, _, _, _)) = wq.graph_weight() else {
7551 return crate::gpu::BatchGraphOutcome::Declined;
7552 };
7553 let model = model.clone();
7554 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7555 gw(wq),
7556 gw(wk),
7557 gw(wv),
7558 gw(wo),
7559 gw(&d.gate_proj),
7560 gw(&d.up_proj),
7561 gw(&d.down_proj),
7562 ) else {
7563 return crate::gpu::BatchGraphOutcome::Declined;
7564 };
7565 let layer = crate::gpu::GraphLayer {
7566 input_norm: &lw.input_norm,
7567 attn: crate::gpu::GraphAttn::Full {
7568 wq: gwq,
7569 wk: gwk,
7570 wv: gwv,
7571 wo: gwo,
7572 q_norm: q_norm.as_deref(),
7573 k_norm: k_norm.as_deref(),
7574 late_qk_norm: self.qk_norm_after_rope,
7575 bias: bias
7576 .as_ref()
7577 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7578 output_gate: *output_gate,
7579 cpu_k: m.kv.k_heads(),
7580 cpu_v: m.kv.v_heads(),
7581 cpu_base: m.kv.base(),
7582 geom: None,
7583 head_gate: None,
7584 },
7585 post_norm: &lw.post_norm,
7586 attn_out_norm: None,
7587 ffn_out_norm: None,
7588 layer_scale: None,
7589 ffn: crate::gpu::GraphFfn::Dense {
7590 gate: gg,
7591 up: gu,
7592 down: gd,
7593 act: gact,
7594 },
7595 };
7596 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7597 let nh = self.num_heads;
7598 let (nkv, hd, rd) = self.layer_geom(0);
7599 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7600 crate::gpu::forward_batch_graph(
7601 &model,
7602 self.mtp_kv_id(),
7603 std::slice::from_ref(&layer),
7604 &self.inv_freq,
7605 &mut hiddens,
7606 nh,
7607 nkv,
7608 hd,
7609 rd,
7610 hs,
7611 d.gate_proj.rows(),
7614 &positions,
7615 self.kv_cache.max_seq_len,
7616 gemma,
7617 self.rms_eps as f32,
7618 self.attn_scale,
7619 pairs.len(),
7620 &[],
7621 0,
7622 None,
7623 None,
7624 )
7625 }
7626
7627 #[cfg(feature = "gpu")]
7634 fn mtp_warm_graph_fallback(
7635 &mut self,
7636 m: &mut MtpModule,
7637 pairs: &[(&[f32], u32)],
7638 first_pos: usize,
7639 ) -> bool {
7640 if pairs.is_empty() {
7641 return true;
7642 }
7643 let graphable = self.mtp_block_graph_ok(m);
7644 if !graphable {
7645 if self.mtp_graph_mode == Some(true) {
7649 return false;
7650 }
7651 self.mtp_graph_mode = Some(false);
7652 for (j, (h, t)) in pairs.iter().enumerate() {
7653 self.mtp_warm(m, h, *t, first_pos + j);
7654 }
7655 return true;
7656 }
7657
7658 for (j, (h, t)) in pairs.iter().enumerate() {
7663 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7664 return false;
7665 }
7666 }
7667 self.mtp_graph_mode = Some(true);
7668 true
7669 }
7670
7671 #[cfg(feature = "gpu")]
7676 fn mtp_warm_prefill_pairs(
7677 &mut self,
7678 m: &mut MtpModule,
7679 pairs: &[(&[f32], u32)],
7680 first_pos: usize,
7681 ) -> Result<(), &'static str> {
7682 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7687 if self.mtp_graph_mode == Some(true) {
7688 return Err("MTP token graph became unavailable after admission");
7689 }
7690 self.mtp_graph_mode = Some(false);
7691 for (j, (h, t)) in pairs.iter().enumerate() {
7692 self.mtp_warm(m, h, *t, first_pos + j);
7693 }
7694 return Ok(());
7695 }
7696 match self.mtp_warm_graph(m, pairs, first_pos) {
7697 crate::gpu::BatchGraphOutcome::Completed => {
7698 if !pairs.is_empty() {
7699 self.mtp_graph_mode = Some(true);
7700 }
7701 Ok(())
7702 }
7703 crate::gpu::BatchGraphOutcome::Declined => {
7704 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7705 Ok(())
7706 } else {
7707 Err("MTP warm-up fallback failed after device admission")
7708 }
7709 }
7710 crate::gpu::BatchGraphOutcome::Failed => {
7711 Err("MTP warm batch graph failed after admission")
7712 }
7713 }
7714 }
7715
7716 #[cfg(not(feature = "gpu"))]
7717 fn mtp_warm_prefill_pairs(
7718 &mut self,
7719 m: &mut MtpModule,
7720 pairs: &[(&[f32], u32)],
7721 first_pos: usize,
7722 ) -> Result<(), &'static str> {
7723 for (j, (h, t)) in pairs.iter().enumerate() {
7724 self.mtp_warm(m, h, *t, first_pos + j);
7725 }
7726 Ok(())
7727 }
7728
7729 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7733 let e = self.embed_single(next_token);
7734 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7735 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7736 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7737 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7738 let mut x = vec![0.0f32; self.hidden_size];
7739 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7740 inference::rms_norm_into(
7741 &x,
7742 &m.layer.input_norm,
7743 self.rms_eps,
7744 self.norm_style,
7745 &mut self.ws.n1,
7746 );
7747 let attn = match &m.layer.attn {
7748 AttnKind::Full {
7749 wq,
7750 wk,
7751 wv,
7752 wo,
7753 q_norm,
7754 k_norm,
7755 output_gate,
7756 softplus_gate,
7757 bias,
7758 } => {
7759 let mut cfg = self.attn_cfg(position);
7760 cfg.q_norm = q_norm.as_deref();
7761 cfg.k_norm = k_norm.as_deref();
7762 cfg.output_gate = *output_gate;
7763 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7764 cfg.bias = bias
7765 .as_ref()
7766 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7767 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7768 }
7769 _ => return,
7770 };
7771 let _ = attn;
7772 }
7773
7774 #[cfg(feature = "gpu")]
7781 #[allow(clippy::too_many_arguments)]
7782 fn graph_spec_step(
7783 &mut self,
7784 m: &mut MtpModule,
7785 hidden: &[f32],
7786 t_next: u32,
7787 next_pos: usize,
7788 drafted: &mut usize,
7789 accepted: &mut usize,
7790 all_ids: &mut Vec<u32>,
7794 room: usize,
7799 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7800 #[cfg(target_os = "macos")]
7811 let metal_native = crate::gpu::q1_force();
7812 #[cfg(not(target_os = "macos"))]
7813 let metal_native = false;
7814 #[cfg(feature = "gpu")]
7815 let k_default = if metal_native {
7816 7
7819 } else if crate::gpu_wgpu::verify_i8_on() {
7820 5
7821 } else {
7822 4
7823 };
7824 #[cfg(not(feature = "gpu"))]
7825 let k_default = 4;
7826 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7827 .ok()
7828 .and_then(|v| v.parse().ok())
7829 .filter(|&v| (1..=8).contains(&v));
7830 let (k_start, k_max) = if metal_native {
7835 (7, 7)
7836 } else {
7837 (3, k_default.max(5))
7838 };
7839 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7840 let k_spec = k_full.min(room).max(1);
7841 let k_capped = k_spec < k_full;
7844 if next_pos == 0 {
7845 return None;
7846 }
7847 let t_round = std::time::Instant::now();
7848 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7864 let sub0 = subs();
7865 let cfg = self.sampler_config.clone();
7870 let penalized = !(cfg.repetition_penalty == 1.0
7871 && cfg.presence_penalty == 0.0
7872 && cfg.suppress_tokens.is_empty());
7873 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7878 let sampling = cfg.temperature >= 1e-6;
7879 let sparse = sampling && sampler::sparse_ok(&cfg);
7885 let base_len = all_ids.len();
7886 if sampling && !sparse && self.spec_q.len() < k_spec {
7887 self.spec_q.resize_with(k_spec, Vec::new);
7888 }
7889 if sparse && self.spec_qs.len() < k_spec {
7890 self.spec_qs.resize_with(k_spec, Vec::new);
7891 }
7892 let mut drafts = Vec::with_capacity(k_spec);
7897 let mut hx = hidden.to_vec();
7898 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7901 spec_stamp("pro");
7902 #[cfg(target_os = "macos")]
7908 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7909 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7910 Ok(ids) => {
7911 self.mtp_graph_mode = Some(true);
7912 drafts = ids;
7913 }
7914 Err(true) => {
7915 tracing::error!("mtp Metal draft chain failed after commit");
7916 self.clear_sequence_state();
7917 self.graph_failed
7918 .store(true, std::sync::atomic::Ordering::Relaxed);
7919 self.cancel
7920 .store(true, std::sync::atomic::Ordering::Relaxed);
7921 return None;
7922 }
7923 Err(false) => {}
7924 }
7925 }
7926 for j in drafts.len()..k_spec {
7927 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7928 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7929 if spec_dbg {
7930 let saved = self.mtp_graph_mode;
7931 self.mtp_graph_mode = Some(false);
7932 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7933 self.mtp_graph_mode = saved;
7934 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7935 return None;
7936 }
7937 m.kv.truncate_last(1);
7938 dbg_ref = Some(r);
7939 }
7940 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7941 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7942 return None;
7943 }
7944 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7945 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7946 let dl = lg
7947 .iter()
7948 .zip(&lg_cpu)
7949 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7950 let dh = hj
7951 .iter()
7952 .zip(&h_cpu)
7953 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7954 eprintln!(
7955 "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 {}",
7956 next_pos - 1 + j,
7957 sampler::argmax(&lg_cpu),
7958 sampler::argmax(&lg),
7959 n(&h_cpu),
7960 n(&hj),
7961 m.kv.seq_len
7962 );
7963 }
7964 let dj = if sparse {
7965 let mut q = std::mem::take(&mut self.spec_qs[j]);
7966 let ok = sampler::sparse_distribution_into(
7967 &lg,
7968 &cfg,
7969 all_ids,
7970 &mut self.sampler_scratch,
7971 self.pool.as_deref(),
7972 &mut q,
7973 );
7974 let d = if ok {
7975 sampler::draw_sparse(&q, &mut self.rng)
7976 } else {
7977 let t = sampler::argmax(&lg);
7979 q.clear();
7980 q.push((t, 1.0));
7981 t
7982 };
7983 self.spec_qs[j] = q;
7984 all_ids.push(d);
7985 d
7986 } else if sampling {
7987 let mut q = std::mem::take(&mut self.spec_q[j]);
7988 sampler::distribution_into(
7989 &lg,
7990 &cfg,
7991 all_ids,
7992 &mut self.sampler_scratch,
7993 self.pool.as_deref(),
7994 &mut q,
7995 );
7996 let d = sampler::draw(&q, &mut self.rng);
7997 self.spec_q[j] = q;
7998 all_ids.push(d); d
8000 } else if greedy_pen {
8001 let d = sampler::argmax_penalized(
8002 &lg,
8003 &cfg,
8004 all_ids,
8005 &mut self.sampler_scratch,
8006 self.pool.as_deref(),
8007 );
8008 all_ids.push(d);
8009 d
8010 } else {
8011 sampler::argmax(&lg)
8012 };
8013 attention::recycle_buf(&mut lg);
8014 drafts.push(dj);
8015 hx = hj;
8016 spec_stamp("d.pick");
8017 }
8018 all_ids.truncate(base_len);
8019 *drafted += k_spec;
8020 let t_draft = t_round.elapsed();
8021 let sub_draft = subs();
8022 let b = k_spec + 1;
8025 let mut hiddens = vec![0.0f32; b * self.hidden_size];
8026 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
8027 let e = self.embed_single(t);
8028 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
8029 }
8030 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
8031 spec_stamp("v.emb");
8032 let (lm_gw, lm_rows) = {
8033 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
8034 (
8035 crate::gpu::GraphW {
8036 idx: i,
8037 kind,
8038 row_scale: rs,
8039 data: &[],
8040 prism: crate::gpu::GraphPrismOp::None,
8041 affine: false,
8042 },
8043 self.weights.lm_head.rows(),
8044 )
8045 };
8046 let mut logits = Vec::new();
8047 let final_norm = self.weights.final_norm.clone();
8048 #[cfg(target_os = "macos")]
8057 let greedy_dev = metal_native
8058 && !sampling
8059 && !greedy_pen
8060 && !self.confidence_on
8061 && self.final_softcap.is_none()
8062 && self.vocab_size == lm_rows
8068 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
8069 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
8070 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
8071 #[cfg(not(target_os = "macos"))]
8072 let greedy_dev = false;
8073 let mut dev_ids: Vec<u32> = Vec::new();
8074 #[cfg(target_os = "macos")]
8075 let verify_outcome = if metal_native {
8076 let lm = self.weights.lm_head.q1_parts()?;
8077 let n_score = self.vocab_size.min(lm_rows);
8078 self.try_batch_graph_metal(
8079 &mut hiddens,
8080 &positions,
8081 b,
8082 Some((lm, &final_norm, &mut logits)),
8083 if greedy_dev {
8084 Some((n_score, &mut dev_ids))
8085 } else {
8086 None
8087 },
8088 )
8089 } else {
8090 self.try_batch_graph_wgpu(
8091 &mut hiddens,
8092 &positions,
8093 b,
8094 Some(crate::gpu::SpecTail {
8095 lm: lm_gw,
8096 lm_rows,
8097 final_norm: &final_norm,
8098 logits_out: &mut logits,
8099 pick: None,
8100 }),
8101 )
8102 };
8103 #[cfg(not(target_os = "macos"))]
8104 let verify_outcome = self.try_batch_graph_wgpu(
8105 &mut hiddens,
8106 &positions,
8107 b,
8108 Some(crate::gpu::SpecTail {
8109 lm: lm_gw,
8110 lm_rows,
8111 final_norm: &final_norm,
8112 logits_out: &mut logits,
8113 pick: None,
8114 }),
8115 );
8116 match verify_outcome {
8117 crate::gpu::BatchGraphOutcome::Completed => {}
8118 crate::gpu::BatchGraphOutcome::Declined => {
8119 m.kv.truncate_last(k_spec);
8123 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
8124 self.clear_sequence_state();
8125 self.graph_failed
8126 .store(true, std::sync::atomic::Ordering::Relaxed);
8127 self.cancel
8128 .store(true, std::sync::atomic::Ordering::Relaxed);
8129 tracing::error!("MTP graph mirror rewind failed after verify decline");
8130 }
8131 return None;
8132 }
8133 crate::gpu::BatchGraphOutcome::Failed => {
8134 self.clear_sequence_state();
8138 self.graph_failed
8139 .store(true, std::sync::atomic::Ordering::Relaxed);
8140 self.cancel
8141 .store(true, std::sync::atomic::Ordering::Relaxed);
8142 tracing::error!("MTP verify batch graph failed after admission");
8143 return None;
8144 }
8145 }
8146 #[cfg(target_os = "macos")]
8152 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
8153 let snap: Vec<Vec<f32>> = self
8154 .kv_cache
8155 .layers
8156 .iter()
8157 .map(|l| l.linear_state.clone())
8158 .collect();
8159 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8160 let toks: Vec<u32> = std::iter::once(t_next)
8161 .chain(drafts.iter().copied())
8162 .collect();
8163 let want_save = self.graph_want_logits;
8164 self.graph_want_logits = false;
8165 for (i, &t) in toks.iter().enumerate() {
8166 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8167 let _ = self.graph_logits.take();
8168 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
8172 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
8173 }
8174 let ref_lg = self.logits_from_hidden(&hi);
8175 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
8176 let ra = sampler::argmax(&ref_lg);
8177 let va = sampler::argmax(row);
8178 let mut md = 0f32;
8179 let mut rms = 0f64;
8180 for j in 0..lm_rows.min(ref_lg.len()) {
8181 let d = (ref_lg[j] - row[j]).abs();
8182 md = md.max(d);
8183 rms += (d as f64) * (d as f64);
8184 }
8185 let mut hd = 0f32;
8186 for j in 0..self.hidden_size {
8187 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8188 }
8189 eprintln!(
8190 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8191 next_pos + i,
8192 if ra == va { "OK" } else { "MISMATCH" },
8193 (rms / lm_rows as f64).sqrt()
8194 );
8195 }
8196 self.graph_want_logits = want_save;
8197 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8200 if l.linear_state.len() == st.len() {
8201 l.linear_state.copy_from_slice(&st);
8202 } else {
8203 l.linear_state = st;
8204 }
8205 }
8206 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8207 let extra = l.seq_len.saturating_sub(n0);
8208 if extra > 0 {
8209 l.truncate_last(extra);
8210 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8211 }
8212 }
8213 }
8214 let t_verify = t_round.elapsed();
8215 let sub_verify = subs();
8216 let mut a = 0usize;
8221 let mut forced: Option<u32> = None;
8222 let ids: Vec<u32> = if sparse {
8223 let mut p = std::mem::take(&mut self.spec_ps);
8224 let mut res = std::mem::take(&mut self.spec_ress);
8225 while a < k_spec {
8226 let ok = sampler::sparse_distribution_into(
8227 &logits[a * lm_rows..(a + 1) * lm_rows],
8228 &cfg,
8229 all_ids,
8230 &mut self.sampler_scratch,
8231 self.pool.as_deref(),
8232 &mut p,
8233 );
8234 if !ok {
8235 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8236 p.clear();
8237 p.push((t, 1.0));
8238 }
8239 match sampler::spec_accept_or_correct_sparse(
8240 &p,
8241 &self.spec_qs[a],
8242 drafts[a],
8243 &mut self.rng,
8244 &mut res,
8245 ) {
8246 None => {
8247 all_ids.push(drafts[a]);
8248 a += 1;
8249 }
8250 Some(c) => {
8251 forced = Some(c);
8252 break;
8253 }
8254 }
8255 }
8256 all_ids.truncate(base_len);
8257 self.spec_ps = p;
8258 self.spec_ress = res;
8259 drafts.clone()
8260 } else if sampling {
8261 let mut p = std::mem::take(&mut self.spec_p);
8262 let mut res = std::mem::take(&mut self.spec_res);
8263 while a < k_spec {
8264 sampler::distribution_into(
8265 &logits[a * lm_rows..(a + 1) * lm_rows],
8266 &cfg,
8267 all_ids,
8268 &mut self.sampler_scratch,
8269 self.pool.as_deref(),
8270 &mut p,
8271 );
8272 match sampler::spec_accept_or_correct(
8273 &p,
8274 &self.spec_q[a],
8275 drafts[a],
8276 &mut self.rng,
8277 &mut res,
8278 self.pool.as_deref(),
8279 ) {
8280 None => {
8281 all_ids.push(drafts[a]);
8282 a += 1;
8283 }
8284 Some(c) => {
8285 forced = Some(c);
8286 break;
8287 }
8288 }
8289 }
8290 all_ids.truncate(base_len);
8291 self.spec_p = p;
8292 self.spec_res = res;
8293 drafts.clone()
8295 } else if greedy_pen {
8296 let mut ids: Vec<u32> = Vec::with_capacity(b);
8300 for i in 0..b {
8301 let t = sampler::argmax_penalized(
8302 &logits[i * lm_rows..(i + 1) * lm_rows],
8303 &cfg,
8304 all_ids,
8305 &mut self.sampler_scratch,
8306 self.pool.as_deref(),
8307 );
8308 ids.push(t);
8309 if i < k_spec && t == drafts[i] {
8310 all_ids.push(t);
8311 } else {
8312 break;
8313 }
8314 }
8315 all_ids.truncate(base_len);
8316 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8317 a += 1;
8318 }
8319 ids
8322 } else if greedy_dev && dev_ids.len() == b {
8323 let ids = std::mem::take(&mut dev_ids);
8324 while a < k_spec && ids[a] == drafts[a] {
8325 a += 1;
8326 }
8327 ids
8328 } else {
8329 if logits.len() < b * lm_rows {
8330 self.clear_sequence_state();
8333 self.graph_failed
8334 .store(true, std::sync::atomic::Ordering::Relaxed);
8335 self.cancel
8336 .store(true, std::sync::atomic::Ordering::Relaxed);
8337 tracing::error!("Metal verify returned neither logits nor argmax ids");
8338 return None;
8339 }
8340 let ids: Vec<u32> = (0..b)
8341 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8342 .collect();
8343 while a < k_spec && ids[a] == drafts[a] {
8344 a += 1;
8345 }
8346 ids
8347 };
8348 spec_stamp("acc");
8349 if spec_dbg {
8350 eprintln!(
8351 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8352 drafts, ids
8353 );
8354 }
8355 #[cfg(target_os = "macos")]
8359 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8360 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8361 {
8362 let snap: Vec<Vec<f32>> = self
8363 .kv_cache
8364 .layers
8365 .iter()
8366 .map(|l| l.linear_state.clone())
8367 .collect();
8368 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8369 let toks: Vec<u32> = std::iter::once(t_next)
8370 .chain(drafts.iter().copied())
8371 .collect();
8372 let want_save = self.graph_want_logits;
8373 self.graph_want_logits = false;
8374 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8375 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8376 let _ = self.graph_logits.take();
8377 }
8378 self.graph_want_logits = want_save;
8379 let plain_states: Vec<Vec<f32>> = self
8380 .kv_cache
8381 .layers
8382 .iter()
8383 .map(|l| l.linear_state.clone())
8384 .collect();
8385 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8386 let mut rows = Vec::new();
8387 for (li, (l, n0)) in self
8388 .kv_cache
8389 .layers
8390 .iter_mut()
8391 .zip(attn_lens.iter())
8392 .enumerate()
8393 {
8394 let extra = l.seq_len.saturating_sub(*n0);
8395 if extra > 0 {
8396 let mut kk = Vec::new();
8397 let mut vv = Vec::new();
8398 for g in 0..nkv {
8399 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8400 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8401 }
8402 rows.push((li, kk, vv));
8403 l.truncate_last(extra);
8404 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8405 }
8406 }
8407 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8408 if l.linear_state.len() == st.len() {
8409 l.linear_state.copy_from_slice(&st);
8410 } else {
8411 l.linear_state = st;
8412 }
8413 }
8414 Some((plain_states, rows))
8415 } else {
8416 None
8417 };
8418 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8419 #[cfg(target_os = "macos")]
8428 let mut warm_pending: Option<MetalWarmPending> = None;
8429 #[cfg(target_os = "macos")]
8430 if metal_native {
8431 m.kv.truncate_last(k_spec.saturating_sub(1));
8432 if self.mtp_graph_mode == Some(true) {
8433 crate::gpu_metal::kv_mirror_set_stored(
8436 self.mtp_kv_id(),
8437 Self::MTP_LAYER_BASE,
8438 m.kv.seq_len,
8439 );
8440 if !warm_off && a > 0 {
8441 let pairs: Vec<(&[f32], u32)> = (0..a)
8442 .map(|j| {
8443 (
8444 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8445 ids[j],
8446 )
8447 })
8448 .collect();
8449 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8450 }
8451 }
8452 spec_stamp("c.wsub");
8453 }
8454 #[cfg(target_os = "macos")]
8456 if metal_native {
8457 if !self.metal_verify_commit(a) {
8460 self.clear_sequence_state();
8461 self.graph_failed
8462 .store(true, std::sync::atomic::Ordering::Relaxed);
8463 self.cancel
8464 .store(true, std::sync::atomic::Ordering::Relaxed);
8465 tracing::error!("Metal verify state/KV handoff failed after admission");
8466 return None;
8467 }
8468 if let Some((plain_states, rows)) = commit_ref {
8469 crate::gpu_metal::queue_fence();
8470 let _ = crate::gpu_metal::wait_replay();
8473 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8474 let mut worst_s = 0f32;
8475 let mut worst_li = 0usize;
8476 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8477 if l.linear_state.len() != ps.len() || ps.is_empty() {
8478 continue;
8479 }
8480 let d = l
8481 .linear_state
8482 .iter()
8483 .zip(ps)
8484 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8485 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8486 let rel = d / n.max(1e-6);
8487 if rel > worst_s {
8488 worst_s = rel;
8489 worst_li = li;
8490 }
8491 }
8492 let mut worst_k = 0f32;
8493 for (li, kk, vv) in &rows {
8494 let l = &self.kv_cache.layers[*li];
8495 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8496 let mut ck = Vec::new();
8497 let mut cv = Vec::new();
8498 for g in 0..nkv {
8499 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8500 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8501 }
8502 if ck.len() == kk.len() {
8503 let dk = ck
8504 .iter()
8505 .zip(kk)
8506 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8507 let dv = cv
8508 .iter()
8509 .zip(vv)
8510 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8511 worst_k = worst_k.max(dk).max(dv);
8512 } else {
8513 eprintln!(
8514 "commit-check L{li}: kv row count mismatch {} vs {}",
8515 ck.len(),
8516 kk.len()
8517 );
8518 }
8519 }
8520 eprintln!(
8521 "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}"
8522 );
8523 }
8524 }
8525 if !metal_native && a + 1 < b {
8526 let expected_gdn_layers = self.graph_gdn_layer_count();
8527 if expected_gdn_layers > 0
8528 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8529 {
8530 self.clear_sequence_state();
8531 self.graph_failed
8532 .store(true, std::sync::atomic::Ordering::Relaxed);
8533 self.cancel
8534 .store(true, std::sync::atomic::Ordering::Relaxed);
8535 tracing::error!("GDN speculative restore failed after verify");
8536 return None;
8537 }
8538 }
8539 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8540 self.clear_sequence_state();
8545 self.graph_failed
8546 .store(true, std::sync::atomic::Ordering::Relaxed);
8547 self.cancel
8548 .store(true, std::sync::atomic::Ordering::Relaxed);
8549 tracing::error!("trunk graph KV rewind failed after speculative verify");
8550 return None;
8551 }
8552 *accepted += a;
8553 if !metal_native {
8564 m.kv.truncate_last(k_spec.saturating_sub(1));
8566 }
8567 spec_stamp("c.trunc");
8568 if !metal_native
8569 && self.mtp_graph_mode == Some(true)
8570 && !self.rewind_mtp_graph_mirror(next_pos)
8571 {
8572 self.clear_sequence_state();
8576 self.graph_failed
8577 .store(true, std::sync::atomic::Ordering::Relaxed);
8578 self.cancel
8579 .store(true, std::sync::atomic::Ordering::Relaxed);
8580 tracing::error!("MTP graph mirror rewind failed after verify commit");
8581 return None;
8582 }
8583 if !warm_off && a > 0 {
8584 let mut warmed = false;
8587 #[cfg(target_os = "macos")]
8588 if metal_native && self.mtp_graph_mode == Some(true) {
8589 warmed = match warm_pending.take() {
8593 Some(p) => self.mtp_warm_batch_finish(m, p),
8594 None => false,
8595 };
8596 if !warmed {
8597 warmed = true;
8598 for j in 0..a {
8599 let row =
8600 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8601 if self
8602 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8603 .is_none()
8604 {
8605 warmed = false;
8606 break;
8607 }
8608 }
8609 }
8610 }
8611 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8612 let rows: Vec<Vec<f32>> = (0..a)
8613 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8614 .collect();
8615 let pairs: Vec<(&[f32], u32)> = rows
8616 .iter()
8617 .zip(ids.iter())
8618 .map(|(r, &t)| (r.as_slice(), t))
8619 .collect();
8620 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8621 Ok(()) => warmed = true,
8622 Err(err) => {
8623 tracing::error!("{err}");
8629 self.clear_sequence_state();
8630 self.graph_failed
8631 .store(true, std::sync::atomic::Ordering::Relaxed);
8632 self.cancel
8633 .store(true, std::sync::atomic::Ordering::Relaxed);
8634 return None;
8635 }
8636 }
8637 }
8638 if !warmed {
8639 for j in 0..a {
8640 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8641 let row = row.to_vec();
8642 self.mtp_warm(m, &row, ids[j], next_pos + j);
8643 }
8644 }
8645 }
8646 spec_stamp("c.warm");
8650 if let Some(c) = forced {
8651 self.spec_forced = Some(c);
8652 self.graph_logits = None;
8653 } else if greedy_dev && logits.is_empty() {
8654 self.spec_forced = Some(ids[a]);
8657 self.graph_logits = None;
8658 } else {
8659 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8660 row.resize(self.vocab_size, 0.0);
8661 if let Some(c) = self.final_softcap {
8662 for l in row.iter_mut() {
8663 *l = c * (*l / c).tanh();
8664 }
8665 }
8666 self.graph_logits = Some(row);
8667 }
8668 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8669 spec_stamp("c.row");
8670 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8676 let end = subs();
8677 eprintln!(
8678 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8679 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8680 t_draft.as_secs_f64() * 1e3,
8681 sub_draft - sub0,
8682 (t_verify - t_draft).as_secs_f64() * 1e3,
8683 sub_verify - sub_draft,
8684 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8685 end - sub_verify,
8686 self.draft_full_streak,
8687 );
8688 }
8689 if k_env.is_none() && !metal_native && !k_capped {
8694 let f = a as f32 / k_spec.max(1) as f32;
8698 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8699 let mut k_next = k_spec;
8700 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8701 k_next = k_spec + 1;
8702 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8703 k_next = k_spec - 1;
8704 }
8705 if k_next != k_spec {
8706 self.spec_acc_ewma = 0.6;
8707 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8708 eprintln!("spec-k: {k_spec} → {k_next}");
8709 }
8710 }
8711 self.spec_k_adapt = Some(k_next);
8712 }
8713 spec_stamp("end");
8714 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8715 }
8716
8717 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8726 if !self.pair_supported() {
8727 return (0.0, 0.0);
8728 }
8729 if self.gdn_cfg.is_some()
8737 && self.graph_prefill_preferred()
8738 && self.automatic_gpu_prefix().is_some()
8739 {
8740 tracing::info!(
8741 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8742 );
8743 return (0.0, 0.0);
8744 }
8745 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8752 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8753 let emb1 = self.embed_single(1);
8754 let emb2 = self.embed_single(2);
8755 let pos = self.kv_cache.seq_len();
8756
8757 let t0 = std::time::Instant::now();
8758 for _ in 0..iters {
8759 let _ = self.forward_layers(&emb1, pos, None);
8760 let _ = self.forward_layers(&emb2, pos + 1, None);
8761 for l in &mut self.kv_cache.layers {
8762 l.truncate_last(2);
8763 }
8764 }
8765 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8766
8767 let t1 = std::time::Instant::now();
8768 for _ in 0..iters {
8769 let _ = self.forward_pair(&emb1, &emb2, pos);
8770 for l in &mut self.kv_cache.layers {
8771 l.truncate_last(2);
8772 }
8773 }
8774 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8775 match graph_env {
8776 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8777 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8778 }
8779 (singles_ms, pair_ms)
8780 }
8781
8782 fn pair_supported(&self) -> bool {
8790 !self.weights.layers.is_empty()
8797 && !self.per_token_stack()
8798 && !self
8799 .weights
8800 .layers
8801 .iter()
8802 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8803 }
8804
8805 fn forward_pair(
8806 &mut self,
8807 emb1: &[f32],
8808 emb2: &[f32],
8809 position: usize,
8810 ) -> (Vec<f32>, Vec<f32>) {
8811 self.mimo_moe_prepare();
8814 let mut h1 = emb1.to_vec();
8815 let mut h2 = emb2.to_vec();
8816 let (_nkv, _hd, hs, _rd, eps) = (
8817 self.num_kv_heads,
8818 self.head_dim,
8819 self.hidden_size,
8820 self.rotary_dim,
8821 self.rms_eps,
8822 );
8823 let pool = self.pool.clone();
8824 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8831
8832 for li in 0..self.num_layers {
8833 let _capacity_tail = automatic_gpu_prefix
8834 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8835 .map(|_| crate::gpu::enter_cpu_scope());
8836 crate::gpu::set_layer(li as i64);
8839 let lw = &self.weights.layers[self.phys_layer(li)];
8840 inference::rms_norm_into(
8843 &h1,
8844 &lw.input_norm,
8845 self.rms_eps,
8846 self.norm_style,
8847 &mut self.ws.n1,
8848 );
8849 inference::rms_norm_into(
8850 &h2,
8851 &lw.input_norm,
8852 self.rms_eps,
8853 self.norm_style,
8854 &mut self.ws.n2,
8855 );
8856
8857 let (a1, a2) = match &lw.attn {
8858 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8859 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8860 AttnKind::Bounded(w) => {
8861 let rope = self
8864 .bounded_rope
8865 .clone()
8866 .expect("bounded layer without an installed rotation table");
8867 let cfg = crate::bounded::BoundedAttnCfg {
8868 num_heads: self.num_heads,
8869 num_kv_heads: self.num_kv_heads,
8870 head_dim: self.head_dim,
8871 hidden_size: hs,
8872 scale: self.attn_scale,
8873 rope: &rope,
8874 pool: pool.as_deref(),
8875 };
8876 let a1 = crate::bounded::bounded_attention(
8877 &self.ws.n1,
8878 w,
8879 &mut self.kv_cache.layers[li],
8880 &cfg,
8881 );
8882 let a2 = crate::bounded::bounded_attention(
8883 &self.ws.n2,
8884 w,
8885 &mut self.kv_cache.layers[li],
8886 &cfg,
8887 );
8888 (a1, a2)
8889 }
8890 AttnKind::Linear(w) => {
8891 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8892 let layer = &mut self.kv_cache.layers[li];
8893 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8894 vmf_phase_pair(
8895 &self.ws.n1,
8896 &self.ws.n2,
8897 w,
8898 &cfg,
8899 state,
8900 scratch,
8901 self.pool.as_deref(),
8902 )
8903 }
8904 AttnKind::LinearGdn(w) => {
8905 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8906 let layer = &mut self.kv_cache.layers[li];
8907 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8908 gdn_pair(
8909 &self.ws.n1,
8910 &self.ws.n2,
8911 w,
8912 &cfg,
8913 state,
8914 scratch,
8915 self.pool.as_deref(),
8916 )
8917 }
8918 AttnKind::ShortConv(w) => {
8919 let cfg = self
8920 .short_conv_cfg
8921 .expect("short-conv layer without short_conv_cfg");
8922 let layer = &mut self.kv_cache.layers[li];
8923 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8924 short_conv_pair(
8925 &self.ws.n1,
8926 &self.ws.n2,
8927 w,
8928 &cfg,
8929 state,
8930 scratch,
8931 self.pool.as_deref(),
8932 )
8933 }
8934 AttnKind::Full {
8935 wq,
8936 wk,
8937 wv,
8938 wo,
8939 q_norm,
8940 k_norm,
8941 output_gate,
8942 softplus_gate,
8943 bias,
8944 } => {
8945 let inv_freq_l = self.layer_inv_freq(li);
8946 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8947 let cfg = QwenAttnCfg {
8948 num_heads: self.layer_num_heads(li),
8949 num_kv_heads: nkv_l,
8950 head_dim: hd_l,
8951 hidden_size: hs,
8952 position,
8953 inv_freq: &inv_freq_l,
8954 rotary_dim: rd_l,
8955 scale: self.attn_scale,
8956 softcap: self.attn_softcap,
8957 window: self.layer_window(li),
8958 v_norm: self.attn_v_norm,
8959 qk_norm_after_rope: self.qk_norm_after_rope,
8960 gate_sigmoid: self.proj_gate_sigmoid,
8961 q_norm: q_norm.as_deref(),
8962 k_norm: k_norm.as_deref(),
8963 output_gate: *output_gate,
8964 softplus_gate: softplus_gate
8965 .as_ref()
8966 .map(|(gate, per_head)| (gate, *per_head)),
8967 rope_scale: self.layer_rope_scale(li),
8968 bias: bias
8969 .as_ref()
8970 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8971 rms_eps: eps,
8972 norm_style: self.norm_style,
8973 pool: pool.as_deref(),
8974 v_head_dim: self.layer_v_dim(li),
8975 };
8976 attention::qwen_attention_pair(
8977 &self.ws.n1,
8978 &self.ws.n2,
8979 wq,
8980 wk,
8981 wv,
8982 wo,
8983 &mut self.kv_cache.layers[li],
8984 &cfg,
8985 )
8986 }
8987 };
8988 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8989 Some(w) => (
8990 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8991 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8992 ),
8993 None => (a1, a2),
8994 };
8995 for i in 0..self.hidden_size {
8996 h1[i] += a1[i];
8997 h2[i] += a2[i];
8998 }
8999 let (mut a1, mut a2) = (a1, a2);
9000 attention::recycle_buf(&mut a1);
9001 attention::recycle_buf(&mut a2);
9002
9003 let lw = &self.weights.layers[self.phys_layer(li)];
9004 inference::rms_norm_into(
9005 &h1,
9006 &lw.post_norm,
9007 self.rms_eps,
9008 self.norm_style,
9009 &mut self.ws.p1,
9010 );
9011 inference::rms_norm_into(
9012 &h2,
9013 &lw.post_norm,
9014 self.rms_eps,
9015 self.norm_style,
9016 &mut self.ws.p2,
9017 );
9018 let (f1, f2) = match &lw.ffn {
9019 FfnKind::DenseMoe(dm) => (
9022 dense_moe_ffn(
9023 dm,
9024 &self.ws.p1,
9025 &h1,
9026 self.rms_eps,
9027 self.norm_style,
9028 self.pool.as_deref(),
9029 ),
9030 dense_moe_ffn(
9031 dm,
9032 &self.ws.p2,
9033 &h2,
9034 self.rms_eps,
9035 self.norm_style,
9036 self.pool.as_deref(),
9037 ),
9038 ),
9039 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
9040 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
9041 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
9042 ),
9043 _ => ffn_forward_pair(
9044 &lw.ffn,
9045 &self.ws.p1,
9046 &self.ws.p2,
9047 self.pool.as_deref(),
9048 None,
9049 ),
9050 };
9051 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
9052 Some(w) => (
9053 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
9054 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
9055 ),
9056 None => (f1, f2),
9057 };
9058 for i in 0..self.hidden_size {
9059 h1[i] += f1[i];
9060 h2[i] += f2[i];
9061 }
9062 let (mut f1, mut f2) = (f1, f2);
9063 attention::recycle_buf(&mut f1);
9064 attention::recycle_buf(&mut f2);
9065 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
9066 for i in 0..self.hidden_size {
9067 h1[i] *= sc;
9068 h2[i] *= sc;
9069 }
9070 }
9071 if self.is_loop_end(li) && li + 1 < self.num_layers {
9073 h1 = inference::rms_norm(
9074 &h1,
9075 &self.weights.final_norm,
9076 self.rms_eps,
9077 self.norm_style,
9078 );
9079 h2 = inference::rms_norm(
9080 &h2,
9081 &self.weights.final_norm,
9082 self.rms_eps,
9083 self.norm_style,
9084 );
9085 }
9086 }
9087 crate::gpu::set_layer(-1); if self.o1_active() {
9094 self.commit_linear_scratch();
9095 }
9096 self.o1_progress();
9097 self.swa_trim_tails();
9098 (h1, h2)
9099 }
9100
9101 fn commit_linear_scratch(&mut self) {
9103 for layer in &mut self.kv_cache.layers {
9104 if !layer.linear_scratch.is_empty() {
9105 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
9106 layer.linear_scratch.clear();
9107 }
9108 }
9109 }
9110
9111 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9119 #[cfg(target_os = "macos")]
9120 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9121 if ids.is_empty() {
9122 return Err("empty id sequence".to_string());
9123 }
9124
9125 self.nll_begin()?;
9126 let result = (|| {
9127 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9128 self.nll_check_graph("encoder prefill", 0)?;
9129 let expected = ids.len() * self.hidden_size;
9130 if hidden.len() != expected {
9131 return Err(format!(
9132 "encoder prefill returned {} values for {} rows of hidden size {}",
9133 hidden.len(),
9134 ids.len(),
9135 self.hidden_size
9136 ));
9137 }
9138
9139 let mut out = vec![0.0f32; expected];
9140 for (src, dst) in hidden
9141 .chunks_exact(self.hidden_size)
9142 .zip(out.chunks_exact_mut(self.hidden_size))
9143 {
9144 inference::rms_norm_into(
9145 src,
9146 &self.weights.final_norm,
9147 self.rms_eps,
9148 self.norm_style,
9149 dst,
9150 );
9151 }
9152 Ok(out)
9153 })();
9154 self.nll_end();
9155 result
9156 }
9157
9158 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9163 #[cfg(target_os = "macos")]
9164 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9165 if ids.is_empty() {
9166 return Err("empty id sequence".to_string());
9167 }
9168 self.nll_begin()?;
9169 let result = (|| {
9170 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9171 self.nll_check_graph("hidden prefill", 0)?;
9172 if hidden.len() != ids.len() * self.hidden_size {
9173 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
9174 }
9175 Ok(hidden)
9176 })();
9177 self.nll_end();
9178 result
9179 }
9180
9181 pub fn forward_ids(
9184 &mut self,
9185 ids: &[u32],
9186 task_mask: Option<&TaskMask>,
9187 ) -> Result<Vec<f32>, String> {
9188 #[cfg(target_os = "macos")]
9189 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9190 if ids.is_empty() {
9191 return Err("empty id sequence".to_string());
9192 }
9193 self.clear_sequence_state();
9194 self.check_forward_graph("forward_ids setup", 0)?;
9195 if task_mask.is_none() {
9196 self.o1_begin();
9197 }
9198 let mut hidden = vec![0.0f32; self.hidden_size];
9199 let mut pos = 0usize;
9200 if let Some(b) = &mut self.dsv41 {
9201 let pool = self.pool.clone();
9202 let mut logits = Vec::new();
9203 crate::dsv41::forward_chunk(
9204 &b.0,
9205 &b.1,
9206 &b.2,
9207 &mut b.3,
9208 ids,
9209 0,
9210 pool.as_deref(),
9211 &mut logits,
9212 );
9213 if let Err(err) = self.o1_seal_checked() {
9214 self.clear_sequence_state();
9215 return Err(err);
9216 }
9217 return Ok(logits);
9218 }
9219 #[cfg(not(target_os = "macos"))]
9230 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9231 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9232 Ok(r) => r,
9233 Err(e) => {
9234 self.clear_sequence_state();
9235 return Err(e);
9236 }
9237 };
9238 if n > 0 {
9239 hidden.copy_from_slice(&last);
9240 pos = n;
9241 }
9242 }
9243 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9244 let chunk = self.prefill_chunk();
9248 let hs = self.hidden_size;
9249 while pos < ids.len() {
9250 let end = (pos + chunk).min(ids.len());
9251 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9252 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9253 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9254 pos = end;
9255 }
9256 }
9257 if task_mask.is_none()
9266 && !self.graph_prefill_preferred()
9267 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9268 && self.pair_supported()
9269 {
9270 while pos + 1 < ids.len() {
9271 let e1 = self.embed_single(ids[pos]);
9272 let e2 = self.embed_single(ids[pos + 1]);
9273 let (_, h2) = self.forward_pair(&e1, &e2, pos);
9274 self.check_forward_graph("forward_ids pair", pos + 1)?;
9275 self.commit_linear_scratch();
9276 hidden = h2;
9277 pos += 2;
9278 }
9279 }
9280 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9283 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9284 self.graph_logits = Some(lg);
9285 hidden = vec![0.0; self.hidden_size];
9286 pos = ids.len();
9287 }
9288 }
9289 while pos < ids.len() {
9290 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9291 self.check_forward_graph("forward_ids", pos)?;
9292 pos += 1;
9293 }
9294 if let Some(logits) = self.graph_logits.take() {
9295 if let Err(err) = self.o1_seal_checked() {
9299 self.clear_sequence_state();
9300 return Err(err);
9301 }
9302 return Ok(logits);
9303 }
9304 if let Err(err) = self.o1_seal_checked() {
9308 self.clear_sequence_state();
9309 return Err(err);
9310 }
9311 let normed = inference::rms_norm(
9312 &hidden,
9313 &self.weights.final_norm,
9314 self.rms_eps,
9315 self.norm_style,
9316 );
9317 Ok(self.lm_head_forward(&normed))
9318 }
9319
9320 #[doc(hidden)]
9324 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9325 #[cfg(target_os = "macos")]
9326 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9327 if ids.is_empty() {
9328 return Err("empty id sequence".to_string());
9329 }
9330 self.clear_sequence_state();
9331 self.dsv41
9332 .as_ref()
9333 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9334 self.o1_begin();
9335 let rows = {
9336 let pool = self.pool.clone();
9337 let b = self
9338 .dsv41
9339 .as_mut()
9340 .expect("dsv41 checked above; state cannot change during forward");
9341 let mut rows = Vec::with_capacity(ids.len());
9342 for (position, &id) in ids.iter().enumerate() {
9343 let mut logits = Vec::new();
9344 crate::dsv41::forward_token(
9345 &b.0,
9346 &b.1,
9347 &b.2,
9348 &mut b.3,
9349 id,
9350 position,
9351 pool.as_deref(),
9352 &mut logits,
9353 );
9354 rows.push(logits);
9355 }
9356 rows
9357 };
9358 self.o1_seal();
9359 Ok(rows)
9360 }
9361
9362 pub fn dsv41_moe_capture(
9366 &mut self,
9367 ids: &[u32],
9368 lo: usize,
9369 hi: usize,
9370 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9371 #[cfg(target_os = "macos")]
9372 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9373 if ids.is_empty() {
9374 return Err("empty id sequence".to_string());
9375 }
9376 self.dsv41
9377 .as_ref()
9378 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9379 self.clear_sequence_state();
9380 let res = {
9381 let pool = self.pool.clone();
9382 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9383 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9384 };
9385 self.clear_sequence_state();
9386 res
9387 }
9388
9389 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9396 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9397 Ok((nll / cnt.max(1) as f64).exp())
9398 }
9399
9400 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9407 if !self.can_prefill_batched() {
9408 return self.nll_ids_from(ids, 0).map(|_| ());
9409 }
9410 self.nll_begin()?;
9411 let result: Result<(), String> = (|| {
9412 let chunk = chunk.max(1);
9413 let mut pos = 0usize;
9414 while pos < ids.len() {
9415 let end = (pos + chunk).min(ids.len());
9416 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9417 self.nll_check_graph("calibration prefill", pos)?;
9418 pos = end;
9419 }
9420 Ok(())
9421 })();
9422 self.nll_end();
9423 result
9424 }
9425
9426 pub fn calib_window_span(
9435 &mut self,
9436 ids: &[u32],
9437 input: Option<&[f32]>,
9438 from: usize,
9439 mid: usize,
9440 upto: usize,
9441 ) -> Result<Vec<f32>, String> {
9442 if !self.can_prefill_batched() {
9443 return Err("calib_window_span: this architecture has no batched prefill".into());
9444 }
9445 self.nll_begin()?;
9446 let result: Result<Vec<f32>, String> = (|| {
9447 let h = if mid > from {
9448 let inp = match input {
9449 Some(h) => PrefillIn::Hidden(h),
9450 None => PrefillIn::Ids(ids),
9451 };
9452 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9453 self.nll_check_graph("calibration span", 0)?;
9454 h
9455 } else {
9456 input.map(<[f32]>::to_vec).unwrap_or_default()
9457 };
9458 if upto > mid {
9459 let inp = if mid > from || input.is_some() {
9460 PrefillIn::Hidden(&h)
9461 } else {
9462 PrefillIn::Ids(ids)
9463 };
9464 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9465 self.nll_check_graph("calibration span", 0)?;
9466 }
9467 Ok(h)
9468 })();
9469 self.nll_end();
9470 result
9471 }
9472
9473 #[cfg(feature = "gpu")]
9477 pub fn qwen4_capture(
9478 &mut self,
9479 ids: &[u32],
9480 lo: usize,
9481 hi: usize,
9482 ) -> Result<Vec<Vec<f32>>, String> {
9483 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9484 crate::qwen4_exp::capture_window(
9485 &b.0,
9486 &b.1,
9487 &b.2,
9488 &mut b.3,
9489 ids,
9490 lo,
9491 hi,
9492 &self.inv_freq,
9493 self.pool.as_deref(),
9494 )
9495 }
9496
9497 #[cfg(feature = "gpu")]
9503 pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9504 use crate::gpu_wgpu::clover as cap;
9505 let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9506 let ne = (lo..hi)
9507 .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9508 FfnKind::Moe(m) => Some(m.experts.len()),
9509 _ => None,
9510 })
9511 .ok_or("no MoE layer in the capture range")?;
9512 let chunk = self.prefill_chunk().max(1);
9513 self.clear_sequence_state();
9514 if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9515 return Err("capture unavailable (no wgpu device)".into());
9516 }
9517 let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9518 let res = (|| {
9519 for (ci, part) in ids.chunks(chunk).enumerate() {
9520 let mut hiddens = Vec::with_capacity(part.len() * hs);
9521 for &id in part {
9522 hiddens.extend_from_slice(&self.embed_single(id));
9523 }
9524 let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9525 match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9526 crate::gpu::BatchGraphOutcome::Completed => {}
9527 _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9528 }
9529 let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9530 if written < hi - lo {
9531 return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9532 }
9533 for (o, r) in out.iter_mut().zip(rows) {
9534 o.extend_from_slice(&r);
9535 }
9536 }
9537 Ok(())
9538 })();
9539 cap::moe_capture_end();
9540 self.clear_sequence_state();
9541 res.map(|_| out)
9542 }
9543
9544 pub fn moe_expert_dirs(
9547 &self,
9548 li: usize,
9549 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9550 let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9551 return None;
9552 };
9553 let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9554 let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9555 let shared = match &m.shared {
9556 Some((se, _)) => Some(dirs(se)?),
9557 None => None,
9558 };
9559 Some((routed, shared))
9560 }
9561
9562 pub fn qwen4_expert_dirs(
9564 &self,
9565 li: usize,
9566 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9567 let b = self.qwen4_exp.as_ref()?;
9568 crate::qwen4_exp::expert_dirs(&b.1, li)
9569 }
9570
9571 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9586 self.reset_mtp_probe_state();
9587 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9593 let result = if ids.len() < 2 {
9594 Err("MTP FFN probe needs at least two token ids".into())
9595 } else if ids
9596 .iter()
9597 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9598 {
9599 Err("MTP FFN probe received a token id outside the embedding table".into())
9600 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9601 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9604 } else if self.mimo_mtp.is_some() {
9605 let mut st = self
9606 .mimo_mtp
9607 .take()
9608 .expect("checked MiMo MTP before detaching it");
9609 let result = match self.probe_trunk_rows(ids) {
9610 Ok(trunk) => crate::gpu::cpu_scope(|| {
9611 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
9612 }),
9613 Err(e) => Err(e),
9614 };
9615 st.reset();
9618 self.mimo_mtp = Some(st);
9619 result
9620 } else if self.mtp.is_some() {
9621 let mut mtp = self
9622 .mtp
9623 .take()
9624 .expect("checked canonical MTP before detaching it");
9625 let result = match self.probe_trunk_rows(ids) {
9626 Ok(trunk) => crate::gpu::cpu_scope(|| {
9627 self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
9628 }),
9629 Err(e) => Err(e),
9630 };
9631 mtp.kv.clear();
9632 self.mtp = Some(mtp);
9633 result
9634 } else {
9635 Err("MTP FFN probe: this pipeline has no draft stack".into())
9636 };
9637 self.reset_mtp_probe_state();
9640 result
9641 }
9642
9643 fn reset_mtp_probe_state(&mut self) {
9645 self.clear_sequence_state();
9646 if let Some(mtp) = self.mtp.as_mut() {
9647 mtp.kv.clear();
9648 }
9649 if let Some(mimo) = self.mimo_mtp.as_mut() {
9650 mimo.reset();
9651 }
9652 self.mtp_graph_mode = None;
9653 }
9654
9655 pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9661 if ids.len() < 2 {
9662 return Err("MTP FFN probe needs at least two token ids".into());
9663 }
9664 let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
9665 self.clear_sequence_state();
9666 Ok(rows)
9667 }
9668
9669 fn mtp_ffn_probe_teacher_forced(
9675 &mut self,
9676 mtp: &mut MtpModule,
9677 ids: &[u32],
9678 trunk: &[f32],
9679 ) -> Result<Vec<MtpFfnProbe>, String> {
9680 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9681 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9682 };
9683 let inter = dense.gate_proj.rows();
9684 let (hs, n) = (self.hidden_size, ids.len() - 1);
9685 if trunk.len() != n * hs {
9686 return Err(format!(
9687 "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
9688 trunk.len()
9689 ));
9690 }
9691 let mut mass = vec![0.0f64; inter];
9692 let mut squared_mass = vec![0.0f64; inter];
9693 let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
9694 for position in 0..n {
9695 self.mtp_probe_ffn_input(
9696 mtp,
9697 &trunk[position * hs..(position + 1) * hs],
9698 ids[position + 1],
9699 position,
9700 )?;
9701 inputs.extend_from_slice(&self.ws.p1[..hs]);
9702 if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
9703 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9704 unreachable!("checked dense above");
9705 };
9706 accumulate_dense_ffn_activation_mass(
9707 dense,
9708 &inputs,
9709 inputs.len() / hs,
9710 self.pool.as_deref(),
9711 &mut mass,
9712 &mut squared_mass,
9713 )?;
9714 inputs.clear();
9715 }
9716 }
9717 Ok(vec![MtpFfnProbe {
9718 mass,
9719 squared_mass,
9720 observed_positions: n,
9721 }])
9722 }
9723
9724 fn mtp_probe_ffn_input(
9730 &mut self,
9731 mtp: &mut MtpModule,
9732 hidden: &[f32],
9733 next_token: u32,
9734 position: usize,
9735 ) -> Result<(), String> {
9736 let e = self.embed_single(next_token);
9737 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9738 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9739 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9740 inference::rms_norm_into(
9741 hidden,
9742 &mtp.hnorm,
9743 self.rms_eps,
9744 self.norm_style,
9745 cat_h,
9746 );
9747 let mut x = vec![0.0f32; self.hidden_size];
9748 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9749
9750 let lw = &mtp.layer;
9751 inference::rms_norm_into(
9752 &x,
9753 &lw.input_norm,
9754 self.rms_eps,
9755 self.norm_style,
9756 &mut self.ws.n1,
9757 );
9758 let attn = match &lw.attn {
9759 AttnKind::Full {
9760 wq,
9761 wk,
9762 wv,
9763 wo,
9764 q_norm,
9765 k_norm,
9766 output_gate,
9767 softplus_gate,
9768 bias,
9769 } => {
9770 let mut cfg = self.attn_cfg(position);
9771 cfg.q_norm = q_norm.as_deref();
9772 cfg.k_norm = k_norm.as_deref();
9773 cfg.output_gate = *output_gate;
9774 cfg.softplus_gate = softplus_gate
9775 .as_ref()
9776 .map(|(gate, per_head)| (gate, *per_head));
9777 cfg.bias = bias
9778 .as_ref()
9779 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9780 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
9781 }
9782 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
9783 };
9784 for (dst, src) in x.iter_mut().zip(attn) {
9785 *dst += src;
9786 }
9787 inference::rms_norm_into(
9788 &x,
9789 &lw.post_norm,
9790 self.rms_eps,
9791 self.norm_style,
9792 &mut self.ws.p1,
9793 );
9794 Ok(())
9795 }
9796
9797 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9802 self.clear_sequence_state();
9803 FFN_PROBE.with(|p| {
9804 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9805 });
9806 crate::gpu::cpu_scope(|| {
9807 for (pos, &id) in ids.iter().enumerate() {
9808 let emb = self.embed_single(id);
9809 let _ = self.forward_layers(&emb, pos, None);
9810 }
9811 });
9812 self.clear_sequence_state();
9813 FFN_PROBE
9814 .with(|p| p.borrow_mut().take())
9815 .unwrap_or_default()
9816 }
9817
9818 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9822 if let Err(err) = self.nll_begin() {
9823 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9827 self.nll_end();
9828 return Err(err);
9829 }
9830 FFN_PROBE.with(|p| {
9831 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9832 });
9833 let result: Result<(), String> = (|| {
9834 for chunk in ids.chunks(256) {
9835 if chunk.len() < 2 {
9836 continue;
9837 }
9838 self.nll_ids_masked(chunk, 0, None)?;
9839 }
9840 Ok(())
9841 })();
9842 self.nll_end();
9843 let probe = FFN_PROBE
9844 .with(|p| p.borrow_mut().take())
9845 .unwrap_or_default();
9846 match result {
9847 Ok(()) => Ok(probe),
9848 Err(err) => {
9849 drop(probe);
9850 Err(err)
9851 }
9852 }
9853 }
9854
9855 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9859 self.nll_begin()?;
9860 let result: Result<f64, String> = (|| {
9861 let mut nll = 0f64;
9862 let mut cnt = 0usize;
9863 let mut hidden = vec![0f32; self.hidden_size];
9864 for (pos, &id) in ids.iter().enumerate() {
9865 if pos > 0 {
9866 inference::rms_norm_into(
9867 &hidden,
9868 &self.weights.final_norm,
9869 self.rms_eps,
9870 self.norm_style,
9871 &mut self.ws.n1,
9872 );
9873 let mut logits = self.lm_head_forward(&self.ws.n1);
9874 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9875 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9876 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9877 nll -= p.max(1e-300).ln();
9878 cnt += 1;
9879 attention::recycle_buf(&mut logits);
9880 }
9881 let emb = self.embed_single(id);
9882 hidden = self.forward_layers(&emb, pos, Some(mask));
9883 self.nll_check_graph("masked serial forward", pos)?;
9884 let _ = self.graph_logits.take();
9888 }
9889 Ok((nll / cnt.max(1) as f64).exp())
9890 })();
9891 self.nll_end();
9892 result
9893 }
9894
9895 pub fn nll_ids_masked(
9914 &mut self,
9915 ids: &[u32],
9916 start: usize,
9917 task_mask: Option<&TaskMask>,
9918 ) -> Result<(f64, usize), String> {
9919 let task_mask = self.drop_open_mask(task_mask);
9920 self.nll_ids_inner(ids, start, task_mask)
9921 }
9922
9923 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9924 self.nll_ids_inner(ids, start, None)
9925 }
9926
9927 pub fn prefill_parity_probe(
9936 &mut self,
9937 ids: &[u32],
9938 split: usize,
9939 bk: usize,
9940 ) -> Result<String, String> {
9941 use std::fmt::Write as _;
9942 let (n, hs) = (ids.len(), self.hidden_size);
9943 if split == 0 || split >= n || bk == 0 || !self.can_prefill_batched() {
9944 return Err(format!(
9945 "prefill parity: needs 0 < split < {n} tokens, k > 0 and a batched host prefill"
9946 ));
9947 }
9948 let kinds: Vec<u8> = (0..self.num_layers)
9950 .map(|li| match &self.weights.layers[self.phys_layer(li)].attn {
9951 AttnKind::LinearGdn(_) => 1,
9952 AttnKind::Full { .. } => 2,
9953 _ => 0,
9954 })
9955 .collect();
9956 let kv_id = self.graph_kv_id;
9957 let host_chunk = self.prefill_chunk().max(1);
9958 let host_states = |p: &Self| -> Vec<Vec<f32>> {
9959 (0..p.num_layers)
9960 .map(|li| {
9961 if kinds[li] == 1 {
9962 p.kv_cache.layers[li].linear_state.clone()
9963 } else {
9964 Vec::new()
9965 }
9966 })
9967 .collect()
9968 };
9969 let graph_states = |_p: &Self| -> Vec<Vec<f32>> {
9970 (0..kinds.len())
9971 .map(|li| {
9972 if kinds[li] == 1 {
9973 crate::gpu::graph_gdn_state_read(kv_id, li).unwrap_or_default()
9974 } else {
9975 Vec::new()
9976 }
9977 })
9978 .collect()
9979 };
9980 let host_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
9982 (0..p.num_layers)
9983 .map(|li| {
9984 let c = &p.kv_cache.layers[li];
9985 if kinds[li] != 2
9986 || c.mode != crate::kv_cache::KvMode::F32
9987 || c.base() != 0
9988 || c.seq_len != n
9989 {
9990 return (Vec::new(), Vec::new());
9991 }
9992 let (nkv, hd) = (c.num_kv_heads, c.head_dim);
9993 let (mut k, mut v) = (vec![0f32; n * nkv * hd], vec![0f32; n * nkv * hd]);
9994 for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
9995 for pos in 0..n {
9996 let d = (pos * nkv + h) * hd;
9997 k[d..d + hd].copy_from_slice(&kh[pos * hd..(pos + 1) * hd]);
9998 v[d..d + hd].copy_from_slice(&vh[pos * hd..(pos + 1) * hd]);
9999 }
10000 }
10001 (k, v)
10002 })
10003 .collect()
10004 };
10005 let graph_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10006 (0..p.num_layers)
10007 .map(|li| {
10008 let c = &p.kv_cache.layers[li];
10009 if kinds[li] != 2 {
10010 return (Vec::new(), Vec::new());
10011 }
10012 crate::gpu::graph_kv_read_rows(kv_id, &[(li, 0, n)], c.num_kv_heads, c.head_dim)
10013 .and_then(|mut v| v.pop())
10014 .unwrap_or_default()
10015 })
10016 .collect()
10017 };
10018 let host_walk = |p: &mut Self,
10019 from: usize,
10020 to: usize,
10021 rows: &mut [f32],
10022 at_split: &mut Option<Vec<Vec<f32>>>| {
10023 let mut pos = from;
10024 while pos < to {
10025 let mut end = (pos + host_chunk).min(to);
10026 if pos < split && end > split {
10027 end = split;
10028 }
10029 let hb = p.prefill_batch(&ids[pos..end], pos);
10030 rows[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10031 pos = end;
10032 if pos == split {
10033 *at_split = Some(host_states(p));
10034 }
10035 }
10036 };
10037 let graph_walk = |p: &mut Self,
10038 from: usize,
10039 to: usize,
10040 rows: &mut [f32],
10041 at_split: &mut Option<Vec<Vec<f32>>>|
10042 -> Result<(), String> {
10043 let mut pos = from;
10044 while pos < to {
10045 let mut end = (pos + bk).min(to);
10046 if pos < split && end > split {
10047 end = split;
10048 }
10049 let k = end - pos;
10050 let mut hid = vec![0f32; k * hs];
10051 for (j, &id) in ids[pos..end].iter().enumerate() {
10052 hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10053 }
10054 let positions: Vec<usize> = (pos..end).collect();
10055 let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10056 if outcome != crate::gpu::BatchGraphOutcome::Completed {
10057 return Err(format!(
10058 "prefill parity: batch graph {outcome:?} at position {pos}"
10059 ));
10060 }
10061 rows[pos * hs..end * hs].copy_from_slice(&hid);
10062 pos = end;
10063 if pos == split {
10064 *at_split = Some(graph_states(p));
10065 }
10066 }
10067 Ok(())
10068 };
10069 let score = |p: &Self, rows: &[f32], from: usize| p.parity_score(ids, rows, from);
10070
10071 let t = std::time::Instant::now();
10072 self.clear_sequence_state();
10073 let (mut hh, mut h_split) = (vec![0f32; n * hs], None);
10074 host_walk(self, 0, n, &mut hh, &mut h_split);
10075 let (h_end, h_kv) = (host_states(self), host_kv(self));
10076 let t_h = t.elapsed().as_secs_f64();
10077
10078 let t = std::time::Instant::now();
10079 self.clear_sequence_state();
10080 let (mut hg, mut g_split) = (vec![0f32; n * hs], None);
10081 let g_run = graph_walk(self, 0, n, &mut hg, &mut g_split);
10082 let (g_end, g_kv) = (graph_states(self), graph_kv(self));
10083 let t_g = t.elapsed().as_secs_f64();
10084
10085 let t = std::time::Instant::now();
10086 self.clear_sequence_state();
10087 let (mut hm, mut m_split) = (vec![0f32; n * hs], None);
10088 host_walk(self, 0, split, &mut hm, &mut m_split);
10089 let m_run = graph_walk(self, split, n, &mut hm, &mut None);
10090 let (m_end, m_kv) = (graph_states(self), graph_kv(self));
10091 let t_m = t.elapsed().as_secs_f64();
10092 self.clear_sequence_state();
10093 g_run?;
10094 m_run?;
10095
10096 let (sh, sg, sm) = (
10097 score(self, &hh, 0),
10098 score(self, &hg, 0),
10099 score(self, &hm, split),
10100 );
10101 let mut r = String::new();
10102 let _ = writeln!(
10103 r,
10104 "prefill parity: {n} tokens, split {split}, host chunk {host_chunk}, graph k {bk} \
10105 (host {t_h:.1} s, graph {t_g:.1} s, host+graph {t_m:.1} s)"
10106 );
10107 let _ = writeln!(
10108 r,
10109 " bin | NLL host graph h+g | top1 host=graph h+g=graph | hidden rel host:graph h+g:graph"
10110 );
10111 let bin = 256usize;
10112 let mut b0 = 0;
10113 while b0 + 1 < n {
10114 let b1 = (b0 + bin).min(n - 1);
10115 let mean = |s: &[(f64, usize)], off: usize| -> Option<f64> {
10116 let v: Vec<f64> = (b0.max(off)..b1).map(|p| s[p - off].0).collect();
10117 (!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
10118 };
10119 let agree = |a: &[(f64, usize)], ao: usize, b: &[(f64, usize)], bo: usize| {
10120 let ps: Vec<usize> = (b0.max(ao).max(bo)..b1).collect();
10121 if ps.is_empty() {
10122 return None;
10123 }
10124 let same = ps.iter().filter(|&&p| a[p - ao].1 == b[p - bo].1).count();
10125 Some(100.0 * same as f64 / ps.len() as f64)
10126 };
10127 let hid_rel = |a: &[f32], from: usize| -> Option<f64> {
10128 (b0.max(from)..b1)
10129 .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &hg[p * hs..(p + 1) * hs]))
10130 .reduce(f64::max)
10131 };
10132 let f =
10133 |v: Option<f64>, prec: usize| v.map_or("-".to_string(), |x| format!("{x:.prec$}"));
10134 let e = |v: Option<f64>| v.map_or("-".to_string(), |x| format!("{x:.1e}"));
10135 let _ = writeln!(
10136 r,
10137 " {b0:5}..{b1:<5} | {:>8} {:>6} {:>6} | {:>14} {:>9} | {:>16} {:>9}",
10138 f(mean(&sh, 0), 3),
10139 f(mean(&sg, 0), 3),
10140 f(mean(&sm, split), 3),
10141 f(agree(&sh, 0, &sg, 0), 1),
10142 f(agree(&sm, split, &sg, 0), 1),
10143 e(hid_rel(&hh, 0)),
10144 e(hid_rel(&hm, split)),
10145 );
10146 b0 = b1;
10147 }
10148 let ring_len = self
10150 .gdn_cfg
10151 .map(|c| c.conv_dim() * c.conv_kernel.saturating_sub(1))
10152 .unwrap_or(0);
10153 let state_line = |r: &mut String, label: &str, a: &[Vec<f32>], b: &[Vec<f32>]| {
10154 let _ = write!(r, " GDN {label} (ring/S rel):");
10155 let mut first = None;
10156 for li in (0..kinds.len()).filter(|&li| kinds[li] == 1) {
10157 let (x, y) = (&a[li], &b[li]);
10158 if x.len() != y.len() || x.len() <= ring_len {
10159 let _ = write!(r, " L{li} len {}/{}", x.len(), y.len());
10160 continue;
10161 }
10162 let (rr, rs) = (
10163 rel_l2(&x[..ring_len], &y[..ring_len]),
10164 rel_l2(&x[ring_len..], &y[ring_len..]),
10165 );
10166 if first.is_none() && rr.max(rs) > 1e-2 {
10167 first = Some(li);
10168 }
10169 let _ = write!(r, " L{li} {rr:.0e}/{rs:.0e}");
10170 }
10171 let _ = writeln!(
10172 r,
10173 "\n first GDN layer past 1e-2: {}",
10174 first.map_or("none".to_string(), |l| l.to_string())
10175 );
10176 };
10177 match (&h_split, &g_split) {
10178 (Some(a), Some(b)) => state_line(&mut r, "host vs graph at split", a, b),
10179 _ => {
10180 let _ = writeln!(r, " GDN at split: not captured");
10181 }
10182 }
10183 if let (Some(a), Some(b)) = (&m_split, &h_split) {
10184 state_line(&mut r, "h+g host part vs host at split", a, b);
10185 }
10186 state_line(&mut r, "host vs graph at end", &h_end, &g_end);
10187 state_line(&mut r, "h+g vs graph at end", &m_end, &g_end);
10188 let kv_line = |r: &mut String, label: &str, a: &[(Vec<f32>, Vec<f32>)], from: usize| {
10191 let _ = writeln!(r, " K/V {label} (worst bin K/V rel, first bin past 1e-2):");
10192 for li in (0..kinds.len()).filter(|&li| kinds[li] == 2) {
10193 let ((ak, av), (gk, gv)) = (&a[li], &g_kv[li]);
10194 if ak.len() != gk.len() || ak.is_empty() {
10195 let _ = writeln!(r, " L{li}: rows {}/{}", ak.len(), gk.len());
10196 continue;
10197 }
10198 let row = ak.len() / n;
10199 let (mut wk, mut wv, mut first) = (0f64, 0f64, None);
10200 let mut b0 = from;
10201 while b0 < n {
10202 let b1 = (b0 + bin).min(n);
10203 let (s0, s1) = (b0 * row, b1 * row);
10204 let (rk, rv) = (
10205 rel_l2(&ak[s0..s1], &gk[s0..s1]),
10206 rel_l2(&av[s0..s1], &gv[s0..s1]),
10207 );
10208 (wk, wv) = (wk.max(rk), wv.max(rv));
10209 if first.is_none() && rk.max(rv) > 1e-2 {
10210 first = Some(b0);
10211 }
10212 b0 = b1;
10213 }
10214 let _ = writeln!(
10215 r,
10216 " L{li}: {wk:.1e}/{wv:.1e} first {}",
10217 first.map_or("none".to_string(), |p| p.to_string())
10218 );
10219 }
10220 };
10221 kv_line(&mut r, "host vs graph", &h_kv, 0);
10222 kv_line(&mut r, "h+g vs graph", &m_kv, 0);
10223 Ok(r)
10224 }
10225
10226 fn parity_score(&self, ids: &[u32], rows: &[f32], from: usize) -> Vec<(f64, usize)> {
10230 let (n, hs) = (ids.len(), self.hidden_size);
10231 let vrows = self.weights.lm_head.rows();
10232 let vs = self.vocab_size.min(vrows);
10233 let mut out = Vec::with_capacity(n.saturating_sub(from + 1));
10234 let mut pos = from;
10235 while pos + 1 < n {
10236 let end = (pos + 32).min(n - 1);
10237 let sb = end - pos;
10238 let mut normed = vec![0f32; sb * hs];
10239 for j in 0..sb {
10240 let r = inference::rms_norm(
10241 &rows[(pos + j) * hs..(pos + j + 1) * hs],
10242 &self.weights.final_norm,
10243 self.rms_eps,
10244 self.norm_style,
10245 );
10246 normed[j * hs..(j + 1) * hs].copy_from_slice(&r);
10247 }
10248 let mut logits = vec![0f32; sb * vrows];
10249 self.weights
10250 .lm_head
10251 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10252 for j in 0..sb {
10253 let lg = &mut logits[j * vrows..j * vrows + vs];
10254 if let Some(mu) = self.logit_multiplier {
10255 lg.iter_mut().for_each(|v| *v *= mu);
10256 }
10257 if let Some(c) = self.final_softcap {
10258 softcap_in_place(lg, c, self.pool.as_deref());
10259 }
10260 let (mut max, mut top) = (f32::NEG_INFINITY, 0usize);
10261 for (i, &v) in lg.iter().enumerate() {
10262 if v > max {
10263 (max, top) = (v, i);
10264 }
10265 }
10266 let lse = lg
10267 .iter()
10268 .map(|&v| ((v - max) as f64).exp())
10269 .sum::<f64>()
10270 .ln()
10271 + max as f64;
10272 out.push((lse - lg[ids[pos + j + 1] as usize] as f64, top));
10273 }
10274 pos = end;
10275 }
10276 out
10277 }
10278
10279 pub fn o1_seal_parity_probe(
10293 &mut self,
10294 ids: &[u32],
10295 cfg: crate::nystrom::O1Cfg,
10296 requested: usize,
10297 bk: usize,
10298 ) -> Result<String, String> {
10299 use std::fmt::Write as _;
10300 let (n, hs) = (ids.len(), self.hidden_size);
10301 self.set_o1(Some(cfg));
10302 let split = self
10303 .o1_effective_boundary(requested)
10304 .ok_or("o1 parity: no O(1) configuration")?;
10305 if split + 1 >= n || bk == 0 || !self.can_prefill_batched() {
10306 return Err(format!(
10307 "o1 parity: needs the boundary {split} inside {n} tokens, k > 0 and a batched host prefill"
10308 ));
10309 }
10310 let o1_layers: Vec<usize> = (0..self.num_layers)
10311 .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
10312 .collect();
10313 let graph_rows = |p: &mut Self, from: usize, to: usize, rows: &mut [f32]| {
10314 let mut pos = from;
10315 while pos < to {
10316 let end = (pos + bk).min(to);
10317 let k = end - pos;
10318 let mut hid = vec![0f32; k * hs];
10319 for (j, &id) in ids[pos..end].iter().enumerate() {
10320 hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10321 }
10322 let positions: Vec<usize> = (pos..end).collect();
10323 let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10324 if outcome != crate::gpu::BatchGraphOutcome::Completed {
10325 return Err(format!(
10326 "o1 parity: batch graph {outcome:?} at position {pos}"
10327 ));
10328 }
10329 rows[pos * hs..end * hs].copy_from_slice(&hid);
10330 pos = end;
10331 }
10332 Ok(())
10333 };
10334 let seal_from = |p: &mut Self, trace: &[(Vec<f32>, Vec<f32>, Vec<f32>)]| {
10337 for (&li, (q, k, v)) in o1_layers.iter().zip(trace) {
10338 let c = &mut p.kv_cache.layers[li];
10339 let (row, qrow) = (k.len() / split, q.len() / split);
10340 for pos in 0..split {
10341 c.append(
10342 &k[pos * row..(pos + 1) * row],
10343 &v[pos * row..(pos + 1) * row],
10344 &[],
10345 );
10346 c.o1_push_q(&q[pos * qrow..(pos + 1) * qrow]);
10347 }
10348 }
10349 p.o1_seal_checked()
10350 };
10351 let steps: usize = std::env::var("CMF_PARITY_GREEDY")
10354 .ok()
10355 .and_then(|v| v.parse().ok())
10356 .unwrap_or(40);
10357 let token_decode = std::env::var("CMF_PARITY_DECODE").as_deref() == Ok("token");
10360 let greedy = |p: &mut Self, rows: &[f32]| -> Result<Vec<u32>, String> {
10361 let argmax = |lg: &[f32], vs: usize| {
10362 (0..vs.min(lg.len())).fold(0, |b, i| if lg[i] > lg[b] { i } else { b }) as u32
10363 };
10364 let normed = inference::rms_norm(
10365 &rows[(n - 1) * hs..n * hs],
10366 &p.weights.final_norm,
10367 p.rms_eps,
10368 p.norm_style,
10369 );
10370 let mut out = vec![argmax(&p.lm_head_forward(&normed), p.vocab_size)];
10371 for s in 1..steps {
10372 let (tok, at) = (out[s - 1], n + s - 1);
10373 let lg = if token_decode {
10374 p.decode_step_logits(tok, at)
10375 } else {
10376 let mut hid = p.embed_single(tok);
10377 let outcome = p.try_batch_graph_wgpu(&mut hid, &[at], 1, None);
10378 if outcome != crate::gpu::BatchGraphOutcome::Completed {
10379 return Err(format!("o1 parity: greedy step {s} {outcome:?}"));
10380 }
10381 let normed =
10382 inference::rms_norm(&hid, &p.weights.final_norm, p.rms_eps, p.norm_style);
10383 p.lm_head_forward(&normed)
10384 };
10385 out.push(argmax(&lg, p.vocab_size));
10386 }
10387 Ok(out)
10388 };
10389
10390 self.clear_sequence_state();
10396 self.o1_begin_with_prefix(Some(requested));
10397 let walk_chunk = self.prefill_chunk().max(1);
10398 let walk_from = (split - 1) / walk_chunk * walk_chunk;
10399 let mut ow = vec![0f32; n * hs];
10400 let mut pos = 0;
10401 while pos < split {
10402 let end = (pos + walk_chunk).min(split);
10403 let hb = self.prefill_batch(&ids[pos..end], pos);
10404 ow[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10405 pos = end;
10406 }
10407 self.o1_seal_checked()?;
10408 let ow_run = graph_rows(self, split, n, &mut ow);
10409 let ow_text = ow_run.clone().and_then(|_| greedy(self, &ow));
10410
10411 let t = std::time::Instant::now();
10415 self.clear_sequence_state();
10416 self.o1_begin_with_prefix(Some(split + 1));
10417 let mut oh = vec![0f32; n * hs];
10418 let mut pos = 0;
10419 while pos < split {
10420 let end = (pos + 128).min(split);
10421 let hb = self.prefill_batch(&ids[pos..end], pos);
10422 oh[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10423 pos = end;
10424 }
10425 let mut host = Vec::with_capacity(o1_layers.len());
10426 for &li in &o1_layers {
10427 let c = &self.kv_cache.layers[li];
10428 let q = c
10429 .o1_q_trace()
10430 .ok_or("o1 parity: host walk sealed early")?
10431 .to_vec();
10432 let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10433 let (mut k, mut v) = (vec![0f32; split * nkv * hd], vec![0f32; split * nkv * hd]);
10434 for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10435 for p in 0..split {
10436 let d = (p * nkv + h) * hd;
10437 k[d..d + hd].copy_from_slice(&kh[p * hd..(p + 1) * hd]);
10438 v[d..d + hd].copy_from_slice(&vh[p * hd..(p + 1) * hd]);
10439 }
10440 }
10441 host.push((q, k, v));
10442 }
10443 for &li in &o1_layers {
10444 self.kv_cache.layers[li].o1_set_boundary(split);
10445 }
10446 self.o1_seal_checked()?;
10447 let oh_run = graph_rows(self, split, n, &mut oh);
10448 let t_oh = t.elapsed().as_secs_f64();
10449 let oh_text = oh_run.clone().and_then(|_| greedy(self, &oh));
10450
10451 self.clear_sequence_state();
10455 self.o1_begin_with_prefix(Some(requested));
10456 let mut og = vec![0f32; n * hs];
10457 graph_rows(self, 0, split, &mut og)?;
10458 let perturb: f32 = std::env::var("CMF_PARITY_PERTURB")
10459 .ok()
10460 .and_then(|v| v.parse().ok())
10461 .unwrap_or(0.0);
10462 if perturb > 0.0 {
10463 let mut seed = 0x9e37_79b9_7f4a_7c15u64;
10464 let noisy: Vec<_> = host
10465 .iter()
10466 .map(|(q, k, v)| {
10467 let mut jitter = |x: &f32| {
10468 seed ^= seed << 13;
10469 seed ^= seed >> 7;
10470 seed ^= seed << 17;
10471 x * if seed & 1 == 0 {
10472 1.0 + perturb
10473 } else {
10474 1.0 - perturb
10475 }
10476 };
10477 let k: Vec<f32> = k.iter().map(&mut jitter).collect();
10478 let v: Vec<f32> = v.iter().map(&mut jitter).collect();
10479 (q.clone(), k, v)
10480 })
10481 .collect();
10482 seal_from(self, &noisy)?;
10483 } else {
10484 seal_from(self, &host)?;
10485 }
10486 let og_run = graph_rows(self, split, n, &mut og);
10487 let og_text = og_run.clone().and_then(|_| greedy(self, &og));
10488
10489 self.clear_sequence_state();
10491 self.o1_begin_with_prefix(Some(requested));
10492 let mut od = vec![0f32; n * hs];
10493 graph_rows(self, 0, split, &mut od)?;
10494 let mut dev = Vec::with_capacity(o1_layers.len());
10495 let mut kv_rel = Vec::with_capacity(o1_layers.len());
10496 for (&li, (q, hk, hv)) in o1_layers.iter().zip(&host) {
10497 let c = &self.kv_cache.layers[li];
10498 let (k, v) = crate::gpu::graph_kv_read_rows(
10499 self.graph_kv_id,
10500 &[(li, 0, split)],
10501 c.num_kv_heads,
10502 c.head_dim,
10503 )
10504 .and_then(|mut rows| rows.pop())
10505 .ok_or_else(|| format!("o1 parity: no graph K/V rows for layer {li}"))?;
10506 kv_rel.push((li, rel_l2(hk, &k), rel_l2(hv, &v)));
10507 dev.push((q.clone(), k, v));
10508 }
10509 seal_from(self, &dev)?;
10510 let od_run = graph_rows(self, split, n, &mut od);
10511 let od_text = od_run.clone().and_then(|_| greedy(self, &od));
10512
10513 self.clear_sequence_state();
10515 self.set_o1(None);
10516 let mut xx = vec![0f32; n * hs];
10517 let x_run = graph_rows(self, 0, n, &mut xx);
10518 let x_text = x_run.clone().and_then(|_| greedy(self, &xx));
10519 self.clear_sequence_state();
10520 oh_run?;
10521 ow_run?;
10522 og_run?;
10523 od_run?;
10524 x_run?;
10525
10526 let sx = self.parity_score(ids, &xx, split);
10527 let arms = [
10528 ("OH", self.parity_score(ids, &oh, split)),
10529 ("OG", self.parity_score(ids, &og, split)),
10530 ("OD", self.parity_score(ids, &od, split)),
10531 ("OW", self.parity_score(ids, &ow, split)),
10532 ];
10533 let mut r = String::new();
10534 let _ = writeln!(
10535 r,
10536 "o1 seal parity: {n} tokens, boundary {split}, graph k {bk} (OH {t_oh:.1} s), \
10537 OW chunk {walk_chunk}, boundary chunk {walk_from}..{split}"
10538 );
10539 let _ = write!(r, " seal K/V rel host vs graph:");
10540 for (li, rk, rv) in &kv_rel {
10541 let _ = write!(r, " L{li} {rk:.0e}/{rv:.0e}");
10542 }
10543 let worst = |a: &[f32], p0: usize, p1: usize| {
10546 (p0..p1)
10547 .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &xx[p * hs..(p + 1) * hs]))
10548 .fold(0f64, f64::max)
10549 };
10550 let _ = writeln!(
10551 r,
10552 "\n prefix rows rel vs exact graph, [0,{walk_from}) / [{walk_from},{split}): \
10553 OH {:.1e} / {:.1e}, OW {:.1e} / {:.1e}",
10554 worst(&oh, 0, walk_from),
10555 worst(&oh, walk_from, split),
10556 worst(&ow, 0, walk_from),
10557 worst(&ow, walk_from, split),
10558 );
10559 let _ = writeln!(
10560 r,
10561 " bin | NLL exact OH OG OD OW | top1=exact OH OG OD OW"
10562 );
10563 let mean = |s: &[(f64, usize)], b0: usize, b1: usize| {
10564 (b0..b1).map(|p| s[p - split].0).sum::<f64>() / (b1 - b0).max(1) as f64
10565 };
10566 let agree = |s: &[(f64, usize)], b0: usize, b1: usize| {
10567 let same = (b0..b1)
10568 .filter(|&p| s[p - split].1 == sx[p - split].1)
10569 .count();
10570 100.0 * same as f64 / (b1 - b0).max(1) as f64
10571 };
10572 let mut b0 = split;
10573 let mut total = [0f64; 5];
10574 while b0 + 1 < n {
10575 let b1 = ((b0 / 256 + 1) * 256).min(n - 1);
10576 let _ = writeln!(
10577 r,
10578 " {b0:5}..{b1:<5} | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | {:>12.1} {:>5.1} {:>5.1} {:>5.1}",
10579 mean(&sx, b0, b1),
10580 mean(&arms[0].1, b0, b1),
10581 mean(&arms[1].1, b0, b1),
10582 mean(&arms[2].1, b0, b1),
10583 mean(&arms[3].1, b0, b1),
10584 agree(&arms[0].1, b0, b1),
10585 agree(&arms[1].1, b0, b1),
10586 agree(&arms[2].1, b0, b1),
10587 agree(&arms[3].1, b0, b1),
10588 );
10589 b0 = b1;
10590 }
10591 for (i, s) in std::iter::once(&sx)
10592 .chain(arms.iter().map(|a| &a.1))
10593 .enumerate()
10594 {
10595 total[i] = s.iter().map(|x| x.0).sum::<f64>() / s.len().max(1) as f64;
10596 }
10597 let _ = writeln!(
10598 r,
10599 " all | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | ppl ratio vs exact OH ×{:.3} OG ×{:.3} OD ×{:.3} OW ×{:.3}",
10600 total[0],
10601 total[1],
10602 total[2],
10603 total[3],
10604 total[4],
10605 (total[1] - total[0]).exp(),
10606 (total[2] - total[0]).exp(),
10607 (total[3] - total[0]).exp(),
10608 (total[4] - total[0]).exp(),
10609 );
10610 for (name, text) in [
10611 ("X ", x_text),
10612 ("OH", oh_text),
10613 ("OG", og_text),
10614 ("OD", od_text),
10615 ("OW", ow_text),
10616 ] {
10617 let s = text.map_or_else(|e| e, |t| self.tokenizer.decode(&t));
10618 let _ = writeln!(r, " greedy {name}: {:?}", s);
10619 }
10620 Ok(r)
10621 }
10622
10623 fn nll_ids_inner(
10624 &mut self,
10625 ids: &[u32],
10626 start: usize,
10627 task_mask: Option<&TaskMask>,
10628 ) -> Result<(f64, usize), String> {
10629 self.nll_begin()?;
10630 #[cfg(target_os = "macos")]
10632 if task_mask.is_none() && self.is_block_diffusion() {
10633 if let Some(r) = self.clover_metal_nll(ids, start) {
10634 self.nll_end();
10635 return r;
10636 }
10637 }
10638 let result: Result<(f64, usize), String> = (|| {
10639 let mut nll = 0f64;
10640 let mut cnt = 0usize;
10641 let (graph_quality, fused_head_quality) = nll_graph_policy(
10654 task_mask.is_none(),
10655 self.graph_prefill_preferred(),
10656 crate::gpu::q1_force(),
10657 );
10658 self.graph_head_required = fused_head_quality;
10659 self.graph_want_logits = fused_head_quality;
10660 #[cfg(target_os = "macos")]
10661 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
10662 match self.nll_batch_metal(ids, start) {
10663 MetalBatchNllOutcome::Completed(nll, count) => {
10664 return Ok((nll, count));
10665 }
10666 MetalBatchNllOutcome::Declined => {}
10667 MetalBatchNllOutcome::Failed(err) => return Err(err),
10668 }
10669 }
10670 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
10675 if self.can_prefill_batched() && !graph_quality && !force_serial {
10676 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
10685 .ok()
10686 .and_then(|v| v.parse().ok())
10687 .filter(|&c: &usize| c > 0)
10688 .unwrap_or(128);
10689 const LM_SUB: usize = 32;
10690 let n = ids.len().saturating_sub(1);
10691 let hs = self.hidden_size;
10692 let rows = self.weights.lm_head.rows();
10693 let mut pos = 0usize;
10694 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
10695 while pos < n {
10696 let end = (pos + chunk_len).min(n);
10697 let bsz = end - pos;
10698 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
10699 self.nll_check_graph("batched prefill", pos)?;
10700 if state_trace && end % 256 == 0 {
10701 self.trace_recurrent_state(end);
10702 }
10703 let mut k0 = 0usize;
10704 while k0 < bsz {
10705 let k1 = (k0 + LM_SUB).min(bsz);
10706 let sb = k1 - k0;
10707 if pos + k1 <= start {
10710 k0 = k1;
10711 continue;
10712 }
10713 let mut normed = vec![0.0f32; sb * hs];
10714 for k in 0..sb {
10715 let r = inference::rms_norm(
10716 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
10717 &self.weights.final_norm,
10718 self.rms_eps,
10719 self.norm_style,
10720 );
10721 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
10722 }
10723 let mut logits = vec![0.0f32; sb * rows];
10724 self.weights
10725 .lm_head
10726 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10727 for k in 0..sb {
10728 if pos + k0 + k < start {
10729 continue;
10730 }
10731 self.nll_check_graph("batched score row", pos + k0 + k)?;
10732 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
10733 if let Some(mu) = self.logit_multiplier {
10734 for v in lg.iter_mut() {
10735 *v *= mu;
10736 }
10737 }
10738 if let Some(c) = self.final_softcap {
10742 softcap_in_place(lg, c, self.pool.as_deref());
10743 }
10744 if let Some(cm) = self.head_clusters.clone() {
10747 self.hierarchical_head_logprobs(
10748 &normed[k * hs..(k + 1) * hs],
10749 &cm,
10750 lg,
10751 );
10752 }
10753 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
10754 let target = ids[pos + k0 + k + 1] as usize;
10755 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10756 let lse: f64 = lg
10757 .iter()
10758 .map(|&v| ((v - max) as f64).exp())
10759 .sum::<f64>()
10760 .ln()
10761 + max as f64;
10762 nll += lse - lg[target] as f64;
10763 cnt += 1;
10764 if std::env::var("CMF_PPL_TRACE").is_ok() {
10765 let top = lg
10766 .iter()
10767 .enumerate()
10768 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10769 .map(|(i, _)| i)
10770 .unwrap_or(0);
10771 eprintln!(
10772 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
10773 pos + k0 + k,
10774 target,
10775 lse - lg[target] as f64,
10776 top,
10777 lg[target],
10778 lg[top]
10779 );
10780 }
10781 }
10782 k0 = k1;
10783 }
10784 pos = end;
10785 }
10786 return Ok((nll, cnt));
10787 }
10788 for pos in 0..ids.len().saturating_sub(1) {
10789 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
10790 self.nll_check_graph("serial forward", pos)?;
10791 let out_of_band = self.graph_logits.take();
10799 if self.graph_head_required && out_of_band.is_none() {
10800 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
10801 return Err(format!(
10802 "fused Metal graph head did not complete at NLL position {pos}"
10803 ));
10804 }
10805 if pos < start {
10806 continue;
10807 }
10808 let logits = match out_of_band {
10809 Some(lg) => lg,
10810 None => {
10811 let normed = inference::rms_norm(
10812 &hidden,
10813 &self.weights.final_norm,
10814 self.rms_eps,
10815 self.norm_style,
10816 );
10817 self.lm_head_forward(&normed)
10821 }
10822 };
10823 let target = ids[pos + 1] as usize;
10824 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10825 let lse: f64 = logits
10826 .iter()
10827 .map(|&v| ((v - max) as f64).exp())
10828 .sum::<f64>()
10829 .ln()
10830 + max as f64;
10831 let tok_nll = lse - logits[target] as f64;
10832 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10833 let top = logits
10834 .iter()
10835 .enumerate()
10836 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10837 .map(|(i, _)| i)
10838 .unwrap_or(0);
10839 eprintln!(
10840 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10841 logits[target], logits[top]
10842 );
10843 }
10844 nll += tok_nll;
10845 cnt += 1;
10846 }
10847 Ok((nll, cnt))
10848 })();
10849 self.nll_end();
10850 result
10851 }
10852
10853 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
10858 let normed = inference::rms_norm(
10859 hidden,
10860 &self.weights.final_norm,
10861 self.rms_eps,
10862 self.norm_style,
10863 );
10864 let mut logits = self.lm_head_forward(&normed);
10867 let target = target as usize;
10868 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10869 let lse: f64 = logits
10870 .iter()
10871 .map(|&v| ((v - max) as f64).exp())
10872 .sum::<f64>()
10873 .ln()
10874 + max as f64;
10875 let tok_nll = lse - logits[target] as f64;
10876 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10877 let top = logits
10878 .iter()
10879 .enumerate()
10880 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10881 .map(|(i, _)| i)
10882 .unwrap_or(0);
10883 eprintln!(
10884 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10885 logits[target], logits[top]
10886 );
10887 }
10888 attention::recycle_buf(&mut logits);
10889 tok_nll
10890 }
10891
10892 fn trace_recurrent_state(&self, pos: usize) {
10900 let stats = |v: &[f32]| -> (f64, f64) {
10901 if v.is_empty() {
10902 return (0.0, 0.0);
10903 }
10904 let (mut ss, mut mx) = (0f64, 0f64);
10905 for &x in v {
10906 ss += (x as f64) * (x as f64);
10907 mx = mx.max((x as f64).abs());
10908 }
10909 ((ss / v.len() as f64).sqrt(), mx)
10910 };
10911 for (li, l) in self.kv_cache.layers.iter().enumerate() {
10912 let lw = &self.weights.layers[self.phys_layer(li)];
10913 let (kind, s_len) = match &lw.attn {
10914 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
10915 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
10916 AttnKind::Bounded(_) => ("bounded", 0),
10917 AttnKind::Full { .. } => ("full", 0),
10918 _ => ("other", 0),
10919 };
10920 let (rms, max) = stats(&l.linear_state);
10921 let s_part = if kind == "vmf" {
10922 &l.linear_state[..s_len.min(l.linear_state.len())]
10923 } else if kind == "gdn" {
10924 let ring = l
10925 .linear_state
10926 .len()
10927 .saturating_sub(s_len)
10928 .min(l.linear_state.len());
10929 &l.linear_state[ring..]
10930 } else {
10931 &l.linear_state[..0]
10932 };
10933 let (s_rms, s_max) = stats(s_part);
10934 let (ring_rms, ring_len) = match &l.bounded {
10935 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
10936 None => (0.0, 0),
10937 };
10938 eprintln!(
10939 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
10940 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
10941 l.linear_state.len(),
10942 l.seq_len
10943 );
10944 }
10945 }
10946
10947 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
10965 self.nll_begin()?;
10970 let requested_prefix = (prefill > 0).then_some(prefill);
10971 self.o1_begin_with_prefix(requested_prefix);
10972 let n = ids.len().saturating_sub(1);
10973 let requested_start = prefill.min(n);
10974 let exact_end = if self.o1_active() {
10979 match requested_prefix {
10980 Some(requested) => self.o1_effective_boundary(requested),
10981 None => self
10982 .o1_cfg
10983 .as_ref()
10984 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
10985 }
10986 .unwrap_or(requested_start)
10987 .min(n)
10988 } else {
10989 requested_start
10990 };
10991 let mut nll = 0f64;
10992 let mut cnt = 0usize;
10993
10994 let mut pos = 0usize;
10998 if self.can_prefill_batched() {
10999 const CHUNK: usize = 128;
11000 while pos < exact_end {
11001 let end = (pos + CHUNK).min(exact_end);
11002 let hiddens = self.prefill_batch(&ids[pos..end], pos);
11003 if self
11004 .graph_failed
11005 .swap(false, std::sync::atomic::Ordering::Relaxed)
11006 {
11007 self.cancel
11008 .store(false, std::sync::atomic::Ordering::Relaxed);
11009 self.nll_end();
11010 return Err("GPU graph failed during O(1) NLL prefix".into());
11011 }
11012 for row in 0..end - pos {
11013 let score_pos = pos + row;
11014 if score_pos >= requested_start && score_pos < n {
11015 nll += self.nll_from_hidden(
11016 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
11017 ids[score_pos + 1],
11018 score_pos,
11019 );
11020 cnt += 1;
11021 }
11022 }
11023 pos = end;
11024 }
11025 } else {
11026 while pos < exact_end {
11027 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11028 if self
11029 .graph_failed
11030 .swap(false, std::sync::atomic::Ordering::Relaxed)
11031 {
11032 self.cancel
11033 .store(false, std::sync::atomic::Ordering::Relaxed);
11034 self.nll_end();
11035 return Err("GPU graph failed during O(1) NLL prefix".into());
11036 }
11037 if pos >= requested_start && pos < n {
11038 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11039 cnt += 1;
11040 }
11041 pos += 1;
11042 }
11043 }
11044 self.o1_seal_checked().map_err(|err| {
11045 self.nll_end();
11046 err
11047 })?;
11048
11049 let batch_k = std::env::var("CMF_BATCH_K")
11058 .ok()
11059 .and_then(|v| v.parse::<usize>().ok())
11060 .unwrap_or(0);
11061 let batch_admitted = batch_k > 0
11062 && self.can_prefill_batched()
11063 && self.o1_active()
11064 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
11065 && (0..self.num_layers).all(|li| {
11066 let cache = &self.kv_cache.layers[self.phys_layer(li)];
11067 cache.o1.is_none() || cache.o1_views().is_some()
11068 });
11069 if std::env::var("CMF_GRAPH_PROF").is_ok() {
11070 eprintln!(
11071 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
11072 batch_admitted,
11073 batch_k,
11074 n.saturating_sub(exact_end),
11075 );
11076 }
11077 let mut batch_completed = false;
11078 if batch_admitted && exact_end < n {
11079 let hs = self.hidden_size;
11080 let mut batch_pos = exact_end;
11081 while batch_pos < n {
11082 let end = (batch_pos + batch_k).min(n);
11083 let bk = end - batch_pos;
11084 let mut hiddens = vec![0.0f32; bk * hs];
11085 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
11086 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
11087 }
11088 let positions: Vec<usize> = (batch_pos..end).collect();
11089 let t_batch = std::time::Instant::now();
11090 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
11091 if std::env::var("CMF_GRAPH_PROF").is_ok() {
11092 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
11093 eprintln!(
11094 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
11095 batch_pos,
11096 end.saturating_sub(1),
11097 bk as f64 / (ms / 1000.0),
11098 );
11099 }
11100 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
11101 self.nll_end();
11102 return Err(err);
11103 }
11104 match outcome {
11105 crate::gpu::BatchGraphOutcome::Completed => {
11106 batch_completed = true;
11107 for row in 0..bk {
11108 nll += self.nll_from_hidden(
11109 &hiddens[row * hs..(row + 1) * hs],
11110 ids[batch_pos + row + 1],
11111 batch_pos + row,
11112 );
11113 cnt += 1;
11114 }
11115 batch_pos = end;
11116 }
11117 crate::gpu::BatchGraphOutcome::Declined => {
11118 if batch_completed {
11119 self.nll_end();
11120 return Err(format!(
11121 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
11122 ));
11123 }
11124 break;
11125 }
11126 crate::gpu::BatchGraphOutcome::Failed => {
11127 self.nll_end();
11128 return Err(format!(
11129 "O(1) NLL batch graph failed after admission at position {batch_pos}"
11130 ));
11131 }
11132 }
11133 }
11134 if batch_completed && cnt == n.saturating_sub(requested_start) {
11135 self.nll_end();
11136 return Ok((nll, cnt));
11137 }
11138 }
11139
11140 for pos in exact_end..n {
11145 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11146 if self
11147 .graph_failed
11148 .swap(false, std::sync::atomic::Ordering::Relaxed)
11149 {
11150 self.cancel
11151 .store(false, std::sync::atomic::Ordering::Relaxed);
11152 self.nll_end();
11153 return Err(format!(
11154 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
11155 ));
11156 }
11157 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11158 cnt += 1;
11159 }
11160 self.nll_end();
11161 Ok((nll, cnt))
11162 }
11163
11164 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
11172 self.clear_sequence_state();
11173 let n = ids.len().saturating_sub(1);
11174 let mut correct = Vec::with_capacity(n);
11175 let mut pmax = Vec::with_capacity(n);
11176 for pos in 0..n {
11177 let emb = self.embed_single(ids[pos]);
11178 let hidden = self.forward_layers(&emb, pos, None);
11179 let logits = if let Some(logits) = self.graph_logits.take() {
11180 logits
11181 } else {
11182 let normed = inference::rms_norm(
11183 &hidden,
11184 &self.weights.final_norm,
11185 self.rms_eps,
11186 self.norm_style,
11187 );
11188 self.lm_head_forward(&normed)
11192 };
11193 let target = ids[pos + 1] as usize;
11194 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
11195 for (i, &v) in logits.iter().enumerate() {
11196 if v > mval {
11197 mval = v;
11198 amax = i;
11199 }
11200 }
11201 correct.push(amax == target);
11202 let row: Vec<f32> = temps
11203 .iter()
11204 .map(|&t| {
11205 let tt = t.max(1e-3);
11206 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
11207 1.0 / s.max(1e-12) })
11209 .collect();
11210 pmax.push(row);
11211 }
11212 self.clear_sequence_state();
11213 (correct, pmax)
11214 }
11215
11216 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
11223 if self.dyn_router.is_none() {
11224 return Ok((self.ppl_ids(ids)?, 0));
11225 }
11226 self.nll_begin()?;
11227 let saved_active = self.dyn_active;
11228 let mut router = self
11229 .dyn_router
11230 .take()
11231 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
11232 router.reset();
11233 self.dyn_phi_seen = 0;
11234 let _ = self.set_active_skill(None);
11235
11236 let result: Result<(f64, usize), String> = (|| {
11237 let mut nll = 0f64;
11238 let mut cnt = 0usize;
11239 for pos in 0..ids.len().saturating_sub(1) {
11240 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11241 self.nll_check_graph("dynamic serial forward", pos)?;
11242 let out_of_band = self.graph_logits.take();
11243 let mut logits = match out_of_band {
11244 Some(lg) => lg,
11245 None => {
11246 let normed = inference::rms_norm(
11247 &hidden,
11248 &self.weights.final_norm,
11249 self.rms_eps,
11250 self.norm_style,
11251 );
11252 self.lm_head_forward(&normed)
11256 }
11257 };
11258 let target = ids[pos + 1] as usize;
11259 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11260 let lse: f64 = logits
11261 .iter()
11262 .map(|&v| ((v - max) as f64).exp())
11263 .sum::<f64>()
11264 .ln()
11265 + max as f64;
11266 let tok_nll = lse - logits[target] as f64;
11267 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11268 let top = logits
11269 .iter()
11270 .enumerate()
11271 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11272 .map(|(i, _)| i)
11273 .unwrap_or(0);
11274 eprintln!(
11275 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11276 logits[target], logits[top]
11277 );
11278 }
11279 nll += tok_nll;
11280 cnt += 1;
11281 attention::recycle_buf(&mut logits);
11282 let phi = self.dyn_phi_ema.clone();
11284 if let Some(new_active) = router.step(&phi, pos) {
11285 let _ = self.set_active_skill(new_active);
11286 }
11287 }
11288 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
11289 })();
11290
11291 let _ = self.set_active_skill(saved_active);
11294 self.dyn_router = Some(router);
11295 self.nll_end();
11296 result
11297 }
11298
11299 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
11301 self.clear_sequence_state();
11302 let mut acc = vec![0f32; self.hidden_size];
11303 for (pos, &id) in ids.iter().enumerate() {
11304 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11305 for (a, v) in acc.iter_mut().zip(&h) {
11306 *a += v;
11307 }
11308 }
11309 let n = ids.len().max(1) as f32;
11310 for a in acc.iter_mut() {
11311 *a /= n;
11312 }
11313 self.clear_sequence_state();
11314 acc
11315 }
11316
11317 pub fn probe_phi_span(
11331 &mut self,
11332 ids: &[u32],
11333 layer: usize,
11334 span: std::ops::Range<usize>,
11335 ) -> Vec<f32> {
11336 #[cfg(target_os = "macos")]
11337 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11338 let end = span.end.min(ids.len());
11339 let start = span.start.min(end);
11340 let reset = |p: &mut Self| p.clear_sequence_state();
11341 reset(self);
11342 let mut acc = vec![0f32; self.hidden_size];
11343 for (pos, &id) in ids[..end].iter().enumerate() {
11344 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11345 if pos >= start {
11346 for (a, v) in acc.iter_mut().zip(&h) {
11347 *a += v;
11348 }
11349 }
11350 }
11351 let n = end - start;
11352 if n > 0 {
11353 let n = n as f32;
11354 for a in acc.iter_mut() {
11355 *a /= n;
11356 }
11357 }
11358 reset(self);
11359 acc
11360 }
11361
11362 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
11369 #[cfg(target_os = "macos")]
11370 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11371 self.graph_logits = None;
11372 let hidden = self.forward_layers(&self.embed_single(token), position, None);
11373 if let Some(logits) = self.graph_logits.take() {
11374 return logits;
11375 }
11376 inference::rms_norm_into(
11377 &hidden,
11378 &self.weights.final_norm,
11379 self.rms_eps,
11380 self.norm_style,
11381 &mut self.ws.n1,
11382 );
11383 self.lm_head_forward(&self.ws.n1)
11384 }
11385
11386 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
11392 self.prefill_batch_masked(ids, start_pos, None)
11393 }
11394
11395 fn prefill_batch_masked(
11401 &mut self,
11402 ids: &[u32],
11403 start_pos: usize,
11404 task_mask: Option<&TaskMask>,
11405 ) -> Vec<f32> {
11406 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
11407 }
11408
11409 fn prefill_rows(
11417 &mut self,
11418 ids: &[u32],
11419 pos: usize,
11420 task_mask: Option<&TaskMask>,
11421 ) -> Result<Vec<f32>, String> {
11422 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
11423 }
11424
11425 fn prefill_input_rows(
11426 &mut self,
11427 input: PrefillIn<'_>,
11428 pos: usize,
11429 task_mask: Option<&TaskMask>,
11430 ) -> Result<Vec<f32>, String> {
11431 self.mimo_moe_prepare();
11432 let hs = self.hidden_size;
11433 let bk = match input {
11434 PrefillIn::Ids(ids) => ids.len(),
11435 PrefillIn::Hidden(rows) => rows.len() / hs,
11436 };
11437 #[cfg(not(target_os = "macos"))]
11438 if task_mask.is_none()
11439 && !self.o1_active()
11440 && bk > 1
11441 && (self.batch_prefix_prefill()
11442 || (self.verify_exact_moe
11443 && crate::gpu::enabled_here()
11444 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
11445 {
11446 let mut hiddens = match input {
11447 PrefillIn::Hidden(rows) => rows.to_vec(),
11448 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
11449 };
11450 let positions: Vec<usize> = (pos..pos + bk).collect();
11451 let mut run = 0usize;
11452 match self.try_batch_graph_wgpu_prefix(
11453 &mut hiddens,
11454 &positions,
11455 bk,
11456 None,
11457 Some(&mut run),
11458 ) {
11459 crate::gpu::BatchGraphOutcome::Completed => {
11460 let out = if run < self.num_layers {
11461 self.prefill_batch_span(
11462 PrefillIn::Hidden(&hiddens),
11463 pos,
11464 None,
11465 run,
11466 self.num_layers,
11467 )
11468 } else {
11469 hiddens
11470 };
11471 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11472 Err("MiMo attention graph failed after admission".into())
11473 } else {
11474 Ok(out)
11475 };
11476 }
11477 crate::gpu::BatchGraphOutcome::Failed => {
11478 return Err("batched prefix prefill failed after admission".into());
11479 }
11480 crate::gpu::BatchGraphOutcome::Declined => {
11481 #[cfg(feature = "gpu")]
11483 self.pull_lagging_host_kv(0, self.num_layers, pos);
11484 }
11485 }
11486 }
11487 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
11488 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11489 Err("batch tail graph failed after admission".into())
11490 } else {
11491 Ok(out)
11492 }
11493 }
11494
11495 fn prefill_batch_span(
11501 &mut self,
11502 input: PrefillIn<'_>,
11503 start_pos: usize,
11504 task_mask: Option<&TaskMask>,
11505 from: usize,
11506 upto_excl: usize,
11507 ) -> Vec<f32> {
11508 let hs = self.hidden_size;
11509 let b = match input {
11510 PrefillIn::Ids(ids) => ids.len(),
11511 PrefillIn::Hidden(hb) => hb.len() / hs,
11512 };
11513 let upto_excl = upto_excl.min(self.num_layers);
11514 let mut h: Vec<f32>;
11518 let mut h_ready;
11519 match input {
11520 PrefillIn::Ids(_) => {
11521 h = vec![0.0; b * hs];
11522 h_ready = false;
11523 }
11524 PrefillIn::Hidden(hb) => {
11525 h = hb.to_vec();
11526 h_ready = true;
11527 }
11528 }
11529 let fill_h = |h: &mut Vec<f32>, me: &Self| {
11530 if let PrefillIn::Ids(ids) = input {
11531 for (bi, &id) in ids.iter().enumerate() {
11532 let e = me.embed_single(id);
11533 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
11534 }
11535 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11536 if let Ok(t) = tp.parse::<usize>() {
11537 if t >= start_pos && t < start_pos + ids.len() {
11538 let bi = t - start_pos;
11539 let row = &h[bi * hs..(bi + 1) * hs];
11540 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11541 eprintln!(
11542 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
11543 ids[bi],
11544 row[0],
11545 row[1],
11546 ids.len(),
11547 &ids[..ids.len().min(8)]
11548 );
11549 }
11550 }
11551 }
11552 }
11553 };
11554 let (_nkv, _hd, _rd, eps) = (
11555 self.num_kv_heads,
11556 self.head_dim,
11557 self.rotary_dim,
11558 self.rms_eps,
11559 );
11560 let pool = self.pool.clone();
11561 let norm_style = self.norm_style;
11562 self.mimo_moe_prepare();
11563 let automatic_gpu_prefix = self.automatic_gpu_prefix();
11564
11565 #[cfg(target_os = "macos")]
11566 let mut chunk_skip_until = 0usize;
11567 for li in from..upto_excl {
11568 let _capacity_tail = automatic_gpu_prefix
11569 .filter(|&prefix| {
11570 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
11571 })
11572 .map(|_| crate::gpu::enter_cpu_scope());
11573 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
11578 let _fast_gemm = self
11579 .proj_gate_sigmoid
11580 .then(crate::gpu::enter_prefill_fast_gemm);
11581 #[cfg(target_os = "macos")]
11587 if task_mask.is_none() {
11588 if li < chunk_skip_until {
11589 continue;
11590 }
11591 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
11597 fill_h(&mut h, self);
11598 h_ready = true;
11599 }
11600 let ids_for_embed = match input {
11601 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
11602 PrefillIn::Hidden(_) => None,
11603 };
11604 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
11605 if end > li {
11606 h_ready = true;
11607 chunk_skip_until = end;
11608 if self.is_loop_end(end - 1) && end < self.num_layers {
11611 for bi in 0..b {
11612 let normed = inference::rms_norm(
11613 &h[bi * hs..(bi + 1) * hs],
11614 &self.weights.final_norm,
11615 eps,
11616 norm_style,
11617 );
11618 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11619 }
11620 }
11621 continue;
11622 }
11623 }
11624 if !h_ready {
11625 fill_h(&mut h, self);
11626 h_ready = true;
11627 }
11628 if task_mask.is_none() && self.verify_exact_moe {
11629 let positions: Vec<_> = (start_pos..start_pos + b).collect();
11630 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
11631 crate::gpu::BatchGraphOutcome::Completed => continue,
11632 crate::gpu::BatchGraphOutcome::Failed => return h,
11633 crate::gpu::BatchGraphOutcome::Declined => {}
11634 }
11635 }
11636 #[cfg(feature = "gpu")]
11637 self.pull_lagging_host_kv(li, li + 1, start_pos);
11638 let lw = &self.weights.layers[self.phys_layer(li)];
11639 let t_attn = std::time::Instant::now();
11640 match &lw.attn {
11642 AttnKind::Kda(w) => {
11643 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
11645 let mut normed = vec![0.0f32; b * hs];
11646 for bi in 0..b {
11647 inference::rms_norm_into(
11648 &h[bi * hs..(bi + 1) * hs],
11649 &lw.input_norm,
11650 eps,
11651 norm_style,
11652 &mut normed[bi * hs..(bi + 1) * hs],
11653 );
11654 }
11655 let attn = crate::linear_core::kda_forward_batch(
11656 &normed,
11657 b,
11658 w,
11659 &cfg,
11660 &mut self.kv_cache.layers[li].linear_state,
11661 pool.as_deref(),
11662 );
11663 for (dst, &a) in h.iter_mut().zip(&attn) {
11664 *dst += a;
11665 }
11666 }
11667 AttnKind::LinearGdn(w) => {
11668 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
11670 let mut normed = vec![0.0f32; b * hs];
11671 for bi in 0..b {
11672 let r = inference::rms_norm(
11673 &h[bi * hs..(bi + 1) * hs],
11674 &lw.input_norm,
11675 eps,
11676 norm_style,
11677 );
11678 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11679 }
11680 let attn = crate::linear_core::gdn_forward_batch(
11681 &normed,
11682 b,
11683 w,
11684 &cfg,
11685 &mut self.kv_cache.layers[li].linear_state,
11686 pool.as_deref(),
11687 );
11688 for (dst, &a) in h.iter_mut().zip(&attn) {
11689 *dst += a;
11690 }
11691 }
11692 AttnKind::ShortConv(w) => {
11693 let cfg = self
11696 .short_conv_cfg
11697 .expect("short-conv layer without short_conv_cfg");
11698 let mut normed = vec![0.0f32; b * hs];
11699 for bi in 0..b {
11700 inference::rms_norm_into(
11701 &h[bi * hs..(bi + 1) * hs],
11702 &lw.input_norm,
11703 eps,
11704 norm_style,
11705 &mut normed[bi * hs..(bi + 1) * hs],
11706 );
11707 }
11708 let mut attn = short_conv_forward_batch(
11709 &normed,
11710 b,
11711 w,
11712 &cfg,
11713 &mut self.kv_cache.layers[li].linear_state,
11714 pool.as_deref(),
11715 );
11716 if let Some(wn) = &lw.attn_out_norm {
11719 for bi in 0..b {
11720 inference::rms_norm_into(
11721 &attn[bi * hs..(bi + 1) * hs],
11722 wn,
11723 eps,
11724 norm_style,
11725 &mut normed[bi * hs..(bi + 1) * hs],
11726 );
11727 }
11728 attn.copy_from_slice(&normed);
11729 }
11730 for (dst, &a) in h.iter_mut().zip(&attn) {
11731 *dst += a;
11732 }
11733 }
11734 AttnKind::Mla(w) => {
11735 let inv_freq_l = self.layer_inv_freq(li);
11738 let rs = self.layer_rope_scale(li);
11739 let mut normed = vec![0.0f32; hs];
11740 for bi in 0..b {
11741 inference::rms_norm_into(
11742 &h[bi * hs..(bi + 1) * hs],
11743 &lw.input_norm,
11744 eps,
11745 norm_style,
11746 &mut normed,
11747 );
11748 let ao = mla_attention(
11749 w,
11750 &normed,
11751 &mut self.kv_cache.layers[li],
11752 start_pos + bi,
11753 &inv_freq_l,
11754 rs,
11755 eps,
11756 pool.as_deref(),
11757 );
11758 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
11759 *dst += a;
11760 }
11761 }
11762 }
11763 AttnKind::Full {
11764 wq,
11765 wk,
11766 wv,
11767 wo,
11768 q_norm,
11769 k_norm,
11770 output_gate,
11771 softplus_gate,
11772 bias,
11773 } => {
11774 let mut normed = vec![0.0f32; b * hs];
11778 for bi in 0..b {
11779 inference::rms_norm_into(
11780 &h[bi * hs..(bi + 1) * hs],
11781 &lw.input_norm,
11782 eps,
11783 norm_style,
11784 &mut normed[bi * hs..(bi + 1) * hs],
11785 );
11786 }
11787 let inv_freq_l = self.layer_inv_freq(li);
11788 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
11789 let cfg = QwenAttnCfg {
11790 num_heads: self.layer_num_heads(li),
11791 num_kv_heads: nkv_l,
11792 head_dim: hd_l,
11793 hidden_size: hs,
11794 position: start_pos,
11795 inv_freq: &inv_freq_l,
11796 rotary_dim: rd_l,
11797 scale: self.attn_scale,
11798 softcap: self.attn_softcap,
11799 window: self.layer_window(li),
11800 v_norm: self.attn_v_norm,
11801 qk_norm_after_rope: self.qk_norm_after_rope,
11802 gate_sigmoid: self.proj_gate_sigmoid,
11803 q_norm: q_norm.as_deref(),
11804 k_norm: k_norm.as_deref(),
11805 output_gate: *output_gate,
11806 softplus_gate: softplus_gate
11807 .as_ref()
11808 .map(|(gate, per_head)| (gate, *per_head)),
11809 rope_scale: self.layer_rope_scale(li),
11810 bias: bias
11811 .as_ref()
11812 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
11813 rms_eps: eps,
11814 norm_style,
11815 pool: pool.as_deref(),
11816 v_head_dim: self.layer_v_dim(li),
11817 };
11818 #[cfg(feature = "gpu")]
11819 let _mirror = self
11820 .prefill_mirror_target(li)
11821 .map(crate::gpu::enter_prefill_mirror);
11822 let mut attn = attention::qwen_attention_batch(
11823 &normed,
11824 b,
11825 wq,
11826 wk,
11827 wv,
11828 wo,
11829 &mut self.kv_cache.layers[li],
11830 &cfg,
11831 );
11832 if let Some(w) = &lw.attn_out_norm {
11833 for bi in 0..b {
11834 inference::rms_norm_into(
11835 &attn[bi * hs..(bi + 1) * hs],
11836 w,
11837 eps,
11838 norm_style,
11839 &mut normed[bi * hs..(bi + 1) * hs],
11840 );
11841 }
11842 attn.copy_from_slice(&normed);
11843 }
11844 for (dst, &a) in h.iter_mut().zip(&attn) {
11845 *dst += a;
11846 }
11847 }
11848 AttnKind::Bounded(w) => {
11849 let mut normed = vec![0.0f32; b * hs];
11852 for bi in 0..b {
11853 inference::rms_norm_into(
11854 &h[bi * hs..(bi + 1) * hs],
11855 &lw.input_norm,
11856 eps,
11857 norm_style,
11858 &mut normed[bi * hs..(bi + 1) * hs],
11859 );
11860 }
11861 let rope = self
11862 .bounded_rope
11863 .clone()
11864 .expect("bounded layer without an installed rotation table");
11865 let cfg = crate::bounded::BoundedAttnCfg {
11866 num_heads: self.num_heads,
11867 num_kv_heads: self.num_kv_heads,
11868 head_dim: self.head_dim,
11869 hidden_size: hs,
11870 scale: self.attn_scale,
11871 rope: &rope,
11872 pool: pool.as_deref(),
11873 };
11874 let mut attn = crate::bounded::bounded_attention_batch(
11875 &normed,
11876 b,
11877 w,
11878 &mut self.kv_cache.layers[li],
11879 &cfg,
11880 );
11881 if let Some(wn) = &lw.attn_out_norm {
11882 for bi in 0..b {
11883 inference::rms_norm_into(
11884 &attn[bi * hs..(bi + 1) * hs],
11885 wn,
11886 eps,
11887 norm_style,
11888 &mut normed[bi * hs..(bi + 1) * hs],
11889 );
11890 }
11891 attn.copy_from_slice(&normed);
11892 }
11893 for (dst, &a) in h.iter_mut().zip(&attn) {
11894 *dst += a;
11895 }
11896 attention::recycle_buf(&mut attn);
11897 }
11898 AttnKind::Linear(w) => {
11899 for bi in 0..b {
11900 let normed = inference::rms_norm(
11901 &h[bi * hs..(bi + 1) * hs],
11902 &lw.input_norm,
11903 eps,
11904 norm_style,
11905 );
11906 vmf_phase_forward(
11907 &normed,
11908 w,
11909 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
11910 &mut self.kv_cache.layers[li].linear_state,
11911 pool.as_deref(),
11912 )
11913 .iter()
11914 .enumerate()
11915 .for_each(|(i, &a)| h[bi * hs + i] += a);
11916 }
11917 }
11918 }
11919
11920 let lw = &self.weights.layers[self.phys_layer(li)];
11922 let mut post = vec![0.0f32; b * hs];
11923 for bi in 0..b {
11924 let r =
11925 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
11926 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11927 }
11928 let mask_row = task_mask
11931 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
11932 .and_then(|m| m.ffn_masks.get(li))
11933 .map(|v| v.as_slice());
11934 let attn_ns = t_attn.elapsed().as_nanos() as u64;
11935 let t_ffn = std::time::Instant::now();
11936 let mut ffn = match &lw.ffn {
11937 FfnKind::Dense(d) if !d.segs.is_empty() => {
11938 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
11939 }
11940 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
11941 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
11942 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
11943 }
11944 FfnKind::Moe(m) if self.verify_exact_moe => {
11945 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
11946 }
11947 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
11950 let before = m.stats.borrow().clone();
11951 let out = crate::gpu::cpu_scope(|| {
11952 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
11953 });
11954 self.mimo_moe.prime(li, m, &before);
11955 out
11956 }
11957 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
11958 FfnKind::DenseMoe(dm) => {
11961 let mut out = vec![0.0f32; b * hs];
11962 for bi in 0..b {
11963 let r = dense_moe_ffn(
11964 dm,
11965 &post[bi * hs..(bi + 1) * hs],
11966 &h[bi * hs..(bi + 1) * hs],
11967 eps,
11968 norm_style,
11969 pool.as_deref(),
11970 );
11971 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11972 }
11973 out
11974 }
11975 };
11976 if prefill_prof_on() {
11977 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
11978 PREFILL_SPLIT[1].fetch_add(
11979 t_ffn.elapsed().as_nanos() as u64,
11980 std::sync::atomic::Ordering::Relaxed,
11981 );
11982 }
11983 if let Some(w) = &lw.ffn_out_norm {
11984 for bi in 0..b {
11985 inference::rms_norm_into(
11986 &ffn[bi * hs..(bi + 1) * hs],
11987 w,
11988 eps,
11989 norm_style,
11990 &mut post[bi * hs..(bi + 1) * hs],
11991 );
11992 }
11993 ffn.copy_from_slice(&post);
11994 }
11995 for (dst, &f) in h.iter_mut().zip(&ffn) {
11996 *dst += f;
11997 }
11998 if let Some(sc) = lw.layer_scale {
11999 for v in h.iter_mut() {
12000 *v *= sc;
12001 }
12002 }
12003 if self.layer_dump.is_some() {
12005 for bi in 0..b {
12006 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
12007 }
12008 }
12009 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
12010 if let Ok(t) = tp.parse::<usize>() {
12011 if t >= start_pos && t < start_pos + b {
12012 let bi = t - start_pos;
12013 let row = &h[bi * hs..(bi + 1) * hs];
12014 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
12015 eprintln!(
12016 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
12017 row[0], row[1]
12018 );
12019 }
12020 }
12021 }
12022 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
12026 let row = &h[(b - 1) * hs..b * hs];
12027 let rms =
12028 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
12029 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
12030 eprintln!(
12031 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
12032 match &self.weights.layers[self.phys_layer(li)].attn {
12033 AttnKind::LinearGdn(_) => "gdn",
12034 AttnKind::Linear(_) => "vmf",
12035 AttnKind::ShortConv(_) => "conv",
12036 _ => "attn",
12037 },
12038 match &lw.ffn {
12039 FfnKind::Moe(_) => "moe",
12040 FfnKind::Dense(_) => "dense",
12041 FfnKind::DenseMoe(_) => "dense+moe",
12042 },
12043 );
12044 }
12045 if self.is_loop_end(li) && li + 1 < self.num_layers {
12047 for bi in 0..b {
12048 let normed = inference::rms_norm(
12049 &h[bi * hs..(bi + 1) * hs],
12050 &self.weights.final_norm,
12051 eps,
12052 norm_style,
12053 );
12054 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
12055 }
12056 }
12057 if std::env::var("CMF_TRACE_H").is_ok() {
12058 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
12059 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
12060 eprintln!(
12061 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
12062 lw.layer_scale
12063 );
12064 }
12065 }
12066 crate::gpu::set_layer(-1); if prefill_prof_on() {
12068 let ms = |a: &std::sync::atomic::AtomicU64| {
12069 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
12070 };
12071 let sp = &attention::ATTN_SPLIT;
12072 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
12073 eprintln!(
12074 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
12075 host gate {:.1}], host loop {:.1}, attend {:.1} \
12076 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
12077 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
12078 ms(&PREFILL_SPLIT[0]),
12079 ms(&sp[0]),
12080 qkv_enq,
12081 qkv_wait,
12082 ms(&sp[6]),
12083 ms(&sp[1]),
12084 ms(&sp[2]),
12085 ms(&sp[4]),
12086 ms(&sp[5]),
12087 ca_wait,
12088 ms(&sp[3]),
12089 ms(&PREFILL_SPLIT[1]),
12090 );
12091 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
12092 eprintln!(
12093 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
12094 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
12095 fallbacks (cumulative)",
12096 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
12097 );
12098 }
12099 self.o1_progress();
12104 self.swa_trim_tails();
12106 h
12107 }
12108
12109 fn embed_single(&self, id: u32) -> Vec<f32> {
12111 let mut out = vec![0.0f32; self.hidden_size];
12112 if (id as usize) < self.weights.embed_tokens.rows() {
12113 self.weights.embed_tokens.row_f32(id as usize, &mut out);
12114 }
12115 if self.embed_multiplier != 1.0 {
12116 for v in out.iter_mut() {
12117 *v *= self.embed_multiplier;
12118 }
12119 }
12120 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
12124 let mut v = vec![0.0f32; self.hidden_size.max(1)];
12125 v[0] = id as f32;
12126 return v;
12127 }
12128 if let Some(b) = &self.g4e {
12132 return b.0.extend_embedding(id, &out, self.pool.as_deref());
12133 }
12134 if let Some(b) = &self.g3n {
12135 return b.0.extend_embedding(id, &out, self.pool.as_deref());
12136 }
12137 out
12138 }
12139
12140 #[cfg(target_os = "macos")]
12146 fn chunk_run_gpu(
12147 &mut self,
12148 li0: usize,
12149 h: &mut [f32],
12150 b: usize,
12151 pos0: usize,
12152 embed_ids: Option<&[u32]>,
12153 cap: usize,
12154 ) -> usize {
12155 let gemma4 =
12162 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
12163 if !crate::gpu::enabled_here()
12164 || std::env::var("CMF_GPU_CHUNK")
12165 .map(|v| v == "0")
12166 .unwrap_or(false)
12167 || b < self.chunk_min_rows
12168 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
12171 || (self.global_attn.is_some() && !gemma4)
12172 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
12176 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
12179 || self.o1_active()
12182 || (self.attn_v_norm && !gemma4)
12183 || !self.metal_rope_scale_ok()
12184 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
12185 {
12186 return li0;
12187 }
12188 let Some(model) = self.model.clone() else {
12189 return li0;
12190 };
12191 let (nh, nkv, hd, hs) = (
12192 self.num_heads,
12193 self.num_kv_heads,
12194 self.head_dim,
12195 self.hidden_size,
12196 );
12197 let loop_end = if self.loop_final_norm {
12201 ((li0 / self.physical_layers) + 1) * self.physical_layers
12202 } else {
12203 self.num_layers
12204 };
12205 let run_end = self.num_layers.min(loop_end).min(cap);
12206 let tables: Vec<std::sync::Arc<Vec<f32>>> =
12211 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
12212 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
12213 let mut stored_at: Vec<usize> = Vec::new();
12214 for li in li0..run_end {
12215 let lw = &self.weights.layers[self.phys_layer(li)];
12216 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
12217 && !gemma4
12218 {
12219 break;
12220 }
12221 let AttnKind::Full {
12222 wq,
12223 wk,
12224 wv,
12225 wo,
12226 q_norm,
12227 k_norm,
12228 output_gate: false,
12229 softplus_gate,
12230 bias,
12231 } = &lw.attn
12232 else {
12233 break;
12234 };
12235 let head_gate = match softplus_gate {
12237 None => None,
12238 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
12239 Some((d, r, c)) if r == nh && c == hs => Some(d),
12240 _ => break,
12241 },
12242 Some(_) => break,
12243 };
12244 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
12248 FfnKind::Dense(d)
12249 if (matches!(d.act, Act::Silu | Act::Gelu)
12250 || (gemma4 && d.act == Act::GeluTanh))
12251 && d.segs.is_empty() =>
12252 {
12253 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
12254 }
12255 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
12256 Some((router, experts, q8)) => {
12257 let e0 = &m.experts[0];
12258 let route = Box::new(move |lg: &[f32]| {
12259 let (idx, p, wsum) = moe_route(lg, m, None);
12261 let mut st = m.stats.borrow_mut();
12262 if st.len() < m.experts.len() {
12263 st.resize(m.experts.len(), 0);
12264 }
12265 for &e in &idx {
12266 st[e] += 1;
12267 }
12268 let w = idx.iter().map(|&e| p[e] / wsum).collect();
12269 (idx, w)
12270 });
12271 (
12272 &e0.gate_proj,
12273 &e0.up_proj,
12274 &e0.down_proj,
12275 Act::Silu,
12276 Some(crate::gpu_metal::ChunkMoe {
12277 router,
12278 experts,
12279 q8,
12280 route,
12281 }),
12282 )
12283 }
12284 None => break,
12285 },
12286 _ => break,
12287 };
12288 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
12294 t.q8_row_parts()
12295 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
12296 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12297 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12298 }
12299 fn cf(t: &QTensor) -> &[f32] {
12300 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
12301 }
12302 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
12303 let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
12304 if moe.is_none()
12314 && !gemma4
12315 && ffn_act != Act::Gelu
12316 && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
12317 {
12318 break;
12319 }
12320 let parts = (
12321 cw(wq),
12322 cw(wk),
12323 cw(wv),
12324 cw(wo),
12325 cw(gate_t),
12326 cw(up_t),
12327 cw(down_t),
12328 );
12329 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
12330 else {
12331 break;
12332 };
12333 let layer = &self.kv_cache.layers[li];
12334 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
12335 break;
12336 }
12337 debug_assert!(
12339 layer.base() == 0
12340 || self
12341 .layer_window(li)
12342 .is_some_and(|w| layer.head_len(0) + 1 >= w)
12343 );
12344 stored_at.push(layer.head_len(0));
12345 let (nkv, hd) = if gemma4 {
12348 let (a, bb, _) = self.layer_geom(li);
12349 (a, bb)
12350 } else {
12351 (nkv, hd)
12352 };
12353 layers.push(crate::gpu_metal::ChunkLayer {
12354 model: &model,
12355 kv_id: self.graph_kv_id,
12356 layer: li,
12357 wq: pq,
12358 wk: pk,
12359 wv: pv,
12360 wo: po,
12361 gate: pg,
12362 up: pu,
12363 down: pd,
12364 input_norm: &lw.input_norm,
12365 post_norm: &lw.post_norm,
12366 bias: bias
12367 .as_ref()
12368 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
12369 q_norm: q_norm.as_deref(),
12370 k_norm: k_norm.as_deref(),
12371 inv_freq: &tables[li - li0],
12372 rope_scale: self.layer_rope_scale(li),
12373 rd: self.layer_geom(li).2,
12374 nh,
12375 nkv,
12376 hd,
12377 hs,
12378 inter: gate_t.rows(),
12379 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
12380 late_qk_norm: self.qk_norm_after_rope,
12381 eps: self.rms_eps as f32,
12382 window: self.layer_window(li),
12383 head_gate,
12384 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
12385 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
12386 v_norm: self.attn_v_norm,
12387 scale: self.attn_scale,
12388 attn_out_norm: lw.attn_out_norm.as_deref(),
12389 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12390 layer_scale: lw.layer_scale,
12391 }),
12392 col,
12393 ffn_col: if moe.is_none() && !gemma4 {
12396 ffn_col
12397 } else {
12398 [&[][..]; 3]
12399 },
12400 moe,
12401 });
12402 }
12403 if layers.is_empty() {
12404 return li0;
12405 }
12406 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
12408 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
12409 .iter()
12410 .zip(&rows_kv)
12411 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
12412 .collect();
12413 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
12414 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
12415 let li = layers[i].layer;
12416 let layer = &self.kv_cache.layers[li];
12417 let nkv = rows_kv[i].0;
12418 io.push(crate::gpu_metal::ChunkIo {
12419 cpu_stored: stored_at[i],
12420 cpu_gen: layer.generation(),
12421 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
12422 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
12423 out_k: ok,
12424 out_v: ov,
12425 imp: oi,
12426 });
12427 }
12428 let n_run = layers.len();
12429 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
12430 let ep = embed_ids.and_then(|ids| {
12433 self.weights
12434 .embed_tokens
12435 .q8_row_parts()
12436 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
12437 idx,
12438 rows,
12439 row_scale: rs,
12440 ids,
12441 mult: self.embed_multiplier,
12442 })
12443 });
12444 if embed_ids.is_some() && ep.is_none() {
12445 return li0;
12446 }
12447 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
12448 return li0;
12449 }
12450 drop(io);
12451 drop(layers);
12452 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
12455 let li = li0 + i;
12456 let row = rows_kv[i].1;
12457 let layer = &mut self.kv_cache.layers[li];
12458 for bi in 0..b {
12459 layer.append(
12460 &ok[bi * row..(bi + 1) * row],
12461 &ov[bi * row..(bi + 1) * row],
12462 &[],
12463 );
12464 }
12465 layer.accumulate_imp(oi);
12466 }
12467 last
12468 }
12469
12470 fn layer_is_local(&self, li: usize) -> bool {
12473 if let Some(layers) = &self.sliding_layers {
12474 return layers.get(li).copied().unwrap_or(false);
12475 }
12476 match self.swa {
12477 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
12478 None => false,
12479 }
12480 }
12481
12482 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
12485 if self.layer_is_local(li) {
12486 if let Some(f) = &self.inv_freq_local {
12487 return f.clone();
12488 }
12489 } else if let Some(f) = &self.inv_freq_global {
12490 return f.clone();
12491 }
12492 self.inv_freq.clone()
12493 }
12494
12495 fn layer_window(&self, li: usize) -> Option<usize> {
12497 self.swa
12498 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
12499 }
12500
12501 fn swa_trim_tails(&mut self) {
12510 let Some((slack, align)) = self.swa_trim else {
12511 return;
12512 };
12513 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
12514 return;
12515 }
12516 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
12517 if let Some(w) = self.layer_window(li) {
12518 self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
12519 }
12520 }
12521 }
12522
12523 fn layer_num_heads(&self, li: usize) -> usize {
12524 self.attention_heads_per_layer
12525 .as_ref()
12526 .and_then(|v| v.get(li).copied())
12527 .unwrap_or(self.num_heads)
12528 }
12529
12530 fn layer_rope_scale(&self, li: usize) -> f32 {
12531 if self.layer_is_local(li) {
12532 self.rope_scale_local
12533 } else {
12534 self.rope_scale
12535 }
12536 }
12537
12538 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
12541 if !self.layer_is_local(li) {
12542 if let Some((ghd, gkv)) = self.global_attn {
12543 return (gkv, ghd, ghd);
12544 }
12545 }
12546 let rd = if self.layer_is_local(li) {
12547 self.rotary_dim_local.unwrap_or(self.rotary_dim)
12548 } else {
12549 self.rotary_dim
12550 };
12551 let rd = if self
12553 .no_rope_layers
12554 .as_ref()
12555 .and_then(|v| v.get(li))
12556 .copied()
12557 == Some(true)
12558 {
12559 0
12560 } else {
12561 rd
12562 };
12563 (self.layer_num_kv_heads(li), self.head_dim, rd)
12564 }
12565
12566 fn layer_num_kv_heads(&self, li: usize) -> usize {
12569 self.kv_heads_per_layer
12570 .as_ref()
12571 .and_then(|v| v.get(self.phys_layer(li)).copied())
12572 .unwrap_or(self.num_kv_heads)
12573 }
12574
12575 fn layer_v_dim(&self, li: usize) -> usize {
12577 let (_, hd, _) = self.layer_geom(li);
12578 self.v_head_dim.unwrap_or(hd).min(hd)
12579 }
12580
12581 pub fn set_attn_geometry(
12590 &mut self,
12591 kv_heads_per_layer: Option<Vec<usize>>,
12592 v_head_dim: Option<usize>,
12593 ) -> Result<(), String> {
12594 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
12595 if self.global_attn.is_some() {
12596 return Err(
12597 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
12598 attention geometry"
12599 .into(),
12600 );
12601 }
12602 if self
12603 .weights
12604 .layers
12605 .iter()
12606 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
12607 {
12608 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
12609 }
12610 }
12611 if let Some(vd) = v_head_dim {
12612 if vd == 0 || vd > self.head_dim {
12613 return Err(format!(
12614 "v_head_dim {vd} must be in 1..={} (head_dim)",
12615 self.head_dim
12616 ));
12617 }
12618 }
12619 if let Some(v) = &kv_heads_per_layer {
12620 if v.len() != self.physical_layers {
12621 return Err(format!(
12622 "kv_heads_per_layer has {} entries, expected {} layers",
12623 v.len(),
12624 self.physical_layers
12625 ));
12626 }
12627 for (li, &nkv) in v.iter().enumerate() {
12628 let is_attn = matches!(
12629 self.weights.layers.get(li).map(|lw| &lw.attn),
12630 Some(AttnKind::Full { .. }) | None
12631 );
12632 if !is_attn {
12633 continue;
12634 }
12635 let nh = self
12636 .attention_heads_per_layer
12637 .as_ref()
12638 .and_then(|h| h.get(li).copied())
12639 .unwrap_or(self.num_heads);
12640 if nkv == 0 || nh % nkv != 0 {
12641 return Err(format!(
12642 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
12643 ));
12644 }
12645 }
12646 }
12647 self.kv_heads_per_layer = kv_heads_per_layer;
12648 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
12649 if self.kv_heads_per_layer.is_some() {
12650 for li in 0..self.kv_cache.layers.len() {
12651 let full = matches!(
12652 self.weights
12653 .layers
12654 .get(self.phys_layer(li))
12655 .map(|lw| &lw.attn),
12656 Some(AttnKind::Full { .. })
12657 );
12658 let nkv = self.layer_num_kv_heads(li);
12659 let cache = &self.kv_cache.layers[li];
12660 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
12661 let sinks = cache.sinks.clone();
12662 self.kv_cache.layers[li] =
12663 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
12664 self.kv_cache.layers[li].sinks = sinks;
12665 }
12666 }
12667 }
12668 Ok(())
12669 }
12670
12671 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
12675 let Some(lw) = self.weights.layers.get(phys) else {
12676 return Err(format!("sinks for layer {phys}: no such layer"));
12677 };
12678 if !matches!(lw.attn, AttnKind::Full { .. }) {
12679 return Err(format!(
12680 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
12681 ));
12682 }
12683 let nh = self
12684 .attention_heads_per_layer
12685 .as_ref()
12686 .and_then(|h| h.get(phys).copied())
12687 .unwrap_or(self.num_heads);
12688 if sinks.len() != nh {
12689 return Err(format!(
12690 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
12691 sinks.len()
12692 ));
12693 }
12694 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
12695 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
12696 }
12697 for li in 0..self.kv_cache.layers.len() {
12698 if self.phys_layer(li) == phys {
12699 self.kv_cache.layers[li].sinks = Some(sinks.clone());
12700 }
12701 }
12702 Ok(())
12703 }
12704
12705 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
12715 if self.kv_heads_per_layer.is_some() {
12716 return Some("per-layer KV head counts");
12717 }
12718 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
12719 return Some("V heads narrower than Q/K heads");
12720 }
12721 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
12722 return Some("learned attention sinks");
12723 }
12724 if self.swa.is_some() || self.sliding_layers.is_some() {
12725 return Some("sliding-window layers");
12726 }
12727 None
12728 }
12729
12730 #[cfg(target_os = "macos")]
12738 fn metal_rope_scale_ok(&self) -> bool {
12739 !(self.qk_norm_after_rope
12740 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
12741 }
12742
12743 #[cfg(target_os = "macos")]
12760 fn metal_graph_swa(&self) -> bool {
12761 (self.swa.is_some() || self.sliding_layers.is_some())
12762 && self.attention_heads_per_layer.is_none()
12763 && !self.attn_v_norm
12764 && self.attn_softcap == 0.0
12765 && self.kv_heads_per_layer.is_none()
12766 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
12767 && self.global_attn.is_none()
12768 && self.inv_freq_global.is_none()
12769 && self.metal_rope_scale_ok()
12770 && !(0..self.num_layers).any(|li| {
12771 self.layer_is_local(li)
12772 && self.inv_freq_local.is_none()
12773 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
12774 })
12775 }
12776
12777 #[cfg(target_os = "macos")]
12785 fn metal_gemma4(&self) -> bool {
12786 crate::gpu_metal::gemma4::enabled()
12787 && (self.global_attn.is_some() || self.attn_v_norm)
12788 && self.attention_heads_per_layer.is_none()
12789 && self.kv_heads_per_layer.is_none()
12790 && self.v_head_dim.is_none()
12791 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
12792 && self.attn_softcap == 0.0
12793 && !self.qk_norm_after_rope
12794 && self.metal_rope_scale_ok()
12795 && self
12796 .weights
12797 .layers
12798 .iter()
12799 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
12800 && !(0..self.num_layers).any(|li| {
12801 self.layer_is_local(li)
12802 && self.inv_freq_local.is_none()
12803 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
12804 })
12805 }
12806
12807 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
12814 let geometry_reason = self.graph_attn_decline_reason();
12818 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
12819 if has_scaled_rope && geometry_reason.is_none() {
12820 return Some("scaled RoPE without per-layer geometry");
12821 }
12822 if has_scaled_rope && self.qk_norm_after_rope {
12827 return Some("scaled RoPE with post-RoPE Q/K norm");
12828 }
12829 geometry_reason?;
12830 if self.attention_heads_per_layer.is_some() {
12836 return Some("per-layer Q head counts with per-layer geometry");
12837 }
12838 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
12843 let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
12844 if !gemma4
12845 && self.weights.layers.iter().any(|lw| {
12846 lw.layer_scale.is_some()
12847 || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some()))
12848 })
12849 {
12850 return Some("sandwich norms / layer scale with per-layer geometry");
12851 }
12852 if self.weights.layers.iter().any(|lw| {
12856 matches!(
12857 &lw.attn,
12858 AttnKind::Full {
12859 output_gate: true,
12860 ..
12861 }
12862 )
12863 }) && self.v_head_dim.is_some()
12864 {
12865 return Some("gated attention with V narrower than K");
12866 }
12867 if (0..self.num_layers).any(|li| {
12868 self.layer_is_local(li)
12869 && self.inv_freq_local.is_none()
12870 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
12871 }) {
12872 return Some("local rotary width without a local RoPE table");
12873 }
12874 None
12875 }
12876
12877 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
12881 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
12884 return None;
12885 }
12886 if self.global_attn.is_some() {
12887 return Some("per-layer head width (Gemma-4 global layers)");
12888 }
12889 if self.attn_v_norm {
12890 return Some("V norm");
12891 }
12892 if self.weights.layers.iter().any(|lw| {
12893 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
12894 }) {
12895 return Some("sandwich norms / layer scale");
12896 }
12897 None
12898 }
12899
12900 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
12905 self.graph_attn_decline_reason()?;
12906 let (nkv, hd, rd) = self.layer_geom(li);
12907 let invf: &[f32] = if self.layer_is_local(li) {
12908 match &self.inv_freq_local {
12909 Some(f) => f.as_slice(),
12910 None => self.inv_freq.as_slice(),
12911 }
12912 } else {
12913 match &self.inv_freq_global {
12914 Some(f) => f.as_slice(),
12915 None => self.inv_freq.as_slice(),
12916 }
12917 };
12918 Some(crate::gpu::GraphAttnGeom {
12919 nkv,
12920 hd,
12921 dv: self.layer_v_dim(li),
12922 rd,
12923 invf,
12924 rope_scale: self.layer_rope_scale(li),
12925 window: self.layer_window(li),
12926 sink: self.kv_cache.layers[li].sinks.as_deref(),
12927 v_norm: self.attn_v_norm,
12928 })
12929 }
12930
12931 #[cfg(feature = "gpu")]
12943 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
12944 if !self.proj_gate_sigmoid
12945 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
12946 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
12947 || self.graph_refused()
12948 || self.o1_active()
12949 || self.attn_softcap > 0.0
12950 || self.wgpu_graph_attn_decline().is_some()
12951 {
12952 return None;
12953 }
12954 let g = self.graph_attn_geom(li)?;
12955 let (nkv, hd, _) = self.layer_geom(li);
12956 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
12958 return None;
12959 }
12960 Some(crate::gpu::PrefillMirror {
12961 kv_id: self.graph_kv_id,
12962 layer: li,
12963 limit: self.kv_cache.max_seq_len,
12964 })
12965 }
12966
12967 #[cfg(feature = "gpu")]
12976 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
12977 let kv_id = self.graph_kv_id;
12978 for li in from..upto.min(self.num_layers) {
12979 if !matches!(
12980 self.weights.layers[self.phys_layer(li)].attn,
12981 AttnKind::Full { .. }
12982 ) {
12983 continue;
12984 }
12985 let host = self.kv_cache.layers[li].pos_len();
12987 if host >= position {
12988 continue;
12989 }
12990 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
12991 continue;
12992 };
12993 let to = dev.min(position);
12994 if to <= host {
12995 continue;
12996 }
12997 let (nkv, hd) = {
12998 let c = &self.kv_cache.layers[li];
12999 (c.num_kv_heads, c.head_dim)
13000 };
13001 let Some((k, v, first_valid)) =
13002 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
13003 else {
13004 continue;
13005 };
13006 let need_from = match self.layer_window(li) {
13009 Some(w) => host.max((position + 1).saturating_sub(w)),
13010 None => host,
13011 };
13012 if first_valid > need_from {
13013 tracing::warn!(
13014 "layer {li}: device KV rows {host}..{to} no longer resident \
13015 (from {first_valid}); host attention will miss them"
13016 );
13017 }
13018 let row = nkv * hd;
13019 let cache = &mut self.kv_cache.layers[li];
13020 for p in 0..to - host {
13021 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
13022 }
13023 }
13024 }
13025
13026 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
13029 let mut seen = self.graph_declines.borrow_mut();
13030 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
13031 tracing::warn!("{site} declined: {reason} (CPU attention path)");
13032 seen.push((site, reason));
13033 }
13034 }
13035
13036 pub fn graph_declines(&self) -> Vec<String> {
13039 self.graph_declines
13040 .borrow()
13041 .iter()
13042 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
13043 .collect()
13044 }
13045
13046 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
13055 let Some(dir) = &self.layer_dump else {
13056 return;
13057 };
13058 let mut bytes = Vec::with_capacity(row.len() * 4);
13059 for v in row {
13060 bytes.extend_from_slice(&v.to_le_bytes());
13061 }
13062 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
13063 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
13064 use std::sync::atomic::{AtomicBool, Ordering};
13065 static SAID: AtomicBool = AtomicBool::new(false);
13066 if !SAID.swap(true, Ordering::Relaxed) {
13067 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
13068 }
13069 }
13070 }
13071
13072 fn mimo_moe_prepare(&mut self) {
13075 if !self.mimo_moe.is_undecided() {
13076 return;
13077 }
13078 let slot = {
13079 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
13080 .filter_map(
13081 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
13082 FfnKind::Moe(m) => Some((li, m)),
13083 _ => None,
13084 },
13085 )
13086 .collect();
13087 if layers.is_empty()
13090 || self.physical_layers != self.num_layers
13091 || self.gpu_plan.is_some()
13092 {
13093 crate::mimo_moe::Slot::Off
13094 } else {
13095 let graph_prefix =
13099 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
13100 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
13101 }
13102 };
13103 self.mimo_moe = slot;
13104 }
13105
13106 #[cfg(test)]
13107 pub(crate) fn test_graph_kv_id(&self) -> u64 {
13108 self.graph_kv_id
13109 }
13110
13111 pub(crate) fn mimo_graph_layer_rows(
13115 &mut self,
13116 li: usize,
13117 h: &mut [f32],
13118 positions: &[usize],
13119 ) -> crate::gpu::BatchGraphOutcome {
13120 use crate::gpu::BatchGraphOutcome as Out;
13121 let b = positions.len();
13122 if !(1..=4).contains(&b)
13123 || h.len() != b * self.hidden_size
13124 || !self.mimo_moe.is_dynamic(li, true)
13125 || !crate::gpu::enabled_here()
13126 || !crate::gpu::wgpu_active()
13127 || self.o1_active()
13128 || self.physical_layers != self.num_layers
13129 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
13133 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
13134 || self.wgpu_graph_attn_decline().is_some()
13135 {
13136 return Out::Declined;
13137 }
13138 let attn_started = std::time::Instant::now();
13139 let outcome = {
13140 let lw = &self.weights.layers[li];
13141 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13142 return Out::Declined;
13143 }
13144 let FfnKind::Moe(m) = &lw.ffn else {
13145 return Out::Declined;
13146 };
13147 let AttnKind::Full {
13148 wq,
13149 wk,
13150 wv,
13151 wo,
13152 q_norm,
13153 k_norm,
13154 output_gate,
13155 softplus_gate,
13156 bias,
13157 } = &lw.attn
13158 else {
13159 return Out::Declined;
13160 };
13161 if *output_gate || softplus_gate.is_some() {
13162 return Out::Declined;
13163 }
13164 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
13165 m.experts
13166 .first()?
13167 .gate_proj
13168 .mapped_q4tp()
13169 .map(|(m, _)| m.clone())
13170 }) else {
13171 return Out::Declined;
13172 };
13173 fn gw<'a>(
13174 t: &'a QTensor,
13175 owner: &std::sync::Arc<cortiq_core::CmfModel>,
13176 ) -> Option<crate::gpu::GraphW<'a>> {
13177 if let Some((m, idx, kind, rs)) = t.graph_weight() {
13178 if m.uid() != owner.uid() || t.has_prism_contract() {
13179 return None;
13180 }
13181 return Some(crate::gpu::GraphW {
13182 idx,
13183 kind,
13184 row_scale: rs,
13185 data: &[],
13186 prism: crate::gpu::GraphPrismOp::None,
13187 affine: false,
13188 });
13189 }
13190 t.as_f32().map(|data| crate::gpu::GraphW {
13191 idx: 0,
13192 kind: 4,
13193 row_scale: &[],
13194 data,
13195 prism: crate::gpu::GraphPrismOp::None,
13196 affine: false,
13197 })
13198 }
13199 let (Some(q), Some(k), Some(v), Some(o)) = (
13200 gw(wq, &model),
13201 gw(wk, &model),
13202 gw(wv, &model),
13203 gw(wo, &model),
13204 ) else {
13205 return Out::Declined;
13206 };
13207 let layer = crate::gpu::GraphLayer {
13208 input_norm: &lw.input_norm,
13209 post_norm: &lw.post_norm,
13210 attn_out_norm: None,
13211 ffn_out_norm: None,
13212 layer_scale: None,
13213 ffn: crate::gpu::GraphFfn::AttentionOnly,
13214 attn: crate::gpu::GraphAttn::Full {
13215 wq: q,
13216 wk: k,
13217 wv: v,
13218 wo: o,
13219 q_norm: q_norm.as_deref(),
13220 k_norm: k_norm.as_deref(),
13221 late_qk_norm: self.qk_norm_after_rope,
13222 bias: bias
13223 .as_ref()
13224 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
13225 output_gate: false,
13226 cpu_k: self.kv_cache.layers[li].k_heads(),
13227 cpu_v: self.kv_cache.layers[li].v_heads(),
13228 cpu_base: self.kv_cache.layers[li].base(),
13229 geom: self.graph_attn_geom(li),
13230 head_gate: None,
13231 },
13232 };
13233 let (nkv, hd, rd) = self.layer_geom(li);
13234 crate::gpu::forward_batch_graph_at(
13235 &model,
13236 self.graph_kv_id,
13237 li,
13238 &[layer],
13239 &self.inv_freq,
13240 h,
13241 self.layer_num_heads(li),
13242 nkv,
13243 hd,
13244 rd,
13245 self.hidden_size,
13246 1,
13247 positions,
13248 self.kv_cache.max_seq_len,
13249 self.norm_style == cortiq_core::NormStyle::Gemma,
13250 self.rms_eps as f32,
13251 self.attn_scale,
13252 b,
13253 &[],
13254 self.o1_epoch,
13255 None,
13256 None,
13257 )
13258 };
13259 match outcome {
13260 Out::Completed => {}
13261 Out::Declined => return Out::Declined,
13262 Out::Failed => {
13263 self.graph_failed
13264 .store(true, std::sync::atomic::Ordering::Relaxed);
13265 return Out::Failed;
13266 }
13267 }
13268 let attn_ns = attn_started.elapsed().as_nanos() as u64;
13269 let hs = self.hidden_size;
13270 let lw = &self.weights.layers[li];
13271 let FfnKind::Moe(m) = &lw.ffn else {
13272 unreachable!()
13273 };
13274 let mut post = vec![0.0; h.len()];
13275 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
13276 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
13277 }
13278 let mut ffn = if b == 1 {
13279 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
13280 } else {
13281 moe_ffn_banked_rows(
13282 &mut self.mimo_moe,
13283 li,
13284 m,
13285 &post,
13286 b,
13287 hs,
13288 self.pool.as_deref(),
13289 )
13290 };
13291 for (x, &f) in h.iter_mut().zip(&ffn) {
13292 *x += f;
13293 }
13294 attention::recycle_buf(&mut ffn);
13295 if self.layer_dump.is_some() {
13296 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
13297 self.dump_layer_row(pos, li, row);
13298 }
13299 }
13300 crate::mimo_moe::note_attention_graph(b, attn_ns);
13301 Out::Completed
13302 }
13303
13304 fn layer_attn_plain(&self, li: usize) -> bool {
13305 self.kv_heads_per_layer.is_none()
13306 && self.v_head_dim.is_none()
13307 && self.global_attn.is_none()
13308 && self.layer_window(li).is_none()
13309 && self.kv_cache.layers[li].sinks.is_none()
13310 }
13311
13312 fn forward_layers(
13314 &mut self,
13315 hidden: &[f32],
13316 position: usize,
13317 task_mask: Option<&TaskMask>,
13318 ) -> Vec<f32> {
13319 let out = self.forward_layers_upto(hidden, position, task_mask, None);
13320 self.o1_progress();
13321 self.swa_trim_tails();
13322 out
13323 }
13324
13325 pub fn embed_id(&self, id: u32) -> Vec<f32> {
13333 self.embed_single(id)
13334 }
13335
13336 pub fn split_supported(&self) -> Result<(), String> {
13340 if self.dsv4.is_some() {
13341 return Err(
13342 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
13343 );
13344 }
13345 if self.dsv41.is_some() {
13346 return Err(
13347 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
13348 .into(),
13349 );
13350 }
13351 if self.qwen4_exp.is_some() {
13352 return Err(
13353 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
13354 );
13355 }
13356 if self.per_token_stack() {
13357 return Err(
13358 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
13359 );
13360 }
13361 Ok(())
13362 }
13363
13364 pub fn forward_span(
13369 &mut self,
13370 hidden: &[f32],
13371 position: usize,
13372 from: usize,
13373 upto: usize,
13374 task_mask: Option<&TaskMask>,
13375 ) -> Result<Vec<f32>, String> {
13376 self.split_supported()?;
13377 if from > upto || upto >= self.num_layers {
13378 return Err(format!(
13379 "forward_span: layer range {from}..={upto} outside 0..{}",
13380 self.num_layers
13381 ));
13382 }
13383 if hidden.len() != self.hidden_size {
13384 return Err(format!(
13385 "forward_span: hidden len {} ≠ hidden_size {}",
13386 hidden.len(),
13387 self.hidden_size
13388 ));
13389 }
13390 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
13391 self.o1_progress();
13392 self.swa_trim_tails();
13393 if self
13394 .graph_failed
13395 .swap(false, std::sync::atomic::Ordering::Relaxed)
13396 {
13397 self.cancel
13398 .store(false, std::sync::atomic::Ordering::Relaxed);
13399 self.clear_sequence_state();
13400 return Err("forward_span: deferred O(1) transition failed".into());
13401 }
13402 Ok(out)
13403 }
13404
13405 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
13408 let normed = inference::rms_norm(
13409 hidden,
13410 &self.weights.final_norm,
13411 self.rms_eps,
13412 self.norm_style,
13413 );
13414 self.lm_head_forward(&normed)
13415 }
13416
13417 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
13419 sampler::sample_with_scratch(
13420 logits,
13421 &self.sampler_config,
13422 past_tokens,
13423 &mut self.rng,
13424 &mut self.sampler_scratch,
13425 )
13426 }
13427
13428 pub fn reset_session(&mut self) {
13430 self.clear_sequence_state();
13431 }
13432
13433 pub fn prefill_span_ids(
13439 &mut self,
13440 ids: &[u32],
13441 start_pos: usize,
13442 upto: usize,
13443 task_mask: Option<&TaskMask>,
13444 ) -> Result<Vec<f32>, String> {
13445 self.split_supported()?;
13446 if upto >= self.num_layers {
13447 return Err(format!(
13448 "prefill_span_ids: upto {upto} outside 0..{}",
13449 self.num_layers
13450 ));
13451 }
13452 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13456 let out =
13457 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
13458 self.check_o1_progress_failure("prefill_span_ids")?;
13459 Ok(out)
13460 } else {
13461 let hs = self.hidden_size;
13462 let mut out = Vec::with_capacity(ids.len() * hs);
13463 for (i, &id) in ids.iter().enumerate() {
13464 let emb = self.embed_id(id);
13465 out.extend_from_slice(&self.forward_span(
13466 &emb,
13467 start_pos + i,
13468 0,
13469 upto,
13470 task_mask,
13471 )?);
13472 }
13473 Ok(out)
13474 }
13475 }
13476
13477 pub fn prefill_span_hidden(
13480 &mut self,
13481 hidden: &[f32],
13482 start_pos: usize,
13483 from: usize,
13484 upto: usize,
13485 task_mask: Option<&TaskMask>,
13486 ) -> Result<Vec<f32>, String> {
13487 self.split_supported()?;
13488 let hs = self.hidden_size;
13489 if hidden.is_empty() || hidden.len() % hs != 0 {
13490 return Err(format!(
13491 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
13492 hidden.len()
13493 ));
13494 }
13495 if from > upto || upto >= self.num_layers {
13496 return Err(format!(
13497 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
13498 self.num_layers
13499 ));
13500 }
13501 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13502 let out = self.prefill_batch_span(
13503 PrefillIn::Hidden(hidden),
13504 start_pos,
13505 task_mask,
13506 from,
13507 upto + 1,
13508 );
13509 self.check_o1_progress_failure("prefill_span_hidden")?;
13510 Ok(out)
13511 } else {
13512 let b = hidden.len() / hs;
13513 let mut out = Vec::with_capacity(hidden.len());
13514 for i in 0..b {
13515 let h = self.forward_span(
13516 &hidden[i * hs..(i + 1) * hs],
13517 start_pos + i,
13518 from,
13519 upto,
13520 task_mask,
13521 )?;
13522 out.extend_from_slice(&h);
13523 }
13524 Ok(out)
13525 }
13526 }
13527
13528 fn try_token_graph_wgpu(
13532 &self,
13533 hidden: &[f32],
13534 position: usize,
13535 logits_out: &mut Vec<f32>,
13536 layers_run: &mut usize,
13537 ) -> Option<Result<Vec<f32>, ()>> {
13538 self.try_token_graph_wgpu_steps(
13539 hidden,
13540 position,
13541 logits_out,
13542 1,
13543 None,
13544 Some(layers_run),
13545 0,
13546 self.num_layers,
13547 )
13548 }
13549
13550 fn try_token_graph_wgpu_span(
13554 &self,
13555 hidden: &[f32],
13556 position: usize,
13557 logits_out: &mut Vec<f32>,
13558 from: usize,
13559 upto_excl: usize,
13560 layers_run: &mut usize,
13561 ) -> Option<Result<Vec<f32>, ()>> {
13562 self.try_token_graph_wgpu_steps(
13563 hidden,
13564 position,
13565 logits_out,
13566 1,
13567 None,
13568 Some(layers_run),
13569 from,
13570 upto_excl,
13571 )
13572 }
13573
13574 #[cfg(feature = "gpu")]
13578 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
13579 if !crate::minicpm_tail::minimax_pool_on()
13580 || !crate::gpu::enabled_here()
13581 || self.head_clusters.is_some()
13582 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
13583 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
13584 {
13585 return None;
13586 }
13587 let (model, i, kind, rs) = self
13588 .weights
13589 .lm_head
13590 .graph_weight()
13591 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
13592 let name = &model.tensors[i].name;
13593 let prism = if crate::prism::is_inverse_embedding(&model, name) {
13594 crate::gpu::GraphPrismOp::InverseEmbedding
13595 } else if crate::prism::is_forward_weight(&model, name) {
13596 crate::gpu::GraphPrismOp::Forward
13597 } else {
13598 crate::gpu::GraphPrismOp::None
13599 };
13600 let gw = crate::gpu::GraphW {
13601 idx: i,
13602 kind,
13603 row_scale: rs,
13604 data: &[],
13605 prism,
13606 affine: crate::prism::is_affine_target(&model, name),
13607 };
13608 let rows = self.weights.lm_head.rows();
13609 let mut h = hidden.to_vec();
13610 let mut lg = Vec::new();
13611 let mut layers_run = 0usize;
13612 let outcome = crate::gpu::forward_token_graph(
13613 &model,
13614 self.graph_kv_id,
13615 &[],
13616 &[],
13617 self.o1_epoch,
13618 &self.inv_freq,
13619 &mut h,
13620 self.num_heads,
13621 self.num_kv_heads,
13622 self.head_dim,
13623 self.attn_scale,
13624 self.head_dim,
13625 self.hidden_size,
13626 self.intermediate_size,
13627 0,
13628 self.kv_cache.max_seq_len,
13629 self.norm_style == cortiq_core::NormStyle::Gemma,
13630 self.rms_eps as f32,
13631 Some((&gw, rows)),
13632 &self.weights.final_norm,
13633 &mut lg,
13634 &[],
13635 1,
13636 None,
13637 None,
13638 Some(&mut layers_run),
13639 0,
13640 false,
13641 );
13642 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
13643 return None;
13644 }
13645 lg.resize(self.vocab_size, 0.0);
13648 if let Some(m) = self.logit_multiplier {
13649 for l in lg.iter_mut() {
13650 *l *= m;
13651 }
13652 }
13653 if let Some(c) = self.final_softcap {
13654 softcap_in_place(&mut lg, c, self.pool.as_deref());
13655 }
13656 Some(lg)
13657 }
13658
13659 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
13662 if winners.is_empty() {
13663 return;
13664 }
13665 if self.m3_n_exp == 0 {
13666 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
13667 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
13668 }
13669 for &e in winners {
13670 let e = e as usize;
13671 if e < self.m3_n_exp && li < self.num_layers {
13672 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
13673 }
13674 }
13675 if li + 1 == self.num_layers {
13676 self.m3_tokens += 1;
13677 if self.m3_tokens % 16 == 0 {
13678 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
13679 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
13680 let _ = std::fs::write(&path, bytes);
13681 }
13682 }
13683 }
13684 }
13685
13686 fn m3_prewarm(&mut self) {
13691 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
13692 return;
13693 };
13694 let Ok(bytes) = std::fs::read(&path) else {
13695 tracing::warn!("m3 prewarm: cannot read profile {path}");
13696 return;
13697 };
13698 let counts: Vec<u32> = bytes
13699 .chunks_exact(4)
13700 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
13701 .collect();
13702 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
13703 if counts.len() != self.num_layers * n_exp {
13704 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
13705 return;
13706 }
13707 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
13708 .ok()
13709 .and_then(|v| v.parse().ok())
13710 .unwrap_or(300);
13711 let mut ranked: Vec<(u32, usize, usize)> = counts
13712 .iter()
13713 .enumerate()
13714 .filter(|(_, c)| **c > 0)
13715 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
13716 .collect();
13717 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
13718 ranked.truncate(limit);
13719 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
13720 for (_, li, e) in ranked {
13721 per_layer[li].push(e);
13722 }
13723 let mut admitted = 0usize;
13724 for (li, ids) in per_layer.iter().enumerate() {
13725 if ids.is_empty() {
13726 continue;
13727 }
13728 let lw = &self.weights.layers[self.phys_layer(li)];
13729 let FfnKind::Moe(m) = &lw.ffn else {
13730 continue;
13731 };
13732 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
13734 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
13735 admitted += ids.len();
13736 }
13737 }
13738 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
13739 }
13740
13741 #[cfg(feature = "gpu")]
13752 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
13753 if self.o1_active() || self.attn_softcap > 0.0 {
13754 return Ok(false);
13755 }
13756 if !self.m3_prewarmed {
13757 self.m3_prewarmed = true;
13758 self.m3_prewarm();
13759 }
13760 let frame = {
13761 let lw = &self.weights.layers[self.phys_layer(li)];
13762 let FfnKind::Moe(m) = &lw.ffn else {
13763 return Ok(false);
13764 };
13765 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
13766 return Ok(false);
13767 }
13768 let t_arm = std::time::Instant::now();
13769 if self.m3_winners.len() != self.num_layers {
13770 self.m3_winners = vec![Vec::new(); self.num_layers];
13771 }
13772 let pred = std::mem::take(&mut self.m3_winners[li]);
13773 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
13774 if armed.is_none() {
13775 self.m3_winners[li] = pred; }
13777 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
13778 match armed {
13779 Some(frame) => frame,
13780 None => return Ok(false),
13783 }
13784 };
13785 crate::gpu_wgpu::pooled_frame_enter(frame);
13786 let want_logits = self.graph_want_logits;
13790 self.graph_want_logits = false;
13791 let mut lg = Vec::new();
13792 let mut gl = 0usize;
13793 let t_span = std::time::Instant::now();
13794 let span = crate::gpu::gpu_scope(|| {
13795 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
13796 });
13797 self.graph_want_logits = want_logits;
13798 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
13799 let hh = match span {
13800 Some(Ok(hh)) => hh,
13801 Some(Err(())) => return Err(()),
13802 None => {
13803 return Ok(false);
13806 }
13807 };
13808 if gl != 1 {
13809 return if gl == 0 { Ok(false) } else { Err(()) };
13812 }
13813 *h = hh;
13814 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
13815 self.m3_note_route(li, &winners);
13816 if self.m3_winners.len() != self.num_layers {
13817 self.m3_winners = vec![Vec::new(); self.num_layers];
13818 }
13819 self.m3_winners[li] = winners;
13820 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
13821 cold.retain(|&(_, w)| {
13824 if w.is_finite() {
13825 true
13826 } else {
13827 tracing::error!("pooled tail: non-finite cold weight from expert route");
13828 false
13829 }
13830 });
13831 if cold.is_empty() {
13832 return Ok(true);
13833 }
13834 let x = crate::gpu_wgpu::pooled_frame_take_input();
13835 if x.len() != self.hidden_size {
13836 return Err(());
13837 }
13838 let lw = &self.weights.layers[self.phys_layer(li)];
13844 let FfnKind::Moe(m) = &lw.ffn else {
13845 return Err(());
13846 };
13847 let t_cold = std::time::Instant::now();
13848 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
13849 let mut out = crate::attention::take_buf(self.hidden_size);
13850 let on_device = !force_host
13851 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
13852 if !on_device {
13853 crate::attention::recycle_buf(&mut out);
13854 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
13855 .iter()
13856 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
13857 .collect::<Result<_, ()>>()?;
13858 out = crate::gpu::cpu_scope(|| {
13859 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
13860 });
13861 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
13862 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
13863 }
13864 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
13865 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
13866 if out.len() != h.len() {
13867 crate::attention::recycle_buf(&mut out);
13868 return Err(());
13869 }
13870 for (dst, src) in h.iter_mut().zip(out.iter()) {
13871 *dst += src;
13872 }
13873 crate::attention::recycle_buf(&mut out);
13874 if on_device {
13875 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
13876 }
13877 Ok(true)
13878 }
13879
13880 #[cfg(not(feature = "gpu"))]
13881 fn m3_pooled_layer(
13882 &mut self,
13883 _li: usize,
13884 _position: usize,
13885 _h: &mut Vec<f32>,
13886 ) -> Result<bool, ()> {
13887 Ok(false)
13888 }
13889
13890 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
13894 if self.o1_active() || self.attn_softcap > 0.0 {
13895 return None;
13896 }
13897 if let Some(reason) = self.wgpu_graph_attn_decline() {
13900 self.note_graph_decline("wgpu multi-burst", reason);
13901 return None;
13902 }
13903 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
13904 if !graph_on || self.graph_refused() {
13905 return None;
13912 }
13913 let emb = self.embed_single(t_next);
13914 let mut lg = Vec::new();
13915 let mut ids = Vec::new();
13916 match self.try_token_graph_wgpu_steps(
13917 &emb,
13918 position,
13919 &mut lg,
13920 k,
13921 Some(&mut ids),
13922 None,
13923 0,
13924 self.num_layers,
13925 ) {
13926 Some(Ok(_)) => {}
13927 Some(Err(())) => {
13928 self.graph_failed
13933 .store(true, std::sync::atomic::Ordering::Relaxed);
13934 return None;
13935 }
13936 None => return None,
13937 }
13938 (ids.len() == k).then_some(ids)
13939 }
13940
13941 fn try_token_graph_wgpu_steps(
13945 &self,
13946 hidden: &[f32],
13947 position: usize,
13948 logits_out: &mut Vec<f32>,
13949 steps: usize,
13950 ids_out: Option<&mut Vec<u32>>,
13951 layers_run: Option<&mut usize>,
13952 from: usize,
13953 upto_excl: usize,
13954 ) -> Option<Result<Vec<f32>, ()>> {
13955 let upto_excl = match self.mimo_moe.graph_prefix_end() {
13958 Some(end) if end < upto_excl => {
13959 if steps != 1 || layers_run.is_none() || from >= end {
13960 return None;
13961 }
13962 end
13963 }
13964 _ => upto_excl,
13965 };
13966 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
13969 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
13970 return None;
13974 }
13975 if let Some(reason) = self.wgpu_graph_attn_decline() {
13982 self.note_graph_decline("wgpu token graph", reason);
13983 return None;
13984 }
13985 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
13990 .map(|li| {
13991 if !o1_gpu {
13992 return None;
13993 }
13994 self.kv_cache.layers[self.phys_layer(li)].o1_views()
13995 })
13996 .collect();
13997 if self.o1_active() && o1_gpu {
13998 let want: usize = (from..upto_excl)
14001 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
14002 .count();
14003 let have = o1_views.iter().filter(|v| v.is_some()).count();
14004 if want == 0 || have != want {
14005 use std::sync::atomic::{AtomicUsize, Ordering};
14015 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
14016 let code = have * 1000 + want;
14017 if LAST.swap(code, Ordering::Relaxed) != code {
14018 tracing::warn!(
14019 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
14020 );
14021 }
14022 return None;
14023 }
14024 }
14025 let nh = self.num_heads;
14026 let (nkv, hd, rd) = self.layer_geom(0);
14027 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14028 let mut layers = Vec::with_capacity(upto_excl - from);
14029 let mut model = None;
14030 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
14031 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14032 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
14034 let name = &m.tensors[i].name;
14035 let prism = if crate::prism::is_inverse_embedding(m, name) {
14036 crate::gpu::GraphPrismOp::InverseEmbedding
14037 } else if crate::prism::is_forward_weight(m, name) {
14038 crate::gpu::GraphPrismOp::Forward
14039 } else {
14040 crate::gpu::GraphPrismOp::None
14041 };
14042 return Some(crate::gpu::GraphW {
14043 idx: i,
14044 kind,
14045 row_scale: rs,
14046 data: &[],
14047 prism,
14048 affine: crate::prism::is_affine_target(m, name),
14049 });
14050 }
14051 match t.as_f32() {
14053 Some(d) => Some(crate::gpu::GraphW {
14054 idx: 0,
14055 kind: 4,
14056 row_scale: &[],
14057 data: d,
14058 prism: crate::gpu::GraphPrismOp::None,
14059 affine: false,
14060 }),
14061 None => {
14062 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14063 eprintln!("batch graph: weight has no graph/f32 representation");
14064 }
14065 None
14066 }
14067 }
14068 }
14069 for li in from..upto_excl {
14070 let lw = &self.weights.layers[self.phys_layer(li)];
14071 if dbg {
14072 let ak = match &lw.attn {
14073 AttnKind::Mla(_) => "Mla".into(),
14074 AttnKind::Full {
14075 output_gate, bias, ..
14076 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
14077 AttnKind::LinearGdn(_) => "LinearGdn".into(),
14078 AttnKind::Kda(_) => "Kda".into(),
14079 AttnKind::Linear(_) => "Linear".into(),
14080 AttnKind::ShortConv(_) => "ShortConv".into(),
14081 AttnKind::Bounded(_) => "Bounded".into(),
14082 };
14083 let fk = match &lw.ffn {
14084 FfnKind::Dense(_) => "Dense",
14085 FfnKind::Moe(_) => "Moe",
14086 FfnKind::DenseMoe(_) => "DenseMoe",
14087 };
14088 eprintln!("graph L{li}: attn={ak} ffn={fk}");
14089 }
14090 let gffn = match &lw.ffn {
14091 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
14095 FfnKind::Dense(d) => {
14096 let Some(act) = d.act.wgpu_graph_act() else {
14100 self.note_graph_decline(
14101 "wgpu token graph",
14102 "dense FFN activation without a graph kernel",
14103 );
14104 return None;
14105 };
14106 crate::gpu::GraphFfn::Dense {
14107 gate: gw(&d.gate_proj)?,
14108 up: gw(&d.up_proj)?,
14109 down: gw(&d.down_proj)?,
14110 act,
14111 }
14112 }
14113 FfnKind::Moe(m) => {
14114 if m.route_tau.is_some() || m.mask.is_some() {
14122 return None;
14123 }
14124 let shared = m.shared.as_ref();
14125 let inter = m.experts.first()?.gate_proj.rows();
14131 let detached_shared = match shared {
14139 Some((se, None))
14140 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14141 {
14142 if !matches!(se.act, Act::Silu)
14143 || se.gate_proj.mapped_q4tp().is_none()
14144 || se.up_proj.mapped_q4tp().is_none()
14145 || se.down_proj.mapped_q4tp().is_none()
14146 {
14147 return None;
14148 }
14149 Some(crate::gpu::GraphMoeDetachedShared {
14150 gate: gw(&se.gate_proj)?,
14151 up: gw(&se.up_proj)?,
14152 down: gw(&se.down_proj)?,
14153 width: se.gate_proj.rows(),
14154 })
14155 }
14156 Some((se, Some(_)))
14160 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14161 {
14162 return None;
14163 }
14164 _ => None,
14165 };
14166 let has_shared = shared.is_some() && detached_shared.is_none();
14167 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
14168 let sgate = match shared {
14169 Some((_, Some(sg))) if has_shared => gw(sg)?,
14170 _ => gw(&m.router)?,
14174 };
14175 let router = gw(&m.router)?;
14176 if router.prism != crate::gpu::GraphPrismOp::None
14182 || sgate.prism != crate::gpu::GraphPrismOp::None
14183 || router.affine
14184 || sgate.affine
14185 {
14186 tracing::warn!(
14187 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
14188 );
14189 return None;
14190 }
14191 let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
14192 let edge = |t: &QTensor| {
14193 t.graph_weight()
14194 .or_else(|| t.graph_weight_descriptor())
14195 .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
14196 };
14197 let bank_key: Option<GraphMoeKey> = match (
14198 m.experts.first().and_then(|e| edge(&e.gate_proj)),
14199 m.experts.last().and_then(|e| edge(&e.down_proj)),
14200 ) {
14201 (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
14202 self.phys_layer(li),
14203 m.experts.as_ptr() as usize,
14204 m.experts.len(),
14205 in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
14206 fm,
14207 fi,
14208 di,
14209 )),
14210 _ => None,
14211 };
14212 let cached =
14213 bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
14214 let bank = if let Some(b) = cached {
14215 b
14216 } else {
14217 let mut experts = Vec::with_capacity(m.experts.len() + 1);
14218 let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14219 let mut q4tp: Option<bool> = None;
14222 let mut gu_q2: Option<bool> = None;
14225 let mut q82: Option<bool> = None;
14227 let mut moe_act = None;
14231 for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
14232 let Some(e_act) = e.act.wgpu_graph_act() else {
14233 return None;
14234 };
14235 match moe_act {
14236 Some(existing) if existing != e_act => return None,
14237 None => moe_act = Some(e_act),
14238 _ => {}
14239 }
14240 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
14241 return None;
14242 }
14243 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
14248 let Some((em, ei, _, _)) = expert_weight
14249 .graph_weight()
14250 .or_else(|| expert_weight.graph_weight_descriptor())
14251 else {
14252 return None;
14253 };
14254 let name = &em.tensors[ei].name;
14255 if crate::prism::is_forward_weight(em, name)
14256 || crate::prism::is_inverse_embedding(em, name)
14257 || crate::prism::is_affine_target(em, name)
14258 {
14259 tracing::warn!(
14260 "resident MoE declined: expert Prism/affine transform is not implemented"
14261 );
14262 return None;
14263 }
14264 }
14265 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
14266 Some((mm, gi)) => (
14267 mm,
14268 gi,
14269 e.up_proj.mapped_q4t()?.1,
14270 e.down_proj.mapped_q4t()?.1,
14271 false,
14272 false,
14273 false,
14274 ),
14275 None => match e.gate_proj.mapped_q2tp() {
14276 Some((mm, gi)) => (
14277 mm,
14278 gi,
14279 e.up_proj.mapped_q2tp()?.1,
14280 e.down_proj.mapped_q4tp()?.1,
14281 true,
14282 true,
14283 false,
14284 ),
14285 None => match e.gate_proj.mapped_q4tp() {
14286 Some((mm, gi)) => (
14287 mm,
14288 gi,
14289 e.up_proj.mapped_q4tp()?.1,
14290 e.down_proj.mapped_q4tp()?.1,
14291 true,
14292 false,
14293 false,
14294 ),
14295 None => {
14298 if !crate::gpu::moe_q82_graph_on() {
14299 return None;
14300 }
14301 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14302 (
14303 mm,
14304 gi,
14305 e.up_proj.mapped_q8_2f()?.1,
14306 e.down_proj.mapped_q8_2f()?.1,
14307 false,
14308 false,
14309 true,
14310 )
14311 }
14312 },
14313 },
14314 };
14315 if *q4tp.get_or_insert(is_p) != is_p
14316 || *gu_q2.get_or_insert(is_q2) != is_q2
14317 || *q82.get_or_insert(is_8) != is_8
14318 {
14319 tracing::warn!(
14325 "MoE layer mixes expert layouts (q4tp={is_p}, q2tp gate/up={is_q2}, q8_2f={is_8}) — every expert of a layer, INCLUDING the shared one, must share a layout. The whole-token graph declines this layer."
14326 );
14327 return None;
14328 }
14329 bank_model.get_or_insert_with(|| mm.clone());
14330 experts.push((gi, ui, di));
14331 }
14332 let b = GraphMoeBank {
14333 experts,
14334 q4tp,
14335 gu_q2,
14336 q82,
14337 act: moe_act,
14338 model: bank_model,
14339 };
14340 if let Some(k) = bank_key {
14341 self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
14342 }
14343 b
14344 };
14345 if let Some(mm) = &bank.model {
14346 model.get_or_insert_with(|| mm.clone());
14347 }
14348 crate::gpu::GraphFfn::Moe {
14349 router,
14350 shared_gate: sgate,
14351 experts: bank.experts,
14352 n_exp: m.experts.len(),
14353 top_k: std::env::var("CMF_TOPK_PROBE")
14359 .ok()
14360 .and_then(|v| v.parse::<usize>().ok())
14361 .filter(|k| *k > 0 && *k <= m.top_k)
14362 .unwrap_or(m.top_k),
14363 inter,
14364 act: bank.act?,
14365 norm_topk: m.norm_topk_prob,
14366 q4tp: bank.q4tp?,
14367 gu_q2: bank.gu_q2.unwrap_or(false),
14368 q82: bank.q82.unwrap_or(false),
14369 sigmoid: m.router_sigmoid,
14370 sigmoid_denom_eps: m.sigmoid_denom_eps,
14371 bias: m.expert_bias.as_deref(),
14372 has_shared,
14373 detached_shared,
14374 shared_gated,
14375 route_scale: m.routed_scaling,
14376 }
14377 }
14378 };
14379 let attn = match &lw.attn {
14380 AttnKind::Full {
14381 wq,
14382 wk,
14383 wv,
14384 wo,
14385 q_norm,
14386 k_norm,
14387 output_gate,
14388 softplus_gate,
14389 bias,
14390 } => {
14391 if self.attention_heads_per_layer.is_some() {
14392 return None;
14393 }
14394 let head_gate = match softplus_gate {
14398 None => None,
14399 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
14400 Some(_) => {
14401 self.note_graph_decline(
14402 "wgpu token graph",
14403 "projected softplus / per-element output gate",
14404 );
14405 return None;
14406 }
14407 };
14408 let (m, _, _, _) = wq.graph_weight_or_blr()?;
14409 model = Some(m.clone());
14410 crate::gpu::GraphAttn::Full {
14411 wq: gw(wq)?,
14412 wk: gw(wk)?,
14413 wv: gw(wv)?,
14414 wo: gw(wo)?,
14415 q_norm: q_norm.as_deref(),
14416 k_norm: k_norm.as_deref(),
14417 late_qk_norm: self.qk_norm_after_rope,
14418 bias: bias
14419 .as_ref()
14420 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14421 output_gate: *output_gate,
14422 cpu_k: self.kv_cache.layers[li].k_heads(),
14423 cpu_v: self.kv_cache.layers[li].v_heads(),
14424 cpu_base: self.kv_cache.layers[li].base(),
14425 geom: self.graph_attn_geom(li),
14426 head_gate,
14427 }
14428 }
14429 AttnKind::LinearGdn(w) => {
14430 let cfg = self.gdn_cfg?;
14431 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
14432 model = Some(m.clone());
14433 crate::gpu::GraphAttn::Gdn {
14434 qkv: gw(&w.in_proj_qkv)?,
14435 z: gw(&w.in_proj_z)?,
14436 a: gw(&w.in_proj_a)?,
14437 b: gw(&w.in_proj_b)?,
14438 out: gw(&w.out_proj)?,
14439 conv1d: &w.conv1d,
14440 a_log: &w.a_log,
14441 dt_bias: &w.dt_bias,
14442 norm: &w.norm,
14443 nv: cfg.num_v_heads,
14444 nk: cfg.num_k_heads,
14445 dk: cfg.key_head_dim,
14446 dv: cfg.value_head_dim,
14447 kk: cfg.conv_kernel,
14448 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14449 preview: None,
14450 }
14451 }
14452 AttnKind::ShortConv(w) => {
14453 let cfg = self.short_conv_cfg?;
14454 let (m, _, _, _) = w
14455 .in_proj
14456 .graph_weight()
14457 .or_else(|| w.in_proj.graph_weight_descriptor())?;
14458 model = Some(m.clone());
14459 crate::gpu::GraphAttn::ShortConv {
14460 inp: gw(&w.in_proj)?,
14461 out: gw(&w.out_proj)?,
14462 taps: &w.conv,
14463 kernel: cfg.kernel,
14464 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14465 }
14466 }
14467 _ => return None,
14468 };
14469 layers.push(crate::gpu::GraphLayer {
14470 input_norm: &lw.input_norm,
14471 attn,
14472 post_norm: &lw.post_norm,
14473 attn_out_norm: lw.attn_out_norm.as_deref(),
14474 ffn_out_norm: lw.ffn_out_norm.as_deref(),
14475 layer_scale: lw.layer_scale,
14476 ffn: gffn,
14477 });
14478 }
14479 let model = model?;
14480 let lm_gw = if upto_excl == self.num_layers
14486 && self.graph_want_logits
14487 && std::env::var("CMF_GPU_LMHEAD")
14488 .map(|v| v != "0")
14489 .unwrap_or(true)
14490 {
14491 self.weights
14492 .lm_head
14493 .graph_weight()
14494 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
14495 .map(|(m, i, kind, rs)| {
14496 let name = &m.tensors[i].name;
14497 let prism = if crate::prism::is_inverse_embedding(m, name) {
14498 crate::gpu::GraphPrismOp::InverseEmbedding
14499 } else if crate::prism::is_forward_weight(m, name) {
14500 crate::gpu::GraphPrismOp::Forward
14501 } else {
14502 crate::gpu::GraphPrismOp::None
14503 };
14504 (
14505 crate::gpu::GraphW {
14506 idx: i,
14507 kind,
14508 row_scale: rs,
14509 data: &[],
14510 prism,
14511 affine: crate::prism::is_affine_target(m, name),
14512 },
14513 self.weights.lm_head.rows(),
14514 )
14515 })
14516 } else {
14517 None
14518 };
14519 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
14520 let emb_gw = if steps > 1 {
14522 self.weights
14523 .embed_tokens
14524 .graph_weight()
14525 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
14526 .map(|(m, i, kind, rs)| {
14527 let name = &m.tensors[i].name;
14528 let prism = if crate::prism::is_inverse_embedding(m, name) {
14529 crate::gpu::GraphPrismOp::InverseEmbedding
14530 } else if crate::prism::is_forward_weight(m, name) {
14531 crate::gpu::GraphPrismOp::Forward
14532 } else {
14533 crate::gpu::GraphPrismOp::None
14534 };
14535 (
14536 crate::gpu::GraphW {
14537 idx: i,
14538 kind,
14539 row_scale: rs,
14540 data: &[],
14541 prism,
14542 affine: crate::prism::is_affine_target(m, name),
14543 },
14544 self.weights.embed_tokens.rows(),
14545 self.embed_multiplier,
14546 )
14547 })
14548 } else {
14549 None
14550 };
14551
14552 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
14558 (from..upto_excl.min(self.num_layers - 1))
14559 .filter(|&li| (li + 1) % self.physical_layers == 0)
14560 .map(|li| li - from)
14561 .collect()
14562 } else {
14563 Vec::new()
14564 };
14565 let mut h = hidden.to_vec();
14566 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
14572 let outcome = crate::gpu::forward_token_graph(
14573 &model,
14574 self.graph_kv_id,
14575 &layers,
14576 &o1_views,
14577 self.o1_epoch,
14578 &self.inv_freq,
14579 &mut h,
14580 nh,
14581 nkv,
14582 hd,
14583 self.attn_scale,
14584 rd,
14585 self.hidden_size,
14586 self.intermediate_size,
14587 position,
14588 self.kv_cache.max_seq_len,
14589 gemma,
14590 self.rms_eps as f32,
14591 lm,
14592 &self.weights.final_norm,
14593 logits_out,
14594 &loop_norm_at,
14595 steps,
14596 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
14597 ids_out,
14598 layers_run,
14599 from,
14600 dump_hidden,
14601 );
14602 match outcome {
14603 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
14604 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
14605 crate::gpu::TokenGraphOutcome::Declined => None,
14606 }
14607 }
14608
14609 #[cfg(target_os = "macos")]
14618 #[allow(clippy::type_complexity)]
14619 fn metal_rows_plan(
14620 &self,
14621 ) -> Option<(
14622 Vec<MetalRowsItem<'_>>,
14623 std::sync::Arc<cortiq_core::CmfModel>,
14624 Option<crate::gpu_metal::GdnGpuCfg>,
14625 )> {
14626 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
14627 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
14628 if !graph_force
14629 || !crate::gpu::enabled_here()
14630 || std::env::var("CMF_GPU_BLOCK")
14631 .map(|v| v == "0")
14632 .unwrap_or(false)
14633 || self.attn_softcap > 0.0
14634 || self.o1_active()
14635 || self.swa.is_some()
14636 || self.global_attn.is_some()
14637 || self.attention_heads_per_layer.is_some()
14638 || self.graph_attn_decline_reason().is_some()
14640 || self.attn_v_norm
14641 || self.loop_final_norm
14642 || !self.metal_rope_scale_ok()
14643 {
14644 return None;
14645 }
14646 let attend_contract = self.head_dim % 4 == 0
14647 && self.head_dim <= 256
14648 && self.rotary_dim >= 2
14649 && self.rotary_dim <= self.head_dim
14650 && (self.rotary_dim / 2) % 32 == 0
14651 && self.num_kv_heads > 0
14652 && self.num_heads % self.num_kv_heads == 0;
14653 if !attend_contract {
14654 return None;
14655 }
14656 let mut plan: Vec<MetalRowsItem> = Vec::new();
14657 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14658 for li in 0..self.num_layers {
14659 let lw = &self.weights.layers[self.phys_layer(li)];
14660 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14661 return None;
14662 }
14663 let ffn = match &lw.ffn {
14664 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
14665 let (Some(g), Some(u), Some(dn)) = (
14666 d.gate_proj.metal_graph_parts(),
14667 d.up_proj.metal_graph_parts(),
14668 d.down_proj.metal_graph_parts(),
14669 ) else {
14670 return None;
14671 };
14672 MetalFfn::Dense {
14673 gate: g,
14674 up: u,
14675 down: dn,
14676 act: 0, }
14678 }
14679 _ => return None,
14680 };
14681 match &lw.attn {
14682 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
14683 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
14684 w.in_proj_qkv.metal_graph_parts(),
14685 w.in_proj_z.metal_graph_parts(),
14686 w.in_proj_a.f32_parts(),
14687 w.in_proj_b.f32_parts(),
14688 w.out_proj.metal_graph_parts(),
14689 ) else {
14690 return None;
14691 };
14692 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
14693 model_ref.get_or_insert_with(|| model.clone());
14694 }
14695 let gl = GdnGpuLayer {
14696 attn_norm: &lw.input_norm,
14697 post_norm: &lw.post_norm,
14698 qkv,
14699 z,
14700 a,
14701 b: bb,
14702 out,
14703 ffn,
14704 conv1d: &w.conv1d,
14705 a_log: &w.a_log,
14706 dt_bias: &w.dt_bias,
14707 gnorm: &w.norm,
14708 };
14709 match plan.last_mut() {
14710 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
14711 _ => plan.push(MetalRowsItem::Gdn {
14712 run: vec![gl],
14713 first: li,
14714 }),
14715 }
14716 }
14717 AttnKind::Full {
14718 wq,
14719 wk,
14720 wv,
14721 wo,
14722 q_norm,
14723 k_norm,
14724 output_gate,
14725 softplus_gate: None,
14726 bias: None,
14727 } => {
14728 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
14729 wq.metal_graph_parts(),
14730 wk.metal_graph_parts(),
14731 wv.metal_graph_parts(),
14732 wo.metal_graph_parts(),
14733 ) else {
14734 return None;
14735 };
14736 if let QTensor::Mapped { model, .. } = wq {
14737 model_ref.get_or_insert_with(|| model.clone());
14738 }
14739 let cache = &self.kv_cache.layers[li];
14740 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
14741 return None;
14742 }
14743 plan.push(MetalRowsItem::Attn {
14744 l: AttnGpuLayer {
14745 attn_norm: &lw.input_norm,
14746 post_norm: &lw.post_norm,
14747 wq: pq,
14748 wk: pk,
14749 wv: pv,
14750 wo: po,
14751 ffn,
14752 attn_out_norm: None,
14753 ffn_out_norm: None,
14754 layer_scale: None,
14755 },
14756 li,
14757 q_norm: q_norm.as_deref(),
14758 k_norm: k_norm.as_deref(),
14759 output_gate: *output_gate,
14760 });
14761 }
14762 _ => return None,
14763 }
14764 }
14765 let model = model_ref?;
14766 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
14767 nv: cfg.num_v_heads,
14768 nk: cfg.num_k_heads,
14769 dk: cfg.key_head_dim,
14770 dv: cfg.value_head_dim,
14771 kk: cfg.conv_kernel,
14772 hidden: self.hidden_size,
14773 inter: self.intermediate_size,
14774 c_dim: cfg.conv_dim(),
14775 eps: cfg.rms_eps as f32,
14776 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14777 });
14778 Some((plan, model, gcfg))
14779 }
14780
14781 #[cfg(target_os = "macos")]
14783 #[allow(clippy::too_many_arguments)]
14784 fn metal_attn_params<'a>(
14785 li: usize,
14786 cache: &'a crate::kv_cache::LayerKvCache,
14787 q_norm: Option<&'a [f32]>,
14788 k_norm: Option<&'a [f32]>,
14789 output_gate: bool,
14790 inv_freq: &'a [f32],
14791 rope_scale: f32,
14792 geom: (usize, usize, usize, usize),
14793 pos0: usize,
14794 kv_id: u64,
14795 scale: f32,
14796 eps: f32,
14797 gemma: bool,
14798 late_qk_norm: bool,
14799 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
14800 let (nh, nkv, hd, rd) = geom;
14801 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14802 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14803 let cpu_stored = cpu_k[0].len() / hd;
14804 (
14805 crate::gpu_metal::AttnDeviceParams {
14806 kv_id,
14807 layer: li,
14808 nh,
14809 nkv,
14810 hd,
14811 rd,
14812 position: pos0,
14813 scale,
14814 eps,
14815 gemma,
14816 late_qk_norm,
14817 output_gate,
14818 q_norm,
14819 k_norm,
14820 inv_freq,
14821 rope_scale,
14822 cpu_k,
14823 cpu_v,
14824 cpu_stored,
14825 cpu_gen: cache.generation(),
14826 o1: None,
14827 window: None,
14828 softmax_sink: cache.sinks.as_deref(),
14829 head_gate: None,
14830 v_norm: false,
14831 },
14832 cpu_stored,
14833 )
14834 }
14835
14836 #[cfg(target_os = "macos")]
14841 #[allow(clippy::type_complexity)]
14842 fn metal_rows_run(
14843 &mut self,
14844 hiddens: &mut [f32],
14845 pos0: usize,
14846 b: usize,
14847 prefill: bool,
14848 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14849 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
14853 ) -> MetalRowsRun {
14854 use crate::gpu_metal::{GraphDims, VerifyGraph};
14855 if !crate::gpu_metal::wait_replay() {
14861 tracing::error!("Metal rows graph: the pending async replay failed");
14862 return MetalRowsRun::Failed;
14863 }
14864 spec_stamp("v.wait");
14865 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
14871 if want > 0 {
14872 let phys = self.physical_layers.max(1);
14873 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
14874 let is_gdn = self
14875 .weights
14876 .layers
14877 .get(li % phys)
14878 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
14879 if is_gdn && l.linear_state.len() != want {
14880 l.linear_state = vec![0f32; want];
14881 }
14882 }
14883 }
14884 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
14885 return MetalRowsRun::Declined;
14886 };
14887 spec_stamp("v.plan");
14888 let dims = GraphDims {
14889 hidden: self.hidden_size,
14890 eps: self.rms_eps as f32,
14891 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14892 };
14893 let Some(mut graph) = (if prefill {
14894 VerifyGraph::new_prefill(&model, dims, hiddens, b)
14895 } else {
14896 VerifyGraph::new(&model, dims, hiddens, b)
14897 }) else {
14898 return MetalRowsRun::Declined;
14899 };
14900 let geom = (
14901 self.num_heads,
14902 self.num_kv_heads,
14903 self.head_dim,
14904 self.rotary_dim,
14905 );
14906 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14907 let eps = self.rms_eps as f32;
14908 let kv_id = self.graph_kv_id;
14909 let inv_freq = self.inv_freq.clone();
14910 for item in &plan {
14911 let ok = match item {
14912 MetalRowsItem::Gdn { run, .. } => gcfg
14913 .as_ref()
14914 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
14915 .unwrap_or(false),
14916 MetalRowsItem::Attn {
14917 l,
14918 li,
14919 q_norm,
14920 k_norm,
14921 output_gate,
14922 } => {
14923 let (p, _) = Self::metal_attn_params(
14924 *li,
14925 &self.kv_cache.layers[*li],
14926 *q_norm,
14927 *k_norm,
14928 *output_gate,
14929 &inv_freq,
14930 self.layer_rope_scale(*li),
14931 geom,
14932 pos0,
14933 kv_id,
14934 self.attn_scale,
14935 eps,
14936 gemma,
14937 self.qk_norm_after_rope,
14938 );
14939 graph.attn_ok(l, &p)
14940 }
14941 };
14942 if !ok {
14943 use std::sync::atomic::{AtomicBool, Ordering};
14944 static SAID: AtomicBool = AtomicBool::new(false);
14945 if !SAID.swap(true, Ordering::Relaxed) {
14946 tracing::warn!("metal rows graph: a layer failed preflight — declining");
14947 }
14948 return MetalRowsRun::Declined;
14949 }
14950 }
14951 let lm = match &spec {
14952 Some((lm, _, _)) => {
14953 if !graph.lm_head_ok(*lm) {
14954 return MetalRowsRun::Declined;
14955 }
14956 Some(*lm)
14957 }
14958 None => None,
14959 };
14960 let mut gdn_layers = Vec::new();
14961 let mut attn_layers = Vec::new();
14962 for item in &plan {
14963 match item {
14964 MetalRowsItem::Gdn { run, first } => {
14965 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
14966 .iter()
14967 .map(|l| l.linear_state.as_slice())
14968 .collect();
14969 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
14970 return MetalRowsRun::Declined;
14971 }
14972 gdn_layers.extend(*first..*first + run.len());
14973 }
14974 MetalRowsItem::Attn {
14975 l,
14976 li,
14977 q_norm,
14978 k_norm,
14979 output_gate,
14980 } => {
14981 let (p, cpu_stored) = Self::metal_attn_params(
14982 *li,
14983 &self.kv_cache.layers[*li],
14984 *q_norm,
14985 *k_norm,
14986 *output_gate,
14987 &inv_freq,
14988 self.layer_rope_scale(*li),
14989 geom,
14990 pos0,
14991 kv_id,
14992 self.attn_scale,
14993 eps,
14994 gemma,
14995 self.qk_norm_after_rope,
14996 );
14997 if !graph.encode_attn_b(l, &p) {
14998 return MetalRowsRun::Declined;
14999 }
15000 attn_layers.push((*li, cpu_stored));
15001 }
15002 }
15003 }
15004 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
15005 if !graph.encode_lm_head_b(final_norm, lm) {
15006 return MetalRowsRun::Declined;
15007 }
15008 if let Some((n, _)) = argmax_out.as_ref() {
15013 if !graph.encode_argmax_b(*n) {
15014 argmax_out = None;
15015 }
15016 }
15017 }
15018 spec_stamp("v.enc");
15019 if !graph.sync() {
15020 return MetalRowsRun::Failed;
15021 }
15022 spec_stamp("v.gpu");
15023 match (spec, argmax_out) {
15024 (Some(_), Some((_, ids))) => {
15025 ids.resize(b, 0);
15026 if !graph.read_argmax(ids) {
15027 return MetalRowsRun::Failed;
15028 }
15029 spec_stamp("v.am");
15030 }
15031 (Some((lm, _, logits)), None) => {
15032 logits.resize(b * lm.1, 0.0);
15033 if !graph.read_logits(logits) {
15034 return MetalRowsRun::Failed;
15035 }
15036 spec_stamp("v.lg");
15037 }
15038 (None, _) => {}
15039 }
15040 if !graph.read_hidden(hiddens) {
15041 return MetalRowsRun::Failed;
15042 }
15043 spec_stamp("v.hid");
15044 MetalRowsRun::Completed(MetalVerifyPending {
15045 graph,
15046 gdn_layers,
15047 attn_layers,
15048 })
15049 }
15050
15051 #[cfg(target_os = "macos")]
15057 fn try_batch_graph_metal(
15058 &mut self,
15059 hiddens: &mut [f32],
15060 positions: &[usize],
15061 b: usize,
15062 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15063 argmax_out: Option<(usize, &mut Vec<u32>)>,
15064 ) -> crate::gpu::BatchGraphOutcome {
15065 let _t0 = std::time::Instant::now();
15066 if positions.len() != b
15067 || positions.windows(2).any(|w| w[1] != w[0] + 1)
15068 || hiddens.len() != b * self.hidden_size
15069 {
15070 return crate::gpu::BatchGraphOutcome::Declined;
15071 }
15072 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
15073 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
15074 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
15075 MetalRowsRun::Completed(pending) => pending,
15076 };
15077 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15078 eprintln!(
15079 "metal-verify: {:.1} ms | b={b}",
15080 _t0.elapsed().as_secs_f64() * 1e3
15081 );
15082 }
15083 self.metal_verify = Some(pending);
15084 crate::gpu::BatchGraphOutcome::Completed
15085 }
15086
15087 #[cfg(target_os = "macos")]
15092 fn prefill_rows_metal(
15093 &mut self,
15094 ids: &[u32],
15095 start_pos: usize,
15096 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15097 ) -> MetalPrefillOutcome {
15098 let b = ids.len();
15099 if b == 0 || b > 512 {
15100 return MetalPrefillOutcome::Declined;
15101 }
15102 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15103 let with_head = spec.is_some();
15104 let hs = self.hidden_size;
15105 let mut hiddens = vec![0f32; b * hs];
15106 for (j, &id) in ids.iter().enumerate() {
15107 let e = self.embed_single(id);
15108 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
15109 }
15110 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
15111 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
15112 MetalRowsRun::Failed => {
15113 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15114 return MetalPrefillOutcome::Failed;
15115 }
15116 MetalRowsRun::Completed(pending) => pending,
15117 };
15118 let idxs = pending.gdn_layers.clone();
15120 let mut outs: Vec<&mut [f32]> = self
15121 .kv_cache
15122 .layers
15123 .iter_mut()
15124 .enumerate()
15125 .filter(|(i, _)| idxs.binary_search(i).is_ok())
15126 .map(|(_, l)| l.linear_state.as_mut_slice())
15127 .collect();
15128 if !pending.graph.finish_states(&mut outs) {
15129 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15130 return MetalPrefillOutcome::Failed;
15131 }
15132 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15133 let mut rows = Vec::with_capacity(pending.attn_layers.len());
15137 for (li, cpu_stored) in &pending.attn_layers {
15138 let mut kbuf = vec![0f32; b * nkv * hd];
15139 let mut vbuf = vec![0f32; b * nkv * hd];
15140 if !crate::gpu_metal::kv_mirror_read_rows(
15141 self.graph_kv_id,
15142 *li,
15143 nkv,
15144 hd,
15145 *cpu_stored,
15146 b,
15147 &mut kbuf,
15148 &mut vbuf,
15149 ) {
15150 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15151 return MetalPrefillOutcome::Failed;
15152 }
15153 rows.push((*li, *cpu_stored, kbuf, vbuf));
15154 }
15155 for (li, cpu_stored, kbuf, vbuf) in rows {
15156 let cache = &mut self.kv_cache.layers[li];
15157 for r in 0..b {
15158 cache.append(
15159 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15160 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15161 &[],
15162 );
15163 }
15164 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
15165 }
15166 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15167 if with_head {
15168 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15169 }
15170 MetalPrefillOutcome::Completed(hiddens)
15171 }
15172
15173 #[cfg(target_os = "macos")]
15174 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
15175 self.prefill_rows_metal(ids, start_pos, None)
15176 }
15177
15178 #[cfg(target_os = "macos")]
15183 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
15184 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
15185 return MetalBatchNllOutcome::Declined;
15186 }
15187 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
15188 return MetalBatchNllOutcome::Declined;
15189 };
15190 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
15191 .ok()
15192 .and_then(|v| v.parse::<usize>().ok())
15193 .filter(|&v| (1..=512).contains(&v))
15194 .unwrap_or(32);
15195 let final_norm = self.weights.final_norm.clone();
15196 let mut nll = 0.0f64;
15197 let mut count = 0usize;
15198 let mut pos = 0usize;
15199 let mut completed = 0usize;
15200 while pos < ids.len() {
15201 let end = (pos + chunk).min(ids.len());
15202 let mut logits = Vec::new();
15203 let outcome =
15204 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
15205 match outcome {
15206 MetalPrefillOutcome::Declined => {
15207 return if completed == 0 {
15208 MetalBatchNllOutcome::Declined
15209 } else {
15210 MetalBatchNllOutcome::Failed(format!(
15211 "ordinary Metal NLL batch declined after {completed} chunks"
15212 ))
15213 };
15214 }
15215 MetalPrefillOutcome::Failed => {
15216 return MetalBatchNllOutcome::Failed(
15217 "ordinary Metal NLL batch failed after admission".to_string(),
15218 );
15219 }
15220 MetalPrefillOutcome::Completed(_) => {}
15221 }
15222 completed += 1;
15223 let vocab = self.vocab_size.min(lm.1);
15224 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
15225 return MetalBatchNllOutcome::Failed(
15226 "ordinary Metal NLL head returned an invalid shape".to_string(),
15227 );
15228 }
15229 for row in 0..(end - pos) {
15230 let absolute = pos + row;
15231 if absolute < start || absolute + 1 >= ids.len() {
15232 continue;
15233 }
15234 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
15235 if let Some(mu) = self.logit_multiplier {
15236 for v in lg.iter_mut() {
15237 *v *= mu;
15238 }
15239 }
15240 if let Some(c) = self.final_softcap {
15241 softcap_in_place(lg, c, self.pool.as_deref());
15242 }
15243 let target = ids[absolute + 1] as usize;
15244 if target >= vocab {
15245 return MetalBatchNllOutcome::Failed(format!(
15246 "target token {target} exceeds Metal head rows {vocab}"
15247 ));
15248 }
15249 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
15250 let lse: f64 = lg
15251 .iter()
15252 .map(|&v| ((v - max) as f64).exp())
15253 .sum::<f64>()
15254 .ln()
15255 + max as f64;
15256 nll += lse - lg[target] as f64;
15257 count += 1;
15258 }
15259 pos = end;
15260 }
15261 MetalBatchNllOutcome::Completed(nll, count)
15262 }
15263
15264 #[cfg(target_os = "macos")]
15268 fn metal_verify_commit(&mut self, a: usize) -> bool {
15269 let Some(mut pending) = self.metal_verify.take() else {
15270 return false;
15271 };
15272 let n = a + 1;
15273 let idxs = pending.gdn_layers.clone();
15275 let mut outs: Vec<&mut [f32]> = self
15276 .kv_cache
15277 .layers
15278 .iter_mut()
15279 .enumerate()
15280 .filter(|(i, _)| idxs.binary_search(i).is_ok())
15281 .map(|(_, l)| l.linear_state.as_mut_slice())
15282 .collect();
15283 if !pending.graph.commit(n, &mut outs) {
15284 return false;
15285 }
15286 spec_stamp("c.replay");
15287 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15288 let mut rows = Vec::with_capacity(pending.attn_layers.len());
15292 for (li, cpu_stored) in &pending.attn_layers {
15293 let mut kbuf = vec![0f32; n * nkv * hd];
15294 let mut vbuf = vec![0f32; n * nkv * hd];
15295 if !crate::gpu_metal::kv_mirror_read_rows(
15296 self.graph_kv_id,
15297 *li,
15298 nkv,
15299 hd,
15300 *cpu_stored,
15301 n,
15302 &mut kbuf,
15303 &mut vbuf,
15304 ) {
15305 return false;
15306 }
15307 rows.push((*li, *cpu_stored, kbuf, vbuf));
15308 }
15309 for (li, cpu_stored, kbuf, vbuf) in rows {
15310 let cache = &mut self.kv_cache.layers[li];
15311 for r in 0..n {
15312 cache.append(
15313 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15314 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15315 &[],
15316 );
15317 }
15318 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
15319 }
15320 spec_stamp("c.kv");
15321 true
15322 }
15323
15324 #[cfg(target_os = "macos")]
15331 fn mtp_warm_batch_submit(
15332 &mut self,
15333 m: &mut MtpModule,
15334 pairs: &[(&[f32], u32)],
15335 first_pos: usize,
15336 ) -> Option<MetalWarmPending> {
15337 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
15338 let b = pairs.len();
15339 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
15340 return None;
15341 }
15342 let AttnKind::Full {
15343 wq,
15344 wk,
15345 wv,
15346 wo,
15347 q_norm,
15348 k_norm,
15349 output_gate,
15350 softplus_gate: None,
15351 bias: None,
15352 } = &m.layer.attn
15353 else {
15354 return None;
15355 };
15356 let FfnKind::Dense(d) = &m.layer.ffn else {
15357 return None;
15358 };
15359 if !d.segs.is_empty() {
15360 return None;
15361 }
15362 let (Some(pq), Some(pk), Some(pv), Some(po)) =
15363 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15364 else {
15365 return None;
15366 };
15367 let (Some(g), Some(u), Some(dn)) = (
15368 d.gate_proj.q1_parts(),
15369 d.up_proj.q1_parts(),
15370 d.down_proj.q1_parts(),
15371 ) else {
15372 return None;
15373 };
15374 let Some(eh) = m.eh_proj.q1_parts() else {
15375 return None;
15376 };
15377 let QTensor::Mapped { model, .. } = wq else {
15378 return None;
15379 };
15380 let model = model.clone();
15381 let hs = self.hidden_size;
15382 let mut cat = vec![0f32; b * 2 * hs];
15384 for (j, (h, tok)) in pairs.iter().enumerate() {
15385 let e = self.embed_single(*tok);
15386 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
15387 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
15388 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
15389 }
15390 let dims = GraphDims {
15391 hidden: hs,
15392 eps: self.rms_eps as f32,
15393 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15394 };
15395 spec_stamp("w.cat");
15396 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
15397 return None;
15398 };
15399 spec_stamp("w.new");
15400 let l = AttnGpuLayer {
15401 attn_norm: &m.layer.input_norm,
15402 post_norm: &m.layer.post_norm,
15403 wq: pq,
15404 wk: pk,
15405 wv: pv,
15406 wo: po,
15407 ffn: MetalFfn::Dense {
15408 gate: g,
15409 up: u,
15410 down: dn,
15411 act: (d.act == Act::Gelu) as u8,
15412 },
15413 attn_out_norm: None,
15414 ffn_out_norm: None,
15415 layer_scale: None,
15416 };
15417 let (nh, nkv, hd, rd) = (
15418 self.num_heads,
15419 self.num_kv_heads,
15420 self.head_dim,
15421 self.rotary_dim,
15422 );
15423 let inv_freq = self.inv_freq.clone();
15424 let cpu_stored;
15425 {
15426 let cache = &m.kv;
15427 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15428 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15429 cpu_stored = cpu_k[0].len() / hd;
15430 if cpu_stored > first_pos {
15435 spec_stamp("w.decl");
15436 return None;
15437 }
15438 let p = AttnDeviceParams {
15439 kv_id: self.mtp_kv_id(),
15440 layer: Self::MTP_LAYER_BASE,
15441 nh,
15442 nkv,
15443 hd,
15444 rd,
15445 position: first_pos,
15446 scale: self.attn_scale,
15447 eps: self.rms_eps as f32,
15448 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15449 late_qk_norm: self.qk_norm_after_rope,
15450 output_gate: *output_gate,
15451 q_norm: q_norm.as_deref(),
15452 k_norm: k_norm.as_deref(),
15453 inv_freq: &inv_freq,
15454 rope_scale: self.rope_scale,
15455 cpu_k,
15456 cpu_v,
15457 cpu_stored,
15458 cpu_gen: cache.generation(),
15459 o1: None,
15460 window: None,
15461 softmax_sink: None,
15462 head_gate: None,
15463 v_norm: false,
15464 };
15465 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
15466 return None;
15467 }
15468 }
15469 spec_stamp("w.enc");
15470 if !graph.submit() {
15471 return None;
15472 }
15473 spec_stamp("w.sub");
15474 Some(MetalWarmPending {
15475 graph,
15476 cpu_stored,
15477 b,
15478 })
15479 }
15480
15481 #[cfg(target_os = "macos")]
15484 fn mtp_warm_batch_metal(
15485 &mut self,
15486 m: &mut MtpModule,
15487 pairs: &[(&[f32], u32)],
15488 first_pos: usize,
15489 ) -> bool {
15490 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
15491 Some(p) => self.mtp_warm_batch_finish(m, p),
15492 None => false,
15493 }
15494 }
15495
15496 #[cfg(target_os = "macos")]
15501 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
15502 let MetalWarmPending {
15503 mut graph,
15504 cpu_stored,
15505 b,
15506 } = pending;
15507 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15508 if !graph.sync() {
15509 return false;
15510 }
15511 spec_stamp("w.gpu");
15512 let mut kbuf = vec![0f32; b * nkv * hd];
15513 let mut vbuf = vec![0f32; b * nkv * hd];
15514 if !crate::gpu_metal::kv_mirror_read_rows(
15515 self.mtp_kv_id(),
15516 Self::MTP_LAYER_BASE,
15517 nkv,
15518 hd,
15519 cpu_stored,
15520 b,
15521 &mut kbuf,
15522 &mut vbuf,
15523 ) {
15524 return false;
15525 }
15526 for r in 0..b {
15527 m.kv.append(
15528 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15529 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15530 &[],
15531 );
15532 }
15533 crate::gpu_metal::kv_mirror_set_stored(
15534 self.mtp_kv_id(),
15535 Self::MTP_LAYER_BASE,
15536 cpu_stored + b,
15537 );
15538 spec_stamp("w.kv");
15539 true
15540 }
15541
15542 pub(crate) fn note_draft_id(&mut self, id: u32) {
15549 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
15550 if (id as usize) >= cut {
15551 self.draft_full_streak = 16;
15552 } else {
15553 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
15554 }
15555 }
15556
15557 fn draft_head_rows(&self, head_rows: usize) -> usize {
15560 if self.draft_full_streak > 0 {
15561 head_rows
15562 } else {
15563 Self::draft_vocab_rows(head_rows)
15564 }
15565 }
15566
15567 fn draft_vocab_rows(head_rows: usize) -> usize {
15570 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15571 let n = *N.get_or_init(|| {
15572 std::env::var("CMF_DRAFT_VOCAB")
15573 .ok()
15574 .and_then(|v| v.parse().ok())
15575 .unwrap_or(65536)
15576 });
15577 if n == 0 { head_rows } else { n.min(head_rows) }
15578 }
15579
15580 #[cfg(target_os = "macos")]
15585 fn mtp_step_metal(
15586 &mut self,
15587 m: &mut MtpModule,
15588 hidden: &[f32],
15589 next_token: u32,
15590 position: usize,
15591 want_logits: bool,
15592 ) -> Option<(Vec<f32>, Vec<f32>)> {
15593 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15594 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15595 || !crate::gpu::q1_force()
15596 || !crate::gpu::enabled_here()
15597 || self.attn_softcap > 0.0
15598 || self.attention_heads_per_layer.is_some()
15599 || m.kv.mode != crate::kv_cache::KvMode::F32
15600 || m.kv.o1.is_some()
15601 {
15602 return None;
15603 }
15604 let AttnKind::Full {
15605 wq,
15606 wk,
15607 wv,
15608 wo,
15609 q_norm,
15610 k_norm,
15611 output_gate,
15612 softplus_gate: None,
15613 bias: None,
15614 } = &m.layer.attn
15615 else {
15616 return None;
15617 };
15618 let FfnKind::Dense(d) = &m.layer.ffn else {
15619 return None;
15620 };
15621 if d.act != Act::Silu || !d.segs.is_empty() {
15622 return None;
15623 }
15624 let (pq, pk, pv, po) = (
15625 wq.q1_parts()?,
15626 wk.q1_parts()?,
15627 wv.q1_parts()?,
15628 wo.q1_parts()?,
15629 );
15630 let (g, u, dn) = (
15631 d.gate_proj.q1_parts()?,
15632 d.up_proj.q1_parts()?,
15633 d.down_proj.q1_parts()?,
15634 );
15635 let QTensor::Mapped { model, .. } = wq else {
15636 return None;
15637 };
15638 let model = model.clone();
15639 let lm = if want_logits {
15640 Some(self.weights.lm_head.q1_parts()?)
15641 } else {
15642 None
15643 };
15644 let dims = GraphDims {
15645 hidden: self.hidden_size,
15646 eps: self.rms_eps as f32,
15647 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15648 };
15649 let hs = self.hidden_size;
15652 let mut x = vec![0f32; hs];
15653 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15660 crate::gpu_metal::DENSE_MV,
15661 );
15662 let mut graph = TokenGraph::new(&model, dims, &x)?;
15663 let mut folded = false;
15664 if let Some(eh) = m.eh_proj.q1_parts() {
15665 let e = self.embed_single(next_token);
15666 let mut cat = vec![0.0f32; 2 * hs];
15667 let (cat_e, cat_h) = cat.split_at_mut(hs);
15668 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
15669 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
15670 folded = graph.encode_input_proj(eh, &cat);
15671 }
15672 if !folded {
15673 x = self.mtp_block_input(m, hidden, next_token);
15674 graph = TokenGraph::new(&model, dims, &x)?;
15675 }
15676 spec_stamp("d.in");
15677 let l = AttnGpuLayer {
15678 attn_norm: &m.layer.input_norm,
15679 post_norm: &m.layer.post_norm,
15680 wq: pq,
15681 wk: pk,
15682 wv: pv,
15683 wo: po,
15684 ffn: MetalFfn::Dense {
15685 gate: g,
15686 up: u,
15687 down: dn,
15688 act: (d.act == Act::Gelu) as u8,
15689 },
15690 attn_out_norm: None,
15691 ffn_out_norm: None,
15692 layer_scale: None,
15693 };
15694 let (nh, nkv, hd, rd) = (
15695 self.num_heads,
15696 self.num_kv_heads,
15697 self.head_dim,
15698 self.rotary_dim,
15699 );
15700 let inv_freq = self.inv_freq.clone();
15701 {
15702 let cache = &m.kv;
15703 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15704 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15705 let cpu_stored = cpu_k[0].len() / hd;
15706 let p = AttnDeviceParams {
15707 kv_id: self.mtp_kv_id(),
15708 layer: Self::MTP_LAYER_BASE,
15709 nh,
15710 nkv,
15711 hd,
15712 rd,
15713 position,
15714 scale: self.attn_scale,
15715 eps: self.rms_eps as f32,
15716 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15717 late_qk_norm: self.qk_norm_after_rope,
15718 output_gate: *output_gate,
15719 q_norm: q_norm.as_deref(),
15720 k_norm: k_norm.as_deref(),
15721 inv_freq: &inv_freq,
15722 rope_scale: self.rope_scale,
15723 cpu_k,
15724 cpu_v,
15725 cpu_stored,
15726 cpu_gen: cache.generation(),
15727 o1: None,
15728 window: None,
15729 softmax_sink: None,
15730 head_gate: None,
15731 v_norm: false,
15732 };
15733 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
15734 return None;
15735 }
15736 }
15737 let draft_rows = if let Some(lm) = lm {
15743 self.draft_head_rows(lm.1)
15744 } else {
15745 0
15746 };
15747 if let Some(lm) = lm {
15748 if !graph.lm_head_ok(lm) {
15749 return None;
15750 }
15751 if draft_rows < lm.1 {
15752 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
15753 return None;
15754 }
15755 } else {
15756 graph.encode_lm_head(&m.final_norm, lm);
15757 }
15758 }
15759 spec_stamp("d.enc");
15760 if graph.sync_checked().is_err() {
15761 return None;
15762 }
15763 spec_stamp("d.gpu");
15764 let mut logits = Vec::new();
15765 if let Some(lm) = lm {
15766 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
15767 logits = attention::take_buf(n_read);
15768 graph.read_logits(&mut logits);
15769 pad_mtp_draft_logits(&mut logits, self.vocab_size);
15771 }
15772 graph.finish(&mut x);
15773 let mut krow = attention::take_buf(nkv * hd);
15774 let mut vrow = attention::take_buf(nkv * hd);
15775 if crate::gpu_metal::kv_mirror_read_last(
15776 self.mtp_kv_id(),
15777 Self::MTP_LAYER_BASE,
15778 nkv,
15779 hd,
15780 &mut krow,
15781 &mut vrow,
15782 ) {
15783 m.kv.append(&krow, &vrow, &[]);
15784 }
15785 attention::recycle_buf(&mut krow);
15786 attention::recycle_buf(&mut vrow);
15787 spec_stamp("d.rd");
15788 Some((logits, x))
15789 }
15790
15791 fn mtp_chain_on() -> bool {
15804 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15805 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
15806 }
15807
15808 #[cfg(target_os = "macos")]
15820 fn mtp_draft_chain_metal(
15821 &mut self,
15822 m: &mut MtpModule,
15823 hidden: &[f32],
15824 t_next: u32,
15825 position: usize,
15826 k: usize,
15827 ) -> Result<Vec<u32>, bool> {
15828 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15829 if k == 0
15830 || k > 64
15831 || !Self::mtp_chain_on()
15832 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15833 || !crate::gpu::q1_force()
15834 || !crate::gpu::enabled_here()
15835 || self.attn_softcap > 0.0
15836 || self.attention_heads_per_layer.is_some()
15837 || m.kv.mode != crate::kv_cache::KvMode::F32
15838 || m.kv.o1.is_some()
15839 || self.dsv4.is_some()
15841 || self.dsv41.is_some()
15842 || self.qwen4_exp.is_some()
15843 || self.per_token_stack()
15844 {
15845 return Err(false);
15846 }
15847 let AttnKind::Full {
15848 wq,
15849 wk,
15850 wv,
15851 wo,
15852 q_norm,
15853 k_norm,
15854 output_gate,
15855 softplus_gate: None,
15856 bias: None,
15857 } = &m.layer.attn
15858 else {
15859 return Err(false);
15860 };
15861 let FfnKind::Dense(d) = &m.layer.ffn else {
15862 return Err(false);
15863 };
15864 if d.act != Act::Silu || !d.segs.is_empty() {
15865 return Err(false);
15866 }
15867 let (Some(pq), Some(pk), Some(pv), Some(po)) =
15868 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15869 else {
15870 return Err(false);
15871 };
15872 let (Some(g), Some(u), Some(dn)) = (
15873 d.gate_proj.q1_parts(),
15874 d.up_proj.q1_parts(),
15875 d.down_proj.q1_parts(),
15876 ) else {
15877 return Err(false);
15878 };
15879 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
15880 return Err(false);
15881 };
15882 let QTensor::Mapped { model, .. } = wq else {
15883 return Err(false);
15884 };
15885 let model = model.clone();
15886 let QTensor::Mapped {
15889 model: em,
15890 idx: eidx,
15891 dtype: cortiq_core::TensorDtype::Q4TiledP,
15892 ..
15893 } = &self.weights.embed_tokens
15894 else {
15895 return Err(false);
15896 };
15897 if !std::sync::Arc::ptr_eq(em, &model)
15898 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
15899 {
15900 return Err(false);
15901 }
15902 let embed = (
15903 *eidx,
15904 self.weights.embed_tokens.rows(),
15905 self.weights.embed_tokens.cols(),
15906 );
15907 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
15908 return Err(false);
15909 }
15910 let dims = GraphDims {
15911 hidden: self.hidden_size,
15912 eps: self.rms_eps as f32,
15913 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15914 };
15915 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15919 crate::gpu_metal::DENSE_MV,
15920 );
15921 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
15922 return Err(false);
15923 };
15924 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
15925 return Err(false);
15926 }
15927 let l = AttnGpuLayer {
15928 attn_norm: &m.layer.input_norm,
15929 post_norm: &m.layer.post_norm,
15930 wq: pq,
15931 wk: pk,
15932 wv: pv,
15933 wo: po,
15934 ffn: MetalFfn::Dense {
15935 gate: g,
15936 up: u,
15937 down: dn,
15938 act: (d.act == Act::Gelu) as u8,
15939 },
15940 attn_out_norm: None,
15941 ffn_out_norm: None,
15942 layer_scale: None,
15943 };
15944 let (nh, nkv, hd, rd) = (
15945 self.num_heads,
15946 self.num_kv_heads,
15947 self.head_dim,
15948 self.rotary_dim,
15949 );
15950 let inv_freq = self.inv_freq.clone();
15951 let draft_rows = self.draft_head_rows(lm.1);
15952 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
15953 if n_arg == 0 {
15954 return Err(false);
15955 }
15956 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
15963 let t_chain = std::time::Instant::now();
15964 graph.chain_ids_init(t_next, k);
15965 let cpu_stored;
15966 {
15967 let cache = &m.kv;
15968 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15969 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15970 cpu_stored = cpu_k[0].len() / hd;
15971 for j in 0..k {
15972 if !graph.encode_chain_input(
15973 embed,
15974 j as u32,
15975 &m.enorm,
15976 &m.hnorm,
15977 self.embed_multiplier,
15978 eh,
15979 ) {
15980 return Err(false);
15981 }
15982 let p = AttnDeviceParams {
15986 kv_id: self.mtp_kv_id(),
15987 layer: Self::MTP_LAYER_BASE,
15988 nh,
15989 nkv,
15990 hd,
15991 rd,
15992 position: position + j,
15993 scale: self.attn_scale,
15994 eps: self.rms_eps as f32,
15995 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15996 late_qk_norm: self.qk_norm_after_rope,
15997 output_gate: *output_gate,
15998 q_norm: q_norm.as_deref(),
15999 k_norm: k_norm.as_deref(),
16000 inv_freq: &inv_freq,
16001 rope_scale: self.rope_scale,
16002 cpu_k: cpu_k.clone(),
16003 cpu_v: cpu_v.clone(),
16004 cpu_stored: cpu_stored + j,
16005 cpu_gen: cache.generation(),
16006 o1: None,
16007 window: None,
16008 softmax_sink: None,
16009 head_gate: None,
16010 v_norm: false,
16011 };
16012 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16013 return Err(false);
16014 }
16015 if draft_rows < lm.1 {
16016 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16017 return Err(false);
16018 }
16019 } else {
16020 graph.encode_lm_head(&m.final_norm, lm);
16021 }
16022 if !graph.encode_argmax(n_arg, j as u32 + 1) {
16023 return Err(false);
16024 }
16025 if split {
16026 graph.commit();
16029 }
16030 }
16031 }
16032 let t_enc = t_chain.elapsed();
16033 if graph.sync_checked().is_err() {
16034 return Err(true);
16035 }
16036 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
16037 eprintln!(
16038 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
16039 t_enc.as_secs_f64() * 1e3,
16040 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
16041 if split { ", split" } else { "" }
16042 );
16043 }
16044 let mut ids = vec![0u32; k];
16045 if !graph.chain_ids_read(&mut ids) {
16046 return Err(true);
16047 }
16048 let mut kbuf = vec![0f32; k * nkv * hd];
16049 let mut vbuf = vec![0f32; k * nkv * hd];
16050 if !crate::gpu_metal::kv_mirror_read_rows(
16051 self.mtp_kv_id(),
16052 Self::MTP_LAYER_BASE,
16053 nkv,
16054 hd,
16055 cpu_stored,
16056 k,
16057 &mut kbuf,
16058 &mut vbuf,
16059 ) {
16060 return Err(true);
16061 }
16062 for r in 0..k {
16063 m.kv.append(
16064 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
16065 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
16066 &[],
16067 );
16068 }
16069 Ok(ids)
16070 }
16071
16072 fn try_batch_graph_wgpu(
16073 &self,
16074 hiddens: &mut [f32],
16075 positions: &[usize],
16076 k: usize,
16077 spec: Option<crate::gpu::SpecTail<'_>>,
16078 ) -> crate::gpu::BatchGraphOutcome {
16079 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
16080 }
16081
16082 fn try_batch_graph_wgpu_prefix(
16087 &self,
16088 hiddens: &mut [f32],
16089 positions: &[usize],
16090 k: usize,
16091 spec: Option<crate::gpu::SpecTail<'_>>,
16092 layers_run: Option<&mut usize>,
16093 ) -> crate::gpu::BatchGraphOutcome {
16094 let graph_end = match self.mimo_moe.graph_prefix_end() {
16095 Some(end) if end < self.num_layers => {
16096 if layers_run.is_none() || spec.is_some() || end == 0 {
16097 return crate::gpu::BatchGraphOutcome::Declined;
16098 }
16099 end
16100 }
16101 _ => self.num_layers,
16102 };
16103 let _tb = std::time::Instant::now();
16104 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
16105 if self.attn_softcap > 0.0 {
16106 return crate::gpu::BatchGraphOutcome::Declined; }
16108 if let Some(reason) = self
16111 .wgpu_graph_attn_decline()
16112 .or_else(|| self.batch_graph_gemma_decline())
16113 {
16114 self.note_graph_decline("wgpu batch graph", reason);
16115 return crate::gpu::BatchGraphOutcome::Declined;
16116 }
16117 let nh = self.num_heads;
16118 let (nkv, hd, rd) = self.layer_geom(0);
16119 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16120 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
16121 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
16122 {
16123 let name = &m.tensors[i].name;
16124 let prism = if crate::prism::is_inverse_embedding(m, name) {
16125 crate::gpu::GraphPrismOp::InverseEmbedding
16126 } else if crate::prism::is_forward_weight(m, name) {
16127 crate::gpu::GraphPrismOp::Forward
16128 } else {
16129 crate::gpu::GraphPrismOp::None
16130 };
16131 return Some(crate::gpu::GraphW {
16132 idx: i,
16133 kind,
16134 row_scale: rs,
16135 data: &[],
16136 prism,
16137 affine: crate::prism::is_affine_target(m, name),
16138 });
16139 }
16140 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
16141 eprintln!(
16142 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
16143 t.rows(),
16144 t.cols()
16145 );
16146 }
16147 t.as_f32().map(|d| crate::gpu::GraphW {
16148 idx: 0,
16149 kind: 4,
16150 row_scale: &[],
16151 data: d,
16152 prism: crate::gpu::GraphPrismOp::None,
16153 affine: false,
16154 })
16155 }
16156 let built: Option<(
16157 Vec<crate::gpu::GraphLayer<'_>>,
16158 std::sync::Arc<cortiq_core::CmfModel>,
16159 )> = (|| {
16160 let mut layers = Vec::with_capacity(graph_end);
16161 let mut model = None;
16162 for li in 0..graph_end {
16163 let lw = &self.weights.layers[self.phys_layer(li)];
16164 let gffn = match &lw.ffn {
16171 FfnKind::Dense(d) if !d.segs.is_empty() => {
16172 if batch_debug {
16173 eprintln!("batch graph: dense segmented FFN at layer {li}");
16174 }
16175 return None;
16176 }
16177 FfnKind::Dense(d) => {
16178 let Some(act) = d.act.wgpu_graph_act() else {
16179 if batch_debug {
16180 eprintln!(
16181 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
16182 d.act
16183 );
16184 }
16185 return None;
16186 };
16187 crate::gpu::GraphFfn::Dense {
16188 gate: gw(&d.gate_proj)?,
16189 up: gw(&d.up_proj)?,
16190 down: gw(&d.down_proj)?,
16191 act,
16192 }
16193 }
16194 FfnKind::Moe(m) => {
16195 if m.route_tau.is_some() || m.mask.is_some() {
16202 return None;
16203 }
16204 let shared = m.shared.as_ref();
16208 let has_shared = shared.is_some();
16209 let shared_gated = matches!(shared, Some((_, Some(_))));
16210 let sgate = match shared {
16211 Some((_, Some(sg))) => gw(sg)?,
16212 _ => gw(&m.router)?,
16216 };
16217 let router = gw(&m.router)?;
16218 if router.prism != crate::gpu::GraphPrismOp::None
16224 || router.affine
16225 || sgate.prism != crate::gpu::GraphPrismOp::None
16226 || sgate.affine
16227 {
16228 return None;
16229 }
16230 let inter = m.experts.first()?.gate_proj.rows();
16231 let mut experts = Vec::with_capacity(m.experts.len() + 1);
16232 let mut q4tp: Option<bool> = None;
16233 let mut gu_q2: Option<bool> = None;
16234 let mut q82: Option<bool> = None;
16235 let mut moe_act = None;
16239 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
16240 let Some(e_act) = e.act.wgpu_graph_act() else {
16241 return None;
16242 };
16243 match moe_act {
16244 Some(existing) if existing != e_act => return None,
16245 None => moe_act = Some(e_act),
16246 _ => {}
16247 }
16248 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
16249 return None;
16250 }
16251 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
16255 {
16256 Some((mm, gi)) => (
16257 mm,
16258 gi,
16259 e.up_proj.mapped_q4t()?.1,
16260 e.down_proj.mapped_q4t()?.1,
16261 false,
16262 false,
16263 false,
16264 ),
16265 None => match e.gate_proj.mapped_q2tp() {
16266 Some((mm, gi)) => (
16267 mm,
16268 gi,
16269 e.up_proj.mapped_q2tp()?.1,
16270 e.down_proj.mapped_q4tp()?.1,
16271 true,
16272 true,
16273 false,
16274 ),
16275 None => match e.gate_proj.mapped_q4tp() {
16276 Some((mm, gi)) => (
16277 mm,
16278 gi,
16279 e.up_proj.mapped_q4tp()?.1,
16280 e.down_proj.mapped_q4tp()?.1,
16281 true,
16282 false,
16283 false,
16284 ),
16285 None => {
16286 if !crate::gpu::moe_q82_graph_on() {
16287 return None;
16288 }
16289 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
16290 (
16291 mm,
16292 gi,
16293 e.up_proj.mapped_q8_2f()?.1,
16294 e.down_proj.mapped_q8_2f()?.1,
16295 false,
16296 false,
16297 true,
16298 )
16299 }
16300 },
16301 },
16302 };
16303 if *q4tp.get_or_insert(is_p) != is_p
16304 || *gu_q2.get_or_insert(is_q2) != is_q2
16305 || *q82.get_or_insert(is_8) != is_8
16306 {
16307 return None;
16308 }
16309 if [gi, ui, di].into_iter().any(|idx| {
16310 mm.tensors.get(idx).is_some_and(|t| {
16311 crate::prism::is_forward_weight(mm, &t.name)
16312 || crate::prism::is_affine_target(mm, &t.name)
16313 })
16314 }) {
16315 return None;
16316 }
16317 model.get_or_insert_with(|| mm.clone());
16318 experts.push((gi, ui, di));
16319 }
16320 crate::gpu::GraphFfn::Moe {
16321 router,
16322 shared_gate: sgate,
16323 experts,
16324 n_exp: m.experts.len(),
16325 top_k: m.top_k,
16326 inter,
16327 act: moe_act?,
16328 norm_topk: m.norm_topk_prob,
16329 q4tp: q4tp?,
16330 gu_q2: gu_q2.unwrap_or(false),
16331 q82: q82.unwrap_or(false),
16332 sigmoid: m.router_sigmoid,
16333 sigmoid_denom_eps: m.sigmoid_denom_eps,
16334 bias: m.expert_bias.as_deref(),
16335 has_shared,
16336 detached_shared: None,
16337 shared_gated,
16338 route_scale: m.routed_scaling,
16339 }
16340 }
16341 _ => return None,
16342 };
16343 let attn = match &lw.attn {
16344 AttnKind::Full {
16345 wq,
16346 wk,
16347 wv,
16348 wo,
16349 q_norm,
16350 k_norm,
16351 output_gate,
16352 softplus_gate,
16353 bias,
16354 } => {
16355 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
16356 if batch_debug {
16357 eprintln!(
16358 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
16359 softplus_gate.is_some(),
16360 self.attention_heads_per_layer.is_some()
16361 );
16362 }
16363 return None;
16364 }
16365 let (m, _, _, _) =
16366 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
16367 model = Some(m.clone());
16368 crate::gpu::GraphAttn::Full {
16369 wq: gw(wq)?,
16370 wk: gw(wk)?,
16371 wv: gw(wv)?,
16372 wo: gw(wo)?,
16373 q_norm: q_norm.as_deref(),
16374 k_norm: k_norm.as_deref(),
16375 late_qk_norm: self.qk_norm_after_rope,
16376 bias: bias
16377 .as_ref()
16378 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16379 output_gate: *output_gate,
16380 cpu_k: self.kv_cache.layers[li].k_heads(),
16381 cpu_v: self.kv_cache.layers[li].v_heads(),
16382 cpu_base: self.kv_cache.layers[li].base(),
16383 geom: self.graph_attn_geom(li),
16384 head_gate: None,
16387 }
16388 }
16389 AttnKind::LinearGdn(w) => {
16390 let Some(cfg) = self.gdn_cfg else {
16391 if batch_debug {
16392 eprintln!("batch graph: no GDN config at layer {li}");
16393 }
16394 return None;
16395 };
16396 let (m, _, _, _) = w
16397 .in_proj_qkv
16398 .graph_weight()
16399 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
16400 model = Some(m.clone());
16401 crate::gpu::GraphAttn::Gdn {
16402 qkv: gw(&w.in_proj_qkv)?,
16403 z: gw(&w.in_proj_z)?,
16404 a: gw(&w.in_proj_a)?,
16405 b: gw(&w.in_proj_b)?,
16406 out: gw(&w.out_proj)?,
16407 conv1d: &w.conv1d,
16408 a_log: &w.a_log,
16409 dt_bias: &w.dt_bias,
16410 norm: &w.norm,
16411 nv: cfg.num_v_heads,
16412 nk: cfg.num_k_heads,
16413 dk: cfg.key_head_dim,
16414 dv: cfg.value_head_dim,
16415 kk: cfg.conv_kernel,
16416 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
16417 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
16418 }
16419 }
16420 _ => return None,
16421 };
16422 let g4 = self.global_attn.is_some() || self.attn_v_norm;
16425 layers.push(crate::gpu::GraphLayer {
16426 input_norm: &lw.input_norm,
16427 attn,
16428 post_norm: &lw.post_norm,
16429 attn_out_norm: if g4 {
16430 lw.attn_out_norm.as_deref()
16431 } else {
16432 None
16433 },
16434 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
16435 layer_scale: if g4 { lw.layer_scale } else { None },
16436 ffn: gffn,
16437 });
16438 }
16439 Some((layers, model?))
16440 })();
16441 let Some((layers, model)) = built else {
16442 {
16443 use std::sync::atomic::{AtomicBool, Ordering};
16444 static SAID: AtomicBool = AtomicBool::new(false);
16445 if !SAID.swap(true, Ordering::Relaxed) {
16446 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
16447 }
16448 }
16449 return crate::gpu::BatchGraphOutcome::Declined;
16450 };
16451 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
16452 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
16453 }
16454 crate::gpu::forward_batch_graph(
16455 &model,
16456 self.graph_kv_id,
16457 &layers,
16458 &self.inv_freq,
16459 hiddens,
16460 nh,
16461 nkv,
16462 hd,
16463 rd,
16464 self.hidden_size,
16465 self.intermediate_size,
16466 positions,
16467 self.kv_cache.max_seq_len,
16468 gemma,
16469 self.rms_eps as f32,
16470 self.attn_scale,
16471 k,
16472 &(0..graph_end)
16473 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
16474 .collect::<Vec<_>>(),
16475 self.o1_epoch,
16476 spec,
16477 layers_run,
16478 )
16479 }
16480
16481 fn draft_probe() -> bool {
16485 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16486 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
16487 }
16488
16489 #[cfg(feature = "gpu")]
16501 fn dsv4_spec_on() -> bool {
16502 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16503 *ON.get_or_init(|| {
16504 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
16508 return v != "0";
16509 }
16510 std::env::var("CMF_DSV4_SPEC")
16517 .map(|v| v != "0")
16518 .unwrap_or_else(|_| {
16519 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
16520 })
16521 })
16522 }
16523
16524 #[cfg(feature = "gpu")]
16531 fn dsv4_spec_step(
16532 &mut self,
16533 tip_token: u32,
16534 t_next: u32,
16535 next_pos: usize,
16536 max_extra: usize,
16537 drafted: &mut usize,
16538 accepted_ctr: &mut usize,
16539 ) -> Option<(Vec<u32>, usize)> {
16540 let t_all = std::time::Instant::now();
16541 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16542 thread_local! {
16543 static LAST: std::cell::Cell<Option<std::time::Instant>> =
16544 const { std::cell::Cell::new(None) };
16545 }
16546 LAST.with(|l| {
16547 if let Some(prev) = l.get() {
16548 eprintln!(
16549 "between rounds {:.1} ms",
16550 prev.elapsed().as_secs_f64() * 1e3
16551 );
16552 }
16553 l.set(Some(std::time::Instant::now()));
16554 });
16555 }
16556 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16557 eprintln!("spec_step: enter pos={next_pos}");
16558 }
16559 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
16560 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
16561 if self.dspark.is_none() {
16563 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16564 if t.is_empty() {
16565 return None;
16566 }
16567 crate::dsv4::dspark_arm(&t, cfg.dim);
16568 self.dspark = Some(crate::dsv4::DsparkState::new(
16569 self.dsv4_mtp.len(),
16570 &cfg,
16571 t.len(),
16572 ));
16573 }
16574 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16575 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
16576 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16577 eprintln!("spec_step: the pack was not built (targets {targets:?})");
16578 }
16579 let pack = pack?;
16580 let block = crate::dsv4::dspark_block();
16581 let b_box = self.dsv4.as_mut()?;
16582 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
16583 let ds = self.dspark.as_mut()?;
16584 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
16587 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
16588 if dbg {
16589 eprintln!("spec_step: no capture");
16590 }
16591 return None;
16592 }
16593 ds.have_hidden = true;
16594 let tip_pos = next_pos.checked_sub(1)?;
16595 let draft_started = std::time::Instant::now();
16596 let mut conf = Vec::new();
16597 let props = crate::dsv4::dspark_draft_gpu(
16598 g,
16599 &self.dsv4_mtp,
16600 &cfg,
16601 ds,
16602 pack,
16603 st.kv_id,
16604 tip_token,
16605 tip_pos,
16606 self.pool.as_deref(),
16607 &mut conf,
16608 );
16609 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16610 *drafted += block;
16611 if props.is_empty() || props[0] != t_next {
16612 if dbg {
16613 eprintln!(
16614 "spec_step: draft {} (props0={:?} t_next={t_next})",
16615 if props.is_empty() {
16616 "empty"
16617 } else {
16618 "miss"
16619 },
16620 props.first()
16621 );
16622 }
16623 return None;
16624 }
16625 let mut k_verify = crate::dsv4::dspark_verify_k()
16632 .min(props.len())
16633 .min(max_extra.saturating_add(1));
16634 let conf_min = {
16640 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
16641 *M.get_or_init(|| {
16642 std::env::var("CMF_DSPARK_CONF_MIN")
16643 .ok()
16644 .and_then(|v| v.parse().ok())
16645 .unwrap_or(0.0)
16646 })
16647 };
16648 if conf_min > 0.0 && conf.len() >= props.len() {
16649 let mut keep = 1usize;
16650 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
16651 keep += 1;
16652 }
16653 k_verify = k_verify.min(keep.max(2));
16654 }
16655 if k_verify < 2 {
16656 return None;
16657 }
16658 let mut fed = Vec::with_capacity(k_verify);
16659 fed.push(t_next);
16660 fed.extend_from_slice(&props[1..k_verify]);
16661 let mut argmax = Vec::new();
16662 let mut logits_all = Vec::new();
16663 let mut walked = Vec::new();
16664 let txn = crate::dsv4::dsv4_verify_chunk(
16665 g,
16666 layers,
16667 &cfg,
16668 st,
16669 &fed,
16670 next_pos,
16671 &self.inv_freq,
16672 self.pool.as_deref(),
16673 &targets,
16674 &mut argmax,
16675 &mut logits_all,
16676 &mut walked,
16677 );
16678 if txn.is_none() && dbg {
16679 eprintln!("spec_step: verify declined");
16680 }
16681 let txn = txn?;
16682 let spec_gpu_end = txn.gpu_end;
16683 let b = fed.len();
16684 let mut accepted = 1usize;
16685 while accepted < b && fed[accepted] == argmax[accepted - 1] {
16686 accepted += 1;
16687 }
16688 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
16693 accepted = 1;
16694 }
16695 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
16696 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
16697 }
16698 let t_fin = std::time::Instant::now();
16699 if !crate::dsv4::dsv4_spec_finish(
16700 g,
16701 layers,
16702 &cfg,
16703 st,
16704 txn,
16705 accepted,
16706 &fed,
16707 &self.inv_freq,
16708 self.pool.as_deref(),
16709 ) {
16710 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
16711 return None;
16712 }
16713 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16714 eprintln!(
16715 "finish(k={accepted}): {:.1} ms",
16716 t_fin.elapsed().as_secs_f64() * 1e3
16717 );
16718 }
16719 *accepted_ctr += accepted - 1;
16720 let (hc, dim) = (cfg.hc_mult, cfg.dim);
16725 let dev_caps: Vec<usize> = targets
16730 .iter()
16731 .copied()
16732 .filter(|&t| t < spec_gpu_end)
16733 .collect();
16734 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
16735 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
16736 return None;
16737 }
16738 for t in 0..accepted {
16739 let tip = t + 1 == accepted;
16740 for (slot, &tl) in targets.iter().enumerate() {
16741 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
16742 let lo = (di * b + t) * hc * dim;
16743 crate::dsv4::dspark_capture(
16744 &caps_all[lo..lo + hc * dim],
16745 &cfg,
16746 slot,
16747 &mut ds.main_hidden,
16748 );
16749 } else if tip
16750 && crate::dsv4::dspark_peek_slot(slot, dim, {
16751 let lo = slot * dim;
16752 &mut ds.main_hidden[lo..lo + dim]
16753 })
16754 {
16755 } else {
16760 crate::dsv4::dspark_capture(
16764 &walked[t * hc * dim..(t + 1) * hc * dim],
16765 &cfg,
16766 slot,
16767 &mut ds.main_hidden,
16768 );
16769 }
16770 }
16771 crate::dsv4::dspark_ring_append(
16772 g,
16773 &self.dsv4_mtp,
16774 &cfg,
16775 ds,
16776 next_pos + t,
16777 self.pool.as_deref(),
16778 );
16779 }
16780 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
16781 self.graph_logits = Some(row);
16782 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
16787 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
16788 crate::dsv4::pick_tally_arm();
16789 }
16790 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16791 eprintln!(
16792 "spec_step total {:.1} ms (k={accepted})",
16793 t_all.elapsed().as_secs_f64() * 1e3
16794 );
16795 }
16796 Some((fed[1..accepted].to_vec(), next_pos + accepted))
16797 }
16798
16799 fn dspark_probe(&mut self, position: usize, token_id: u32) {
16800 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
16801 return;
16802 }
16803 let trunk_now = crate::dsv4::pick_tally_take();
16805 crate::dsv4::trunk_freq_note(&trunk_now);
16806 if !trunk_now.is_empty() {
16807 self.dspark_trunk_picks.push(trunk_now);
16808 let keep = crate::dsv4::dspark_block();
16809 if self.dspark_trunk_picks.len() > keep {
16810 self.dspark_trunk_picks.remove(0);
16811 }
16812 }
16813 for p in std::mem::take(&mut self.dspark_pending) {
16816 let Some(i) = position.checked_sub(p.0 + 1) else {
16817 continue;
16818 };
16819 let mut p = p;
16820 if i < p.1.len() {
16821 if p.2 && p.1[i] == token_id {
16822 p.3 = i + 1;
16823 } else {
16824 p.2 = false;
16825 }
16826 if i + 1 < p.1.len() {
16827 self.dspark_pending.push(p);
16828 continue;
16829 }
16830 }
16831 self.dspark_hist.push(p.3);
16832 self.dspark_real.push(token_id);
16833 }
16834 let Some(b) = &mut self.dsv4 else { return };
16835 let (g, layers, cfg) = (&b.0, &b.1, b.2);
16836 let n_layers = layers.len();
16837 if self.dspark.is_none() {
16838 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16839 if t.is_empty() {
16840 return;
16841 }
16842 eprintln!(
16843 "DSpark: capture from layers {t:?}, block {}",
16844 crate::dsv4::dspark_block()
16845 );
16846 crate::dsv4::dspark_arm(&t, cfg.dim);
16847 self.dspark = Some(crate::dsv4::DsparkState::new(
16848 self.dsv4_mtp.len(),
16849 &cfg,
16850 t.len(),
16851 ));
16852 }
16853 let ds = self.dspark.as_mut().unwrap();
16854 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
16855 return; }
16857 let mut conf = Vec::new();
16858 crate::dsv4::pick_tally_arm();
16859 let draft_started = std::time::Instant::now();
16864 #[cfg(feature = "gpu")]
16865 let gpu_draft = crate::dsv4::dspark_gpu_on();
16866 #[cfg(not(feature = "gpu"))]
16867 let gpu_draft = false;
16868 let props = if gpu_draft {
16869 #[cfg(feature = "gpu")]
16870 {
16871 let kv_id = b.3.kv_id;
16872 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
16873 Some(pk) => crate::dsv4::dspark_draft_gpu(
16874 g,
16875 &self.dsv4_mtp,
16876 &cfg,
16877 ds,
16878 pk,
16879 kv_id,
16880 token_id,
16881 position,
16882 self.pool.as_deref(),
16883 &mut conf,
16884 ),
16885 None => Vec::new(),
16886 }
16887 }
16888 #[cfg(not(feature = "gpu"))]
16889 Vec::new()
16890 } else {
16891 crate::gpu::cpu_scope(|| {
16892 crate::dsv4::dspark_draft(
16893 g,
16894 &self.dsv4_mtp,
16895 &cfg,
16896 ds,
16897 token_id,
16898 position,
16899 self.pool.as_deref(),
16900 &mut conf,
16901 )
16902 })
16903 };
16904 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16905 let draft_picks = crate::dsv4::pick_tally_take();
16906 crate::dsv4::dspark_freq_note(&draft_picks);
16907 crate::dsv4::pick_tally_arm();
16910 if !props.is_empty() {
16911 let (tu, tt) = {
16915 let flat: Vec<(usize, Vec<usize>)> = self
16916 .dspark_trunk_picks
16917 .iter()
16918 .flat_map(|v| v.iter().cloned())
16919 .collect();
16920 let mut per: std::collections::HashMap<usize, Vec<usize>> =
16922 std::collections::HashMap::new();
16923 for (li, picks) in flat {
16924 per.entry(li).or_default().extend(picks);
16925 }
16926 let n = per.len().max(1);
16927 let mut u = 0usize;
16928 let mut t = 0usize;
16929 for (_, v) in per {
16930 t += v.len();
16931 u += v.iter().collect::<std::collections::HashSet<_>>().len();
16932 }
16933 (u / n, t / n)
16934 };
16935 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
16936 self.dspark_exp.push((tu, tt, du, dt));
16937 self.dspark_pending.push((position, props, true, 0));
16938 }
16939 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
16940 let n = self.dspark_hist.len() as f32;
16941 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
16942 let block = crate::dsv4::dspark_block();
16943 let mut at = vec![0usize; block + 1];
16944 for &k in &self.dspark_hist {
16945 at[k] += 1;
16946 }
16947 let mut surv = Vec::with_capacity(block);
16949 for i in 1..=block {
16950 let k = at[i..].iter().sum::<usize>() as f32 / n;
16951 surv.push(format!("{k:.2}"));
16952 }
16953 let distinct = self
16954 .dspark_real
16955 .iter()
16956 .collect::<std::collections::HashSet<_>>()
16957 .len();
16958 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
16959 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
16960 });
16961 let m = self.dspark_exp.len().max(1);
16962 eprintln!(
16963 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
16964 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
16965 self.dspark_hist.len(),
16966 mean + 1.0,
16967 surv.join(" ")
16968 );
16969 eprintln!(
16970 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
16971 trunk experts {}/{} per layer over {block} tokens, \
16972 draft {}/{} per block, draft {:.2} ms/block",
16973 self.dspark_real.len(),
16974 tu / m,
16975 tt / m,
16976 du / m,
16977 dt / m,
16978 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
16979 );
16980 }
16981 }
16982
16983 fn forward_layers_upto(
16984 &mut self,
16985 hidden: &[f32],
16986 position: usize,
16987 task_mask: Option<&TaskMask>,
16988 upto: Option<usize>,
16989 ) -> Vec<f32> {
16990 if let Some(plan) = self.gpu_plan.clone() {
16996 if upto.is_none() && plan.len() > 1 {
16997 let mut h = hidden.to_vec();
16998 for &(dev, from, upto_incl) in plan.iter() {
16999 h = crate::gpu::with_device(dev, || {
17000 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
17001 });
17002 }
17003 return h;
17004 }
17005 }
17006 self.forward_layers_span(hidden, position, task_mask, 0, upto)
17007 }
17008
17009 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
17014 self.set_gpu_plan_at(devices, None)
17015 }
17016
17017 pub fn set_gpu_plan_at(
17021 &mut self,
17022 devices: Option<&[usize]>,
17023 at: Option<usize>,
17024 ) -> Result<(), String> {
17025 let Some(devs) = devices.filter(|d| d.len() > 1) else {
17026 self.gpu_plan = None;
17027 return Ok(());
17028 };
17029 self.split_supported()?;
17030 let n = self.num_layers;
17031 if devs.len() > n {
17032 return Err(format!("{} devices for {n} layers", devs.len()));
17033 }
17034 if let Some(k) = at {
17035 if k == 0 || k >= n {
17036 return Err(format!("split at {k}: the model has {n} layers"));
17037 }
17038 if devs.len() == 2 {
17039 self.gpu_plan = Some(std::sync::Arc::new(vec![
17040 (devs[0], 0, k - 1),
17041 (devs[1], k, n - 1),
17042 ]));
17043 return Ok(());
17044 }
17045 return Err(format!(
17046 "an explicit split point takes exactly 2 devices, got {}",
17047 devs.len()
17048 ));
17049 }
17050 let per = n.div_ceil(devs.len());
17051 let mut plan = Vec::with_capacity(devs.len());
17052 let mut from = 0usize;
17053 for &d in devs {
17054 if from >= n {
17055 break;
17056 }
17057 let upto = (from + per - 1).min(n - 1);
17058 plan.push((d, from, upto));
17059 from = upto + 1;
17060 }
17061 self.gpu_plan = Some(std::sync::Arc::new(plan));
17062 Ok(())
17063 }
17064
17065 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
17067 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
17068 }
17069
17070 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
17076 if let Some(x) = t.as_f32() {
17077 return x.to_vec();
17078 }
17079 let mut out = vec![0.0; t.rows() * t.cols()];
17080 for r in 0..t.rows() {
17081 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
17082 }
17083 out
17084 }
17085
17086 fn embryo_resident_eligible(&self) -> bool {
17087 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
17090 || self.num_layers != self.physical_layers
17091 || self.loop_final_norm
17092 || self.weights.layers.len() != self.num_layers
17093 || self.head_clusters.is_none()
17094 || self.final_softcap.is_some()
17095 || self.logit_multiplier.is_some()
17096 || self.attn_softcap != 0.0
17097 || self.mtp.is_some()
17098 || self.per_token_stack()
17099 || self.dsv4.is_some()
17100 || self.dsv41.is_some()
17101 || self.qwen4_exp.is_some()
17102 || self.dyn_router.is_some()
17109 || self.dyn_phi_layer.is_some()
17110 || self.dyn_blend_loaded
17111 || self.o1_cfg.is_some()
17112 || self.swa.is_some()
17113 || self.sliding_layers.is_some()
17114 || self.global_attn.is_some()
17115 || self.attention_heads_per_layer.is_some()
17116 || self.attn_v_norm
17117 || self
17118 .kv_cache
17119 .layers
17120 .iter()
17121 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
17122 || self.rope_scale != 1.0
17123 || self.rope_scale_local != 1.0
17124 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
17125 || self.hidden_size == 0
17126 || self.hidden_size > 1024
17127 || self.intermediate_size > 1024
17128 || self.num_heads == 0
17129 || self.num_kv_heads == 0
17130 || self.num_heads % self.num_kv_heads != 0
17131 || self.num_heads.saturating_mul(self.head_dim) > 1024
17132 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
17133 || self.vocab_size == 0
17134 || self.kv_cache.max_seq_len == 0
17135 || self.rotary_dim == 0
17136 || self.rotary_dim > self.head_dim
17137 || self.rotary_dim % 2 != 0
17138 || self.inv_freq.len() < self.rotary_dim / 2
17139 {
17140 return false;
17141 }
17142 if self.weights.lm_head.as_f32().is_none()
17153 || self.weights.embed_tokens.as_f32().is_none()
17154 || self.weights.lm_head.rows() < self.vocab_size
17155 || self.weights.lm_head.cols() != self.hidden_size
17156 || self.weights.embed_tokens.rows() < self.vocab_size
17157 || self.weights.embed_tokens.cols() != self.hidden_size
17158 || self.weights.final_norm.len() != self.hidden_size
17159 {
17160 return false;
17161 }
17162 if let Some(cfg) = self.vmf_cfg {
17163 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
17164 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
17165 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
17166 || cfg.state_len() == 0
17167 {
17168 return false;
17169 }
17170 }
17171 if let Some(g) = self.gdn_cfg {
17172 if g.num_v_heads == 0
17177 || g.num_k_heads == 0
17178 || g.num_v_heads % g.num_k_heads != 0
17179 || g.key_head_dim == 0
17180 || g.key_head_dim > 128
17181 || g.value_head_dim == 0
17182 || g.value_head_dim > 256
17183 || g.value_head_dim % 4 != 0
17184 || g.conv_kernel == 0
17185 || g.num_v_heads > 512
17186 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
17187 || g.conv_dim() > 2048
17188 || g.conv_dim() % 4 != 0
17189 || g.hidden_size != self.hidden_size
17190 || g.output_gate_sigmoid
17191 || g.rms_eps != self.rms_eps
17192 || g.state_len() == 0
17193 {
17194 return false;
17195 }
17196 }
17197 let mut full_seen = false;
17198 for lw in &self.weights.layers {
17199 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
17200 return false;
17201 }
17202 match &lw.attn {
17203 AttnKind::LinearGdn(w) => {
17204 let Some(g) = self.gdn_cfg else {
17205 return false;
17206 };
17207 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
17208 if w.in_proj_qkv.rows() != g.conv_dim()
17209 || w.in_proj_qkv.cols() != self.hidden_size
17210 || w.in_proj_qkv.as_f32().is_none()
17211 || w.in_proj_z.rows() != nv * dv
17212 || w.in_proj_z.cols() != self.hidden_size
17213 || w.in_proj_z.as_f32().is_none()
17214 || w.in_proj_a.rows() != nv
17215 || w.in_proj_a.cols() != self.hidden_size
17216 || w.in_proj_a.as_f32().is_none()
17217 || w.in_proj_b.rows() != nv
17218 || w.in_proj_b.cols() != self.hidden_size
17219 || w.in_proj_b.as_f32().is_none()
17220 || w.conv1d.len() != g.conv_dim() * kk
17221 || w.a_log.len() != nv
17222 || w.dt_bias.len() != nv
17223 || w.norm.len() != dv
17224 || w.out_proj.rows() != self.hidden_size
17225 || w.out_proj.cols() != nv * dv
17226 || w.out_proj.as_f32().is_none()
17227 {
17228 return false;
17229 }
17230 }
17231 AttnKind::Linear(w) => {
17232 let Some(cfg) = self.vmf_cfg else {
17233 return false;
17234 };
17235 if w.thq.rows() != cfg.num_heads * cfg.nphase
17236 || w.thq.cols() != self.hidden_size
17237 || w.thq.as_f32().is_none()
17238 || w.thk.rows() != cfg.num_heads * cfg.nphase
17239 || w.thk.cols() != self.hidden_size
17240 || w.thk.as_f32().is_none()
17241 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
17242 || w.v_proj.cols() != self.hidden_size
17243 || w.v_proj.as_f32().is_none()
17244 || w.out_proj.rows() != self.hidden_size
17245 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
17246 || w.out_proj.as_f32().is_none()
17247 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
17248 {
17249 return false;
17250 }
17251 if let Some((kg, kb)) = &w.k_gate {
17252 if kg.rows() != cfg.num_heads
17253 || kg.cols() != self.hidden_size
17254 || kg.as_f32().is_none()
17255 || kb.len() != cfg.num_heads
17256 {
17257 return false;
17258 }
17259 }
17260 if let Some(conv) = &w.conv {
17261 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
17262 return false;
17263 }
17264 }
17265 }
17266 AttnKind::Full {
17267 wq,
17268 wk,
17269 wv,
17270 wo,
17271 q_norm,
17272 k_norm,
17273 output_gate,
17274 softplus_gate,
17275 bias,
17276 } => {
17277 if full_seen
17278 || q_norm.is_some()
17279 || k_norm.is_some()
17280 || *output_gate
17281 || softplus_gate.is_some()
17282 || bias.is_some()
17283 || wq.as_f32().is_none()
17284 || wk.as_f32().is_none()
17285 || wv.as_f32().is_none()
17286 || wo.as_f32().is_none()
17287 || wq.rows() != self.num_heads * self.head_dim
17288 || wk.rows() != self.num_kv_heads * self.head_dim
17289 || wv.rows() != self.num_kv_heads * self.head_dim
17290 || wq.cols() != self.hidden_size
17291 || wk.cols() != self.hidden_size
17292 || wv.cols() != self.hidden_size
17293 || wo.rows() != self.hidden_size
17294 || wo.cols() != self.num_heads * self.head_dim
17295 {
17296 return false;
17297 }
17298 full_seen = true;
17299 }
17300 AttnKind::Bounded(w) => {
17301 let Some(ac) = self.anchor_core.as_ref() else {
17304 return false;
17305 };
17306 if self.bounded_rope.is_none()
17307 || w.window != ac.window
17308 || w.sink != ac.sink
17309 || w.window == 0
17310 || w.window + w.sink > 256
17311 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
17312 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
17313 || w.wq.as_f32().is_none()
17314 || w.wk.as_f32().is_none()
17315 || w.wv.as_f32().is_none()
17316 || w.wo.as_f32().is_none()
17317 || w.wq.rows() != self.num_heads * self.head_dim
17318 || w.wk.rows() != self.num_kv_heads * self.head_dim
17319 || w.wv.rows() != self.num_kv_heads * self.head_dim
17320 || w.wq.cols() != self.hidden_size
17321 || w.wk.cols() != self.hidden_size
17322 || w.wv.cols() != self.hidden_size
17323 || w.wo.rows() != self.hidden_size
17324 || w.wo.cols() != self.num_heads * self.head_dim
17325 {
17326 return false;
17327 }
17328 }
17329 _ => return false,
17330 }
17331 match &lw.ffn {
17332 FfnKind::Dense(d) => {
17333 if d.act != Act::Silu
17334 || !d.segs.is_empty()
17335 || d.gate_proj.as_f32().is_none()
17336 || d.up_proj.as_f32().is_none()
17337 || d.down_proj.as_f32().is_none()
17338 || d.gate_proj.rows() != self.intermediate_size
17339 || d.gate_proj.cols() != self.hidden_size
17340 || d.up_proj.rows() != self.intermediate_size
17341 || d.up_proj.cols() != self.hidden_size
17342 || d.down_proj.rows() != self.hidden_size
17343 || d.down_proj.cols() != self.intermediate_size
17344 {
17345 return false;
17346 }
17347 }
17348 FfnKind::Moe(m) => {
17349 if m.resonance.is_none()
17350 || m.top_k != 1
17351 || m.router_sigmoid
17352 || !m.norm_topk_prob
17353 || m.expert_bias.is_some()
17354 || m.routed_scaling != 1.0
17355 || m.route_tau.is_some()
17356 || m.shared.is_none()
17357 || m.mask.is_some()
17358 || m.per_expert_scale.is_some()
17359 || m.router_input_norm
17360 || m.experts.is_empty()
17361 || m.experts.len() > 8
17362 {
17363 return false;
17364 }
17365 let r = m.resonance.as_ref().unwrap();
17366 if r.mu.len() != m.experts.len() * self.hidden_size
17367 || r.bias.len() != m.experts.len()
17368 || r.u.len() != m.experts.len() * r.k * self.hidden_size
17369 || r.k > 128
17370 {
17371 return false;
17372 }
17373 let Some((shared, gate)) = &m.shared else {
17374 return false;
17375 };
17376 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
17377 return false;
17378 }
17379 if shared.gate_proj.as_f32().is_none()
17380 || shared.up_proj.as_f32().is_none()
17381 || shared.down_proj.as_f32().is_none()
17382 || shared.gate_proj.rows() != self.intermediate_size
17383 || shared.gate_proj.cols() != self.hidden_size
17384 || shared.up_proj.rows() != self.intermediate_size
17385 || shared.up_proj.cols() != self.hidden_size
17386 || shared.down_proj.rows() != self.hidden_size
17387 || shared.down_proj.cols() != self.intermediate_size
17388 {
17389 return false;
17390 }
17391 for e in &m.experts {
17392 if e.act != Act::Silu
17393 || !e.segs.is_empty()
17394 || e.gate_proj.as_f32().is_none()
17395 || e.up_proj.as_f32().is_none()
17396 || e.down_proj.as_f32().is_none()
17397 || e.gate_proj.rows() != self.intermediate_size
17398 || e.gate_proj.cols() != self.hidden_size
17399 || e.up_proj.rows() != self.intermediate_size
17400 || e.up_proj.cols() != self.hidden_size
17401 || e.down_proj.rows() != self.hidden_size
17402 || e.down_proj.cols() != self.intermediate_size
17403 {
17404 return false;
17405 }
17406 }
17407 }
17408 FfnKind::DenseMoe(_) => return false,
17409 }
17410 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
17411 return false;
17412 }
17413 }
17414 if full_seen && self.anchor_core.is_some() {
17415 return false;
17416 }
17417 full_seen || self.num_layers > 0
17418 }
17419
17420 fn embryo_resident_wanted(&self) -> bool {
17425 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17426 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17427 && matches!(
17428 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17429 Ok("1") | Ok("parallel")
17430 )
17431 && crate::gpu::enabled_here()
17432 && !self.graph_refused()
17433 && self.embryo_resident_eligible()
17434 }
17435
17436 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
17444 if ids.len() < 2
17445 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
17446 || !self.embryo_resident_wanted()
17447 {
17448 return None;
17449 }
17450 let model = self.ensure_embryo_graph()?;
17451 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
17452 .ok()
17453 .and_then(|v| v.parse::<usize>().ok())
17454 .filter(|&v| v >= 1)
17455 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
17456 .min(crate::gpu::EMBRYO_CHUNK_MAX);
17457 let hs = self.hidden_size;
17458 let n = ids.len();
17459 let mut pos = start;
17460 let mut last = None;
17461 let mut rows = Vec::with_capacity(cmax * hs);
17462 while pos < n {
17463 let end = (pos + cmax).min(n);
17464 rows.clear();
17465 for &id in &ids[pos..end] {
17466 rows.extend_from_slice(&self.embed_single(id));
17467 }
17468 let mut lg = Vec::new();
17469 if !crate::gpu::forward_embryo_graph_chunk(
17470 &model,
17471 self.graph_kv_id,
17472 &rows,
17473 pos,
17474 end - pos,
17475 &mut lg,
17476 ) {
17477 if pos == start {
17478 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17479 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
17480 }
17481 return None;
17482 }
17483 self.kv_cache.clear();
17487 self.clear_history();
17488 crate::gpu::graph_kv_reset(self.graph_kv_id);
17489 panic!(
17490 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
17491 );
17492 }
17493 last = Some(lg);
17494 pos = end;
17495 }
17496 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17497 eprintln!(
17498 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
17499 n - start,
17500 (n - start).div_ceil(cmax)
17501 );
17502 }
17503 last
17504 }
17505
17506 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
17507 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
17508 const UMAX: u32 = u32::MAX;
17509 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
17510 const REC: usize = 64;
17511 struct Pack {
17512 data: Vec<f32>,
17513 }
17514 impl Pack {
17515 fn put(&mut self, x: &[f32]) -> u32 {
17516 if x.is_empty() {
17517 return u32::MAX;
17518 }
17519 let off = self.data.len();
17520 self.data.extend_from_slice(x);
17521 off as u32
17522 }
17523 }
17524 let vmf = self.vmf_cfg;
17528 let gdn = self.gdn_cfg;
17529 let mut pack = Pack { data: Vec::new() };
17530 let mut meta = vec![0u32; HEADER];
17531 meta[0] = self.hidden_size as u32;
17532 meta[1] = self.intermediate_size as u32;
17533 meta[2] = self.vocab_size as u32;
17534 meta[3] = self.num_layers as u32;
17535 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
17536 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
17537 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
17538 if let Some(g) = gdn {
17539 meta[24] = g.num_v_heads as u32;
17540 meta[25] = g.num_k_heads as u32;
17541 meta[26] = g.key_head_dim as u32;
17542 meta[27] = g.value_head_dim as u32;
17543 meta[28] = g.conv_kernel as u32;
17544 meta[29] = g.conv_dim() as u32;
17545 }
17546 meta[7] = self.num_heads as u32;
17547 meta[8] = self.num_kv_heads as u32;
17548 meta[9] = self.head_dim as u32;
17549 meta[10] = self.kv_cache.max_seq_len as u32;
17550 let clusters = self.head_clusters.as_ref().unwrap();
17551 let cluster_count = clusters.len() / self.hidden_size;
17552 if clusters.len() % self.hidden_size != 0
17553 || cluster_count == 0
17554 || cluster_count > 1024
17555 || self.vocab_size % cluster_count != 0
17556 || self.weights.lm_head.rows() < self.vocab_size
17557 || self.weights.final_norm.len() != self.hidden_size
17558 {
17559 return None;
17560 }
17561 meta[11] = cluster_count as u32;
17562 meta[12] = (self.vocab_size / cluster_count) as u32;
17563 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
17564 meta[16] = self.rotary_dim as u32;
17565 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
17566 meta[19] = (self.rms_eps as f32).to_bits();
17567 let max_conv = self
17568 .weights
17569 .layers
17570 .iter()
17571 .filter_map(|lw| match &lw.attn {
17572 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
17573 _ => None,
17574 })
17575 .max()
17576 .unwrap_or(1);
17577 let phase_stride = vmf
17583 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
17584 .unwrap_or(0);
17585 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
17586 let state_stride = phase_stride.max(gdn_stride);
17587 let bounded = self.anchor_core.clone();
17591 let (anchor_window, anchor_sink) = bounded
17592 .as_ref()
17593 .map(|ac| (ac.window, ac.sink))
17594 .unwrap_or((0, 0));
17595 let kv_stride = if bounded.is_some() {
17596 2usize
17597 .saturating_mul(self.num_kv_heads)
17598 .saturating_mul(anchor_window)
17599 .saturating_mul(self.head_dim)
17600 } else {
17601 2usize
17602 .saturating_mul(self.num_kv_heads)
17603 .saturating_mul(self.kv_cache.max_seq_len)
17604 .saturating_mul(self.head_dim)
17605 };
17606 meta[14] = state_stride as u32;
17607 meta[15] = kv_stride as u32;
17608 meta[18] = anchor_window as u32;
17609 meta[20] = anchor_sink as u32;
17610 meta[21] = match &self.bounded_rope {
17611 Some(rope) => {
17612 let off = pack.put(&rope.cos);
17614 let _ = pack.put(&rope.sin);
17615 off
17616 }
17617 None => UMAX,
17618 };
17619 let mut full_seen = false;
17620 let mut bounded_seen = 0usize;
17621 let mut phase_seen = 0usize;
17625 let mut gdn_seen = 0usize;
17626 for (li, lw) in self.weights.layers.iter().enumerate() {
17627 let base = meta.len();
17628 meta.resize(base + REC, UMAX);
17629 meta[base] = match &lw.attn {
17630 AttnKind::Linear(w) if w.phase_delta => 1,
17631 AttnKind::Linear(_) => 0,
17632 AttnKind::Full { .. } => 2,
17633 AttnKind::Bounded(_) => 3,
17634 AttnKind::LinearGdn(_) => 4,
17635 _ => UMAX,
17636 };
17637 meta[base + 1] = pack.put(&lw.input_norm);
17638 meta[base + 2] = pack.put(&lw.post_norm);
17639 meta[base + 25] = match &lw.attn {
17640 AttnKind::Linear(_) => {
17641 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
17642 phase_seen += 1;
17643 off
17644 }
17645 AttnKind::LinearGdn(_) => {
17646 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
17647 gdn_seen += 1;
17648 off
17649 }
17650 _ => UMAX,
17651 };
17652 match &lw.attn {
17653 AttnKind::LinearGdn(w) => {
17654 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
17657 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
17658 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
17659 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
17660 meta[base + 60] = pack.put(&w.conv1d);
17661 meta[base + 61] = pack.put(&w.a_log);
17662 meta[base + 62] = pack.put(&w.dt_bias);
17663 meta[base + 63] = pack.put(&w.norm);
17664 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
17665 meta[base + 24] = 0;
17666 }
17667 AttnKind::Linear(w) => {
17668 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
17669 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
17670 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
17671 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
17672 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
17673 meta[base + 7] = pack.put(&decay);
17674 if let Some((kg, kb)) = &w.k_gate {
17675 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
17676 meta[base + 9] = pack.put(kb);
17677 }
17678 if let Some(conv) = &w.conv {
17679 meta[base + 10] = pack.put(conv);
17680 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
17681 } else {
17682 meta[base + 24] = 0;
17683 }
17684 }
17685 AttnKind::Full { wq, wk, wv, wo, .. } => {
17686 full_seen = true;
17687 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
17688 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
17689 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
17690 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
17691 meta[base + 26] = (li * kv_stride) as u32;
17692 }
17693 AttnKind::Bounded(w) => {
17694 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
17695 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
17696 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
17697 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
17698 meta[base + 26] = (bounded_seen * kv_stride) as u32;
17700 meta[base + 27] = pack.put(&w.sink_k);
17701 meta[base + 28] = pack.put(&w.sink_v);
17702 bounded_seen += 1;
17703 }
17704 _ => return None,
17705 }
17706 match &lw.ffn {
17707 FfnKind::Dense(d) => {
17708 meta[base + 15] = 0;
17709 meta[base + 16] = 0;
17710 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
17711 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
17712 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
17713 }
17714 FfnKind::Moe(m) => {
17715 let r = m.resonance.as_ref().unwrap();
17716 let (shared, _) = m.shared.as_ref().unwrap();
17717 meta[base + 15] = 1;
17718 meta[base + 16] = m.experts.len() as u32;
17719 meta[base + 17] = pack.put(&r.mu);
17720 meta[base + 18] = pack.put(&r.u);
17721 meta[base + 19] = pack.put(&r.bias);
17722 meta[base + 20] = r.k as u32;
17723 let mut shell = r.effective_shell(m.experts.len());
17732 shell.push(f32::NEG_INFINITY);
17733 meta[base + 30] = pack.put(&shell);
17734 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
17735 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
17736 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
17737 for (e, ex) in m.experts.iter().enumerate() {
17738 meta[base + 32 + e * 3] =
17739 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
17740 meta[base + 33 + e * 3] =
17741 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
17742 meta[base + 34 + e * 3] =
17743 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
17744 }
17745 }
17746 FfnKind::DenseMoe(_) => return None,
17747 }
17748 }
17749 if !full_seen && self.num_layers == 0 {
17750 return None;
17751 }
17752 let id = {
17753 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
17754 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
17755 };
17756 let model = crate::gpu::EmbryoGraphModel {
17757 id,
17758 hidden: self.hidden_size,
17759 intermediate: self.intermediate_size,
17760 vocab: self.vocab_size,
17761 layers: self.num_layers,
17762 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
17763 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
17764 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
17765 anchor_q_heads: self.num_heads,
17766 anchor_kv_heads: self.num_kv_heads,
17767 anchor_head_dim: self.head_dim,
17768 rotary_dim: self.rotary_dim,
17769 max_seq: self.kv_cache.max_seq_len,
17770 cluster_count,
17771 cluster_size: self.vocab_size / cluster_count,
17772 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
17773 state_stride,
17774 kv_stride,
17775 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
17776 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
17777 weights: pack.data,
17778 meta,
17779 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
17780 clusters: clusters.as_ref().clone(),
17781 final_norm: self.weights.final_norm.clone(),
17782 inv_freq: self.inv_freq.as_ref().clone(),
17783 bounded: bounded.is_some(),
17784 kv_layers: if bounded.is_some() {
17785 bounded_seen
17786 } else {
17787 self.num_layers
17788 },
17789 state_layers: phase_seen + gdn_seen,
17790 anchor_window,
17791 anchor_sink,
17792 phase_layers: phase_seen,
17793 gdn_layers: gdn_seen,
17794 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
17795 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
17796 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
17797 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
17798 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
17799 };
17800 self.embryo_graph = Some(std::sync::Arc::new(model));
17801 }
17802 self.embryo_graph.clone()
17803 }
17804
17805 fn forward_layers_span(
17806 &mut self,
17807 hidden: &[f32],
17808 position: usize,
17809 task_mask: Option<&TaskMask>,
17810 from: usize,
17811 upto: Option<usize>,
17812 ) -> Vec<f32> {
17813 #[cfg(target_os = "macos")]
17814 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
17815 position,
17816 from,
17817 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
17818 );
17819 debug_assert!(
17820 from == 0
17821 || (self.dsv4.is_none()
17822 && self.dsv41.is_none()
17823 && self.qwen4_exp.is_none()
17824 && !self.per_token_stack())
17825 );
17826 #[cfg(target_os = "macos")]
17832 if !crate::gpu_metal::wait_replay() {
17833 self.fail_metal_graph("the pending async replay failed before a plain forward");
17834 return vec![0.0; self.hidden_size];
17835 }
17836 if let Some(b) = &mut self.qwen4_exp {
17837 let _ = (task_mask, upto);
17838 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17839 let mut logits = Vec::new();
17840 crate::qwen4_exp::forward_token(
17841 &b.0,
17842 &b.1,
17843 &b.2,
17844 &mut b.3,
17845 token_id,
17846 position,
17847 &self.inv_freq,
17848 self.pool.as_deref(),
17849 &mut logits,
17850 true,
17851 );
17852 self.graph_logits = Some(logits);
17853 return vec![0.0; self.hidden_size];
17854 }
17855 if let Some(b) = &mut self.dsv4 {
17861 let _ = (task_mask, upto);
17862 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17863 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
17864 st.pos = position;
17865 let mut logits = Vec::new();
17866 crate::dsv4::forward_token(
17867 g,
17868 layers,
17869 &cfg,
17870 st,
17871 token_id,
17872 &self.inv_freq,
17873 self.pool.as_deref(),
17874 &mut logits,
17875 );
17876 self.graph_logits = Some(logits);
17877 self.dspark_probe(position, token_id);
17878 return vec![0.0; self.hidden_size];
17881 }
17882 if let Some(b) = &mut self.dsv41 {
17884 let _ = (task_mask, upto);
17885 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17886 let mut logits = Vec::new();
17887 crate::dsv41::forward_token(
17888 &b.0,
17889 &b.1,
17890 &b.2,
17891 &mut b.3,
17892 token_id,
17893 position,
17894 self.pool.as_deref(),
17895 &mut logits,
17896 );
17897 self.graph_logits = Some(logits);
17898 return vec![0.0; self.hidden_size];
17899 }
17900 if let Some(b) = &self.g4e {
17906 let _ = (task_mask, upto);
17907 return crate::g4e::g4e_forward(
17908 &b.0,
17909 &b.1,
17910 hidden,
17911 position,
17912 &mut self.kv_cache.layers,
17913 self.num_heads,
17914 self.num_kv_heads,
17915 self.pool.as_deref(),
17916 );
17917 }
17918 if let Some(b) = &self.g3n {
17919 let _ = (task_mask, upto);
17920 return crate::g3n::g3n_forward(
17921 &b.0,
17922 &b.1,
17923 hidden,
17924 position,
17925 &mut self.kv_cache.layers,
17926 self.num_heads,
17927 self.num_kv_heads,
17928 self.head_dim,
17929 self.pool.as_deref(),
17930 );
17931 }
17932 if from == 0
17939 && upto.is_none()
17940 && task_mask.is_none()
17941 && self.anchor_core.is_some()
17942 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
17943 {
17944 static ONCE: std::sync::Once = std::sync::Once::new();
17945 ONCE.call_once(|| {
17946 eprintln!(
17947 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
17948 unsupported={} eligible={}",
17949 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
17950 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
17951 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
17952 crate::gpu::enabled_here(),
17953 self.graph_refused(),
17954 self.embryo_resident_eligible(),
17955 );
17956 });
17957 }
17958 if from == 0
17959 && upto.is_none()
17960 && task_mask.is_none()
17961 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17965 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17966 && matches!(
17970 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17971 Ok("1") | Ok("parallel")
17972 )
17973 && crate::gpu::enabled_here()
17974 && !self.graph_refused()
17975 && (position == 0 || self.device_sequence_position().is_some())
17981 && self.embryo_resident_eligible()
17982 && let Some(model) = self.ensure_embryo_graph()
17983 {
17984 let mut lg = Vec::new();
17985 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
17986 {
17987 self.graph_logits = Some(lg);
17988 return vec![0.0; self.hidden_size];
17989 }
17990 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17991 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
17992 }
17993 self.mark_graph_refused();
17999 if position != 0 {
18000 self.kv_cache.clear();
18004 self.clear_history();
18005 crate::gpu::graph_kv_reset(self.graph_kv_id);
18006 panic!(
18007 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
18008 );
18009 }
18010 }
18011 let mut h = hidden.to_vec();
18012 self.mimo_moe_prepare();
18015 let _mimo_q8 = self
18016 .mimo_moe
18017 .is_on()
18018 .then(crate::qtensor::enter_full_gpu_q8_scope);
18019 let (nh, _nkv, _hd, hs, _rd, eps) = (
18022 self.num_heads,
18023 self.num_kv_heads,
18024 self.head_dim,
18025 self.hidden_size,
18026 self.rotary_dim,
18027 self.rms_eps,
18028 );
18029 let pool = self.pool.clone();
18030 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
18042 let graph_on = match graph_env.as_deref() {
18043 Some("0") => false,
18044 Some("prefill") => false, Some(_) => true,
18046 None => crate::gpu::wgpu_graph_default(),
18052 };
18053 let graph_trusted =
18054 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
18055 let race_eligible =
18056 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
18057 let mut tail_start = 0usize;
18058 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
18059 let t_graph = std::time::Instant::now();
18060 let mut lg = Vec::new();
18061 let mut gl = 0usize;
18062 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
18063 let declined = built.is_none();
18064 let built = match built {
18065 Some(Ok(hh)) => Some(hh),
18066 Some(Err(())) => {
18067 self.clear_sequence_state();
18071 self.graph_failed
18072 .store(true, std::sync::atomic::Ordering::Relaxed);
18073 self.cancel
18074 .store(true, std::sync::atomic::Ordering::Relaxed);
18075 tracing::error!("token graph failed after admission; sequence state cleared");
18076 return vec![0.0; self.hidden_size];
18077 }
18078 None => None,
18079 };
18080 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
18085 self.mark_graph_refused();
18086 }
18087 graph_note(built.is_some(), gl, self.num_layers);
18088 if let Some(hh) = built {
18089 let dur = t_graph.elapsed();
18090 if std::env::var("CMF_GRAPH_PROF").is_ok() {
18091 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
18092 }
18093 if gl > 0 && gl < self.num_layers {
18094 h = hh;
18100 tail_start = gl;
18101 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
18102 if !graph_trusted {
18103 crate::gpu::graph_race_record(true, dur);
18104 }
18105 if !lg.is_empty() {
18106 lg.resize(self.vocab_size, 0.0);
18109 if let Some(c) = self.final_softcap {
18110 softcap_in_place(&mut lg, c, self.pool.as_deref());
18111 }
18112 self.graph_logits = Some(lg);
18113 }
18114 return hh;
18115 }
18116 }
18122 }
18123 let span = from > 0 || upto.is_some();
18147 if span && graph_on && task_mask.is_none() && graph_trusted {
18148 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
18149 let mut lg = Vec::new();
18150 let mut gl = 0usize;
18151 let span_res =
18152 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
18153 let span_res = match span_res {
18154 Some(Ok(hh)) => Some(hh),
18155 Some(Err(())) => {
18156 self.clear_sequence_state();
18157 self.graph_failed
18158 .store(true, std::sync::atomic::Ordering::Relaxed);
18159 self.cancel
18160 .store(true, std::sync::atomic::Ordering::Relaxed);
18161 tracing::error!(
18162 "span token graph failed after admission; sequence state cleared"
18163 );
18164 return vec![0.0; self.hidden_size];
18165 }
18166 None => None,
18167 };
18168 graph_note(span_res.is_some(), gl, upto_excl - from);
18169 if std::env::var("CMF_GPU_DEBUG").is_ok() {
18170 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
18174 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
18175 eprintln!(
18176 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
18177 upto_excl - from,
18178 span_res.is_some()
18179 );
18180 }
18181 }
18182 if let Some(hh) = span_res {
18183 if gl == upto_excl - from {
18184 if !lg.is_empty() {
18185 lg.resize(self.vocab_size, 0.0);
18186 if let Some(c) = self.final_softcap {
18187 softcap_in_place(&mut lg, c, self.pool.as_deref());
18188 }
18189 self.graph_logits = Some(lg);
18190 }
18191 crate::gpu::set_layer(-1);
18192 return hh;
18193 }
18194 h = hh;
18196 tail_start = from + gl;
18197 }
18198 }
18199 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
18204
18205 let host_tail = tail_start > from;
18215 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
18216 let automatic_gpu_prefix = self.automatic_gpu_prefix();
18217
18218 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
18219 #[cfg(target_os = "macos")]
18220 let mut gpu_skip_until = 0usize;
18221 for li in tail_start.max(from)..self.num_layers {
18222 let _capacity_tail = automatic_gpu_prefix
18223 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
18224 .map(|_| crate::gpu::enter_cpu_scope());
18225 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
18227 if li > u {
18228 break;
18229 }
18230 }
18231 if let Some(mask) = task_mask {
18232 if !mask.layer_alive(li) {
18233 continue; }
18235 }
18236 if host_tail && task_mask.is_none() {
18241 match self.m3_pooled_layer(li, position, &mut h) {
18242 Ok(true) => {
18243 crate::gpu::set_layer(-1);
18244 continue;
18245 }
18246 Ok(false) => {}
18247 Err(()) => {
18248 self.clear_sequence_state();
18249 self.graph_failed
18250 .store(true, std::sync::atomic::Ordering::Relaxed);
18251 self.cancel
18252 .store(true, std::sync::atomic::Ordering::Relaxed);
18253 tracing::error!(
18254 "pooled tail token graph failed after admission; sequence state cleared"
18255 );
18256 return vec![0.0; self.hidden_size];
18257 }
18258 }
18259 }
18260 #[cfg(target_os = "macos")]
18264 {
18265 if li < gpu_skip_until {
18266 continue;
18267 }
18268 if task_mask.is_none() {
18269 let end = self.q1_graph_gpu(li, upto, position, &mut h);
18270 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
18271 return vec![0.0; self.hidden_size];
18275 }
18276 if end > li {
18277 gpu_skip_until = end;
18278 if self.is_loop_end(end - 1) && end < self.num_layers {
18281 h = inference::rms_norm(
18282 &h,
18283 &self.weights.final_norm,
18284 self.rms_eps,
18285 self.norm_style,
18286 );
18287 }
18288 continue;
18289 }
18290 }
18291 }
18292
18293 if task_mask.is_none() {
18294 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
18295 crate::gpu::BatchGraphOutcome::Completed => continue,
18296 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
18297 crate::gpu::BatchGraphOutcome::Declined => {}
18298 }
18299 }
18300 #[cfg(feature = "gpu")]
18301 self.pull_lagging_host_kv(li, li + 1, position);
18302 let lw = &self.weights.layers[self.phys_layer(li)];
18303 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
18304 if tp.parse::<usize>().ok() == Some(position) {
18305 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
18306 eprintln!(
18307 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
18308 h[0], h[1]
18309 );
18310 }
18311 }
18312 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
18315 inference::rms_norm_into(
18316 &h,
18317 &lw.input_norm,
18318 self.rms_eps,
18319 self.norm_style,
18320 &mut self.ws.n1,
18321 );
18322 drop(prof);
18323
18324 let attn_out = match &lw.attn {
18325 AttnKind::Mla(w) => {
18326 let inv_freq_l = self.layer_inv_freq(li);
18327 let rs = self.layer_rope_scale(li);
18328 let eps = self.rms_eps;
18329 let pool = self.pool.clone();
18330 mla_attention(
18331 w,
18332 &self.ws.n1,
18333 &mut self.kv_cache.layers[li],
18334 position,
18335 &inv_freq_l,
18336 rs,
18337 eps,
18338 pool.as_deref(),
18339 )
18340 }
18341 AttnKind::Linear(w) => {
18342 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
18343 vmf_phase_forward(
18344 &self.ws.n1,
18345 w,
18346 &cfg,
18347 &mut self.kv_cache.layers[li].linear_state,
18348 self.pool.as_deref(),
18349 )
18350 }
18351 AttnKind::Kda(w) => {
18352 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
18353 crate::linear_core::kda_forward(
18354 &self.ws.n1,
18355 w,
18356 &cfg,
18357 &mut self.kv_cache.layers[li].linear_state,
18358 self.pool.as_deref(),
18359 )
18360 }
18361 AttnKind::LinearGdn(w) => {
18362 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
18363 gdn_forward(
18364 &self.ws.n1,
18365 w,
18366 &cfg,
18367 &mut self.kv_cache.layers[li].linear_state,
18368 self.pool.as_deref(),
18369 )
18370 }
18371 AttnKind::ShortConv(w) => {
18372 let cfg = self
18373 .short_conv_cfg
18374 .expect("short-conv layer without short_conv_cfg");
18375 short_conv_forward(
18376 &self.ws.n1,
18377 w,
18378 &cfg,
18379 &mut self.kv_cache.layers[li].linear_state,
18380 self.pool.as_deref(),
18381 )
18382 }
18383 AttnKind::Bounded(w) => {
18384 let rope = self
18387 .bounded_rope
18388 .clone()
18389 .expect("bounded layer without an installed rotation table");
18390 let cfg = crate::bounded::BoundedAttnCfg {
18391 num_heads: self.num_heads,
18392 num_kv_heads: self.num_kv_heads,
18393 head_dim: self.head_dim,
18394 hidden_size: hs,
18395 scale: self.attn_scale,
18396 rope: &rope,
18397 pool: pool.as_deref(),
18398 };
18399 crate::bounded::bounded_attention(
18400 &self.ws.n1,
18401 w,
18402 &mut self.kv_cache.layers[li],
18403 &cfg,
18404 )
18405 }
18406 AttnKind::Full {
18407 wq,
18408 wk,
18409 wv,
18410 wo,
18411 q_norm,
18412 k_norm,
18413 output_gate,
18414 softplus_gate,
18415 bias,
18416 } if self.kv_cache.layers[li].o1_sealed() => {
18417 let inv_freq_l = self.layer_inv_freq(li);
18420 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18421 let cfg = QwenAttnCfg {
18422 num_heads: self.layer_num_heads(li),
18423 num_kv_heads: nkv_l,
18424 head_dim: hd_l,
18425 hidden_size: hs,
18426 position,
18427 inv_freq: &inv_freq_l,
18428 rotary_dim: rd_l,
18429 scale: self.attn_scale,
18430 softcap: self.attn_softcap,
18431 window: None,
18432 v_norm: self.attn_v_norm,
18433 qk_norm_after_rope: self.qk_norm_after_rope,
18434 gate_sigmoid: self.proj_gate_sigmoid,
18435 q_norm: q_norm.as_deref(),
18436 k_norm: k_norm.as_deref(),
18437 output_gate: *output_gate,
18438 softplus_gate: softplus_gate
18439 .as_ref()
18440 .map(|(gate, per_head)| (gate, *per_head)),
18441 rope_scale: self.layer_rope_scale(li),
18442 bias: bias
18443 .as_ref()
18444 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18445 rms_eps: eps,
18446 norm_style: self.norm_style,
18447 pool: pool.as_deref(),
18448 v_head_dim: self.layer_v_dim(li),
18449 };
18450 attention::qwen_attention_nystrom(
18451 &self.ws.n1,
18452 wq,
18453 wk,
18454 wv,
18455 wo,
18456 &mut self.kv_cache.layers[li],
18457 &cfg,
18458 )
18459 }
18460 AttnKind::Full {
18461 wq,
18462 wk,
18463 wv,
18464 wo,
18465 q_norm,
18466 k_norm,
18467 output_gate,
18468 softplus_gate,
18469 bias,
18470 } => 'attn: {
18471 let dropin_reason =
18476 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
18477 if let Some(reason) = dropin_reason {
18478 self.note_graph_decline("wgpu attn dropin", reason);
18479 }
18480 if graph_on
18481 && dropin_reason.is_none()
18482 && !*output_gate
18483 && softplus_gate.is_none()
18484 && self.attention_heads_per_layer.is_none()
18485 && bias.is_none()
18486 && task_mask.is_none()
18487 {
18488 let inv_freq_l = self.layer_inv_freq(li);
18489 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18490 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
18491 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
18492 wq.mapped_q1(),
18493 wk.mapped_q1(),
18494 wv.mapped_q1(),
18495 wo.mapped_q1(),
18496 ) {
18497 let gm = gm.clone();
18498 let mut out = vec![0f32; hs];
18499 let cache = &self.kv_cache.layers[li];
18500 if crate::gpu::attn_dropin(
18501 &gm,
18502 self.graph_kv_id,
18503 li,
18504 &self.ws.n1,
18505 qi,
18506 ki,
18507 vi,
18508 oi,
18509 q_norm.as_deref(),
18510 k_norm.as_deref(),
18511 self.qk_norm_after_rope,
18512 &inv_freq_l,
18513 nh,
18514 nkv_l,
18515 hd_l,
18516 rd_l,
18517 hs,
18518 position,
18519 self.kv_cache.max_seq_len,
18520 gemma,
18521 eps as f32,
18522 cache.k_heads(),
18523 cache.v_heads(),
18524 &mut out,
18525 ) {
18526 break 'attn out;
18527 }
18528 }
18529 }
18530 let masked = task_mask
18531 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
18532 .unwrap_or(false);
18533 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
18534 let plain = self.layer_attn_plain(li);
18537 match (masked, f32_view) {
18538 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
18541 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
18542 attention::multi_head_attention(
18543 &self.ws.n1,
18544 q,
18545 k,
18546 v,
18547 o,
18548 &mut self.kv_cache.layers[li],
18549 self.num_heads,
18550 self.num_kv_heads,
18551 self.head_dim,
18552 self.hidden_size,
18553 position,
18554 &active_heads,
18555 &self.inv_freq,
18556 )
18557 }
18558 (masked, _) => {
18559 if masked {
18560 tracing::warn!(
18561 "layer {li}: head mask on quantized weights or on a \
18562 window/sink/per-layer-geometry layer not supported \
18563 yet — executing dense"
18564 );
18565 }
18566 let inv_freq_l = self.layer_inv_freq(li);
18567 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18568 let cfg = QwenAttnCfg {
18569 num_heads: self.layer_num_heads(li),
18570 num_kv_heads: nkv_l,
18571 head_dim: hd_l,
18572 hidden_size: hs,
18573 position,
18574 inv_freq: &inv_freq_l,
18575 rotary_dim: rd_l,
18576 scale: self.attn_scale,
18577 softcap: self.attn_softcap,
18578 window: self.layer_window(li),
18579 v_norm: self.attn_v_norm,
18580 qk_norm_after_rope: self.qk_norm_after_rope,
18581 gate_sigmoid: self.proj_gate_sigmoid,
18582 q_norm: q_norm.as_deref(),
18583 k_norm: k_norm.as_deref(),
18584 output_gate: *output_gate,
18585 softplus_gate: softplus_gate
18586 .as_ref()
18587 .map(|(gate, per_head)| (gate, *per_head)),
18588 rope_scale: self.layer_rope_scale(li),
18589 bias: bias
18590 .as_ref()
18591 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18592 rms_eps: eps,
18593 norm_style: self.norm_style,
18594 pool: pool.as_deref(),
18595 v_head_dim: self.layer_v_dim(li),
18596 };
18597 attention::qwen_attention(
18598 &self.ws.n1,
18599 wq,
18600 wk,
18601 wv,
18602 wo,
18603 &mut self.kv_cache.layers[li],
18604 &cfg,
18605 )
18606 }
18607 }
18608 }
18609 };
18610 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
18613 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
18614 None => attn_out,
18615 };
18616 let lw = &self.weights.layers[self.phys_layer(li)];
18617 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
18618 inference::add_rmsnorm_fused_into(
18619 &mut h,
18620 &attn_out,
18621 &lw.post_norm,
18622 self.rms_eps,
18623 self.norm_style,
18624 &mut self.ws.p1,
18625 );
18626 drop(prof);
18627 let mut attn_out = attn_out;
18628 attention::recycle_buf(&mut attn_out);
18629 let post_normed = &self.ws.p1;
18630
18631 let ffn_masked = task_mask
18632 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
18633 .unwrap_or(false);
18634 let ffn_out = match (ffn_masked, &lw.ffn) {
18646 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
18650 let row = task_mask
18651 .and_then(|tm| tm.ffn_masks.get(li))
18652 .map(|v| v.as_slice());
18653 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
18654 }
18655 (true, FfnKind::Dense(d)) => {
18656 let tm = task_mask.unwrap();
18657 let alive = tm.ffn_active_count(li);
18658 let deep = alive * 2 <= self.intermediate_size;
18659 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
18660 let active = tm.ffn_active_indices(li);
18661 sparse_ffn_quant(
18662 d,
18663 post_normed,
18664 &active,
18665 self.hidden_size,
18666 self.pool.as_deref(),
18667 )
18668 } else if deep
18669 && let (Some(g), Some(u), Some(dn)) = (
18670 d.gate_proj.as_f32(),
18671 d.up_proj.as_f32(),
18672 d.down_proj.as_f32(),
18673 )
18674 {
18675 let active = tm.ffn_active_indices(li);
18676 inference::sparse_ffn_forward(
18677 post_normed,
18678 g,
18679 u,
18680 dn,
18681 self.hidden_size,
18682 self.intermediate_size,
18683 &active,
18684 self.pool.as_deref(),
18685 )
18686 } else {
18687 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
18688 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
18689 }
18690 }
18691 (true, FfnKind::Moe(m)) => {
18692 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
18696 ffn_forward(
18697 &lw.ffn,
18698 post_normed,
18699 self.pool.as_deref(),
18700 allowed.as_deref(),
18701 )
18702 }
18703 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
18704 dm,
18705 post_normed,
18706 &h,
18707 self.rms_eps,
18708 self.norm_style,
18709 self.pool.as_deref(),
18710 ),
18711 (false, _) => match &lw.ffn {
18712 FfnKind::DenseMoe(dm) => dense_moe_ffn(
18713 dm,
18714 post_normed,
18715 &h,
18716 self.rms_eps,
18717 self.norm_style,
18718 self.pool.as_deref(),
18719 ),
18720 FfnKind::Moe(m)
18721 if task_mask.is_none()
18722 && host_tail
18723 && li >= tail_start
18724 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
18725 {
18726 moe_ffn_minicpm_tail(
18727 &mut self.minicpm_tail,
18728 li,
18729 self.num_layers,
18730 m,
18731 post_normed,
18732 self.pool.as_deref(),
18733 )
18734 }
18735 FfnKind::Moe(m)
18736 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
18737 {
18738 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
18739 }
18740 _ => {
18741 let allowed = match (&lw.ffn, task_mask) {
18742 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
18743 _ => None,
18744 };
18745 ffn_forward(
18746 &lw.ffn,
18747 post_normed,
18748 self.pool.as_deref(),
18749 allowed.as_deref(),
18750 )
18751 }
18752 },
18753 };
18754 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
18755 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
18756 None => ffn_out,
18757 };
18758 for (i, &f) in ffn_out.iter().enumerate() {
18759 h[i] += f;
18760 }
18761 let mut ffn_out = ffn_out;
18762 attention::recycle_buf(&mut ffn_out);
18763
18764 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
18766 for v in h.iter_mut() {
18767 *v *= sc;
18768 }
18769 }
18770 if self.layer_dump.is_some() {
18772 self.dump_layer_row(position, li, &h);
18773 }
18774
18775 if self.is_loop_end(li) && li + 1 < self.num_layers {
18778 h = inference::rms_norm(
18779 &h,
18780 &self.weights.final_norm,
18781 self.rms_eps,
18782 self.norm_style,
18783 );
18784 }
18785
18786 if self.dyn_phi_layer == Some(li) {
18790 self.update_dyn_phi(&h);
18791 }
18792 }
18793 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
18795 crate::gpu::graph_race_record(false, t.elapsed());
18796 }
18797
18798 h
18799 }
18800
18801 fn update_dyn_phi(&mut self, h: &[f32]) {
18804 const A: f32 = 0.2;
18805 if self.dyn_phi_ema.len() != h.len() {
18806 self.dyn_phi_ema = vec![0.0; h.len()];
18807 self.dyn_phi_seen = 0;
18808 }
18809 if self.dyn_phi_seen == 0 {
18810 self.dyn_phi_ema.copy_from_slice(h);
18811 } else {
18812 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
18813 *e = (1.0 - A) * *e + A * v;
18814 }
18815 }
18816 self.dyn_phi_seen += 1;
18817 }
18818
18819 pub fn dyn_phi(&self) -> &[f32] {
18821 &self.dyn_phi_ema
18822 }
18823
18824 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
18826 self.dyn_phi_layer = layer;
18827 self.dyn_phi_ema.clear();
18828 self.dyn_phi_seen = 0;
18829 }
18830
18831 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
18833 let Some(model) = &self.model else {
18834 return Vec::new();
18835 };
18836 model
18837 .header
18838 .skills
18839 .iter()
18840 .enumerate()
18841 .filter_map(|(i, sk)| {
18842 if sk.is_v2() {
18846 return None;
18847 }
18848 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
18849 let sel = sk.selection.as_ref()?;
18850 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
18851 })
18852 .collect()
18853 }
18854
18855 pub fn active_skill(&self) -> Option<usize> {
18857 self.dyn_active
18858 }
18859
18860 pub fn enable_dynamic_routing(&mut self) -> usize {
18865 use crate::swarm::{DynRouter, RoutableSkill};
18866 let Some(model) = self.model.clone() else {
18867 return 0;
18868 };
18869 if let Some(r) = &model.header.router {
18876 tracing::warn!(
18877 "dynamic routing disabled: this file declares router policy '{}' with \
18878 granularity \"{}\" — the request-level decision applies instead",
18879 r.policy,
18880 r.granularity
18881 );
18882 return 0;
18883 }
18884 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
18890 || model.header.skills.iter().any(|s| s.is_v2())
18891 {
18892 tracing::warn!(
18893 "dynamic routing disabled: this file carries format-v2 skill records \
18894 (SKILLS_V2) — they route per request through a router policy only"
18895 );
18896 return 0;
18897 }
18898 if self.dyn_blend_loaded {
18901 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
18902 return 0;
18903 }
18904 if let Some(a) = self.dyn_active {
18908 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
18909 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
18910 return 0;
18911 }
18912 }
18913 let hidden = self.hidden_size;
18914 let mut skills = Vec::new();
18915 for (idx, id, _phi) in self.dynamic_skills() {
18916 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
18917 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
18918 skills.push(rs);
18919 }
18920 }
18921 }
18922 if skills.is_empty() {
18923 return 0;
18924 }
18925 let phi = skills[0].phi_layer;
18927 if skills.iter().any(|s| s.phi_layer != phi) {
18928 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
18929 }
18930 let n = skills.len();
18931 self.set_dyn_phi_layer(Some(phi));
18932 self.dyn_router = Some(DynRouter::new(skills));
18933 n
18934 }
18935
18936 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
18938 self.dyn_router
18939 .as_ref()
18940 .map(|r| r.switches.clone())
18941 .unwrap_or_default()
18942 }
18943
18944 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
18947 let _mimo_q8 = self
18948 .mimo_moe
18949 .is_on()
18950 .then(crate::qtensor::enter_full_gpu_q8_scope);
18951 let rows = self.weights.lm_head.rows();
18952 let mut logits = attention::take_buf(rows.min(self.vocab_size));
18953 let served = self.mimo_moe.is_on()
18957 && crate::gpu::mimo_q8_short_enabled()
18958 && rows == self.vocab_size
18959 && !self.weights.lm_head.has_prism_contract()
18960 && self
18961 .weights
18962 .lm_head
18963 .graph_weight()
18964 .is_some_and(|(model, idx, kind, _)| {
18965 kind == 7
18966 && crate::gpu::q82_short_rows(
18967 model,
18968 idx,
18969 hidden,
18970 1,
18971 rows,
18972 self.hidden_size,
18973 &mut logits,
18974 )
18975 });
18976 if !served {
18977 self.weights
18978 .lm_head
18979 .matvec(hidden, &mut logits, self.pool.as_deref());
18980 }
18981 logits.resize(self.vocab_size, 0.0);
18982 if let Some(m) = self.logit_multiplier {
18983 for l in logits.iter_mut() {
18984 *l *= m;
18985 }
18986 }
18987 if let Some(c) = self.final_softcap {
18988 softcap_in_place(&mut logits, c, self.pool.as_deref());
18989 }
18990 if let Some(cm) = self.head_clusters.as_ref() {
18991 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
18992 }
18993 logits
18994 }
18995
18996 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
18999 let h = hidden.len();
19000 let ncl = cm.len() / h.max(1);
19001 if ncl == 0 || logits.len() % ncl != 0 {
19002 return;
19003 }
19004 let cs = logits.len() / ncl;
19005 let mut lc = vec![0.0f32; ncl];
19007 for c in 0..ncl {
19008 let row = &cm[c * h..(c + 1) * h];
19009 let mut s = 0.0f32;
19010 for j in 0..h {
19011 s += row[j] * hidden[j];
19012 }
19013 lc[c] = s;
19014 }
19015 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19016 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
19017 for c in 0..ncl {
19018 let blk = &mut logits[c * cs..(c + 1) * cs];
19019 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19020 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
19021 let add = lc[c] - lse - bl;
19022 for v in blk.iter_mut() {
19023 *v += add;
19024 }
19025 }
19026 }
19027
19028 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
19033 #[cfg(target_os = "macos")]
19034 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
19035 self.clear_sequence_state();
19036 crate::gpu::graph_race_begin_generation();
19040 if task_mask.is_none() {
19041 self.o1_begin();
19042 }
19043 let mut hidden = vec![0.0f32; self.hidden_size];
19044 for (pos, &id) in ids.iter().enumerate() {
19045 let emb = self.embed_single(id);
19046 hidden = self.forward_layers(&emb, pos, task_mask);
19047 }
19048 if let Err(err) = self.o1_seal_checked() {
19049 self.o1_fail(err);
19050 }
19051 if let Some(logits) = self.graph_logits.take() {
19054 return logits;
19055 }
19056 inference::rms_norm_into(
19057 &hidden,
19058 &self.weights.final_norm,
19059 self.rms_eps,
19060 self.norm_style,
19061 &mut self.ws.n1,
19062 );
19063 self.lm_head_forward(&self.ws.n1)
19064 }
19065}
19066
19067pub fn create_test_pipeline(
19069 hidden_size: usize,
19070 intermediate_size: usize,
19071 num_heads: usize,
19072 num_kv_heads: usize,
19073 head_dim: usize,
19074 num_layers: usize,
19075 vocab_size: usize,
19076) -> Pipeline {
19077 let synth = |n: usize, salt: usize| -> Vec<f32> {
19080 (0..n)
19081 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
19082 .collect()
19083 };
19084 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
19085 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19086 };
19087 let layer_weights: Vec<LayerWeights> = (0..num_layers)
19088 .map(|li| LayerWeights {
19089 input_norm: vec![1.0; hidden_size],
19090 post_norm: vec![1.0; hidden_size],
19091 attn_out_norm: None,
19092 ffn_out_norm: None,
19093 layer_scale: None,
19094 ffn: FfnKind::Dense(DenseFfn {
19095 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
19096 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
19097 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
19098 act: Act::Silu,
19099 down_t: None,
19100 segs: Vec::new(),
19101 }),
19102 attn: AttnKind::Full {
19103 bias: None,
19104 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
19105 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
19106 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
19107 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
19108 q_norm: None,
19109 k_norm: None,
19110 output_gate: false,
19111 softplus_gate: None,
19112 },
19113 })
19114 .collect();
19115
19116 Pipeline::new(
19117 Tokenizer::byte_level(),
19118 PipelineWeights {
19119 embed_tokens: qt(vocab_size, hidden_size, 100),
19120 layers: layer_weights,
19121 lm_head: qt(vocab_size, hidden_size, 200),
19122 final_norm: vec![1.0; hidden_size],
19123 },
19124 hidden_size,
19125 intermediate_size,
19126 num_heads,
19127 num_kv_heads,
19128 head_dim,
19129 num_layers,
19130 num_layers, false, vocab_size,
19133 1e-6,
19134 10_000.0,
19135 NormStyle::Qwen,
19136 4096,
19137 SamplerConfig {
19138 seed: Some(42),
19139 ..Default::default()
19140 },
19141 )
19142}
19143
19144#[inline]
19149fn mask_bit(row: &[u8], j: usize) -> bool {
19150 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
19151}
19152
19153fn mask_gain() -> f32 {
19164 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
19165 *G.get_or_init(|| {
19166 std::env::var("CMF_FFN_MASK_GAIN")
19167 .ok()
19168 .and_then(|v| v.parse().ok())
19169 .unwrap_or(1.0)
19170 })
19171}
19172
19173fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
19174 let fill = meanfill().and_then(|(i, v)| {
19177 let li = crate::gpu::cur_layer();
19178 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
19179 });
19180 for r in 0..rows {
19181 let base = r * inter;
19182 for (bi, &byte) in row.iter().enumerate() {
19183 if byte == 0xFF {
19184 continue;
19185 }
19186 let j0 = bi * 8;
19187 for bit in 0..8 {
19188 let j = j0 + bit;
19189 if j < inter && byte & (1 << bit) == 0 {
19190 g[base + j] = fill.map_or(0.0, |f| f[j]);
19191 }
19192 }
19193 }
19194 }
19195 let gain = mask_gain();
19196 if gain != 1.0 {
19197 for v in g[..rows * inter].iter_mut() {
19198 *v *= gain;
19199 }
19200 }
19201}
19202
19203#[inline]
19205fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
19206 row.is_none_or(|r| mask_bit(r, i))
19207}
19208
19209fn all_bits_on(row: &[u8], n: usize) -> bool {
19212 (0..n).all(|i| mask_bit(row, i))
19213}
19214
19215fn tube_topk() -> usize {
19223 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19224 *K.get_or_init(|| {
19225 std::env::var("CMF_TUBE_TOPK")
19226 .ok()
19227 .and_then(|v| v.parse().ok())
19228 .unwrap_or(0)
19229 })
19230}
19231
19232fn tube_score_oracle() -> bool {
19233 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19234 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
19235}
19236
19237fn tube_ffn_routed(
19244 d: &DenseFfn,
19245 xs: &[f32],
19246 b: usize,
19247 pool: Option<&Pool>,
19248 mask_row: Option<&[u8]>,
19249 k: usize,
19250) -> Vec<f32> {
19251 let hidden = d.down_proj.rows();
19252 let core = d.gate_proj.rows();
19253 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19254 let mut out = match (b, core_full, mask_row) {
19255 (1, true, _) => dense_ffn(d, xs, pool),
19256 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19257 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19258 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19259 };
19260 let cand: Vec<usize> = (0..d.segs.len())
19261 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
19262 .collect();
19263 if cand.is_empty() {
19264 return out;
19265 }
19266 let oracle = tube_score_oracle();
19270 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
19271 let mut scores = vec![0f32; b * cand.len()];
19272 for (ci, &i) in cand.iter().enumerate() {
19273 let seg = &d.segs[i];
19274 let w = seg.width;
19275 let mut g = vec![0.0f32; b * w];
19276 if b == 1 {
19277 seg.gate.matvec(xs, &mut g, pool);
19278 } else {
19279 seg.gate.matmat(xs, b, &mut g, pool);
19280 }
19281 for v in g.iter_mut() {
19282 *v = Act::Silu.combine(*v, 1.0);
19283 }
19284 if !oracle {
19285 for t in 0..b {
19286 scores[t * cand.len() + ci] =
19287 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19288 }
19289 }
19290 if oracle || b > 1 {
19291 let mut u = vec![0.0f32; b * w];
19292 if b == 1 {
19293 seg.up.matvec(xs, &mut u, pool);
19294 } else {
19295 seg.up.matmat(xs, b, &mut u, pool);
19296 }
19297 for (a, &v) in g.iter_mut().zip(u.iter()) {
19298 *a *= v;
19299 }
19300 if oracle {
19301 for t in 0..b {
19302 scores[t * cand.len() + ci] =
19303 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19304 }
19305 }
19306 }
19307 acts.push(g);
19308 }
19309 let keep = k.min(cand.len());
19311 let mut scratch: Vec<f32> = Vec::new();
19312 for t in 0..b {
19313 let mut sc: Vec<(f32, usize)> = (0..cand.len())
19314 .map(|ci| (scores[t * cand.len() + ci], ci))
19315 .collect();
19316 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
19317 let mut alive = vec![false; cand.len()];
19318 for &(_, ci) in sc.iter().take(keep) {
19319 alive[ci] = true;
19320 }
19321 if b > 1 {
19322 for (ci, a) in acts.iter_mut().enumerate() {
19323 if !alive[ci] {
19324 let w = d.segs[cand[ci]].width;
19325 a[t * w..(t + 1) * w].fill(0.0);
19326 }
19327 }
19328 } else {
19329 for (ci, &i) in cand.iter().enumerate() {
19333 if !alive[ci] {
19334 continue;
19335 }
19336 let seg = &d.segs[i];
19337 let w = seg.width;
19338 let g = &mut acts[ci];
19339 if !tube_score_oracle() {
19340 scratch.clear();
19341 scratch.resize(w, 0.0);
19342 seg.up.matvec(xs, &mut scratch, pool);
19343 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
19344 *a *= v;
19345 }
19346 }
19347 let mut acc = vec![0.0f32; hidden];
19348 seg.down.matvec(g, &mut acc, pool);
19349 for (o, a) in out.iter_mut().zip(&acc) {
19350 *o += *a;
19351 }
19352 }
19353 }
19354 }
19355 if b > 1 {
19356 for (ci, &i) in cand.iter().enumerate() {
19357 let seg = &d.segs[i];
19358 let mut acc = vec![0.0f32; b * hidden];
19359 seg.down.matmat(&acts[ci], b, &mut acc, pool);
19360 for (o, a) in out.iter_mut().zip(&acc) {
19361 *o += *a;
19362 }
19363 }
19364 }
19365 out
19366}
19367
19368fn tube_ffn(
19374 d: &DenseFfn,
19375 xs: &[f32],
19376 b: usize,
19377 pool: Option<&Pool>,
19378 mask_row: Option<&[u8]>,
19379) -> Vec<f32> {
19380 if tube_topk() > 0 {
19381 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
19382 }
19383 let hidden = d.down_proj.rows();
19384 let core = d.gate_proj.rows();
19385 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19386 let mut out = match (b, core_full, mask_row) {
19387 (1, true, _) => dense_ffn(d, xs, pool),
19388 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19389 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19390 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19391 };
19392 TUBE_SCRATCH.with(|sc| {
19393 let mut sc = sc.borrow_mut();
19394 let [g, u, acc] = &mut *sc;
19395 for seg in &d.segs {
19396 if !tube_bit(mask_row, seg.start) {
19397 continue;
19398 }
19399 let w = seg.width;
19400 g.resize(b * w, 0.0);
19401 if b == 1
19402 && d.act == Act::Silu
19403 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
19404 {
19405 } else {
19407 u.resize(b * w, 0.0);
19408 if b == 1 {
19409 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
19410 } else {
19411 seg.gate.matmat(xs, b, g, pool);
19412 seg.up.matmat(xs, b, u, pool);
19413 }
19414 for i in 0..b * w {
19415 g[i] = d.act.combine(g[i], u[i]);
19416 }
19417 }
19418 acc.resize(b * hidden, 0.0);
19419 acc.fill(0.0);
19420 if b == 1 {
19421 seg.down.matvec(g, acc, pool);
19422 } else {
19423 seg.down.matmat(g, b, acc, pool);
19424 }
19425 for (o, a) in out.iter_mut().zip(acc.iter()) {
19426 *o += *a;
19427 }
19428 }
19429 out
19430 })
19431}
19432
19433thread_local! {
19434 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
19438 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
19439}
19440
19441static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
19444 std::sync::atomic::AtomicU64::new(0),
19445 std::sync::atomic::AtomicU64::new(0),
19446];
19447
19448fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
19452 #[cfg(feature = "gpu")]
19453 {
19454 use std::sync::atomic::Ordering::Relaxed;
19455 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
19456 (
19457 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
19458 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
19459 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
19460 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
19461 )
19462 }
19463 #[cfg(not(feature = "gpu"))]
19464 (0.0, 0.0, 0.0, 0)
19465}
19466
19467fn wgpu_mirror_counters() -> [u64; 6] {
19470 #[cfg(feature = "gpu")]
19471 {
19472 crate::gpu_wgpu::MIRROR_EVENTS
19473 .each_ref()
19474 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
19475 }
19476 #[cfg(not(feature = "gpu"))]
19477 [0; 6]
19478}
19479
19480fn prefill_prof_on() -> bool {
19481 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19482 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
19483}
19484
19485pub(crate) fn accumulate_dense_ffn_activation_mass(
19494 d: &DenseFfn,
19495 xs: &[f32],
19496 b: usize,
19497 pool: Option<&Pool>,
19498 mass: &mut [f64],
19499 squared_mass: &mut [f64],
19500) -> Result<(), String> {
19501 let inter = d.gate_proj.rows();
19502 let hidden = d.gate_proj.cols();
19503 if mass.len() != inter || squared_mass.len() != inter {
19504 return Err(format!(
19505 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
19506 mass.len(),
19507 squared_mass.len()
19508 ));
19509 }
19510 if d.up_proj.rows() != inter
19511 || d.up_proj.cols() != hidden
19512 || d.down_proj.cols() != inter
19513 || d.down_proj.rows() != hidden
19514 {
19515 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
19516 }
19517 if !d.segs.is_empty() {
19518 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
19519 }
19520 if xs.len() != b.saturating_mul(hidden) {
19521 return Err(format!(
19522 "MTP FFN probe: {} input values for {b} × {hidden} rows",
19523 xs.len()
19524 ));
19525 }
19526 if b == 0 {
19527 return Ok(());
19528 }
19529 let mut gate = vec![0.0f32; b * inter];
19530 let mut up = vec![0.0f32; b * inter];
19531 d.gate_proj.matmat(xs, b, &mut gate, pool);
19532 d.up_proj.matmat(xs, b, &mut up, pool);
19533 for row in 0..b {
19534 for i in 0..inter {
19535 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
19536 let a = a as f64;
19537 mass[i] += a.abs();
19538 squared_mass[i] += a * a;
19539 }
19540 }
19541 Ok(())
19542}
19543
19544fn dense_ffn_batch(
19545 d: &DenseFfn,
19546 xs: &[f32],
19547 b: usize,
19548 pool: Option<&Pool>,
19549 mask_row: Option<&[u8]>,
19550) -> Vec<f32> {
19551 let inter = d.gate_proj.rows();
19552 let hidden = d.down_proj.rows();
19553 let fused_act = d.act.wgpu_graph_act();
19565 if mask_row.is_none()
19566 && fused_act.is_some()
19567 && b >= 32
19568 && crate::gpu::enabled_here()
19569 && !crate::gpu::mm_killed()
19570 && refit_dir().is_none()
19575 && !ffn_probe_active()
19580 && !crate::gptq_capture::capturing()
19584 {
19585 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19586 d.gate_proj.mapped_q4t(),
19587 d.up_proj.mapped_q4t(),
19588 d.down_proj.mapped_q4t(),
19589 ) {
19590 let mut out = vec![0.0f32; b * hidden];
19591 let act = fused_act.expect("checked above");
19592 if crate::gpu::q4_ffn_act(
19593 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
19594 ) {
19595 return out;
19596 }
19597 }
19598 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19603 d.gate_proj.mapped_q4tp(),
19604 d.up_proj.mapped_q4tp(),
19605 d.down_proj.mapped_q4tp(),
19606 ) {
19607 let mut out = vec![0.0f32; b * hidden];
19608 let act = fused_act.expect("checked above");
19609 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
19610 {
19611 return out;
19612 }
19613 }
19614 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19620 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
19621 d.gate_proj.mapped_device_gemm(),
19622 d.up_proj.mapped_device_gemm(),
19623 d.down_proj.mapped_device_gemm(),
19624 ) {
19625 let mut out = vec![0.0f32; b * hidden];
19626 let act = fused_act.expect("checked above");
19627 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
19628 return out;
19629 }
19630 }
19631 }
19632 let mut g = vec![0.0f32; b * inter];
19633 d.gate_proj.matmat(xs, b, &mut g, pool);
19634 let mut u = vec![0.0f32; b * inter];
19635 d.up_proj.matmat(xs, b, &mut u, pool);
19636 if gate_topk() > 0 && d.act == Act::Silu {
19637 for t in 0..b {
19638 let row = &mut g[t * inter..(t + 1) * inter];
19639 for v in row.iter_mut() {
19640 *v = Act::Silu.combine(*v, 1.0);
19641 }
19642 keep_top_k(row, gate_topk());
19643 }
19644 for i in 0..b * inter {
19645 g[i] *= u[i];
19646 }
19647 } else {
19648 for i in 0..b * inter {
19649 g[i] = d.act.combine(g[i], u[i]);
19650 }
19651 }
19652 if let Some(row) = mask_row {
19653 zero_masked_cols(&mut g, b, inter, row);
19654 }
19655 if oracle_topk() > 0 {
19656 for t in 0..b {
19657 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
19658 }
19659 }
19660 let mut out = vec![0.0f32; b * hidden];
19661 d.down_proj.matmat(&g, b, &mut out, pool);
19662 if refit_dir().is_some() {
19663 let li = crate::gpu::cur_layer();
19664 if li >= 0 {
19665 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
19666 }
19667 }
19668 FFN_PROBE.with(|pr| {
19672 if let Some(acc) = pr.borrow_mut().as_mut() {
19673 let li = crate::gpu::cur_layer();
19674 if li < 0 {
19675 return;
19676 }
19677 let Some(row) = acc.get_mut(li as usize) else {
19678 return;
19679 };
19680 let sq = probe_sq();
19681 for t in 0..b {
19682 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
19683 *a += if sq {
19684 (v as f64) * (v as f64)
19685 } else {
19686 (v as f64).abs()
19687 };
19688 }
19689 }
19690 }
19691 });
19692 out
19693}
19694
19695fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
19700 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19701 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19702 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
19703 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
19704 if (!on && !dump) || b == 0 {
19705 return;
19706 }
19707 let hidden = xs.len() / b;
19708 if on {
19709 let mut acc = m.act_sq.borrow_mut();
19710 if acc.len() < hidden {
19711 acc.resize(hidden, 0.0);
19712 }
19713 for t in 0..b {
19714 let row = &xs[t * hidden..(t + 1) * hidden];
19715 for (a, &v) in acc.iter_mut().zip(row) {
19716 *a += (v as f64) * (v as f64);
19717 }
19718 }
19719 }
19720 if dump {
19721 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
19724 .ok()
19725 .and_then(|v| v.parse().ok())
19726 .unwrap_or(4096);
19727 let mut rows = m.act_rows.borrow_mut();
19728 if rows.len() < cap * hidden {
19729 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
19730 rows.extend_from_slice(&xs[..take * hidden]);
19731 }
19732 }
19733}
19734
19735#[derive(Clone, Copy)]
19738struct SendVecs(*mut Vec<f32>);
19739unsafe impl Send for SendVecs {}
19740unsafe impl Sync for SendVecs {}
19741impl SendVecs {
19742 #[inline]
19743 fn at(self, i: usize) -> *mut Vec<f32> {
19744 unsafe { self.0.add(i) }
19745 }
19746}
19747
19748fn moe_ffn_batch(
19749 m: &MoeFfn,
19750 xs: &[f32],
19751 b: usize,
19752 hidden: usize,
19753 pool: Option<&Pool>,
19754 allowed: Option<&[bool]>,
19755) -> Vec<f32> {
19756 accumulate_act(m, xs, b);
19757 let ne = m.experts.len();
19758 let mut logits = vec![0.0f32; b * ne];
19759 match &m.resonance {
19760 Some(r) => {
19761 let hdim = xs.len() / b.max(1);
19762 for bi in 0..b {
19763 r.scores(
19764 &xs[bi * hdim..(bi + 1) * hdim],
19765 &mut logits[bi * ne..(bi + 1) * ne],
19766 );
19767 }
19768 }
19769 None => m.router.matmat(xs, b, &mut logits, pool),
19770 }
19771
19772 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
19775 {
19776 let mut st = m.stats.borrow_mut();
19777 if st.len() < ne {
19778 st.resize(ne, 0);
19779 }
19780 for bi in 0..b {
19781 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
19782 for &e in &idx {
19783 st[e] += 1;
19784 assign[e].push((bi, p[e] / wsum));
19785 }
19786 }
19787 }
19788
19789 let mut out = vec![0.0f32; b * hidden];
19790 let cols = m.experts[0].gate_proj.cols();
19791 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
19792 let sb = list.len();
19793 let mut sub = vec![0.0f32; sb * cols];
19794 for (k, &(bi, _)) in list.iter().enumerate() {
19795 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
19796 }
19797 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
19798 for (k, &(bi, w)) in list.iter().enumerate() {
19799 for i in 0..hidden {
19800 out[bi * hidden + i] += w * eo[k * hidden + i];
19801 }
19802 }
19803 };
19804 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
19810 if pool.is_some() && active.len() >= 8 {
19811 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
19812 {
19813 let panel_ptr = SendVecs(panels.as_mut_ptr());
19814 let experts = &m.experts;
19817 let (active_r, assign_r) = (&active, &assign);
19818 let inherit_cpu = crate::gpu::inherit_cpu_scope();
19819 let run = |start: usize, end: usize| {
19820 let _cpu_scope = inherit_cpu();
19821 #[cfg(target_os = "macos")]
19829 let _metal_cpu = crate::gpu::enter_cpu_scope();
19830 for ai in start..end {
19831 let e = active_r[ai];
19832 let list = &assign_r[e];
19833 let sb = list.len();
19834 let mut sub = vec![0.0f32; sb * cols];
19835 for (k, &(bi, _)) in list.iter().enumerate() {
19836 sub[k * cols..(k + 1) * cols]
19837 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
19838 }
19839 unsafe {
19841 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
19842 }
19843 }
19844 };
19845 match pool {
19846 Some(p) => p.run_rows(active.len(), &run),
19847 None => run(0, active.len()),
19848 }
19849 }
19850 for (ai, &e) in active.iter().enumerate() {
19851 for (k, &(bi, w)) in assign[e].iter().enumerate() {
19852 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
19853 for i in 0..hidden {
19854 out[bi * hidden + i] += w * eo[i];
19855 }
19856 }
19857 }
19858 } else {
19859 for &e in &active {
19860 run_expert(&m.experts[e], &assign[e], &mut out);
19861 }
19862 }
19863 if let Some((se, gate)) = &m.shared {
19864 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
19865 let mut gl = vec![0.0f32; b];
19866 gate.matmat(xs, b, &mut gl, pool);
19867 (0..b)
19868 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
19869 .collect()
19870 } else {
19871 (0..b).map(|bi| (bi, 1.0)).collect()
19872 };
19873 run_expert(se, &all, &mut out);
19874 }
19875 out
19876}
19877
19878fn moe_ffn_rows_exact(
19890 m: &MoeFfn,
19891 xs: &[f32],
19892 b: usize,
19893 hidden: usize,
19894 pool: Option<&Pool>,
19895) -> Vec<f32> {
19896 let mut out = vec![0.0f32; b * hidden];
19897 let per_row = |out: &mut [f32]| {
19898 for r in 0..b {
19899 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
19900 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
19901 }
19902 };
19903 let covered = !crate::gpu::enabled_here()
19904 && moe_batch_enabled()
19905 && m.shared.is_none()
19906 && m.resonance.is_none()
19907 && FFN_PROBE.with(|pr| pr.borrow().is_none())
19908 && m.experts.iter().all(|d| d.act == Act::Silu);
19909 if !covered {
19910 per_row(&mut out);
19911 return out;
19912 }
19913 let ne = m.experts.len();
19914 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
19916 for r in 0..b {
19917 let x = &xs[r * hidden..(r + 1) * hidden];
19918 accumulate_act(m, x, 1);
19919 let mut logits = vec![0.0f32; ne];
19920 m.router.matvec(x, &mut logits, pool);
19921 let (idx, p, wsum) = moe_route(&logits, m, None);
19922 {
19923 let mut st = m.stats.borrow_mut();
19924 if st.len() < ne {
19925 st.resize(ne, 0);
19926 }
19927 for &e in &idx {
19928 st[e] += 1;
19929 }
19930 }
19931 let w: Vec<f32> = idx
19932 .iter()
19933 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
19934 .collect();
19935 routes.push((idx, w));
19936 }
19937 if routes.iter().any(|(idx, _)| idx.is_empty()) {
19938 per_row(&mut out);
19939 return out;
19940 }
19941 let mut experts: Vec<usize> = Vec::new();
19943 let mut groups: Vec<Vec<usize>> = Vec::new();
19944 for (r, (idx, _)) in routes.iter().enumerate() {
19945 for &e in idx {
19946 match experts.iter().position(|&x| x == e) {
19947 Some(g) => groups[g].push(r),
19948 None => {
19949 experts.push(e);
19950 groups.push(vec![r]);
19951 }
19952 }
19953 }
19954 }
19955 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
19956 let inter = m.experts[experts[0]].gate_proj.rows();
19957 let pairs: Vec<(&QTensor, &QTensor)> = experts
19958 .iter()
19959 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
19960 .collect();
19961 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
19962 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
19963 per_row(&mut out);
19964 return out;
19965 }
19966 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
19967 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
19968 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
19969 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
19970 per_row(&mut out);
19971 return out;
19972 }
19973 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
19975 let mut p = 0usize;
19976 for (g, &e) in experts.iter().enumerate() {
19977 for &r in &groups[g] {
19978 slot.insert((r, e), p);
19979 p += 1;
19980 }
19981 }
19982 for (r, (idx, w)) in routes.iter().enumerate() {
19983 let terms: Vec<(&[f32], f32)> = idx
19984 .iter()
19985 .zip(w)
19986 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
19987 .collect();
19988 let row = &mut out[r * hidden..(r + 1) * hidden];
19989 for (i, dst) in row.iter_mut().enumerate() {
19990 let mut acc = 0f32;
19992 for (d, we) in &terms {
19993 acc += we * d[i];
19994 }
19995 *dst = acc;
19996 }
19997 }
19998 out
19999}
20000
20001pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
20005 match pool {
20006 Some(p) if v.len() >= 1 << 15 => {
20007 let base = v.as_mut_ptr() as usize;
20008 p.run_rows(v.len(), &|s, e| {
20009 let sl =
20012 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
20013 for x in sl {
20014 *x = c * (*x / c).tanh();
20015 }
20016 });
20017 }
20018 _ => {
20019 for x in v.iter_mut() {
20020 *x = c * (*x / c).tanh();
20021 }
20022 }
20023 }
20024}
20025
20026thread_local! {
20027 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
20031 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
20032}
20033
20034fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20036 if gate_topk() > 0
20039 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
20040 {
20041 return out;
20042 }
20043 let prism_body = d.gate_proj.has_prism_contract()
20059 || d.up_proj.has_prism_contract()
20060 || d.down_proj.has_prism_contract();
20061 if !prism_body
20062 && crate::gpu::enabled_here()
20063 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
20064 {
20065 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
20066 crate::gpu::ProbeArm::Gpu
20067 } else {
20068 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
20069 };
20070 match arm {
20071 crate::gpu::ProbeArm::Gpu => {
20072 let t0 = std::time::Instant::now();
20073 if let Some(out) = dense_ffn_gpu(d, x, pool) {
20074 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
20075 return out;
20076 }
20077 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
20081 }
20082 crate::gpu::ProbeArm::CpuTimed => {
20083 let t0 = std::time::Instant::now();
20084 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20085 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
20086 return out;
20087 }
20088 crate::gpu::ProbeArm::Cpu => {
20089 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20090 }
20091 }
20092 }
20093 dense_ffn_cpu(d, x, pool)
20094}
20095
20096fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20098 let inter = d.gate_proj.rows();
20099 FFN_SCRATCH.with(|s| {
20100 let mut s = s.borrow_mut();
20101 let [g, u, ..] = &mut *s;
20102 g.resize(inter, 0.0);
20103 if gate_topk() > 0 {
20106 u.resize(inter, 0.0);
20110 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20111 for i in 0..inter {
20112 g[i] = Act::Silu.combine(g[i], 1.0);
20113 }
20114 keep_top_k(g, gate_topk());
20115 for i in 0..inter {
20116 g[i] *= u[i];
20117 }
20118 } else if d.act == Act::Silu && {
20119 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20120 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
20121 } {
20122 } else {
20124 u.resize(inter, 0.0);
20125 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20127 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20128 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
20129 for i in 0..inter {
20130 g[i] = d.act.combine(g[i], u[i]);
20131 }
20132 }
20133 FFN_PROBE.with(|pr| {
20141 if let Some(acc) = pr.borrow_mut().as_mut() {
20142 let li = crate::gpu::cur_layer();
20143 if li >= 0 {
20144 if let Some(row) = acc.get_mut(li as usize) {
20145 match probe_topk() {
20146 0 if probe_sq() => {
20147 for (a, &v) in row.iter_mut().zip(g.iter()) {
20148 *a += (v as f64) * (v as f64);
20149 }
20150 }
20151 0 if probe_signed() => {
20152 for (a, &v) in row.iter_mut().zip(g.iter()) {
20153 *a += v as f64;
20154 }
20155 }
20156 0 => {
20157 for (a, &v) in row.iter_mut().zip(g.iter()) {
20158 *a += (v as f64).abs();
20159 }
20160 }
20161 k => {
20162 let n = g.len();
20163 let k = k.min(n);
20164 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
20165 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20166 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20167 });
20168 let thr = *kth;
20169 for (a, &v) in row.iter_mut().zip(g.iter()) {
20170 if v.abs() >= thr {
20171 *a += 1.0;
20172 }
20173 }
20174 }
20175 }
20176 }
20177 }
20178 }
20179 });
20180 if oracle_topk() > 0 {
20181 keep_top_k(g, oracle_topk());
20182 }
20183 {
20184 let li = crate::gpu::cur_layer();
20185 if li >= 0 {
20186 adump_row(li as usize, g);
20187 }
20188 }
20189 let mut out = attention::take_buf(d.down_proj.rows());
20190 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
20191 d.down_proj.matvec(g, &mut out, pool);
20192 out
20193 })
20194}
20195
20196pub struct RefitAcc {
20209 pub support: Vec<u32>,
20210 pub gss: Vec<f32>,
20211 pub ya: Vec<f32>,
20212 pub hidden: usize,
20213 pub tokens: u64,
20214 pub buf_g: Vec<f32>,
20220 pub buf_o: Vec<f32>,
20221 pub buf_t: usize,
20222}
20223
20224type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
20228
20229static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
20230 std::sync::OnceLock::new();
20231
20232fn ffn_probe_active() -> bool {
20235 FFN_PROBE.with(|p| p.borrow().is_some())
20236}
20237
20238fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
20239 REFIT
20240 .get_or_init(|| {
20241 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
20242 (
20243 d,
20244 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
20245 )
20246 })
20247 })
20248 .as_ref()
20249}
20250
20251fn refit_accumulate(
20253 li: usize,
20254 g: &[f32],
20255 b: usize,
20256 inter: usize,
20257 out: &[f32],
20258 hidden: usize,
20259 pool: Option<&Pool>,
20260) {
20261 let Some((dir, map)) = refit_dir() else {
20262 return;
20263 };
20264 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20265 let (from, to) = *SPAN.get_or_init(|| {
20266 let g = |k: &str, d: usize| {
20267 std::env::var(k)
20268 .ok()
20269 .and_then(|v| v.parse().ok())
20270 .unwrap_or(d)
20271 };
20272 (
20273 g("CMF_FFN_REFIT_FROM", 0),
20274 g("CMF_FFN_REFIT_TO", usize::MAX),
20275 )
20276 });
20277 if li < from || li > to {
20278 return;
20279 }
20280 let mut guard = map.lock().unwrap();
20281 let (map, shared) = &mut *guard;
20282 let acc = match map.entry(li) {
20283 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
20284 std::collections::hash_map::Entry::Vacant(e) => {
20285 let path = format!("{dir}/support.{li}.u32");
20286 let Ok(bytes) = std::fs::read(&path) else {
20287 eprintln!("refit: no {path} — layer {li} skipped");
20288 return;
20289 };
20290 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
20291 let support: Vec<u32> = bytes[4..4 + n * 4]
20292 .chunks_exact(4)
20293 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
20294 .collect();
20295 eprintln!(
20296 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
20297 (n * n + hidden * n) as f64 * 4.0 / 1e6
20298 );
20299 e.insert(RefitAcc {
20300 gss: vec![0.0; n * n],
20301 ya: vec![0.0; hidden * n],
20302 buf_g: Vec::new(),
20303 buf_o: Vec::new(),
20304 buf_t: 0,
20305 support,
20306 hidden,
20307 tokens: 0,
20308 })
20309 }
20310 };
20311 let ns = acc.support.len();
20312 let cap = refit_batch();
20314 if acc.buf_g.is_empty() {
20315 acc.buf_g = vec![0.0; ns * cap];
20316 acc.buf_o = vec![0.0; hidden * cap];
20317 }
20318 let take = b.min(cap - acc.buf_t);
20319 for t in 0..take {
20320 let col = acc.buf_t + t;
20321 for (j, &n) in acc.support.iter().enumerate() {
20322 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
20323 }
20324 for h in 0..hidden {
20325 acc.buf_o[h * cap + col] = out[t * hidden + h];
20326 }
20327 }
20328 acc.buf_t += take;
20329 acc.tokens += take as u64;
20330 if acc.buf_t < cap {
20331 return;
20332 }
20333 let bt = acc.buf_t;
20334 acc.buf_t = 0;
20335 let RefitAcc {
20345 gss,
20346 ya,
20347 buf_g,
20348 buf_o,
20349 ..
20350 } = acc;
20351 let need = (ns * ns).max(hidden * ns);
20352 if shared.len() < need {
20353 shared.resize(need, 0.0);
20354 }
20355 let scratch = &mut shared[..];
20356 let _ = bt;
20357 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
20358 add_into(gss, &scratch[..ns * ns], pool);
20359 if crate::gpu::gemm_nt_f32_transient(
20360 buf_o,
20361 buf_g,
20362 &mut scratch[..hidden * ns],
20363 hidden,
20364 cap,
20365 ns,
20366 ) {
20367 add_into(ya, &scratch[..hidden * ns], pool);
20368 } else {
20369 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20370 }
20371 } else {
20372 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
20373 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20374 }
20375 }
20379
20380fn refit_batch() -> usize {
20382 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20383 *B.get_or_init(|| {
20384 std::env::var("CMF_FFN_REFIT_BATCH")
20385 .ok()
20386 .and_then(|v| v.parse().ok())
20387 .unwrap_or(4096)
20388 })
20389}
20390
20391fn accum_outer_t(
20394 c: &mut [f32],
20395 m: usize,
20396 n: usize,
20397 b: usize,
20398 left: &[f32],
20399 right: &[f32],
20400 pool: Option<&Pool>,
20401) {
20402 let ptr = SendMut(c.as_mut_ptr());
20403 let body = |i: usize| {
20404 let ptr = &ptr;
20405 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
20406 for t in 0..b {
20407 let a = left[i * b + t];
20408 if a == 0.0 {
20409 continue;
20410 }
20411 for (j, o) in row.iter_mut().enumerate() {
20412 *o += a * right[j * b + t];
20413 }
20414 }
20415 };
20416 match pool {
20417 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
20418 for i in s..e {
20419 body(i);
20420 }
20421 }),
20422 _ => {
20423 for i in 0..m {
20424 body(i);
20425 }
20426 }
20427 }
20428}
20429
20430fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
20433 let n = dst.len().min(src.len());
20434 match pool {
20435 Some(p) if n >= 1 << 16 => {
20436 let ptr = SendMut(dst.as_mut_ptr());
20437 let f = |s: usize, e: usize| {
20438 let ptr = &ptr;
20439 for blk in s..e {
20440 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
20441 for i in a..b {
20442 unsafe { *ptr.0.add(i) += src[i] };
20443 }
20444 }
20445 };
20446 p.run_rows(n.div_ceil(4096), &f);
20447 }
20448 _ => {
20449 for (d, v) in dst.iter_mut().zip(&src[..n]) {
20450 *d += *v;
20451 }
20452 }
20453 }
20454}
20455
20456fn accum_outer(
20461 c: &mut [f32],
20462 m: usize,
20463 n: usize,
20464 b: usize,
20465 left: &[f32],
20466 right: &[f32],
20467 pool: Option<&Pool>,
20468) {
20469 const TILE: usize = 32;
20470 let tiles = m.div_ceil(TILE);
20471 let cp = SendMut(c.as_mut_ptr());
20472 let body = |ti: usize| {
20473 let cp = &cp;
20474 let i0 = ti * TILE;
20475 let i1 = (i0 + TILE).min(m);
20476 for t in 0..b {
20477 let r = &right[t * n..t * n + n];
20478 for i in i0..i1 {
20479 let a = left[i * b + t];
20480 if a == 0.0 {
20481 continue;
20482 }
20483 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
20485 for (o, v) in row.iter_mut().zip(r) {
20486 *o += a * *v;
20487 }
20488 }
20489 }
20490 };
20491 match pool {
20492 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
20493 for ti in s..e {
20494 body(ti);
20495 }
20496 }),
20497 _ => {
20498 for ti in 0..tiles {
20499 body(ti);
20500 }
20501 }
20502 }
20503}
20504
20505pub fn refit_flush() -> usize {
20507 let Some((dir, map)) = refit_dir() else {
20508 return 0;
20509 };
20510 let guard = map.lock().unwrap();
20511 let mut n = 0;
20512 for (li, acc) in guard.0.iter() {
20513 let w = |name: &str, v: &[f32]| {
20516 let path = format!("{dir}/{name}.{li}.f32");
20517 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
20518 match std::fs::write(&path, &bytes) {
20519 Ok(()) => {}
20520 Err(e) => eprintln!(
20521 "refit: FAILED to write {path} ({} MB): {e}",
20522 bytes.len() / 1_000_000
20523 ),
20524 }
20525 };
20526 w("gss", &acc.gss);
20527 w("ya", &acc.ya);
20528 println!(
20529 "refit L{li}: {} support, {} tokens, hidden {}",
20530 acc.support.len(),
20531 acc.tokens,
20532 acc.hidden
20533 );
20534 n += 1;
20535 }
20536 n
20537}
20538
20539fn adump_row(li: usize, g: &[f32]) {
20544 use std::io::Write as _;
20545 static FILES: std::sync::OnceLock<
20546 Option<(
20547 String,
20548 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
20549 )>,
20550 > = std::sync::OnceLock::new();
20551 let Some((prefix, map)) = FILES
20552 .get_or_init(|| {
20553 std::env::var("CMF_FFN_ADUMP")
20554 .ok()
20555 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
20556 })
20557 .as_ref()
20558 else {
20559 return;
20560 };
20561 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20564 let (from, to) = *SPAN.get_or_init(|| {
20565 let g = |k: &str, d: usize| {
20566 std::env::var(k)
20567 .ok()
20568 .and_then(|v| v.parse().ok())
20569 .unwrap_or(d)
20570 };
20571 (
20572 g("CMF_FFN_ADUMP_FROM", 0),
20573 g("CMF_FFN_ADUMP_TO", usize::MAX),
20574 )
20575 });
20576 if li < from || li > to {
20577 return;
20578 }
20579 let mut map = map.lock().unwrap();
20580 let f = map.entry(li).or_insert_with(|| {
20581 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
20582 });
20583 let mut bytes = Vec::with_capacity(g.len() * 2);
20584 for v in g {
20585 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
20586 }
20587 let _ = f.write_all(&bytes);
20588}
20589
20590fn oracle_topk() -> usize {
20596 if exact_mtp_calibration_active() {
20597 return 0;
20598 }
20599 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20600 *K.get_or_init(|| {
20601 std::env::var("CMF_FFN_ORACLE_TOPK")
20602 .ok()
20603 .and_then(|v| v.parse().ok())
20604 .unwrap_or(0)
20605 })
20606}
20607
20608fn gate_topk() -> usize {
20614 if exact_mtp_calibration_active() {
20615 return 0;
20616 }
20617 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20618 *K.get_or_init(|| {
20619 std::env::var("CMF_FFN_GATE_TOPK")
20620 .ok()
20621 .and_then(|v| v.parse().ok())
20622 .unwrap_or(0)
20623 })
20624}
20625
20626fn gate_block() -> usize {
20633 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20634 *B.get_or_init(|| {
20635 std::env::var("CMF_FFN_GATE_BLOCK")
20636 .ok()
20637 .and_then(|v| v.parse().ok())
20638 .unwrap_or(1)
20639 })
20640}
20641
20642fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
20644 let n = g.len();
20645 let nb = n.div_ceil(block);
20646 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
20647 if kb >= nb {
20648 return;
20649 }
20650 let mut score: Vec<f32> = (0..nb)
20651 .map(|b| {
20652 g[b * block..((b + 1) * block).min(n)]
20653 .iter()
20654 .map(|v| v * v)
20655 .sum::<f32>()
20656 })
20657 .collect();
20658 let mut ord = score.clone();
20659 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
20660 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20661 });
20662 let thr = *kth;
20663 for b in 0..nb {
20664 if score[b] < thr {
20665 g[b * block..((b + 1) * block).min(n)].fill(0.0);
20666 }
20667 }
20668 score.clear();
20669}
20670
20671fn keep_top_k(g: &mut [f32], k: usize) {
20673 if gate_block() > 1 {
20674 return keep_top_blocks(g, k, gate_block());
20675 }
20676 let n = g.len();
20677 if k == 0 || k >= n {
20678 return;
20679 }
20680 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
20681 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20682 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20683 });
20684 let thr = *kth;
20685 for v in g.iter_mut() {
20686 if v.abs() < thr {
20687 *v = 0.0;
20688 }
20689 }
20690}
20691
20692fn probe_sq() -> bool {
20696 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20697 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
20698}
20699
20700fn probe_signed() -> bool {
20704 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20705 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
20706}
20707
20708fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
20716 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
20717 M.get_or_init(|| {
20718 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
20719 let b = std::fs::read(&p).ok()?;
20720 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
20721 let vals: Vec<f32> = b[8..]
20722 .chunks_exact(4)
20723 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
20724 .collect();
20725 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
20726 Some((inter, vals))
20727 })
20728 .as_ref()
20729}
20730
20731fn probe_topk() -> usize {
20734 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20735 *K.get_or_init(|| {
20736 std::env::var("CMF_FFN_PROBE_TOPK")
20737 .ok()
20738 .and_then(|v| v.parse().ok())
20739 .unwrap_or(0)
20740 })
20741}
20742
20743thread_local! {
20744 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
20747 const { std::cell::RefCell::new(None) };
20748}
20749
20750fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
20763 if d.gate_proj.has_prism_contract()
20768 || d.up_proj.has_prism_contract()
20769 || d.down_proj.has_prism_contract()
20770 {
20771 return None;
20772 }
20773 let dt = d.down_t.as_ref()?;
20774 let inter = d.gate_proj.rows();
20775 let hidden = dt.cols();
20776 if k == 0 || k >= inter || d.act != Act::Silu {
20777 return None;
20778 }
20779 DYN_SCRATCH.with(|sc| {
20780 let mut sc = sc.borrow_mut();
20781 let DynScratch {
20782 g,
20783 mag,
20784 live,
20785 parts,
20786 } = &mut *sc;
20787 g.resize(inter, 0.0);
20788 d.gate_proj.matvec(x, g, pool);
20789 for v in g.iter_mut() {
20790 *v = inference::silu(*v);
20791 }
20792 mag.clear();
20795 mag.extend(g.iter().map(|v| v.abs()));
20796 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20797 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20798 });
20799 let thr = *kth;
20800 live.clear();
20801 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
20802 let mut out = vec![0.0f32; hidden];
20803 match pool {
20804 Some(p) if live.len() >= 64 => {
20805 let nw = p.n_workers() + 1;
20806 parts.clear();
20807 parts.resize(nw * hidden, 0.0);
20808 let ptr = SendMut(parts.as_mut_ptr());
20809 let n = live.len();
20810 let live_ref: &[u32] = live;
20811 let g_ref: &[f32] = g;
20812 p.run(&|w, workers| {
20813 let chunk = n.div_ceil(workers);
20814 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
20815 if s >= e {
20816 return;
20817 }
20818 WORKER_SCRATCH.with(|ws| {
20819 let mut ws = ws.borrow_mut();
20820 let [scratch, acc] = &mut *ws;
20821 scratch.resize(hidden.max(x.len()), 0.0);
20822 acc.clear();
20823 acc.resize(hidden, 0.0);
20824 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
20825 if let Some(&nx) = live_ref[s..e].get(o + 1) {
20828 d.up_proj.prefetch_row(nx as usize);
20829 dt.prefetch_row(nx as usize);
20830 }
20831 let idx = nrm as usize;
20832 let up = d.up_proj.row_dot(idx, x, scratch);
20833 let a = g_ref[idx] * up;
20834 if a != 0.0 {
20835 dt.add_row_scaled(idx, a, acc, scratch);
20836 }
20837 }
20838 for (j, v) in acc.iter().enumerate() {
20839 unsafe { *ptr.at(w * hidden + j) = *v };
20840 }
20841 });
20842 });
20843 for w in 0..nw {
20844 for (j, o) in out.iter_mut().enumerate() {
20845 *o += parts[w * hidden + j];
20846 }
20847 }
20848 }
20849 _ => {
20850 WORKER_SCRATCH.with(|ws| {
20851 let mut ws = ws.borrow_mut();
20852 let [scratch, _acc] = &mut *ws;
20853 scratch.resize(hidden.max(x.len()), 0.0);
20854 for &nrm in live.iter() {
20855 let idx = nrm as usize;
20856 let up = d.up_proj.row_dot(idx, x, scratch);
20857 let a = g[idx] * up;
20858 if a != 0.0 {
20859 dt.add_row_scaled(idx, a, &mut out, scratch);
20860 }
20861 }
20862 });
20863 }
20864 }
20865 Some(out)
20866 })
20867}
20868
20869struct DynScratch {
20872 g: Vec<f32>,
20873 mag: Vec<f32>,
20874 live: Vec<u32>,
20875 parts: Vec<f32>,
20876}
20877
20878thread_local! {
20879 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
20880 std::cell::RefCell::new(DynScratch {
20881 g: Vec::new(),
20882 mag: Vec::new(),
20883 live: Vec::new(),
20884 parts: Vec::new(),
20885 })
20886 };
20887 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
20889 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
20890}
20891
20892fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
20897 let inter = d.gate_proj.rows();
20898 FFN_SCRATCH.with(|s| {
20899 let mut s = s.borrow_mut();
20900 let [g, u, ..] = &mut *s;
20901 g.resize(inter, 0.0);
20902 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
20903 } else {
20905 u.resize(inter, 0.0);
20906 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20907 for i in 0..inter {
20908 g[i] = d.act.combine(g[i], u[i]);
20909 }
20910 }
20911 zero_masked_cols(g, 1, inter, mask_row);
20912 let mut out = attention::take_buf(d.down_proj.rows());
20913 d.down_proj.matvec(g, &mut out, pool);
20914 out
20915 })
20916}
20917
20918fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
20924 if d.gate_proj.has_prism_contract()
20925 || d.up_proj.has_prism_contract()
20926 || d.down_proj.has_prism_contract()
20927 {
20928 return None;
20929 }
20930 d.act.moe_job_graph_contract()?;
20933 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
20936 return None;
20937 }
20938 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
20939 let mut model_ref = None;
20940 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
20941 let model = model_ref?;
20942 let hidden = jobs[0].down.1;
20943 let mut out = attention::take_buf(hidden);
20944 if crate::gpu::moe_block(&model, &jobs, &mut out) {
20945 Some(out)
20946 } else {
20947 let mut out = out;
20948 attention::recycle_buf(&mut out);
20949 None
20950 }
20951}
20952
20953#[allow(clippy::type_complexity)]
20958#[allow(clippy::type_complexity)]
20959pub(crate) fn moe_parts(
20960 t: &QTensor,
20961) -> Option<(
20962 &std::sync::Arc<cortiq_core::CmfModel>,
20963 usize,
20964 usize,
20965 usize,
20966 &[f32],
20967 &[f32],
20968 bool,
20969 bool,
20970 bool,
20971)> {
20972 match t {
20973 QTensor::Mapped {
20974 model,
20975 idx,
20976 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
20977 rows,
20978 cols,
20979 row_scale,
20980 col_field,
20981 ..
20982 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
20983 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
20984 )),
20985 QTensor::Mapped {
20987 model,
20988 idx,
20989 dtype: cortiq_core::TensorDtype::Q1,
20990 rows,
20991 cols,
20992 ..
20993 } => Some((
20994 model,
20995 *idx,
20996 *rows,
20997 *cols,
20998 &[][..],
20999 &[][..],
21000 true,
21001 false,
21002 false,
21003 )),
21004 QTensor::Mapped {
21006 model,
21007 idx,
21008 dtype: cortiq_core::TensorDtype::Q4Tiled,
21009 rows,
21010 cols,
21011 ..
21012 } => Some((
21013 model,
21014 *idx,
21015 *rows,
21016 *cols,
21017 &[][..],
21018 &[][..],
21019 false,
21020 true,
21021 false,
21022 )),
21023 QTensor::Mapped {
21025 model,
21026 idx,
21027 dtype: cortiq_core::TensorDtype::Q4TiledP,
21028 rows,
21029 cols,
21030 ..
21031 } => Some((
21032 model,
21033 *idx,
21034 *rows,
21035 *cols,
21036 &[][..],
21037 &[][..],
21038 false,
21039 true,
21040 false,
21041 )),
21042 QTensor::Mapped {
21046 model,
21047 idx,
21048 dtype: cortiq_core::TensorDtype::Q2TiledP,
21049 rows,
21050 cols,
21051 ..
21052 } => Some((
21053 model,
21054 *idx,
21055 *rows,
21056 *cols,
21057 &[][..],
21058 &[][..],
21059 false,
21060 true,
21061 true,
21062 )),
21063 _ => None,
21064 }
21065}
21066
21067#[cfg(target_os = "macos")]
21074#[allow(clippy::type_complexity)]
21075fn chunk_moe_parts(
21076 m: &MoeFfn,
21077 hidden: usize,
21078) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
21079 if m.shared.is_some()
21080 || m.per_expert_scale.is_some()
21081 || m.resonance.is_some()
21082 || m.route_tau.is_some()
21083 || m.router_input_norm
21084 || m.experts.is_empty()
21085 || m.top_k == 0
21086 || std::env::var("CMF_RMS_TRACE").is_ok()
21087 || std::env::var("CMF_ACT_DUMP").is_ok()
21088 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
21089 {
21090 return None;
21091 }
21092 let (rf, rr, rc) = m.router.f32_parts()?;
21093 if rr != m.experts.len() || rc != hidden {
21094 return None;
21095 }
21096 let inter = m.experts[0].gate_proj.rows();
21097 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
21100 let experts = m
21101 .experts
21102 .iter()
21103 .map(|e| {
21104 if e.act != Act::Silu
21105 || !e.segs.is_empty()
21106 || e.gate_proj.rows() != inter
21107 || e.up_proj.rows() != inter
21108 || e.down_proj.cols() != inter
21109 || e.gate_proj.cols() != hidden
21110 || e.down_proj.rows() != hidden
21111 {
21112 return None;
21113 }
21114 if q8 {
21115 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21116 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21117 }
21118 Some((
21119 e.gate_proj.mapped_q4tp()?.1,
21120 e.up_proj.mapped_q4tp()?.1,
21121 e.down_proj.mapped_q4tp()?.1,
21122 ))
21123 })
21124 .collect::<Option<Vec<_>>>()?;
21125 Some((rf, experts, q8))
21126}
21127
21128#[cfg(target_os = "macos")]
21141fn metal_flint_moe_graph_parts(
21142 m: &MoeFfn,
21143 hidden: usize,
21144 scratch_key: usize,
21145) -> Option<crate::gpu::GpuFlintMoe<'_>> {
21146 use std::sync::Arc;
21147
21148 const HIDDEN: usize = 2_048;
21149 const ROUTED_INTER: usize = 512;
21150 const SHARED_INTER: usize = 1_024;
21151 const EXPERTS: usize = 256;
21152 const TOP_K: usize = 6;
21153
21154 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
21155 return None;
21156 }
21157 let routed0 = m.experts.first()?;
21158 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
21159 let (router, router_rows, router_cols) = m.router.f32_parts()?;
21160 if router_rows != EXPERTS || router_cols != HIDDEN {
21161 return None;
21162 }
21163 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
21164 return None;
21165 }
21166 let same_q2 = |t: &QTensor| -> Option<usize> {
21167 let (owner, idx) = t.mapped_q2tp()?;
21168 Arc::ptr_eq(&owner, &model).then_some(idx)
21169 };
21170 let same_q4 = |t: &QTensor| -> Option<usize> {
21171 let (owner, idx) = t.mapped_q4tp()?;
21172 Arc::ptr_eq(&owner, &model).then_some(idx)
21173 };
21174 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21175 if e.act != Act::Silu
21176 || !e.segs.is_empty()
21177 || e.gate_proj.rows() != ROUTED_INTER
21178 || e.gate_proj.cols() != HIDDEN
21179 || e.up_proj.rows() != ROUTED_INTER
21180 || e.up_proj.cols() != HIDDEN
21181 || e.down_proj.rows() != HIDDEN
21182 || e.down_proj.cols() != ROUTED_INTER
21183 {
21184 return None;
21185 }
21186 Some((
21187 same_q2(&e.gate_proj)?,
21188 same_q2(&e.up_proj)?,
21189 same_q4(&e.down_proj)?,
21190 ))
21191 };
21192 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21193 if e.act != Act::Silu
21194 || !e.segs.is_empty()
21195 || e.gate_proj.rows() != SHARED_INTER
21196 || e.gate_proj.cols() != HIDDEN
21197 || e.up_proj.rows() != SHARED_INTER
21198 || e.up_proj.cols() != HIDDEN
21199 || e.down_proj.rows() != HIDDEN
21200 || e.down_proj.cols() != SHARED_INTER
21201 {
21202 return None;
21203 }
21204 Some((
21205 same_q4(&e.gate_proj)?,
21206 same_q4(&e.up_proj)?,
21207 same_q4(&e.down_proj)?,
21208 ))
21209 };
21210 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
21211 if experts.len() != EXPERTS {
21212 return None;
21213 }
21214 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
21215 if shared_gate.is_some() {
21216 return None;
21217 }
21218 Some(crate::gpu::GpuFlintMoe {
21219 router,
21220 bias: m.expert_bias.as_deref(),
21221 experts,
21222 scratch_key,
21223 shared: shared(shared_ffn)?,
21224 top_k: m.top_k,
21225 norm_topk: m.norm_topk_prob,
21226 route_scale: m.routed_scaling,
21227 sigmoid: m.router_sigmoid,
21228 sigmoid_denom_eps: m.sigmoid_denom_eps,
21229 })
21230}
21231
21232#[cfg(target_os = "macos")]
21233fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
21234 if m.router_input_norm
21235 || m.route_tau.is_some()
21236 || m.mask.is_some()
21237 || m.per_expert_scale.is_some()
21238 || m.experts.is_empty()
21239 || m.top_k == 0
21240 || m.resonance.is_some()
21241 {
21242 return None;
21243 }
21244 let (sh, sg) = match &m.shared {
21249 Some((sh, sg)) => (Some(sh), sg.as_ref()),
21250 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
21251 None => return None,
21252 };
21253 let (rf, rr, rc) = m.router.f32_parts()?;
21254 if rr != m.experts.len() || rc != hidden {
21255 return None;
21256 }
21257 let shared_gated = sg.is_some();
21258 let sf = match sg {
21259 Some(sg) => {
21260 let (sf, sr, sc) = sg.f32_parts()?;
21261 if sr * sc != hidden {
21262 return None;
21263 }
21264 sf
21265 }
21266 None => &rf[..hidden],
21269 };
21270 if let Some(b) = &m.expert_bias {
21271 if b.len() != m.experts.len() {
21272 return None;
21273 }
21274 }
21275 let inter = m.experts[0].gate_proj.rows();
21276 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
21279 let q8 = crate::gpu_metal::moe_q8::enabled()
21283 && m.shared.is_none()
21284 && m.experts[0].gate_proj.q8_2f_parts().is_some();
21285 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21286 if e.act != Act::Silu
21287 || e.gate_proj.rows() != inter
21288 || e.gate_proj.cols() != hidden
21289 || e.up_proj.rows() != inter
21290 || e.up_proj.cols() != hidden
21291 || e.down_proj.rows() != hidden
21292 || e.down_proj.cols() != inter
21293 {
21294 return None;
21295 }
21296 if q8 {
21297 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21298 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21299 }
21300 let pick = |t: &QTensor| -> Option<usize> {
21301 if gu_q2 {
21302 t.mapped_q2tp().map(|(_, i)| i)
21303 } else {
21304 t.mapped_q4tp().map(|(_, i)| i)
21305 }
21306 };
21307 Some((
21308 pick(&e.gate_proj)?,
21309 pick(&e.up_proj)?,
21310 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
21311 ))
21312 };
21313 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
21314 let shared = match sh {
21315 Some(sh) => Some(trio(sh)?),
21316 None => None,
21317 };
21318 Some(crate::gpu::GpuMoe {
21319 router: rf,
21320 sgate: sf,
21321 experts,
21322 shared,
21323 n_exp: m.experts.len(),
21324 top_k: m.top_k,
21325 inter,
21326 norm_topk: m.norm_topk_prob,
21327 route_scale: m.routed_scaling,
21328 gu_q2,
21329 q8,
21330 sigmoid: m.router_sigmoid,
21331 sigmoid_denom_eps: m.sigmoid_denom_eps,
21332 bias: m.expert_bias.as_deref(),
21333 shared_gated,
21334 })
21335}
21336
21337fn forced_ids_at(step: usize) -> Option<u32> {
21343 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
21344 IDS.get_or_init(|| {
21345 std::env::var("CMF_FORCE_IDS")
21346 .ok()
21347 .and_then(|p| std::fs::read_to_string(p).ok())
21348 .map(|s| {
21349 s.split_whitespace()
21350 .filter_map(|t| t.parse().ok())
21351 .collect()
21352 })
21353 .unwrap_or_default()
21354 })
21355 .get(step)
21356 .copied()
21357}
21358
21359pub(crate) fn moe_push_job_parts<'a>(
21363 gate: &'a QTensor,
21364 up: &'a QTensor,
21365 down: &'a QTensor,
21366 x: &[f32],
21367 w: f32,
21368 swiglu_limit: f32,
21369 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21370 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21371) -> Option<()> {
21372 use crate::qtensor::prescale;
21373 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
21374 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
21375 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
21376 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21377 return None; }
21379 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
21382 return None;
21383 }
21384 if !gq2 && dq2 {
21385 return None;
21386 }
21387 model_ref.get_or_insert_with(|| gm.clone());
21388 let dt = |cf: &[f32]| {
21389 if cf.is_empty() {
21390 cortiq_core::TensorDtype::Q8Row
21391 } else {
21392 cortiq_core::TensorDtype::Q8_2f
21393 }
21394 };
21395 jobs.push(crate::gpu::MoeJob {
21396 gate: (gi, gr, gc, grs),
21397 up: (ui, ur, uc, urs),
21398 down: (di, dr, dc, drs),
21399 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
21400 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
21401 down_col: dcf,
21402 w,
21403 q1: gq1,
21404 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
21405 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
21406 gu_q2: gq2,
21407 swiglu_limit,
21408 act: 0,
21409 });
21410 Some(())
21411}
21412
21413fn moe_push_job<'a>(
21415 d: &'a DenseFfn,
21416 x: &[f32],
21417 w: f32,
21418 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21419 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21420) -> Option<()> {
21421 use crate::qtensor::prescale;
21422 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
21427 let act = graph_act.code();
21428 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
21429 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
21430 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
21431 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21432 return None; }
21434 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
21435 return None;
21436 }
21437 if !gq2 && dq2 {
21438 return None;
21439 }
21440 model_ref.get_or_insert_with(|| gm.clone());
21441 let gdt = if gcf.is_empty() {
21442 cortiq_core::TensorDtype::Q8Row
21443 } else {
21444 cortiq_core::TensorDtype::Q8_2f
21445 };
21446 let udt = if ucf.is_empty() {
21447 cortiq_core::TensorDtype::Q8Row
21448 } else {
21449 cortiq_core::TensorDtype::Q8_2f
21450 };
21451 jobs.push(crate::gpu::MoeJob {
21452 gate: (gi, gr, gc, grs),
21453 up: (ui, ur, uc, urs),
21454 down: (di, dr, dc, drs),
21455 xs_gate: prescale(x, gcf, gdt).into_owned(),
21456 xs_up: prescale(x, ucf, udt).into_owned(),
21457 down_col: dcf,
21458 w,
21459 q1: gq1,
21460 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
21461 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
21462 gu_q2: gq2,
21463 swiglu_limit,
21464 act,
21465 });
21466 Some(())
21467}
21468
21469fn sparse_ffn_quant(
21476 d: &DenseFfn,
21477 x: &[f32],
21478 active: &[u16],
21479 hidden: usize,
21480 pool: Option<&Pool>,
21481) -> Vec<f32> {
21482 let n = active.len();
21483 let inter = d.gate_proj.rows();
21484 let mut act = vec![0.0f32; n];
21485 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
21488 let compute = |ai: usize| -> f32 {
21489 let idx = active[ai] as usize;
21490 if idx >= inter {
21491 return 0.0; }
21493 let mut s = if need_scratch {
21494 vec![0.0f32; hidden]
21495 } else {
21496 Vec::new()
21497 };
21498 let gate = d.gate_proj.row_dot(idx, x, &mut s);
21499 let up = d.up_proj.row_dot(idx, x, &mut s);
21500 d.act.combine(gate, up)
21501 };
21502 match pool {
21503 Some(p) if n >= 256 => {
21504 let ptr = SendMut(act.as_mut_ptr());
21505 p.run(&|widx, nw| {
21506 let chunk = n.div_ceil(nw);
21507 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
21508 for ai in s..e {
21509 unsafe { *ptr.at(ai) = compute(ai) };
21510 }
21511 });
21512 }
21513 _ => {
21514 for (ai, a) in act.iter_mut().enumerate() {
21515 *a = compute(ai);
21516 }
21517 }
21518 }
21519 let mut out = vec![0.0f32; hidden];
21521 for (ai, &idx) in active.iter().enumerate() {
21522 let w = act[ai];
21523 if w.abs() >= 1e-12 && (idx as usize) < inter {
21524 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
21525 }
21526 }
21527 out
21528}
21529
21530#[doc(hidden)]
21532pub fn sparse_ffn_quant_for_test(
21533 d: &DenseFfn,
21534 x: &[f32],
21535 active: &[u16],
21536 hidden: usize,
21537) -> Vec<f32> {
21538 sparse_ffn_quant(d, x, active, hidden, None)
21539}
21540
21541fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
21545 let deq = |t: &QTensor| -> Vec<f32> {
21546 let (rows, cols) = (t.rows(), t.cols());
21547 let mut out = vec![0.0f32; rows * cols];
21548 for r in 0..rows {
21549 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
21550 }
21551 out
21552 };
21553 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
21554}
21555
21556struct SendMut(*mut f32);
21558unsafe impl Send for SendMut {}
21559unsafe impl Sync for SendMut {}
21560impl SendMut {
21561 #[inline]
21562 #[allow(clippy::mut_from_ref)]
21565 unsafe fn at(&self, i: usize) -> &mut f32 {
21566 unsafe { &mut *self.0.add(i) }
21567 }
21568}
21569
21570pub(crate) fn moe_route(
21582 logits: &[f32],
21583 m: &MoeFfn,
21584 allowed: Option<&[bool]>,
21585) -> (Vec<usize>, Vec<f32>, f32) {
21586 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
21587}
21588
21589pub(crate) fn moe_route_with_eps(
21599 logits: &[f32],
21600 m: &MoeFfn,
21601 allowed: Option<&[bool]>,
21602 sigmoid_denom_eps: f32,
21603) -> (Vec<usize>, Vec<f32>, f32) {
21604 let ne = logits.len();
21605 let admit = |e: usize| {
21611 m.mask.as_ref().is_none_or(|mk| mk[e])
21612 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
21613 };
21614 if m.resonance.is_some() && m.top_k == 1 {
21627 let mut best: Option<usize> = None;
21628 for e in (0..ne).filter(|&e| admit(e)) {
21629 let l = logits[e];
21630 if l == f32::NEG_INFINITY || l.is_nan() {
21631 continue;
21632 }
21633 if best.is_none_or(|b| l > logits[b]) {
21634 best = Some(e);
21635 }
21636 }
21637 if let Some(b) = best {
21638 let mut p = vec![0.0f32; ne];
21639 p[b] = 1.0;
21640 return (vec![b], p, 1.0 / m.routed_scaling);
21641 }
21642 }
21643 let p: Vec<f32> = if m.router_sigmoid {
21649 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
21650 } else {
21651 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
21652 if mx == f32::NEG_INFINITY {
21653 vec![1.0 / ne.max(1) as f32; ne]
21654 } else {
21655 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
21656 let s: f32 = e.iter().sum();
21657 for v in &mut e {
21658 *v /= s;
21659 }
21660 e
21661 }
21662 };
21663 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
21664 match &m.expert_bias {
21666 Some(b) => idx.sort_unstable_by(|&x, &y| {
21667 (p[y] + b[y])
21668 .partial_cmp(&(p[x] + b[x]))
21669 .unwrap()
21670 .then(x.cmp(&y))
21671 }),
21672 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
21673 }
21674 idx.truncate(m.top_k);
21675 if let Some(tau) = m.route_tau {
21679 let total: f32 = idx.iter().map(|&e| p[e]).sum();
21680 if total > 0.0 {
21681 let mut acc = 0.0f32;
21682 let mut keep = idx.len();
21683 for (i, &e) in idx.iter().enumerate() {
21684 acc += p[e];
21685 if acc >= tau * total {
21686 keep = i + 1;
21687 break;
21688 }
21689 }
21690 idx.truncate(keep);
21691 }
21692 }
21693 let wsum: f32 = if m.norm_topk_prob {
21694 let s: f32 = idx.iter().map(|&e| p[e]).sum();
21695 (if m.router_sigmoid {
21699 s + sigmoid_denom_eps
21700 } else {
21701 s
21702 }) / m.routed_scaling
21703 } else {
21704 1.0 / m.routed_scaling
21705 };
21706 (idx, p, wsum)
21707}
21708
21709fn moe_trace(idx: &[usize]) {
21711 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
21712}
21713
21714pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
21717 use std::io::Write;
21718 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
21719 std::sync::OnceLock::new();
21720 let Some(f) = F.get_or_init(|| {
21721 let p = std::env::var("CMF_MOE_TRACE").ok()?;
21722 Some(std::sync::Mutex::new(
21723 std::fs::OpenOptions::new()
21724 .create(true)
21725 .append(true)
21726 .open(p)
21727 .ok()?,
21728 ))
21729 }) else {
21730 return;
21731 };
21732 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
21733 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
21734}
21735
21736pub(crate) fn moe_ffn(
21739 m: &MoeFfn,
21740 x: &[f32],
21741 pool: Option<&Pool>,
21742 allowed: Option<&[bool]>,
21743) -> Vec<f32> {
21744 let r = moe_ffn_route(m, x, pool, allowed);
21745 moe_ffn_experts(m, x, &r, pool)
21746}
21747
21748pub(crate) struct MoeRoute {
21752 pub idx: Vec<usize>,
21753 pub p: Vec<f32>,
21754 pub wsum: f32,
21755 pub logits: Vec<f32>,
21756}
21757
21758pub(crate) fn moe_ffn_route(
21764 m: &MoeFfn,
21765 x: &[f32],
21766 pool: Option<&Pool>,
21767 allowed: Option<&[bool]>,
21768) -> MoeRoute {
21769 accumulate_act(m, x, 1);
21770 let ne = m.experts.len();
21771 let mut logits = vec![0.0f32; ne];
21772 match &m.resonance {
21773 Some(r) => r.scores(x, &mut logits),
21774 None => m.router.matvec(x, &mut logits, pool),
21775 }
21776 let (idx, p, wsum) = moe_route(&logits, m, allowed);
21777 {
21778 let mut st = m.stats.borrow_mut();
21779 if st.len() < ne {
21780 st.resize(ne, 0);
21781 }
21782 for &e in &idx {
21783 st[e] += 1;
21784 }
21785 }
21786 moe_trace(&idx);
21792 MoeRoute {
21793 idx,
21794 p,
21795 wsum,
21796 logits,
21797 }
21798}
21799
21800#[cfg(target_os = "macos")]
21809fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
21810 const HIDDEN: usize = 2_048;
21811 const ROUTED_INTER: usize = 512;
21812 const SHARED_INTER: usize = 1_024;
21813 const EXPERTS: usize = 256;
21814 const TOP_K: usize = 6;
21815
21816 let Some(routed) = m.experts.first() else {
21817 return false;
21818 };
21819 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
21820 return false;
21821 };
21822 let Some((shared, shared_gate)) = &m.shared else {
21823 return false;
21824 };
21825 if shared_gate.is_some()
21826 || model.header.arch.arch_name != "flint"
21827 || model.header.arch.prism_hadamard.is_some()
21828 || model.header.arch.hidden_size != HIDDEN
21829 || m.experts.len() != EXPERTS
21830 || m.top_k != TOP_K
21831 || !m.router_sigmoid
21832 || !m.norm_topk_prob
21833 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
21834 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
21835 || m.route_tau.is_some()
21836 || m.mask.is_some()
21837 || m.per_expert_scale.is_some()
21838 || m.router_input_norm
21839 || m.resonance.is_some()
21840 || !m.grown.is_empty()
21841 {
21842 return false;
21843 }
21844
21845 let routed_q2 = routed.act == Act::Silu
21846 && routed.segs.is_empty()
21847 && routed.gate_proj.rows() == ROUTED_INTER
21848 && routed.gate_proj.cols() == HIDDEN
21849 && routed.up_proj.rows() == ROUTED_INTER
21850 && routed.up_proj.cols() == HIDDEN
21851 && routed.down_proj.rows() == HIDDEN
21852 && routed.down_proj.cols() == ROUTED_INTER
21853 && routed.up_proj.mapped_q2tp().is_some()
21854 && routed.down_proj.mapped_q4tp().is_some();
21855 let shared_q4 = shared.act == Act::Silu
21856 && shared.segs.is_empty()
21857 && shared.gate_proj.rows() == SHARED_INTER
21858 && shared.gate_proj.cols() == HIDDEN
21859 && shared.up_proj.rows() == SHARED_INTER
21860 && shared.up_proj.cols() == HIDDEN
21861 && shared.down_proj.rows() == HIDDEN
21862 && shared.down_proj.cols() == SHARED_INTER
21863 && shared.gate_proj.mapped_q4tp().is_some()
21864 && shared.up_proj.mapped_q4tp().is_some()
21865 && shared.down_proj.mapped_q4tp().is_some();
21866 routed_q2 && shared_q4
21867}
21868
21869pub(crate) fn moe_ffn_experts(
21872 m: &MoeFfn,
21873 x: &[f32],
21874 r: &MoeRoute,
21875 pool: Option<&Pool>,
21876) -> Vec<f32> {
21877 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
21878 if crate::gpu::enabled_here() {
21883 #[cfg(target_os = "macos")]
21884 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
21885 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
21891 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
21892 });
21893 }
21894 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
21895 crate::gpu::ProbeArm::Gpu => {
21896 let t0 = std::time::Instant::now();
21897 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
21898 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
21899 return out;
21900 }
21901 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
21902 }
21903 crate::gpu::ProbeArm::CpuTimed => {
21904 let t0 = std::time::Instant::now();
21905 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
21906 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
21907 return out;
21908 }
21909 crate::gpu::ProbeArm::Cpu => {
21910 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
21911 }
21912 }
21913 }
21914 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
21915}
21916
21917fn moe_ffn_minicpm_tail(
21921 slot: &mut crate::minicpm_tail::MiniCpmTail,
21922 li: usize,
21923 total_layers: usize,
21924 m: &MoeFfn,
21925 x: &[f32],
21926 pool: Option<&Pool>,
21927) -> Vec<f32> {
21928 let route = moe_ffn_route(m, x, pool, None);
21929 match slot.forward(li, m, x, &route, pool, total_layers) {
21930 Some(out) => out,
21931 None => moe_ffn_experts(m, x, &route, pool),
21936 }
21937}
21938
21939fn moe_ffn_banked(
21943 slot: &mut crate::mimo_moe::Slot,
21944 li: usize,
21945 m: &MoeFfn,
21946 x: &[f32],
21947 pool: Option<&Pool>,
21948) -> Vec<f32> {
21949 let t0 = std::time::Instant::now();
21950 let r = moe_ffn_route(m, x, pool, None);
21951 slot.note_route(t0.elapsed().as_nanos() as u64);
21952 match slot.forward(li, m, x, &r, pool) {
21953 Some(out) => out,
21954 None => crate::qtensor::float_activations_scope(|| {
21955 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
21956 }),
21957 }
21958}
21959
21960fn moe_ffn_banked_rows(
21962 slot: &mut crate::mimo_moe::Slot,
21963 li: usize,
21964 m: &MoeFfn,
21965 xs: &[f32],
21966 b: usize,
21967 hidden: usize,
21968 pool: Option<&Pool>,
21969) -> Vec<f32> {
21970 let t0 = std::time::Instant::now();
21971 let routes: Vec<_> = xs
21972 .chunks_exact(hidden)
21973 .map(|x| moe_ffn_route(m, x, pool, None))
21974 .collect();
21975 slot.note_route(t0.elapsed().as_nanos() as u64);
21976 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
21977 return out;
21978 }
21979 let mut out = Vec::with_capacity(b * hidden);
21980 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
21981 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
21982 crate::qtensor::float_activations_scope(|| {
21984 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
21985 })
21986 });
21987 out.extend(row);
21988 }
21989 out
21990}
21991
21992fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
21997 use std::sync::atomic::{AtomicBool, Ordering};
21998 if built {
21999 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
22000 if total_layers > 0 && layers_run < total_layers {
22001 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
22002 } else {
22003 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
22004 }
22005 } else {
22006 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
22007 }
22008 static SAID: AtomicBool = AtomicBool::new(false);
22009 if !SAID.swap(true, Ordering::Relaxed) {
22010 if built {
22011 tracing::info!("wgpu whole-token graph: ACTIVE");
22012 } else {
22013 tracing::warn!("wgpu whole-token graph refused — per-op path");
22014 }
22015 }
22016}
22017
22018pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22022pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22023pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22027pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22029
22030pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22034pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22035pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
22036 std::sync::atomic::AtomicU64::new(0);
22037pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22038pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22039pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
22043 std::sync::atomic::AtomicU64::new(0);
22044pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22045pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
22046 std::sync::atomic::AtomicU64::new(0);
22047pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
22048 std::sync::atomic::AtomicU64::new(0);
22049
22050fn moe_batch_enabled() -> bool {
22053 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
22054 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
22055}
22056
22057fn moe_ffn_cpu_batched(
22063 m: &MoeFfn,
22064 x: &[f32],
22065 idx: &[usize],
22066 p: &[f32],
22067 wsum: f32,
22068 pool: Option<&Pool>,
22069) -> Option<Vec<f32>> {
22070 if idx.is_empty() || !moe_batch_enabled() {
22071 return None;
22072 }
22073 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
22077 return None;
22078 }
22079 let n = idx.len() + usize::from(m.shared.is_some());
22080 let mut pairs = Vec::with_capacity(n);
22081 let mut downs = Vec::with_capacity(n);
22082 let mut ws = Vec::with_capacity(n);
22083 for &e in idx {
22084 let d = &m.experts[e];
22085 if d.act != Act::Silu {
22086 return None;
22087 }
22088 pairs.push((&d.gate_proj, &d.up_proj));
22089 downs.push(&d.down_proj);
22090 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
22091 }
22092 if let Some((se, gate)) = &m.shared {
22095 if se.act != Act::Silu {
22096 return None;
22097 }
22098 let g = gate.as_ref().map_or(1.0, |gate| {
22099 let mut gl = [0.0f32; 1];
22100 gate.matvec(x, &mut gl, pool);
22101 1.0 / (1.0 + (-gl[0]).exp())
22102 });
22103 pairs.push((&se.gate_proj, &se.up_proj));
22104 downs.push(&se.down_proj);
22105 ws.push(g);
22106 }
22107 let inter = pairs[0].0.rows();
22108 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
22109 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
22110 return None;
22111 }
22112 let mut out = attention::take_buf(x.len());
22113 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
22114 attention::recycle_buf(&mut out);
22115 return None;
22116 }
22117 Some(out)
22118}
22119
22120pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22128pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22129pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22130pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22131pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22132
22133pub(crate) fn moe_cold_experts_cpu(
22134 experts: &[(&DenseFfn, f32)],
22135 x: &[f32],
22136 pool: Option<&Pool>,
22137) -> Vec<f32> {
22138 let mut out = attention::take_buf(x.len());
22139 if experts.is_empty() {
22140 return out;
22141 }
22142 let pairs: Vec<_> = experts
22143 .iter()
22144 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
22145 .collect();
22146 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
22147 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
22148 let inter = experts[0].0.gate_proj.rows();
22149 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
22150 let silu_only = experts
22158 .iter()
22159 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
22160 if silu_only
22161 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
22162 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
22163 {
22164 return out;
22165 }
22166 out.fill(0.0);
22167 for &(expert, weight) in experts {
22168 let mut one = dense_ffn(expert, x, pool);
22169 for (o, v) in out.iter_mut().zip(&one) {
22170 *o += weight * v;
22171 }
22172 attention::recycle_buf(&mut one);
22173 }
22174 out
22175}
22176
22177pub(crate) fn moe_cold_experts_rows_cpu(
22181 jobs: &[Vec<(&DenseFfn, f32)>],
22182 xs: &[f32],
22183 hidden: usize,
22184 pool: Option<&Pool>,
22185) -> Vec<f32> {
22186 let mut out = vec![0.0; xs.len()];
22187 let mut experts: Vec<&DenseFfn> = Vec::new();
22188 let mut groups: Vec<Vec<usize>> = Vec::new();
22189 let mut terms = vec![Vec::new(); jobs.len()];
22190 for (r, row) in jobs.iter().enumerate() {
22191 for &(e, w) in row {
22192 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
22193 Some(g) => g,
22194 None => {
22195 experts.push(e);
22196 groups.push(Vec::new());
22197 groups.len() - 1
22198 }
22199 };
22200 terms[r].push((g, groups[g].len(), w));
22201 groups[g].push(r);
22202 }
22203 }
22204 if experts.is_empty() {
22205 return out;
22206 }
22207 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
22208 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
22209 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
22210 let count: usize = lens.iter().sum();
22211 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
22212 let mut ds = vec![vec![0.0; hidden]; count];
22213 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
22214 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
22215 {
22216 let mut offset = 0;
22217 let offsets: Vec<_> = lens
22218 .iter()
22219 .map(|&n| {
22220 let start = offset;
22221 offset += n;
22222 start
22223 })
22224 .collect();
22225 for (r, terms) in terms.iter().enumerate() {
22226 for &(g, slot, w) in terms {
22227 for (o, &v) in out[r * hidden..(r + 1) * hidden]
22228 .iter_mut()
22229 .zip(&ds[offsets[g] + slot])
22230 {
22231 *o += w * v;
22232 }
22233 }
22234 }
22235 } else {
22236 for (r, jobs) in jobs.iter().enumerate() {
22237 if !jobs.is_empty() {
22238 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
22239 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
22240 attention::recycle_buf(&mut row);
22241 }
22242 }
22243 }
22244 out
22245}
22246
22247fn moe_ffn_cpu(
22249 m: &MoeFfn,
22250 x: &[f32],
22251 idx: &[usize],
22252 p: &[f32],
22253 wsum: f32,
22254 pool: Option<&Pool>,
22255) -> Vec<f32> {
22256 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
22257 return out;
22258 }
22259 let mut out = attention::take_buf(x.len());
22260 for &e in idx {
22261 let mut eo = dense_ffn(&m.experts[e], x, pool);
22262 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
22263 for i in 0..out.len() {
22264 out[i] += w * eo[i];
22265 }
22266 attention::recycle_buf(&mut eo);
22267 }
22268 if let Some((se, gate)) = &m.shared {
22269 let mut so = dense_ffn(se, x, pool);
22270 let g = gate.as_ref().map_or(1.0, |gate| {
22271 let mut gl = [0.0f32; 1];
22272 gate.matvec(x, &mut gl, pool);
22273 1.0 / (1.0 + (-gl[0]).exp())
22274 });
22275 for i in 0..out.len() {
22276 out[i] += g * so[i];
22277 }
22278 attention::recycle_buf(&mut so);
22279 }
22280 out
22281}
22282
22283#[allow(clippy::too_many_arguments)]
22291pub(crate) fn mla_attention(
22292 w: &MlaWeights,
22293 normed: &[f32],
22294 cache: &mut crate::kv_cache::LayerKvCache,
22295 position: usize,
22296 inv_freq: &[f32],
22297 rope_scale: f32,
22298 eps: f64,
22299 pool: Option<&Pool>,
22300) -> Vec<f32> {
22301 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
22302 let hd = dr + dn;
22303 let mut q = vec![0.0f32; nh * hd];
22304 match (&w.q_a, &w.q_a_norm) {
22305 (Some(qa), Some(qn)) => {
22306 let mut t = vec![0.0f32; qa.rows()];
22307 qa.matvec(normed, &mut t, pool);
22308 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
22309 w.q_proj.matvec(&tn, &mut q, pool);
22310 }
22311 _ => w.q_proj.matvec(normed, &mut q, pool),
22312 }
22313 let mut ca = vec![0.0f32; lora + dr];
22314 w.kv_a.matvec(normed, &mut ca, pool);
22315 let (c_lat, k_rope) = ca.split_at_mut(lora);
22316 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
22317 let mut kvb = vec![0.0f32; nh * (dn + dv)];
22318 w.kv_b.matvec(&latn, &mut kvb, pool);
22319 if !w.nope {
22320 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
22321 }
22322 for h in 0..nh {
22323 if !w.nope {
22324 attention::rope_rotate_scaled(
22325 &mut q[h * hd..h * hd + dr],
22326 position,
22327 inv_freq,
22328 rope_scale,
22329 );
22330 }
22331 }
22332 let mut k = vec![0.0f32; nh * hd];
22333 let mut v = vec![0.0f32; nh * hd];
22334 for h in 0..nh {
22335 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
22336 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
22337 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
22338 }
22339 cache.append(&k, &v, &vec![true; nh]);
22340 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
22341 attention::recycle_buf(&mut imp);
22342 let mut ov = vec![0.0f32; nh * dv];
22343 for h in 0..nh {
22344 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
22345 }
22346 let mut out = vec![0.0f32; w.o_proj.rows()];
22347 w.o_proj.matvec(&ov, &mut out, pool);
22348 out
22349}
22350
22351fn dense_moe_ffn(
22358 dm: &DenseMoeFfn,
22359 x_normed: &[f32],
22360 h_raw: &[f32],
22361 eps: f64,
22362 norm_style: NormStyle,
22363 pool: Option<&Pool>,
22364) -> Vec<f32> {
22365 let mut d = dense_ffn(&dm.dense, x_normed, pool);
22366 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
22367 let m = &dm.moe;
22368 let ne = m.experts.len();
22369 let mut logits = vec![0.0f32; ne];
22370 if m.router_input_norm {
22371 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
22372 let inv = 1.0 / (ss + eps as f32).sqrt();
22373 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
22374 m.router.matvec(&xr, &mut logits, pool);
22375 } else {
22376 m.router.matvec(h_raw, &mut logits, pool);
22377 }
22378 let (idx, p, wsum) = moe_route(&logits, m, None);
22379 {
22380 let mut st = m.stats.borrow_mut();
22381 if st.len() < ne {
22382 st.resize(ne, 0);
22383 }
22384 for &e in &idx {
22385 st[e] += 1;
22386 }
22387 }
22388 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
22389 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
22390 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
22391 for (di, mi) in d.iter_mut().zip(&mo) {
22392 *di += mi;
22393 }
22394 d
22395}
22396
22397fn moe_gpu_refused(why: &'static str) {
22404 use std::sync::atomic::{AtomicBool, Ordering};
22405 static SAID: AtomicBool = AtomicBool::new(false);
22406 if !SAID.swap(true, Ordering::Relaxed) {
22407 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
22408 }
22409}
22410
22411fn moe_ffn_gpu(
22412 m: &MoeFfn,
22413 x: &[f32],
22414 idx: &[usize],
22415 p: &[f32],
22416 wsum: f32,
22417 pool: Option<&Pool>,
22418) -> Option<Vec<f32>> {
22419 use crate::gpu::MoeJob;
22420
22421 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
22422 let mut model_ref = None;
22423 for &e in idx {
22424 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
22425 moe_gpu_refused("push_job(expert)");
22426 return None;
22427 }
22428 }
22429 if let Some((se, gate)) = &m.shared {
22430 let g = gate.as_ref().map_or(1.0, |gate| {
22431 let mut gl = [0.0f32; 1];
22432 gate.matvec(x, &mut gl, pool);
22433 1.0 / (1.0 + (-gl[0]).exp())
22434 });
22435 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
22436 moe_gpu_refused("push_job(shared)");
22437 return None;
22438 }
22439 }
22440 let Some(model) = model_ref else {
22441 moe_gpu_refused("no model_ref");
22442 return None;
22443 };
22444 let hidden = jobs[0].down.1;
22445 let mut out = vec![0.0f32; hidden];
22446 if crate::gpu::moe_block(&model, &jobs, &mut out) {
22447 Some(out)
22448 } else {
22449 moe_gpu_refused("gpu::moe_block");
22450 None
22451 }
22452}
22453
22454fn ffn_forward(
22456 ffn: &FfnKind,
22457 x: &[f32],
22458 pool: Option<&Pool>,
22459 experts_allowed: Option<&[bool]>,
22460) -> Vec<f32> {
22461 match ffn {
22462 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
22463 FfnKind::Dense(d) => dense_ffn(d, x, pool),
22464 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
22465 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22469 }
22470}
22471
22472fn ffn_forward_pair(
22476 ffn: &FfnKind,
22477 x1: &[f32],
22478 x2: &[f32],
22479 pool: Option<&Pool>,
22480 experts_allowed: Option<&[bool]>,
22481) -> (Vec<f32>, Vec<f32>) {
22482 let d = match ffn {
22483 FfnKind::Dense(d) if !d.segs.is_empty() => {
22486 return (
22487 tube_ffn(d, x1, 1, pool, None),
22488 tube_ffn(d, x2, 1, pool, None),
22489 );
22490 }
22491 FfnKind::Dense(d) => d,
22492 FfnKind::Moe(m) => {
22493 return (
22494 moe_ffn(m, x1, pool, experts_allowed),
22495 moe_ffn(m, x2, pool, experts_allowed),
22496 );
22497 }
22498 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22499 };
22500 let inter = d.gate_proj.rows();
22501 FFN_SCRATCH.with(|s| {
22502 let mut s = s.borrow_mut();
22503 let [g1, g2, u1, u2] = &mut *s;
22504 g1.resize(inter, 0.0);
22505 g2.resize(inter, 0.0);
22506 u1.resize(inter, 0.0);
22507 u2.resize(inter, 0.0);
22508 QTensor::matvec2_many(
22511 [&d.gate_proj, &d.up_proj],
22512 x1,
22513 x2,
22514 [g1.as_mut_slice(), u1.as_mut_slice()],
22515 [g2.as_mut_slice(), u2.as_mut_slice()],
22516 pool,
22517 );
22518 for i in 0..inter {
22519 g1[i] = d.act.combine(g1[i], u1[i]);
22520 g2[i] = d.act.combine(g2[i], u2[i]);
22521 }
22522 let mut o1 = attention::take_buf(d.down_proj.rows());
22523 let mut o2 = attention::take_buf(d.down_proj.rows());
22524 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
22525 (o1, o2)
22526 })
22527}
22528
22529#[cfg(test)]
22530mod tests {
22531 use std::ffi::OsString;
22532 use std::sync::Mutex;
22533
22534 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
22537
22538 struct ScopedTestEnv {
22539 previous: Vec<(&'static str, Option<OsString>)>,
22540 }
22541
22542 impl ScopedTestEnv {
22543 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
22544 let previous = vars
22545 .iter()
22546 .map(|(name, _)| (*name, std::env::var_os(name)))
22547 .collect();
22548 for &(name, value) in vars {
22549 unsafe {
22550 match value {
22551 Some(value) => std::env::set_var(name, value),
22552 None => std::env::remove_var(name),
22553 }
22554 }
22555 }
22556 Self { previous }
22557 }
22558 }
22559
22560 impl Drop for ScopedTestEnv {
22561 fn drop(&mut self) {
22562 for (name, value) in self.previous.drain(..).rev() {
22563 unsafe {
22564 match value {
22565 Some(value) => std::env::set_var(name, value),
22566 None => std::env::remove_var(name),
22567 }
22568 }
22569 }
22570 }
22571 }
22572
22573 #[test]
22574 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
22575 use super::pad_mtp_draft_logits;
22576
22577 let mut logits = vec![-3.0, -1.0, -2.0];
22578 pad_mtp_draft_logits(&mut logits, 6);
22579 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
22580 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
22581 assert_eq!(
22582 logits
22583 .iter()
22584 .enumerate()
22585 .max_by(|(_, a), (_, b)| a.total_cmp(b))
22586 .map(|(i, _)| i),
22587 Some(1),
22588 "an unevaluated row cannot win an all-negative shortlist"
22589 );
22590 }
22591
22592 #[test]
22593 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
22594 use super::{Pipeline, SpecMon, SpecTrial};
22595
22596 let mut mon = SpecMon::default();
22597 let start = std::time::Instant::now();
22598 mon.round(1.0, 1);
22599 let first = Pipeline::spec_trial_round(
22600 SpecTrial::Spec {
22601 t0: start,
22602 gen0: 0,
22603 rounds: 0,
22604 },
22605 &mut mon,
22606 1,
22607 );
22608 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
22609
22610 mon.round(1.0, 1);
22611 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
22612 assert!(
22613 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
22614 "two verifier-tip-only rounds must stop the losing MTP trial"
22615 );
22616
22617 let mut viable = SpecMon::default();
22620 viable.round(1.0, 1);
22621 viable.round(1.0, 2);
22622 let continued = Pipeline::spec_trial_round(
22623 SpecTrial::Spec {
22624 t0: start,
22625 gen0: 0,
22626 rounds: 1,
22627 },
22628 &mut viable,
22629 3,
22630 );
22631 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
22632 }
22633
22634 #[test]
22635 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
22636 let mut p = spark_test_pipeline(None);
22637 let head_rows = 248_070;
22638 p.draft_full_streak = 7;
22639 assert_eq!(p.draft_head_rows(head_rows), head_rows);
22640
22641 p.reset_fresh_mtp_draft_state();
22642 assert_eq!(
22643 p.draft_head_rows(head_rows),
22644 super::Pipeline::draft_vocab_rows(head_rows),
22645 "a fresh request returns to its configured shortlist"
22646 );
22647
22648 p.note_draft_id(u32::MAX);
22651 assert_eq!(p.draft_head_rows(head_rows), head_rows);
22652 }
22653
22654 #[test]
22655 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
22656 use super::graph_spec_wanted_by_policy;
22657
22658 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
22662 assert!(!graph_spec_wanted_by_policy(
22663 Some("anything-but-1"),
22664 true,
22665 false,
22666 false,
22667 true,
22668 ));
22669 assert!(graph_spec_wanted_by_policy(
22670 Some("1"),
22671 false,
22672 true,
22673 false,
22674 true,
22675 ));
22676 assert!(!graph_spec_wanted_by_policy(
22677 Some("0"),
22678 true,
22679 false,
22680 false,
22681 true,
22682 ));
22683
22684 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
22687 assert!(graph_spec_wanted_by_policy(
22688 Some("legacy-force"),
22689 false,
22690 true,
22691 true,
22692 false,
22693 ));
22694 }
22695
22696 #[test]
22697 fn encoder_hidden_states_are_normalized_and_reusable() {
22698 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
22699 let ids = [1, 7, 11, 19];
22700
22701 let first = p.encode_hidden_states(&ids).expect("encoder forward");
22702 let second = p
22703 .encode_hidden_states(&ids)
22704 .expect("reusable encoder forward");
22705 assert_eq!(first.len(), ids.len() * 8);
22706 assert!(first.iter().all(|v| v.is_finite()));
22707 assert_eq!(
22708 first, second,
22709 "encoder forward must start from a fresh state"
22710 );
22711
22712 assert!(p.forward_ids(&ids, None).is_ok());
22715 }
22716
22717 #[test]
22722 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
22723 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
22724 let dense_card = ChunkStackFacts {
22725 plain_dense: true,
22726 discrete: true,
22727 gpu_on: true,
22728 ..Default::default()
22729 };
22730 assert!(dense_card.dense_on_discrete());
22731 assert_eq!(
22733 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
22734 DISCRETE_DENSE_PREFILL_CHUNK
22735 );
22736 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
22737 for (label, facts) in [
22738 (
22739 "GDN hybrid / MoE / DeepSeek stack",
22740 ChunkStackFacts {
22741 plain_dense: false,
22742 ..dense_card
22743 },
22744 ),
22745 (
22746 "integrated GPU",
22747 ChunkStackFacts {
22748 discrete: false,
22749 ..dense_card
22750 },
22751 ),
22752 (
22753 "CPU only",
22754 ChunkStackFacts {
22755 gpu_on: false,
22756 discrete: false,
22757 ..dense_card
22758 },
22759 ),
22760 (
22761 "capacity split",
22762 ChunkStackFacts {
22763 capacity_split: true,
22764 ..dense_card
22765 },
22766 ),
22767 (
22768 "multi-GPU plan",
22769 ChunkStackFacts {
22770 multi_gpu: true,
22771 ..dense_card
22772 },
22773 ),
22774 (
22775 "O(1) layers",
22776 ChunkStackFacts {
22777 o1: true,
22778 ..dense_card
22779 },
22780 ),
22781 ] {
22782 assert!(!facts.dense_on_discrete(), "{label}");
22783 assert_eq!(
22784 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
22785 48,
22786 "{label} keeps the historical x86 chunk"
22787 );
22788 }
22789 for dense in [false, true] {
22791 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
22792 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
22793 }
22794 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
22796 for dense in [false, true] {
22797 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
22798 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
22799 }
22800 }
22801 }
22802
22803 #[test]
22804 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
22805 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
22806 let full = |host_rows, device_rows| ReuseLayer {
22807 full: true,
22808 host_rows,
22809 device_rows,
22810 device_state: false,
22811 };
22812 assert_eq!(
22815 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
22816 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
22817 );
22818 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
22820 assert_eq!(
22822 kv_reuse_plan(339, &[full(339, Some(345))]),
22823 ReusePlan::Ready
22824 );
22825 assert_eq!(
22827 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
22828 ReusePlan::Pull(vec![(0, 300, 339)])
22829 );
22830 assert_eq!(
22832 kv_reuse_plan(339, &[full(300, Some(320))]),
22833 ReusePlan::Fresh
22834 );
22835 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
22836 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
22837 let conv = |device_state| ReuseLayer {
22840 full: false,
22841 host_rows: 0,
22842 device_rows: None,
22843 device_state,
22844 };
22845 assert_eq!(
22846 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
22847 ReusePlan::Fresh
22848 );
22849 assert_eq!(
22850 kv_reuse_plan(339, &[conv(false), full(339, None)]),
22851 ReusePlan::Ready
22852 );
22853 }
22854
22855 #[test]
22856 fn nll_graph_policy_scopes_only_the_fused_head() {
22857 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
22858 ("vulkan graph", true, true, false, true, false),
22860 ("native Metal graph", true, true, true, true, true),
22862 ("masked", false, true, false, false, false),
22864 ("graph disabled", true, false, true, false, false),
22865 ] {
22866 let (graph_quality, graph_head_required) =
22867 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
22868 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
22869 assert_eq!(graph_head_required, want_head, "{label}: fused head");
22870 }
22871 }
22872
22873 #[test]
22874 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
22875 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
22876 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
22877 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
22878 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
22879 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
22880 }
22881
22882 #[test]
22883 fn cancel_flag_stops_generation() {
22884 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
22885 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22888 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
22889 assert_eq!(r.finish_reason, "cancelled");
22890 assert!(
22891 r.token_ids.is_empty(),
22892 "no tokens after cancel: {:?}",
22893 r.token_ids
22894 );
22895 assert_eq!(p.kv_cache.seq_len(), 0);
22896 assert!(p.kv_history.is_empty());
22897 assert!(!p.graph_want_logits);
22898 assert!(p.graph_logits.is_none());
22899 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
22901 assert_ne!(r2.finish_reason, "cancelled");
22902 }
22903 use super::*;
22904
22905 #[test]
22906 fn minimax_m3_arch_selects_checked_oai_activation_only() {
22907 let arch = |arch_name: &str, hidden_act: &str| {
22911 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
22912 "arch_name": arch_name,
22913 "hidden_size": 8,
22914 "intermediate_size": 16,
22915 "num_layers": 1,
22916 "num_attention_heads": 1,
22917 "num_kv_heads": 1,
22918 "head_dim": 8,
22919 "vocab_size": 16,
22920 "layer_types": ["FullAttention"],
22921 "rms_norm_eps": 1e-6,
22922 "max_position_embeddings": 16,
22923 "hidden_act": hidden_act
22924 }))
22925 .expect("minimal architecture")
22926 };
22927 let minimax = arch("minimax_m3_vl", "swigluoai");
22928 assert_eq!(
22929 Act::from_arch_full(&minimax),
22930 Act::SwiGluOai {
22931 alpha: 1.702,
22932 limit: 7.0,
22933 }
22934 );
22935
22936 let unrelated = arch("qwen2", "swigluoai");
22939 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
22940
22941 let non_oai = arch("minimax_m3_vl", "gelu");
22945 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
22946 }
22947
22948 #[test]
22949 fn minimax_swiglu_oai_act_matches_scalar_contract() {
22950 let act = Act::swiglu_oai(1.702, 7.0);
22951 let got = act.combine(10.0, -10.0);
22952 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
22953 assert!((got - expected).abs() < 1e-6);
22954 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
22955 assert_eq!(
22956 act.wgpu_graph_act(),
22957 Some(crate::gpu::GraphAct::SwiGluOaiM3),
22958 "the WGPU-only arm accepts only the frozen M3 constants"
22959 );
22960 assert_eq!(
22961 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
22962 None,
22963 "nearby OAI constants must not borrow MiniMax's kernel"
22964 );
22965 assert_eq!(
22966 act.moe_job_graph_contract(),
22967 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
22968 "the direct MoE path carries the frozen clamp with the marker"
22969 );
22970 assert_eq!(
22971 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
22972 None,
22973 "a nearby clamp must not borrow MiniMax's device contract"
22974 );
22975
22976 let gate_only = act.apply(-10.0);
22979 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
22980 assert!((gate_only - expected_gate).abs() < 1e-10);
22981 }
22982
22983 #[test]
22984 fn explicit_sliding_schedule_selects_local_rope_profile() {
22985 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
22990 p.swa = Some((1024, usize::MAX));
22991 p.sliding_layers = Some(vec![true, true, true, false]);
22992 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
22993 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
22994 p.rope_scale = 1.277_258_9;
22995 p.rope_scale_local = 1.0;
22996
22997 for li in 0..3 {
22998 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
22999 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
23000 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
23001 }
23002 assert_eq!(p.layer_window(3), None);
23003 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
23004 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
23005 }
23006
23007 #[test]
23015 fn dynamic_ffn_equals_the_zeroing_arm() {
23016 let (hidden, inter) = (8usize, 32usize);
23017 let synth = |n: usize, salt: usize| -> Vec<f32> {
23018 (0..n)
23019 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
23020 .collect()
23021 };
23022 let down = synth(hidden * inter, 3);
23023 let mut down_t = vec![0.0f32; inter * hidden];
23024 for r in 0..hidden {
23025 for c in 0..inter {
23026 down_t[c * hidden + r] = down[r * inter + c];
23027 }
23028 }
23029 let d = DenseFfn {
23030 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23031 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23032 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
23033 act: Act::Silu,
23034 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
23035 segs: Vec::new(),
23036 };
23037 let x = synth(hidden, 11);
23038 let k = 12usize;
23039 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
23040 let mut g = vec![0.0f32; inter];
23042 d.gate_proj.matvec(&x, &mut g, None);
23043 let mut u = vec![0.0f32; inter];
23044 d.up_proj.matvec(&x, &mut u, None);
23045 for v in g.iter_mut() {
23046 *v = inference::silu(*v);
23047 }
23048 keep_top_k(&mut g, k);
23049 for i in 0..inter {
23050 g[i] *= u[i];
23051 }
23052 let mut want = vec![0.0f32; hidden];
23053 d.down_proj.matvec(&g, &mut want, None);
23054 for (a, b) in want.iter().zip(&got) {
23055 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
23056 }
23057 }
23058
23059 #[test]
23065 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
23066 let (hidden, core, tube) = (8usize, 12usize, 8usize);
23067 let inter = core + tube;
23068 let synth = |n: usize, salt: usize| -> Vec<f32> {
23069 (0..n)
23070 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
23071 .collect()
23072 };
23073 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
23074 let d_all = synth(hidden * inter, 3);
23075 let dense = DenseFfn {
23077 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
23078 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
23079 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
23080 act: Act::Silu,
23081 down_t: None,
23082 segs: Vec::new(),
23083 };
23084 let rows =
23085 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
23086 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
23087 let mut o = Vec::with_capacity(hidden * (b - a));
23088 for r in 0..hidden {
23089 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
23090 }
23091 o
23092 };
23093 let tubed = DenseFfn {
23094 down_t: None,
23095 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
23096 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
23097 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
23098 act: Act::Silu,
23099 segs: vec![FfnSeg {
23100 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
23101 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
23102 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
23103 start: core,
23104 width: tube,
23105 }],
23106 };
23107 let x = synth(hidden, 7);
23108 let want = dense_ffn(&dense, &x, None);
23109 let got = tube_ffn(&tubed, &x, 1, None, None);
23110 for (a, b) in want.iter().zip(&got) {
23111 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
23112 }
23113 let mut bits = vec![0u8; inter.div_ceil(8)];
23115 for n in 0..core {
23116 bits[n / 8] |= 1 << (n % 8);
23117 }
23118 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23119 let masked = dense_ffn_masked(&dense, &x, None, &bits);
23120 for (a, b) in masked.iter().zip(&closed) {
23121 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
23122 }
23123 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23125 for (a, b) in closed.iter().zip(&batch) {
23126 assert_eq!(a, b, "batch arm disagrees with decode arm");
23127 }
23128 }
23129
23130 #[test]
23132 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
23133 let (hidden, inter) = (16usize, 40usize);
23134 let synth = |n: usize, salt: usize| -> Vec<f32> {
23135 (0..n)
23136 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
23137 .collect()
23138 };
23139 let d = DenseFfn {
23140 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23141 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23142 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
23143 act: Act::Silu,
23144 down_t: None,
23145 segs: Vec::new(),
23146 };
23147 let x = synth(hidden, 9);
23148 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
23150
23151 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
23152
23153 let mut g = vec![0.0f32; inter];
23155 d.gate_proj.matvec(&x, &mut g, None);
23156 let mut u = vec![0.0f32; inter];
23157 d.up_proj.matvec(&x, &mut u, None);
23158 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
23159 for i in 0..inter {
23160 g[i] = if act_set.contains(&(i as u16)) {
23161 inference::silu(g[i]) * u[i]
23162 } else {
23163 0.0
23164 };
23165 }
23166 let mut reference = vec![0.0f32; hidden];
23167 d.down_proj.matvec(&g, &mut reference, None);
23168
23169 let max_d = sparse
23170 .iter()
23171 .zip(&reference)
23172 .map(|(a, b)| (a - b).abs())
23173 .fold(0.0f32, f32::max);
23174 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
23175 }
23176
23177 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
23180 let (h, inter, heads, kv, hd) = (
23181 p.hidden_size,
23182 inter,
23183 p.num_heads,
23184 p.num_kv_heads,
23185 p.head_dim,
23186 );
23187 let synth = |n: usize, salt: usize| -> Vec<f32> {
23188 (0..n)
23189 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
23190 .collect()
23191 };
23192 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
23193 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23194 };
23195 p.mtp = Some(MtpModule {
23196 enorm: vec![1.0; h],
23197 hnorm: vec![1.0; h],
23198 eh_proj: qt(h, 2 * h, 301),
23199 layer: LayerWeights {
23200 input_norm: vec![1.0; h],
23201 post_norm: vec![1.0; h],
23202 attn_out_norm: None,
23203 ffn_out_norm: None,
23204 layer_scale: None,
23205 ffn: FfnKind::Dense(DenseFfn {
23206 gate_proj: qt(inter, h, 315),
23207 up_proj: qt(inter, h, 316),
23208 down_proj: qt(h, inter, 317),
23209 act: Act::Silu,
23210 down_t: None,
23211 segs: Vec::new(),
23212 }),
23213 attn: AttnKind::Full {
23214 bias: None,
23215 wq: qt(heads * hd, h, 311),
23216 wk: qt(kv * hd, h, 312),
23217 wv: qt(kv * hd, h, 313),
23218 wo: qt(h, heads * hd, 314),
23219 q_norm: None,
23220 k_norm: None,
23221 output_gate: false,
23222 softplus_gate: None,
23223 },
23224 },
23225 final_norm: vec![1.0; h],
23226 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
23227 });
23228 }
23229
23230 fn attach_test_mtp(p: &mut Pipeline) {
23232 attach_test_mtp_width(p, p.intermediate_size);
23233 }
23234
23235 #[test]
23236 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
23237 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
23238 attach_test_mtp_width(&mut p, 11);
23241 let ids = [3, 9, 4, 12, 7, 2];
23242 let trunk = p.probe_ffn_mass(&ids);
23243 assert_eq!(trunk[0].len(), p.intermediate_size);
23244
23245 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
23246 assert_eq!(got.len(), 1);
23247 assert_eq!(got[0].mass.len(), 11);
23248 assert_eq!(got[0].squared_mass.len(), 11);
23249 assert_eq!(got[0].observed_positions, ids.len() - 1);
23250 assert!(got[0].mass.iter().any(|&v| v > 0.0));
23251 assert!(got[0]
23252 .mass
23253 .iter()
23254 .zip(&got[0].squared_mass)
23255 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23256 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
23257 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
23258
23259 let h = p.hidden_size;
23263 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
23264 panic!("synthetic MTP must be dense")
23265 };
23266 dense.segs.push(FfnSeg {
23267 gate: QTensor::from_f32(vec![0.0; h], 1, h),
23268 up: QTensor::from_f32(vec![0.0; h], 1, h),
23269 down: QTensor::from_f32(vec![0.0; h], h, 1),
23270 start: dense.gate_proj.rows(),
23271 width: 1,
23272 });
23273 let err = p.probe_mtp_ffn(&ids);
23274 assert!(err
23275 .unwrap_err()
23276 .contains("tube-segmented draft FFNs are not supported"));
23277 assert_eq!(p.kv_cache.seq_len(), 0);
23278 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
23279 }
23280
23281 #[test]
23282 fn speculative_equals_vanilla_greedy() {
23283 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23284 let _env = ScopedTestEnv::set(&[
23287 ("CMF_GPU", Some("0")),
23288 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23289 ("CMF_MTP_PAIR", Some("1")),
23290 ]);
23291 let run = |spec: bool| {
23292 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23293 p.sampler_config.temperature = 0.0;
23294 attach_test_mtp(&mut p);
23295 p.speculative = spec;
23296 let r = p.generate("abcdef", 12, None, None).unwrap();
23297 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23298 };
23299 let (vanilla, d0, _) = run(false);
23300 let (spec, d1, a1) = run(true);
23301 assert_eq!(d0, 0, "vanilla path must not draft");
23302 assert!(d1 > 0, "speculative path must draft");
23303 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
23304 assert_eq!(
23305 vanilla, spec,
23306 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
23307 );
23308 }
23309
23310 #[test]
23311 fn speculative_accepts_constant_oracle() {
23312 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23313 let _env = ScopedTestEnv::set(&[
23314 ("CMF_GPU", Some("0")),
23315 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23316 ("CMF_MTP_PAIR", Some("1")),
23317 ]);
23318 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23319 p.sampler_config.temperature = 0.0;
23320 p.sampler_config.repetition_penalty = 1.0;
23321 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
23324 attach_test_mtp(&mut p);
23325 p.speculative = true;
23326 let r = p.generate("abcd", 10, None, None).unwrap();
23327 assert!(r.mtp_drafted > 0);
23328 assert_eq!(
23329 r.mtp_accepted, r.mtp_drafted,
23330 "constant logits → every draft accepted"
23331 );
23332 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
23335 }
23336
23337 #[test]
23338 fn host_fused_mtp_pair_is_opt_in() {
23339 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23340 let _env = ScopedTestEnv::set(&[
23341 ("CMF_GPU", Some("0")),
23342 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23343 ("CMF_MTP_PAIR", None),
23344 ]);
23345 let run = |spec: bool| {
23346 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23347 p.sampler_config.temperature = 0.0;
23348 attach_test_mtp(&mut p);
23349 p.speculative = spec;
23350 let r = p.generate("abcdef", 12, None, None).unwrap();
23351 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23352 };
23353 let (plain, d0, _) = run(false);
23354 let (guarded, d1, a1) = run(true);
23355 assert_eq!(d0, 0);
23356 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
23357 assert_eq!(a1, 0);
23358 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
23359 }
23360
23361 #[test]
23362 fn empty_prompt_is_an_error_not_a_panic() {
23363 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23364 let r = p.generate("", 4, None, None);
23365 assert!(r.is_err(), "empty prompt must be a clean error");
23366 }
23367
23368 #[test]
23369 fn every_token_enters_kv_exactly_once() {
23370 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23371 p.sampler_config.temperature = 0.0;
23373 let r = p.generate("abc", 2, None, None).unwrap();
23374 assert_eq!(r.prompt_tokens, 3);
23375 assert_eq!(
23379 p.kv_cache.seq_len(),
23380 3 + r.tokens_generated - 1,
23381 "each token must be cached exactly once (v1 cached the last prompt token twice)"
23382 );
23383 }
23384
23385 #[test]
23386 fn generation_is_reproducible_with_seed() {
23387 let run = || {
23388 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23389 p.generate("hello", 8, None, None).unwrap().token_ids
23390 };
23391 assert_eq!(run(), run());
23392 }
23393
23394 #[test]
23395 fn resetting_sampler_restarts_the_seeded_stream() {
23396 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23397 let config = SamplerConfig {
23398 seed: Some(1234),
23399 ..SamplerConfig::default()
23400 };
23401 p.set_sampler_config(config.clone());
23402 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
23403 p.set_sampler_config(config);
23404 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
23405 assert_eq!(first, second);
23406 }
23407
23408 #[test]
23409 fn eviction_bounds_the_cache() {
23410 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23411 p.kv_cache.max_seq_len = 6;
23412 p.sampler_config.temperature = 0.0;
23413 let _ = p.generate("abcd", 12, None, None).unwrap();
23414 assert!(
23415 p.kv_cache.seq_len() <= 6 + 1,
23416 "cache must stay bounded by max_seq_len (got {})",
23417 p.kv_cache.seq_len()
23418 );
23419 }
23420
23421 #[test]
23422 fn confidence_matches_tokens_and_is_a_probability() {
23423 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23424 p.sampler_config.temperature = 0.0;
23425 p.sampler_config.repetition_penalty = 1.0;
23426 let r = p.generate("abcd", 10, None, None).unwrap();
23427 assert_eq!(
23428 r.token_confidence.len(),
23429 r.token_ids.len(),
23430 "one confidence per emitted token"
23431 );
23432 for &c in &r.token_confidence {
23433 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
23434 }
23435 let logits = [1.0f32, 3.0, 0.5, 3.0];
23437 let p0 = top1_prob_t(&logits, 1, 1.0);
23438 let p1 = top1_prob_t(&logits, 3, 1.0);
23439 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
23440 assert!(p0 > 0.0 && p0 < 1.0);
23441 let sharp = top1_prob_t(&logits, 1, 1.0);
23443 let soft = top1_prob_t(&logits, 1, 2.0);
23444 assert!(soft < sharp, "higher temperature lowers peak confidence");
23445 }
23446
23447 #[test]
23448 fn trace_is_opt_in_and_parallels_the_output() {
23449 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23451 p.sampler_config.temperature = 0.0;
23452 p.sampler_config.repetition_penalty = 1.0;
23453 let r = p.generate("abcd", 10, None, None).unwrap();
23454 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
23455
23456 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23458 p.sampler_config.temperature = 0.0;
23459 p.sampler_config.repetition_penalty = 1.0;
23460 p.set_trace(true);
23461 let r = p.generate("abcd", 10, None, None).unwrap();
23462 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
23463 for (i, tr) in r.traces.iter().enumerate() {
23464 assert_eq!(tr.t, i, "trace index is sequential");
23465 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
23466 assert_eq!(
23467 tr.confidence, r.token_confidence[i],
23468 "trace confidence matches the confidence channel"
23469 );
23470 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
23472 }
23473 }
23474
23475 #[test]
23476 fn explain_prefill_logits_match_greedy_first_token() {
23477 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23481 p.sampler_config.temperature = 0.0;
23482 p.sampler_config.repetition_penalty = 1.0;
23483 let ids = p.tokenizer.encode("abcd");
23484 let logits = p.prefill_next_logits(&ids, None);
23485 let argmax = logits
23486 .iter()
23487 .enumerate()
23488 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
23489 .unwrap()
23490 .0 as u32;
23491 let r = p.generate("abcd", 1, None, None).unwrap();
23492 assert_eq!(
23493 argmax, r.token_ids[0],
23494 "explain preview must match greedy emit"
23495 );
23496 }
23497
23498 #[test]
23499 fn laguna_shared_expert_is_unconditionally_added() {
23500 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
23501 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
23502 let zero_dense = || DenseFfn {
23503 gate_proj: matrix(vec![0.0; 4]),
23504 up_proj: matrix(vec![0.0; 4]),
23505 down_proj: matrix(vec![0.0; 4]),
23506 act: Act::Silu,
23507 down_t: None,
23508 segs: Vec::new(),
23509 };
23510 let shared = DenseFfn {
23511 gate_proj: identity(),
23512 up_proj: identity(),
23513 down_proj: identity(),
23514 act: Act::Silu,
23515 down_t: None,
23516 segs: Vec::new(),
23517 };
23518 let x = [1.0, 2.0];
23519 let expected = dense_ffn(&shared, &x, None);
23520 let moe = MoeFfn {
23521 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
23522 experts: vec![zero_dense()],
23523 top_k: 1,
23524 norm_topk_prob: true,
23525 router_sigmoid: true,
23526 sigmoid_denom_eps: 1e-6,
23527 expert_bias: None,
23528 routed_scaling: 1.0,
23529 route_tau: None,
23530 shared: Some((shared, None)),
23531 stats: std::cell::RefCell::new(Vec::new()),
23532 act_sq: std::cell::RefCell::new(Vec::new()),
23533 act_rows: std::cell::RefCell::new(Vec::new()),
23534 mask: None,
23535 per_expert_scale: None,
23536 router_input_norm: false,
23537 resonance: None,
23538 grown: Vec::new(),
23539 };
23540 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
23541 for (actual, expected) in actual.iter().zip(expected) {
23542 assert!((actual - expected).abs() < 1e-6);
23543 }
23544 }
23545
23546 fn mimo_test_pipeline() -> Pipeline {
23555 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
23556 let kvh = [1usize, 2, 2, 1];
23557 let synth = |n: usize, salt: usize| -> Vec<f32> {
23558 (0..n)
23559 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23560 .collect()
23561 };
23562 let qt = |rows: usize, cols: usize, salt: usize| {
23563 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23564 };
23565 let dense = |inter: usize, salt: usize| DenseFfn {
23566 gate_proj: qt(inter, hs, salt),
23567 up_proj: qt(inter, hs, salt + 1),
23568 down_proj: qt(hs, inter, salt + 2),
23569 act: Act::Silu,
23570 down_t: None,
23571 segs: Vec::new(),
23572 };
23573 let layers: Vec<LayerWeights> = (0..4)
23574 .map(|li| LayerWeights {
23575 input_norm: vec![1.0; hs],
23576 post_norm: vec![1.0; hs],
23577 attn_out_norm: None,
23578 ffn_out_norm: None,
23579 layer_scale: None,
23580 ffn: if li == 0 {
23581 FfnKind::Dense(dense(inter, 50))
23582 } else {
23583 FfnKind::Moe(MoeFfn {
23584 router: qt(4, hs, 60 + li),
23585 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
23586 top_k: 2,
23587 norm_topk_prob: true,
23588 router_sigmoid: true,
23589 sigmoid_denom_eps: 1e-6,
23590 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
23591 routed_scaling: 1.0,
23592 route_tau: None,
23593 shared: None,
23594 stats: std::cell::RefCell::new(Vec::new()),
23595 act_sq: std::cell::RefCell::new(Vec::new()),
23596 act_rows: std::cell::RefCell::new(Vec::new()),
23597 mask: None,
23598 per_expert_scale: None,
23599 router_input_norm: false,
23600 resonance: None,
23601 grown: Vec::new(),
23602 })
23603 },
23604 attn: AttnKind::Full {
23605 wq: qt(nh * hd, hs, li * 10 + 1),
23606 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
23607 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
23608 wo: qt(hs, nh * vd, li * 10 + 4),
23609 q_norm: None,
23610 k_norm: None,
23611 output_gate: false,
23612 softplus_gate: None,
23613 bias: None,
23614 },
23615 })
23616 .collect();
23617 let mut p = Pipeline::new(
23618 Tokenizer::byte_level(),
23619 PipelineWeights {
23620 embed_tokens: qt(vocab, hs, 100),
23621 layers,
23622 lm_head: qt(vocab, hs, 200),
23623 final_norm: vec![1.0; hs],
23624 },
23625 hs,
23626 inter,
23627 nh,
23628 1, hd,
23630 4,
23631 4,
23632 false,
23633 vocab,
23634 1e-6,
23635 1e7,
23636 NormStyle::Qwen,
23637 4096,
23638 SamplerConfig {
23639 seed: Some(7),
23640 ..Default::default()
23641 },
23642 );
23643 p.layer_dump = None;
23645 p.set_rotary(4, 1e7);
23646 p.sliding_layers = Some(vec![false, true, true, false]);
23647 p.swa = Some((3, usize::MAX));
23648 p.rotary_dim_local = Some(4);
23649 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
23650 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
23651 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
23652 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
23653 p
23654 }
23655
23656 #[test]
23657 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
23658 let mut p = mimo_test_pipeline();
23659 p.speculative = false;
23660 p.ignore_eos = true;
23661 p.sampler_config.temperature = 0.0;
23662 p.sampler_config.repetition_penalty = 1.0;
23663 let a = vec![3, 5, 7, 9, 11, 13];
23664 let b = vec![4, 8, 12, 16, 20, 24];
23665 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
23666 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
23667 let actual = p
23671 .generate_from_embeds(&a, &rows, 8, None, None)
23672 .unwrap()
23673 .token_ids;
23674 assert_eq!(actual, expected);
23675 assert!(p.kv_history.is_empty());
23676 let mut extended = a.clone();
23677 extended.push(17);
23678 let after_media = p
23679 .generate_from_ids(&extended, 8, None, None)
23680 .unwrap()
23681 .token_ids;
23682 p.reset_session();
23683 let fresh = p
23684 .generate_from_ids(&extended, 8, None, None)
23685 .unwrap()
23686 .token_ids;
23687 assert_eq!(after_media, fresh);
23688 assert!(
23689 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
23690 .is_err()
23691 );
23692 p.reset_session();
23696 p.generate_from_ids(&a, 1, None, None).unwrap();
23697 let mut media_ids = p.kv_history.clone();
23698 assert!(!media_ids.is_empty());
23699 media_ids.extend_from_slice(&[19, 21, 23]);
23700 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
23701 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
23702 let mut oracle = mimo_test_pipeline();
23703 oracle.speculative = false;
23704 oracle.ignore_eos = true;
23705 oracle.sampler_config.temperature = 0.0;
23706 oracle.sampler_config.repetition_penalty = 1.0;
23707 let expected = oracle
23708 .generate_from_ids(&source_ids, 8, None, None)
23709 .unwrap()
23710 .token_ids;
23711 assert_eq!(
23712 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
23713 .unwrap()
23714 .token_ids,
23715 expected
23716 );
23717 assert!(p.kv_history.is_empty());
23718 let mut bad = rows;
23719 bad[0] = f32::NAN;
23720 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
23721 }
23722
23723 fn f32_bits(v: &[f32]) -> Vec<u32> {
23724 v.iter().map(|x| x.to_bits()).collect()
23725 }
23726
23727 #[test]
23734 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
23735 let mut p = mimo_test_pipeline();
23736 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
23737 assert_eq!(kv, vec![1, 2, 2, 1]);
23738 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
23739 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
23740 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
23741 let hs = p.hidden_size;
23742 let mut decode = Vec::new();
23743 for (pos, &id) in ids.iter().enumerate() {
23744 let e = p.embed_single(id);
23745 let h = p.forward_layers(&e, pos, None);
23746 decode.push(p.logits_from_hidden(&h));
23747 }
23748 for l in &p.kv_cache.layers {
23749 assert_eq!(l.seq_len, 12);
23750 assert_eq!(l.head_values(0).len(), 12 * 8);
23752 }
23753 assert!(decode.iter().flatten().all(|v| v.is_finite()));
23754
23755 p.clear_sequence_state();
23756 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23757 for pos in 0..ids.len() {
23758 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
23759 assert_eq!(
23760 f32_bits(&decode[pos]),
23761 f32_bits(&lg),
23762 "whole prompt, pos {pos}"
23763 );
23764 }
23765
23766 p.clear_sequence_state();
23767 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
23768 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
23769 for pos in 0..ids.len() {
23770 let row = if pos < 5 {
23771 &a[pos * hs..(pos + 1) * hs]
23772 } else {
23773 &b[(pos - 5) * hs..(pos - 4) * hs]
23774 };
23775 let lg = p.logits_from_hidden(row);
23776 assert_eq!(
23777 f32_bits(&decode[pos]),
23778 f32_bits(&lg),
23779 "two chunks, pos {pos}"
23780 );
23781 }
23782
23783 let last = |p: &mut Pipeline| {
23786 p.clear_sequence_state();
23787 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23788 p.logits_from_hidden(&hb[11 * hs..12 * hs])
23789 };
23790 let base = last(&mut p);
23791 let mut no_sinks = mimo_test_pipeline();
23792 for l in &mut no_sinks.kv_cache.layers {
23793 l.sinks = None;
23794 }
23795 assert_ne!(
23796 f32_bits(&last(&mut no_sinks)),
23797 f32_bits(&base),
23798 "sinks are live"
23799 );
23800 let mut wide = mimo_test_pipeline();
23801 wide.swa = Some((64, usize::MAX));
23802 assert_ne!(
23803 f32_bits(&last(&mut wide)),
23804 f32_bits(&base),
23805 "window is live"
23806 );
23807
23808 p.clear_sequence_state();
23810 p.ignore_eos = true;
23811 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
23812 assert_eq!(r.token_ids.len(), 4);
23813 }
23814
23815 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
23818 let (hs, nh) = (16usize, 4usize);
23819 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
23820 p.layer_dump = None;
23821 p.swa = Some((6, 4));
23822 p.proj_gate_sigmoid = true;
23823 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
23824 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
23825 let g: Vec<f32> = (0..nh * hs)
23826 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
23827 .collect();
23828 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
23829 }
23830 }
23831 p.swa_trim = trim;
23832 p
23833 }
23834
23835 #[test]
23841 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
23842 let mut a = spark_test_pipeline(None);
23843 let mut b = spark_test_pipeline(Some((2, 4)));
23844 assert_eq!(
23845 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
23846 vec![Some(6), Some(6), Some(6), None]
23847 );
23848 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
23849 let mut pos = 0usize;
23850 for &n in [5usize, 13, 7, 16].iter().cycle() {
23851 if pos >= ids.len() {
23852 break;
23853 }
23854 let end = (pos + n).min(ids.len());
23855 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
23856 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
23857 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
23858 pos = end;
23859 }
23860 for _ in 0..23 {
23861 let e = a.embed_single(((pos * 7) % 64) as u32);
23862 let ha = a.forward_layers(&e, pos, None);
23863 let hb = b.forward_layers(&e, pos, None);
23864 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
23865 for li in 0..3 {
23866 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
23867 }
23868 pos += 1;
23869 }
23870 for round in 0..9 {
23871 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
23872 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
23873 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
23874 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
23875 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
23876 if round % 2 == 1 {
23877 for p in [&mut a, &mut b] {
23879 for l in &mut p.kv_cache.layers {
23880 l.truncate_last(2);
23881 }
23882 }
23883 } else {
23884 pos += 2;
23885 }
23886 }
23887 let e = a.embed_single(9);
23888 let (ha, hb) = (
23889 a.forward_layers(&e, pos, None),
23890 b.forward_layers(&e, pos, None),
23891 );
23892 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
23893 for li in 0..3 {
23894 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
23895 assert!(lb.base() > 0, "layer {li} trimmed");
23896 assert_eq!(la.base(), 0);
23897 assert_eq!(lb.pos_len(), la.seq_len);
23898 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
23899 }
23900 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
23901 assert_eq!(
23902 (gb.base(), gb.seq_len),
23903 (0, ga.seq_len),
23904 "the global layer keeps all"
23905 );
23906 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
23907 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
23908 }
23909
23910 #[test]
23915 fn swa_trim_wire_handoff_continues_bitwise() {
23916 let mut a = spark_test_pipeline(None);
23917 let mut b = spark_test_pipeline(Some((2, 4)));
23918 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
23919 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
23920 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
23921 assert_eq!(f32_bits(&ha), f32_bits(&hb));
23922 let mut pos = ids.len();
23923 for _ in 0..5 {
23924 let e = a.embed_single(((pos * 5) % 64) as u32);
23925 assert_eq!(
23926 f32_bits(&a.forward_layers(&e, pos, None)),
23927 f32_bits(&b.forward_layers(&e, pos, None))
23928 );
23929 pos += 1;
23930 }
23931 let mut c = spark_test_pipeline(Some((2, 4)));
23932 for li in 0..4 {
23933 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
23934 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
23935 }
23936 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
23937 assert_eq!(c.kv_cache.seq_len(), pos);
23938 for step in 0..17 {
23939 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
23940 let ha = a.forward_layers(&e, pos, None);
23941 let hc = c.forward_layers(&e, pos, None);
23942 assert_eq!(
23943 f32_bits(&ha),
23944 f32_bits(&hc),
23945 "after the hand-off, step {step}"
23946 );
23947 pos += 1;
23948 }
23949 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
23950 }
23951
23952 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
23957 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
23958 let synth = |len: usize, salt: usize| -> Vec<f32> {
23959 (0..len)
23960 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
23961 .collect()
23962 };
23963 let qt = |rows: usize, cols: usize, salt: usize| {
23964 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23965 };
23966 let layers = widths
23967 .iter()
23968 .copied()
23969 .enumerate()
23970 .map(|(k, inter)| {
23971 let s = 500 + k * 40;
23972 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
23973 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
23974 MtpModule {
23975 enorm: vec![1.0; hs],
23976 hnorm: vec![1.0; hs],
23977 eh_proj: qt(hs, 2 * hs, s),
23978 layer: LayerWeights {
23979 input_norm: vec![1.0; hs],
23980 post_norm: vec![1.0; hs],
23981 attn_out_norm: None,
23982 ffn_out_norm: None,
23983 layer_scale: None,
23984 attn: AttnKind::Full {
23985 wq: qt(nh * hd, hs, s + 1),
23986 wk: qt(nkv * hd, hs, s + 2),
23987 wv: qt(nkv * vd, hs, s + 3),
23988 wo: qt(hs, nh * vd, s + 4),
23989 q_norm: None,
23990 k_norm: None,
23991 output_gate: false,
23992 softplus_gate: None,
23993 bias: None,
23994 },
23995 ffn: FfnKind::Dense(DenseFfn {
23996 gate_proj: qt(inter, hs, s + 5),
23997 up_proj: qt(inter, hs, s + 6),
23998 down_proj: qt(hs, inter, s + 7),
23999 act: Act::Silu,
24000 down_t: None,
24001 segs: Vec::new(),
24002 }),
24003 },
24004 final_norm: vec![1.0; hs],
24005 kv,
24006 }
24007 })
24008 .collect();
24009 mimo_mtp::MimoMtp::from_layers(layers)
24010 }
24011
24012 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
24013 mimo_test_mtp_widths(&vec![24; n], gain)
24014 }
24015
24016 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
24017 p.clear_sequence_state();
24018 p.speculative = spec;
24019 p.ignore_eos = true;
24020 p.sampler_config.temperature = 0.0;
24021 p.generate_from_ids(ids, n, None, None).unwrap()
24022 }
24023
24024 #[test]
24025 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
24026 let mut p = mimo_test_pipeline();
24027 let widths = [7usize, 13, 29];
24028 assert_ne!(widths[0], p.intermediate_size);
24029 assert_ne!(widths[1], p.intermediate_size);
24030 assert_ne!(widths[2], p.intermediate_size);
24031 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
24032 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
24033
24034 let trunk = p.probe_ffn_mass(&ids);
24035 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
24036 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
24037 assert_eq!(got.len(), widths.len());
24038 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
24039 assert_eq!(stat.mass.len(), width, "draft layer {k}");
24040 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
24041 assert_eq!(stat.observed_positions, ids.len() - k - 1);
24042 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
24043 assert!(stat
24044 .mass
24045 .iter()
24046 .zip(&stat.squared_mass)
24047 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
24048 }
24049 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
24050 let st = p.mimo_mtp.as_ref().unwrap();
24051 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
24052
24053 let h = p.hidden_size;
24056 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
24057 panic!("synthetic MiMo MTP must be dense")
24058 };
24059 dense.segs.push(FfnSeg {
24060 gate: QTensor::from_f32(vec![0.0; h], 1, h),
24061 up: QTensor::from_f32(vec![0.0; h], 1, h),
24062 down: QTensor::from_f32(vec![0.0; h], h, 1),
24063 start: dense.gate_proj.rows(),
24064 width: 1,
24065 });
24066 assert!(p
24067 .probe_mtp_ffn(&ids)
24068 .unwrap_err()
24069 .contains("tube-segmented draft FFNs are not supported"));
24070 assert_eq!(p.kv_cache.seq_len(), 0);
24071 assert!(p
24072 .mimo_mtp
24073 .as_ref()
24074 .unwrap()
24075 .layers
24076 .iter()
24077 .all(|m| m.kv.seq_len == 0));
24078 }
24079
24080 #[test]
24081 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
24082 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
24083 p.clear_sequence_state();
24084 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
24085 let _exact = ExactMtpCalibrationGuard::enter();
24086 let got = crate::gpu::cpu_scope(|| {
24087 let trunk = p.probe_trunk_rows(ids)?;
24088 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
24089 })
24090 .expect("teacher-forced MiMo probe");
24091 st.reset();
24092 p.mimo_mtp = Some(st);
24093 p.reset_mtp_probe_state();
24094 got
24095 }
24096
24097 for post_norm_hidden in [false, true] {
24098 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
24099 let make = || {
24100 let mut p = mimo_test_pipeline();
24101 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
24102 st.post_norm_hidden = post_norm_hidden;
24103 p.mimo_mtp = Some(st);
24104 p
24105 };
24106 let mut full = make();
24107 let expected = collect(&mut full, &ids, usize::MAX);
24108 for rows in [1usize, 7, 128] {
24109 let mut chunked = make();
24110 let got = collect(&mut chunked, &ids, rows);
24111 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
24112 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
24113 assert_eq!(
24114 got.observed_positions, expected.observed_positions,
24115 "post={post_norm_hidden}, rows={rows}, layer={layer}"
24116 );
24117 assert_eq!(
24118 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24119 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24120 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
24121 );
24122 assert_eq!(
24123 got.squared_mass
24124 .iter()
24125 .map(|v| v.to_bits())
24126 .collect::<Vec<_>>(),
24127 expected
24128 .squared_mass
24129 .iter()
24130 .map(|v| v.to_bits())
24131 .collect::<Vec<_>>(),
24132 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
24133 );
24134 }
24135 }
24136 }
24137 }
24138
24139 #[test]
24145 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
24146 for post in [false, true] {
24149 let mut p = mimo_test_pipeline();
24150 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
24152 let mut st0 = mimo_test_mtp(3, 1.0);
24153 st0.post_norm_hidden = post;
24154 p.mimo_mtp = Some(st0);
24155 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
24156 let hs = p.hidden_size;
24157 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24158 p.mimo_note_rows(&hb, 0);
24159 let mut st = p.mimo_mtp.take().unwrap();
24160 let k = 3;
24163 let mut inc = Vec::new();
24164 for t in 0..ids.len() - k - 1 {
24165 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
24166 }
24167 let s = ids.len();
24170 let mut reference = vec![vec![0u32; k]; s - k - 1];
24171 let mut fresh = mimo_test_mtp(3, 1.0);
24172 for (layer, m) in fresh.layers.iter_mut().enumerate() {
24173 let n = s - layer - 1;
24174 let mut cats = vec![0.0f32; n * 2 * hs];
24175 for j in 0..n {
24176 let e = p.embed_single(ids[j + layer + 1]);
24177 let raw = &hb[j * hs..(j + 1) * hs];
24178 let g = if post {
24179 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
24180 } else {
24181 raw.to_vec()
24182 };
24183 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
24184 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
24185 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
24186 }
24187 let mut x = vec![0.0f32; n * hs];
24188 m.eh_proj.matmat(&cats, n, &mut x, None);
24189 p.mimo_mtp_block(m, &mut x, n, 0);
24190 for (t, row) in reference.iter_mut().enumerate() {
24191 let y = inference::rms_norm(
24192 &x[t * hs..(t + 1) * hs],
24193 &m.final_norm,
24194 p.rms_eps,
24195 p.norm_style,
24196 );
24197 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
24198 }
24199 }
24200 assert_eq!(inc, reference, "post_norm_hidden = {post}");
24201 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
24203 assert!(distinct.len() > 3, "{inc:?}");
24204 let last_t = ids.len() - k - 2;
24206 for m in &st.layers {
24207 assert_eq!(m.kv.seq_len, last_t + 1);
24208 }
24209 }
24210 }
24211
24212 #[test]
24219 fn mimo_speculative_greedy_equals_plain_greedy() {
24220 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
24221 let _env = ScopedTestEnv::set(&[
24222 ("CMF_GPU", Some("0")),
24223 ("CMF_GPU_WGPU_GRAPH", Some("0")),
24224 ]);
24225 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
24226 let n = 24;
24227 let mut p = mimo_test_pipeline();
24228 let plain = mimo_greedy(&mut p, &ids, n, false);
24229 assert_eq!(plain.mtp_drafted, 0);
24230 assert_eq!(plain.token_ids.len(), n);
24231 let plain_kv = p.kv_cache.layers[0].seq_len;
24232
24233 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
24235 let spec = mimo_greedy(&mut p, &ids, n, true);
24236 assert!(spec.mtp_drafted > 0, "the round must draft");
24237 assert_eq!(spec.token_ids, plain.token_ids);
24238 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24239
24240 let mut truth: Vec<u32> = ids.clone();
24243 truth.extend(&plain.token_ids);
24244 let mut noisy = truth.clone();
24245 for (i, t) in noisy.iter_mut().enumerate() {
24246 if i % 5 == 0 {
24247 *t = (*t + 1) % 64;
24248 }
24249 }
24250 let mut st = mimo_test_mtp(3, 1.0);
24251 st.draft_override = Some(noisy);
24252 p.mimo_mtp = Some(st);
24253 let spec = mimo_greedy(&mut p, &ids, n, true);
24254 assert_eq!(spec.token_ids, plain.token_ids);
24255 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24256 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
24257 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
24258 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
24259 assert!(
24260 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
24261 "{:?}",
24262 stats.accept_hist
24263 );
24264 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
24265
24266 let mut st = mimo_test_mtp(3, 1.0);
24269 st.draft_override = Some(truth);
24270 p.mimo_mtp = Some(st);
24271 let spec = mimo_greedy(&mut p, &ids, n, true);
24272 assert_eq!(spec.token_ids, plain.token_ids);
24273 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
24274 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24275
24276 let off = mimo_greedy(&mut p, &ids, n, false);
24278 assert_eq!(off.token_ids, plain.token_ids);
24279 assert_eq!(off.mtp_drafted, 0);
24280 }
24281
24282 #[test]
24291 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
24292 let p = mimo_test_pipeline();
24293 assert_eq!(
24294 p.graph_attn_decline_reason(),
24295 Some("per-layer KV head counts")
24296 );
24297 assert_eq!(p.wgpu_graph_attn_decline(), None);
24298 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
24299 assert_eq!(
24300 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
24301 (1, 4, 4, None, false)
24302 );
24303 assert_eq!(g0.invf, p.inv_freq.as_slice());
24304 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
24305 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
24306 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
24307 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
24308 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
24309 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
24310 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
24311
24312 let emb = p.embed_single(3);
24315 let mut lg = Vec::new();
24316 assert!(
24317 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
24318 .is_none()
24319 );
24320 let mut hid = emb.clone();
24321 assert_eq!(
24322 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
24323 crate::gpu::BatchGraphOutcome::Declined
24324 );
24325 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
24326 assert!(p.try_multi_burst(3, 0, 4).is_none());
24327 assert!(
24328 p.graph_declines().is_empty(),
24329 "no attention decline logged: {:?}",
24330 p.graph_declines()
24331 );
24332 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
24337 assert_eq!(plain().graph_attn_decline_reason(), None);
24338 assert_eq!(plain().wgpu_graph_attn_decline(), None);
24339 assert!(
24340 plain().graph_attn_geom(0).is_none(),
24341 "uniform models keep the historical arms"
24342 );
24343 let mut q = plain();
24344 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
24345 assert_eq!(
24346 q.graph_attn_decline_reason(),
24347 Some("learned attention sinks")
24348 );
24349 assert_eq!(
24350 q.graph_attn_geom(1).unwrap().sink,
24351 Some(&[0.25f32, -0.25][..])
24352 );
24353 let mut q = plain();
24354 q.set_attn_geometry(None, Some(2)).unwrap();
24355 assert_eq!(
24356 q.graph_attn_decline_reason(),
24357 Some("V heads narrower than Q/K heads")
24358 );
24359 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
24360 let mut q = plain();
24361 q.sliding_layers = Some(vec![true, false]);
24362 q.swa = Some((4, usize::MAX));
24363 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
24364 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
24365 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
24366
24367 let mut q = mimo_test_pipeline();
24371 q.rope_scale = 2.0;
24372 q.rope_scale_local = 1.25;
24373 assert_eq!(q.wgpu_graph_attn_decline(), None);
24374 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
24375 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
24376
24377 let mut q = plain();
24380 q.rope_scale = 2.0;
24381 assert_eq!(
24382 q.wgpu_graph_attn_decline(),
24383 Some("scaled RoPE without per-layer geometry")
24384 );
24385 }
24386
24387 #[cfg(target_os = "macos")]
24393 #[test]
24394 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
24395 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
24399 p.swa = Some((16, usize::MAX));
24400 p.sliding_layers = Some(vec![true, true]);
24401 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
24402 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
24403
24404 assert_eq!(
24405 p.graph_attn_decline_reason(),
24406 Some("learned attention sinks")
24407 );
24408 let swa_graph = p.metal_graph_swa();
24409 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
24410 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
24411 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
24412 assert!(
24413 native_sink_attend,
24414 "the q1 sink admission must follow the SWA door"
24415 );
24416 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
24419 }
24420
24421 #[test]
24422 fn mimo_verify_rewind_preserves_lagging_host_caches() {
24423 let mut p = mimo_test_pipeline();
24424 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
24425 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
24426 for _ in 0..if li == 0 { 2 } else { 12 } {
24427 layer.append(&row, &row, &[]);
24428 }
24429 }
24430 p.mimo_verify_rewind(9).unwrap();
24431 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
24432 for layer in &p.kv_cache.layers[1..] {
24433 assert_eq!(layer.seq_len, 9);
24434 }
24435 }
24436
24437 #[test]
24441 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
24442 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
24443 let _ = std::fs::remove_dir_all(&dir);
24444 let mut p = mimo_test_pipeline();
24445 let hs = p.hidden_size;
24446 let ids = [5u32, 9, 11, 2, 40];
24447 p.layer_dump = Some(dir.join("decode"));
24448 for (pos, &id) in ids.iter().enumerate() {
24449 let e = p.embed_single(id);
24450 let _ = p.forward_layers(&e, pos, None);
24451 }
24452 p.clear_sequence_state();
24453 p.layer_dump = Some(dir.join("prefill"));
24454 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24455 for pos in 0..ids.len() {
24456 for li in 0..p.num_layers {
24457 let name = format!("p{pos:06}_l{li:02}.f32");
24458 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
24459 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
24460 assert_eq!(a.len(), hs * 4, "{name}");
24461 assert_eq!(a, b, "{name}");
24462 }
24463 }
24464 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
24465 let vals: Vec<f32> = last
24466 .chunks(4)
24467 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
24468 .collect();
24469 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
24470 let _ = std::fs::remove_dir_all(&dir);
24471 }
24472
24473 #[test]
24474 fn attn_geometry_and_sinks_are_validated() {
24475 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
24476 assert!(
24477 p.set_attn_geometry(Some(vec![2]), None).is_err(),
24478 "one entry per layer"
24479 );
24480 assert!(
24481 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
24482 "3 does not divide 4"
24483 );
24484 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
24485 assert!(p.set_attn_geometry(None, Some(0)).is_err());
24486 assert!(
24487 p.set_attn_geometry(None, Some(5)).is_err(),
24488 "V wider than the head"
24489 );
24490 p.set_attn_geometry(None, Some(4)).unwrap();
24491 assert_eq!(
24492 p.v_head_dim, None,
24493 "v_head_dim == head_dim is the uniform case"
24494 );
24495 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
24496 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
24497 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
24498 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
24499 assert!(
24500 p.kv_cache.layers[1].sinks.is_some(),
24501 "a reshape keeps the layer's sinks"
24502 );
24503 assert_eq!(p.layer_geom(1).0, 4);
24504 assert!(
24505 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
24506 "one sink per Q head"
24507 );
24508 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
24509 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
24510 }
24511
24512 #[test]
24515 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
24516 let cfg = || {
24517 Some(crate::nystrom::O1Cfg {
24518 layers: crate::nystrom::O1Layers::All,
24519 m: 4,
24520 w: 8,
24521 sink: 2,
24522 rect: crate::nystrom::O1Rect::Aggregate,
24523 })
24524 };
24525 let mut p = mimo_test_pipeline();
24526 p.set_o1(cfg());
24527 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
24528 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
24529 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
24530 q.sliding_layers = Some(vec![false, false, true]);
24531 q.swa = Some((4, usize::MAX));
24532 q.set_o1(cfg());
24533 assert_eq!(q.o1_flags, vec![true, false, false]);
24534 }
24535
24536 #[test]
24537 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
24538 const B: usize = 19;
24539 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24540 p.set_o1(Some(crate::nystrom::O1Cfg {
24541 layers: crate::nystrom::O1Layers::All,
24542 m: 4,
24543 w: 8,
24544 sink: 2,
24545 rect: crate::nystrom::O1Rect::Aggregate,
24546 }));
24547 p.o1_begin_with_prefix(Some(B));
24548 let ids: Vec<u32> = (0..B as u32).collect();
24549 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24550
24551 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
24552 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
24553 let next = p.embed_single(B as u32);
24554 let _ = p.forward_layers(&next, B, None);
24555 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
24556 }
24557
24558 #[test]
24559 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
24560 const B: usize = 19;
24561 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24562 let gdn_cfg = crate::linear_core::GdnCfg {
24566 num_v_heads: 2,
24567 num_k_heads: 1,
24568 key_head_dim: 2,
24569 value_head_dim: 4,
24570 conv_kernel: 3,
24571 hidden_size: 8,
24572 rms_eps: 1e-6,
24573 output_gate_sigmoid: false,
24574 };
24575 let synth = |n: usize, salt: usize| -> Vec<f32> {
24576 (0..n)
24577 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
24578 .collect()
24579 };
24580 let qt = |rows: usize, cols: usize, salt: usize| {
24581 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24582 };
24583 let c_dim = gdn_cfg.conv_dim();
24584 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
24585 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
24586 in_proj_qkv: qt(c_dim, 8, 1),
24587 in_proj_z: qt(vd, 8, 2),
24588 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
24589 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
24590 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
24591 a_log: vec![0.2, 0.5],
24592 dt_bias: synth(gdn_cfg.num_v_heads, 6),
24593 norm: vec![1.0; gdn_cfg.value_head_dim],
24594 out_proj: qt(8, vd, 7),
24595 });
24596 p.gdn_cfg = Some(gdn_cfg);
24597 p.set_o1(Some(crate::nystrom::O1Cfg {
24598 layers: crate::nystrom::O1Layers::All,
24599 m: 4,
24600 w: 8,
24601 sink: 2,
24602 rect: crate::nystrom::O1Rect::Aggregate,
24603 }));
24604 p.o1_begin_with_prefix(Some(B));
24605 for pos in 0..B - 2 {
24606 let emb = p.embed_single(pos as u32);
24607 let _ = p.forward_layers(&emb, pos, None);
24608 }
24609 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
24610
24611 let e1 = p.embed_single((B - 2) as u32);
24612 let e2 = p.embed_single((B - 1) as u32);
24613 let _ = p.forward_pair(&e1, &e2, B - 2);
24614
24615 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
24616 assert!(
24617 p.kv_cache
24618 .layers
24619 .iter()
24620 .enumerate()
24621 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
24622 );
24623 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
24624 assert_ne!(
24625 p.kv_cache.layers[0].linear_state, lane1_state,
24626 "real pair must commit GDN lane 2 before returning"
24627 );
24628 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
24629 let next = p.embed_single(B as u32);
24630 let _ = p.forward_layers(&next, B, None);
24631 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
24632 }
24633
24634 #[test]
24635 fn o1_error_observation_stays_terminal_until_reset() {
24636 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24637 p.set_o1(Some(crate::nystrom::O1Cfg {
24638 layers: crate::nystrom::O1Layers::All,
24639 m: 4,
24640 w: 8,
24641 sink: 2,
24642 rect: crate::nystrom::O1Rect::Aggregate,
24643 }));
24644 p.o1_begin();
24645 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
24646
24647 assert!(p.o1_seal_checked().is_err());
24648 assert!(
24649 p.o1_seal_checked().is_err(),
24650 "retry must see the sticky error"
24651 );
24652 let k = vec![0.2f32; 4];
24653 let v = vec![0.3f32; 4];
24654 p.kv_cache.layers[0].append(&k, &v, &[]);
24655 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
24656
24657 p.reset_session();
24658 p.o1_begin();
24659 p.kv_cache.layers[0].append(&k, &v, &[]);
24660 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
24661 }
24662
24663 #[test]
24664 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
24665 let ids = vec![1u32, 2, 3, 4, 5, 6];
24666 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24667 p.graph_logits = Some(vec![123.0]);
24668 p.graph_want_logits = true;
24669 p.graph_failed
24670 .store(true, std::sync::atomic::Ordering::Relaxed);
24671 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
24672 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
24673 assert!(err.contains("before NLL"));
24674 assert!(p.graph_logits.is_none());
24675 assert!(!p.graph_want_logits);
24676 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24677 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
24678
24679 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24680 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
24681 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
24682 assert_eq!(actual.1, expected.1);
24683 assert!((actual.0 - expected.0).abs() < 1e-9);
24684 }
24685
24686 #[test]
24687 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
24688 let ids = vec![1u32, 2, 3, 4, 5, 6];
24689 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24690 p.nll_test_fail_at = Some(1);
24691 let err = p
24692 .nll_ids_from(&ids, 0)
24693 .expect_err("one-shot forward failure");
24694 assert!(err.contains("forward") || err.contains("score row"));
24695 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24696 assert!(!p.graph_want_logits);
24697 assert!(p.graph_logits.is_none());
24698 assert!(p.kv_history.is_empty());
24699
24700 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24701 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
24702 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
24703 assert_eq!(actual.1, expected.1);
24704 assert!((actual.0 - expected.0).abs() < 1e-9);
24705 }
24706
24707 #[test]
24708 fn nll_serial_failure_before_first_row_is_reported() {
24709 let ids = vec![1u32, 2, 3, 4];
24710 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24711 p.nll_test_force_serial = true;
24712 p.nll_test_fail_at = Some(0);
24713 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
24714 assert!(err.contains("serial forward"));
24715 assert!(p.kv_history.is_empty());
24716 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24717 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
24718 }
24719
24720 #[test]
24721 fn ffn_probe_failure_discards_recorder_and_state() {
24722 let ids = vec![1u32, 2, 3, 4];
24723 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24724 p.nll_test_fail_at = Some(0);
24725 let err = p
24726 .probe_ffn_mass_batch(&ids)
24727 .expect_err("probe forward failure");
24728 assert!(err.contains("NLL"));
24729 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
24730 assert!(p.kv_history.is_empty());
24731 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24732 }
24733
24734 #[test]
24735 fn nll_test_controls_are_pipeline_scoped() {
24736 let ids = vec![1u32, 2, 3, 4];
24737 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24738 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24739 failing.nll_test_force_serial = true;
24740 failing.nll_test_fail_at = Some(0);
24741
24742 assert!(!failing.can_prefill_batched());
24743 assert!(unaffected.can_prefill_batched());
24744 let expected = unaffected
24745 .nll_ids_from(&ids, 0)
24746 .expect("unaffected pipeline remains usable");
24747 let err = failing
24748 .nll_ids_from(&ids, 0)
24749 .expect_err("failure injection belongs to failing pipeline");
24750 assert!(err.contains("serial forward"));
24751 assert!(failing.nll_test_fail_at.is_none());
24752 assert!(unaffected.can_prefill_batched());
24753 let actual = unaffected
24754 .nll_ids_from(&ids, 0)
24755 .expect("unaffected pipeline remains reusable");
24756 assert_eq!(actual.1, expected.1);
24757 assert!((actual.0 - expected.0).abs() < 1e-9);
24758 }
24759
24760 #[test]
24761 fn forward_ids_failure_channel_is_terminal_and_reusable() {
24762 let ids = vec![1u32, 2, 3, 4, 5, 6];
24763 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
24764 p.graph_logits = Some(vec![123.0]);
24765 p.graph_want_logits = true;
24766 p.graph_failed
24767 .store(true, std::sync::atomic::Ordering::Relaxed);
24768 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
24769
24770 let err = p
24771 .forward_ids(&ids, None)
24772 .expect_err("a failed forward must not become a valid head result");
24773 assert!(err.contains("forward_ids setup"));
24774 assert!(p.graph_logits.is_none());
24775 assert!(!p.graph_want_logits);
24776 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
24777 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
24778 assert_eq!(p.kv_cache.seq_len(), 0);
24779
24780 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
24781 .forward_ids(&ids, None)
24782 .expect("fresh forward_ids");
24783 let actual = p
24784 .forward_ids(&ids, None)
24785 .expect("pipeline remains reusable after a failed forward");
24786 assert_eq!(actual.len(), expected.len());
24787 assert!(
24788 actual
24789 .iter()
24790 .zip(expected)
24791 .all(|(a, b)| (a - b).abs() < 1e-9)
24792 );
24793 assert_eq!(p.kv_cache.seq_len(), ids.len());
24794 }
24795
24796 #[test]
24797 fn sigmoid_router_floor_is_explicit_per_architecture() {
24798 let zero = || DenseFfn {
24804 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24805 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24806 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24807 act: Act::Silu,
24808 down_t: None,
24809 segs: Vec::new(),
24810 };
24811 let m = MoeFfn {
24812 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24813 experts: vec![zero(), zero()],
24814 top_k: 1,
24815 norm_topk_prob: true,
24816 router_sigmoid: true,
24817 sigmoid_denom_eps: 1e-20,
24818 expert_bias: None,
24819 routed_scaling: 2.5,
24820 route_tau: None,
24821 shared: None,
24822 stats: std::cell::RefCell::new(Vec::new()),
24823 act_sq: std::cell::RefCell::new(Vec::new()),
24824 act_rows: std::cell::RefCell::new(Vec::new()),
24825 mask: None,
24826 per_expert_scale: None,
24827 router_input_norm: false,
24828 resonance: None,
24829 grown: Vec::new(),
24830 };
24831 let logits = [-20.0f32, -20.0];
24832 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
24833 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24834 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
24835 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
24836 assert!((flint_wsum - reference_expected).abs() < 1e-15);
24837 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
24838 assert!(legacy_wsum > minimax_wsum * 100.0);
24839 }
24840
24841 #[test]
24842 fn resonance_scores_match_formula_and_stable_tie() {
24843 let r = Resonance {
24844 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
24846 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
24847 k: 1,
24848 bias: vec![1.5, 0.5, 0.0],
24849 shell: Vec::new(),
24850 };
24851 let x = [1.0f32, 1.0];
24852 let mut got = vec![0.0; 3];
24853 r.scores(&x, &mut got);
24854 assert!((got[0] - 0.5).abs() < 1e-6);
24858 assert!((got[1] - 0.5).abs() < 1e-6);
24859 assert!(got[2].abs() < 1e-6);
24860 let best = got
24861 .iter()
24862 .enumerate()
24863 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
24864 .map(|(i, _)| i);
24865 assert_eq!(best, Some(0));
24866 assert!(got.iter().all(|v| v.is_finite()));
24867 }
24868
24869 #[test]
24874 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
24875 let plain = Resonance {
24881 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
24882 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
24883 k: 1,
24884 bias: vec![1.5, 0.5, 0.0, 0.0],
24885 shell: Vec::new(),
24886 };
24887 let shelled = Resonance {
24888 mu: plain.mu.clone(),
24889 u: plain.u.clone(),
24890 k: 1,
24891 bias: plain.bias.clone(),
24892 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
24893 };
24894 assert!(!plain.has_shell());
24895 assert!(shelled.has_shell());
24896 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
24897 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
24898 set_growth_shell(Some(true));
24899 assert!(growth_shell_enabled());
24900 let x = [1.0f32, 1.0];
24903 plain.scores(&x, &mut a);
24904 shelled.scores(&x, &mut b);
24905 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
24906 assert!(a[2] == 0.0 && a[3] == 0.0);
24907 let xo = [3.0f32, 0.0];
24910 plain.scores(&xo, &mut a);
24911 shelled.scores(&xo, &mut b);
24912 assert_eq!(a[2], -6.0);
24913 assert_eq!(a[3], -6.0);
24914 assert_eq!(
24915 bits(&a[..3]),
24916 bits(&b[..3]),
24917 "trunk rows + the boundary row"
24918 );
24919 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
24920 assert_eq!(shelled.effective_shell(4), shelled.shell);
24921 set_growth_shell(Some(false));
24924 assert!(!growth_shell_enabled());
24925 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
24926 shelled.scores(&xo, &mut b);
24927 assert_eq!(bits(&a), bits(&b));
24928 set_growth_shell(None);
24929 let short = Resonance {
24932 shell: vec![f32::INFINITY, f32::INFINITY],
24933 ..shelled
24934 };
24935 set_growth_shell(Some(true));
24936 short.scores(&xo, &mut b);
24937 assert_eq!(bits(&a), bits(&b));
24938 set_growth_shell(None);
24939 }
24940
24941 #[test]
24945 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
24946 let zero = || DenseFfn {
24947 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24948 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24949 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24950 act: Act::Silu,
24951 down_t: None,
24952 segs: Vec::new(),
24953 };
24954 let moe = |sigmoid: bool| MoeFfn {
24955 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
24956 experts: vec![zero(), zero(), zero(), zero()],
24957 top_k: 1,
24958 norm_topk_prob: true,
24959 router_sigmoid: sigmoid,
24960 sigmoid_denom_eps: 1e-6,
24961 expert_bias: None,
24962 routed_scaling: 1.0,
24963 route_tau: None,
24964 shared: None,
24965 stats: std::cell::RefCell::new(Vec::new()),
24966 act_sq: std::cell::RefCell::new(Vec::new()),
24967 act_rows: std::cell::RefCell::new(Vec::new()),
24968 mask: None,
24969 per_expert_scale: None,
24970 router_input_norm: false,
24971 resonance: None,
24972 grown: Vec::new(),
24973 };
24974 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
24975 for sigmoid in [false, true] {
24976 let m = moe(sigmoid);
24977 let (idx, p, wsum) = moe_route(&logits, &m, None);
24978 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
24979 assert_eq!(p[1], 0.0);
24980 assert_eq!(p[3], 0.0);
24981 assert!(p[2] > p[0] && p[0] > 0.0);
24982 assert!(p.iter().all(|v| v.is_finite()));
24983 let w = p[2] / wsum;
24984 if sigmoid {
24985 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
24987 } else {
24988 assert_eq!(
24989 w, 1.0,
24990 "softmax: the renormalized top-1 weight is exactly 1"
24991 );
24992 }
24993 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
24996 assert_eq!(idx, vec![2]);
24997 }
24998 let m = moe(false);
25000 let (idx, _, _) = moe_route(
25001 &[
25002 f32::NEG_INFINITY,
25003 f32::NEG_INFINITY,
25004 f32::NEG_INFINITY,
25005 -9.0,
25006 ],
25007 &m,
25008 None,
25009 );
25010 assert_eq!(idx, vec![3]);
25011 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
25014 assert_eq!(idx, vec![0]);
25015 assert!(p.iter().all(|&v| v == 0.25));
25016 assert!(wsum.is_finite() && wsum > 0.0);
25017 }
25018
25019 #[cfg(feature = "gpu")]
25025 #[test]
25026 fn minimax_device_route_matches_host_sigmoid_contract() {
25027 match crate::gpu_wgpu::selected_and_up() {
25028 None => {
25029 crate::gpu_wgpu::skip_or_fail(module_path!());
25030 return;
25031 }
25032 Some(false) => {
25040 eprintln!(
25041 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25042 module_path!()
25043 );
25044 return;
25045 }
25046 Some(true) => {}
25047 }
25048 let n = 128usize;
25049 let top_k = 4usize;
25050 let logits: Vec<f32> = (0..n)
25051 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25052 .collect();
25053 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25054 let m = MoeFfn {
25055 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25056 experts: Vec::new(),
25057 top_k,
25058 norm_topk_prob: true,
25059 router_sigmoid: true,
25060 sigmoid_denom_eps: 1e-20,
25061 expert_bias: Some(bias.clone()),
25062 routed_scaling: 2.0,
25063 route_tau: None,
25064 shared: None,
25065 stats: std::cell::RefCell::new(Vec::new()),
25066 act_sq: std::cell::RefCell::new(Vec::new()),
25067 act_rows: std::cell::RefCell::new(Vec::new()),
25068 mask: None,
25069 per_expert_scale: None,
25070 router_input_norm: false,
25071 resonance: None,
25072 grown: Vec::new(),
25073 };
25074 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25075 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
25076
25077 for shared in [Some(n as u32), None] {
25078 let mut gi = Vec::new();
25079 let mut gw = Vec::new();
25080 assert!(
25081 crate::gpu_wgpu::minimax_moe_route_for_test(
25082 &logits,
25083 Some(&bias),
25084 top_k,
25085 2.0,
25086 shared,
25087 &mut gi,
25088 &mut gw,
25089 ),
25090 "the device did not come up — the test checked nothing"
25091 );
25092 let routed = if shared.is_some() { top_k } else { gi.len() };
25093 assert_eq!(
25094 &gi[..routed],
25095 &ci[..],
25096 "different experts or order (shared={shared:?})"
25097 );
25098 let d = cw
25099 .iter()
25100 .zip(&gw[..routed])
25101 .map(|(a, b)| (a - b).abs())
25102 .fold(0.0f32, f32::max);
25103 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
25104 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
25105 if let Some(s) = shared {
25106 assert_eq!(gi[top_k], s as usize, "shared pin slot");
25107 assert_eq!(gw[top_k], 1.0, "shared weight");
25108 }
25109 }
25110 }
25111
25112 #[cfg(feature = "gpu")]
25117 #[test]
25118 fn minimax_device_cold_weights_match_host_contract() {
25119 match crate::gpu_wgpu::selected_and_up() {
25120 None => {
25121 crate::gpu_wgpu::skip_or_fail(module_path!());
25122 return;
25123 }
25124 Some(false) => {
25132 eprintln!(
25133 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25134 module_path!()
25135 );
25136 return;
25137 }
25138 Some(true) => {}
25139 }
25140 let n = 128usize;
25141 let top_k = 4usize;
25142 let logits: Vec<f32> = (0..n)
25143 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25144 .collect();
25145 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25146 let m = MoeFfn {
25147 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25148 experts: Vec::new(),
25149 top_k,
25150 norm_topk_prob: true,
25151 router_sigmoid: true,
25152 sigmoid_denom_eps: 1e-20,
25153 expert_bias: Some(bias.clone()),
25154 routed_scaling: 2.0,
25155 route_tau: None,
25156 shared: None,
25157 stats: std::cell::RefCell::new(Vec::new()),
25158 act_sq: std::cell::RefCell::new(Vec::new()),
25159 act_rows: std::cell::RefCell::new(Vec::new()),
25160 mask: None,
25161 per_expert_scale: None,
25162 router_input_norm: false,
25163 resonance: None,
25164 grown: Vec::new(),
25165 };
25166 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25167 assert_eq!(ci.len(), top_k);
25168
25169 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
25171 let mut cold = Vec::new();
25172 let mut idx = Vec::new();
25173 let mut dw = Vec::new();
25174 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25175 &logits,
25176 Some(&bias),
25177 top_k,
25178 2.0,
25179 &resident,
25180 n as u32,
25181 &mut cold,
25182 &mut idx,
25183 &mut dw,
25184 ));
25185 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
25186 for (r, &e) in ci.iter().enumerate() {
25191 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
25192 let host = p[e] / wsum;
25193 let d = (dw[r] - host).abs();
25194 assert!(
25195 d <= 1e-6 * host.abs().max(1.0),
25196 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
25197 dw[r]
25198 );
25199 }
25200 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
25201 assert_eq!(dw[top_k], 1.0, "shared weight");
25202
25203 let mut remap = resident.clone();
25205 remap[ci[0]] = u32::MAX;
25206 remap[ci[2]] = u32::MAX;
25207 let mut cold = Vec::new();
25208 let mut idx2 = Vec::new();
25209 let mut dw2 = Vec::new();
25210 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25211 &logits,
25212 Some(&bias),
25213 top_k,
25214 2.0,
25215 &remap,
25216 n as u32,
25217 &mut cold,
25218 &mut idx2,
25219 &mut dw2,
25220 ));
25221 let mut resident_weight_checked = 0usize;
25229 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
25230 if dw2[r] == 0.0 {
25231 continue; }
25233 let e = slot as usize;
25234 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
25235 let host = p[e] / wsum;
25236 let d = (dw2[r] - host).abs();
25237 assert!(
25238 d <= 1e-6 * host.abs().max(1.0),
25239 "partial-bank resident weight expert {e}: Δ={d:.3e}"
25240 );
25241 resident_weight_checked += 1;
25242 }
25243 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
25244 println!("partial bank: checked {resident_weight_checked} resident weights");
25245 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
25246 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
25247 ids.sort_unstable();
25248 let mut want = vec![ci[0], ci[2]];
25249 want.sort_unstable();
25250 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
25251 for &(e, w) in &cold {
25252 let host = p[e] / wsum;
25253 let d = (w - host).abs();
25254 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
25255 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
25256 }
25257 }
25258
25259 #[test]
25264 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
25265 let zero = || DenseFfn {
25266 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25267 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25268 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25269 act: Act::Silu,
25270 down_t: None,
25271 segs: Vec::new(),
25272 };
25273 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
25274 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
25275 experts: vec![zero(), zero(), zero()],
25276 top_k: 1,
25277 norm_topk_prob: norm_topk,
25278 router_sigmoid: false,
25279 sigmoid_denom_eps: 1e-6,
25280 expert_bias: None,
25281 routed_scaling: 1.0,
25282 route_tau: None,
25283 shared: None,
25284 stats: std::cell::RefCell::new(Vec::new()),
25285 act_sq: std::cell::RefCell::new(Vec::new()),
25286 act_rows: std::cell::RefCell::new(Vec::new()),
25287 mask: None,
25288 per_expert_scale: None,
25289 router_input_norm: false,
25290 resonance: resonant.then(|| Resonance {
25291 mu: vec![0.0; 6],
25292 u: Vec::new(),
25293 k: 0,
25294 bias: vec![0.0; 3],
25295 shell: Vec::new(),
25296 }),
25297 grown: Vec::new(),
25298 };
25299 let lo = -0.1f32;
25302 let hi = f32::from_bits(lo.to_bits() - 1);
25303 assert!(hi > lo && hi - lo < 2f32.powi(-25));
25304 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
25305 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
25307 assert_eq!(
25308 idx,
25309 vec![0],
25310 "softmax tie → lower index (the gated contract)"
25311 );
25312 for norm in [true, false] {
25315 let m = moe(true, norm);
25316 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
25317 assert_eq!(idx, vec![1], "norm_topk {norm}");
25318 assert_eq!(p, vec![0.0, 1.0, 0.0]);
25319 assert_eq!(p[1] / wsum, 1.0);
25320 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
25323 assert_eq!(idx, vec![0]);
25324 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
25326 assert_eq!(idx, vec![2]);
25327 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
25328 assert_eq!(idx, vec![0]);
25329 assert_eq!(p[0] / wsum, 1.0);
25330 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
25333 assert_eq!(idx, vec![0]);
25334 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
25335 }
25336 }
25337}